@mxalbert/context-mode 2.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 (365) hide show
  1. package/.claude-plugin/marketplace.json +33 -0
  2. package/.claude-plugin/plugin.json +31 -0
  3. package/.codex-plugin/hooks.json +65 -0
  4. package/.codex-plugin/mcp.json +13 -0
  5. package/.codex-plugin/plugin.json +31 -0
  6. package/.openclaw-plugin/index.ts +12 -0
  7. package/.openclaw-plugin/openclaw.plugin.json +38 -0
  8. package/.openclaw-plugin/package.json +28 -0
  9. package/LICENSE +94 -0
  10. package/README.md +1619 -0
  11. package/bin/statusline.mjs +378 -0
  12. package/build/adapters/antigravity/index.d.ts +51 -0
  13. package/build/adapters/antigravity/index.js +169 -0
  14. package/build/adapters/antigravity-cli/index.d.ts +51 -0
  15. package/build/adapters/antigravity-cli/index.js +342 -0
  16. package/build/adapters/base.d.ts +92 -0
  17. package/build/adapters/base.js +135 -0
  18. package/build/adapters/claude-code/config.d.ts +8 -0
  19. package/build/adapters/claude-code/config.js +8 -0
  20. package/build/adapters/claude-code/hooks.d.ts +106 -0
  21. package/build/adapters/claude-code/hooks.js +183 -0
  22. package/build/adapters/claude-code/index.d.ts +80 -0
  23. package/build/adapters/claude-code/index.js +569 -0
  24. package/build/adapters/claude-code-base.d.ts +49 -0
  25. package/build/adapters/claude-code-base.js +113 -0
  26. package/build/adapters/client-map.d.ts +10 -0
  27. package/build/adapters/client-map.js +42 -0
  28. package/build/adapters/codex/config.d.ts +8 -0
  29. package/build/adapters/codex/config.js +8 -0
  30. package/build/adapters/codex/hooks.d.ts +55 -0
  31. package/build/adapters/codex/hooks.js +64 -0
  32. package/build/adapters/codex/index.d.ts +87 -0
  33. package/build/adapters/codex/index.js +995 -0
  34. package/build/adapters/codex/paths.d.ts +1 -0
  35. package/build/adapters/codex/paths.js +12 -0
  36. package/build/adapters/codex/usage.d.ts +107 -0
  37. package/build/adapters/codex/usage.js +227 -0
  38. package/build/adapters/copilot-base.d.ts +94 -0
  39. package/build/adapters/copilot-base.js +297 -0
  40. package/build/adapters/copilot-cli/hooks.d.ts +33 -0
  41. package/build/adapters/copilot-cli/hooks.js +64 -0
  42. package/build/adapters/copilot-cli/index.d.ts +48 -0
  43. package/build/adapters/copilot-cli/index.js +341 -0
  44. package/build/adapters/cursor/config.d.ts +4 -0
  45. package/build/adapters/cursor/config.js +4 -0
  46. package/build/adapters/cursor/hooks.d.ts +55 -0
  47. package/build/adapters/cursor/hooks.js +82 -0
  48. package/build/adapters/cursor/index.d.ts +64 -0
  49. package/build/adapters/cursor/index.js +517 -0
  50. package/build/adapters/detect.d.ts +136 -0
  51. package/build/adapters/detect.js +623 -0
  52. package/build/adapters/gemini-cli/config.d.ts +8 -0
  53. package/build/adapters/gemini-cli/config.js +8 -0
  54. package/build/adapters/gemini-cli/hooks.d.ts +83 -0
  55. package/build/adapters/gemini-cli/hooks.js +115 -0
  56. package/build/adapters/gemini-cli/index.d.ts +80 -0
  57. package/build/adapters/gemini-cli/index.js +483 -0
  58. package/build/adapters/jetbrains-copilot/config.d.ts +8 -0
  59. package/build/adapters/jetbrains-copilot/config.js +8 -0
  60. package/build/adapters/jetbrains-copilot/hooks.d.ts +59 -0
  61. package/build/adapters/jetbrains-copilot/hooks.js +87 -0
  62. package/build/adapters/jetbrains-copilot/index.d.ts +31 -0
  63. package/build/adapters/jetbrains-copilot/index.js +131 -0
  64. package/build/adapters/kimi/config.d.ts +8 -0
  65. package/build/adapters/kimi/config.js +8 -0
  66. package/build/adapters/kimi/hooks.d.ts +28 -0
  67. package/build/adapters/kimi/hooks.js +34 -0
  68. package/build/adapters/kimi/index.d.ts +66 -0
  69. package/build/adapters/kimi/index.js +537 -0
  70. package/build/adapters/kimi/paths.d.ts +21 -0
  71. package/build/adapters/kimi/paths.js +52 -0
  72. package/build/adapters/kimi/usage.d.ts +82 -0
  73. package/build/adapters/kimi/usage.js +217 -0
  74. package/build/adapters/kiro/hooks.d.ts +61 -0
  75. package/build/adapters/kiro/hooks.js +94 -0
  76. package/build/adapters/kiro/index.d.ts +61 -0
  77. package/build/adapters/kiro/index.js +301 -0
  78. package/build/adapters/omp/index.d.ts +65 -0
  79. package/build/adapters/omp/index.js +182 -0
  80. package/build/adapters/omp/plugin.d.ts +81 -0
  81. package/build/adapters/omp/plugin.js +331 -0
  82. package/build/adapters/omp/usage.d.ts +49 -0
  83. package/build/adapters/omp/usage.js +110 -0
  84. package/build/adapters/openclaw/config.d.ts +8 -0
  85. package/build/adapters/openclaw/config.js +8 -0
  86. package/build/adapters/openclaw/hooks.d.ts +50 -0
  87. package/build/adapters/openclaw/hooks.js +61 -0
  88. package/build/adapters/openclaw/index.d.ts +74 -0
  89. package/build/adapters/openclaw/index.js +439 -0
  90. package/build/adapters/openclaw/mcp-tools.d.ts +54 -0
  91. package/build/adapters/openclaw/mcp-tools.js +207 -0
  92. package/build/adapters/openclaw/plugin.d.ts +141 -0
  93. package/build/adapters/openclaw/plugin.js +818 -0
  94. package/build/adapters/openclaw/session-db.d.ts +55 -0
  95. package/build/adapters/openclaw/session-db.js +88 -0
  96. package/build/adapters/openclaw/usage.d.ts +34 -0
  97. package/build/adapters/openclaw/usage.js +52 -0
  98. package/build/adapters/openclaw/workspace-router.d.ts +29 -0
  99. package/build/adapters/openclaw/workspace-router.js +64 -0
  100. package/build/adapters/opencode/config.d.ts +8 -0
  101. package/build/adapters/opencode/config.js +8 -0
  102. package/build/adapters/opencode/hooks.d.ts +38 -0
  103. package/build/adapters/opencode/hooks.js +50 -0
  104. package/build/adapters/opencode/index.d.ts +144 -0
  105. package/build/adapters/opencode/index.js +638 -0
  106. package/build/adapters/opencode/plugin.d.ts +249 -0
  107. package/build/adapters/opencode/plugin.js +1536 -0
  108. package/build/adapters/opencode/v2.d.ts +117 -0
  109. package/build/adapters/opencode/v2.js +224 -0
  110. package/build/adapters/opencode/zod3tov4.d.ts +1 -0
  111. package/build/adapters/opencode/zod3tov4.js +132 -0
  112. package/build/adapters/pi/extension.d.ts +81 -0
  113. package/build/adapters/pi/extension.js +861 -0
  114. package/build/adapters/pi/index.d.ts +57 -0
  115. package/build/adapters/pi/index.js +173 -0
  116. package/build/adapters/pi/mcp-bridge.d.ts +268 -0
  117. package/build/adapters/pi/mcp-bridge.js +894 -0
  118. package/build/adapters/qwen-code/hooks.d.ts +26 -0
  119. package/build/adapters/qwen-code/hooks.js +29 -0
  120. package/build/adapters/qwen-code/index.d.ts +40 -0
  121. package/build/adapters/qwen-code/index.js +361 -0
  122. package/build/adapters/qwen-code/usage.d.ts +90 -0
  123. package/build/adapters/qwen-code/usage.js +222 -0
  124. package/build/adapters/types.d.ts +360 -0
  125. package/build/adapters/types.js +118 -0
  126. package/build/adapters/vscode-copilot/config.d.ts +8 -0
  127. package/build/adapters/vscode-copilot/config.js +8 -0
  128. package/build/adapters/vscode-copilot/hooks.d.ts +71 -0
  129. package/build/adapters/vscode-copilot/hooks.js +98 -0
  130. package/build/adapters/vscode-copilot/index.d.ts +32 -0
  131. package/build/adapters/vscode-copilot/index.js +222 -0
  132. package/build/adapters/zed/index.d.ts +40 -0
  133. package/build/adapters/zed/index.js +163 -0
  134. package/build/cli.d.ts +34 -0
  135. package/build/cli.js +1828 -0
  136. package/build/db-base.d.ts +250 -0
  137. package/build/db-base.js +756 -0
  138. package/build/executor.d.ts +72 -0
  139. package/build/executor.js +678 -0
  140. package/build/exit-classify.d.ts +19 -0
  141. package/build/exit-classify.js +12 -0
  142. package/build/fetch/blocks.d.ts +145 -0
  143. package/build/fetch/blocks.js +244 -0
  144. package/build/fetch/extract.d.ts +68 -0
  145. package/build/fetch/extract.js +123 -0
  146. package/build/fetch/page-store.d.ts +66 -0
  147. package/build/fetch/page-store.js +174 -0
  148. package/build/fetch-cache.d.ts +13 -0
  149. package/build/fetch-cache.js +15 -0
  150. package/build/lifecycle.d.ts +124 -0
  151. package/build/lifecycle.js +278 -0
  152. package/build/package-identity.d.ts +26 -0
  153. package/build/package-identity.js +28 -0
  154. package/build/runPool.d.ts +36 -0
  155. package/build/runPool.js +51 -0
  156. package/build/runtime.d.ts +71 -0
  157. package/build/runtime.js +659 -0
  158. package/build/search/auto-memory.d.ts +47 -0
  159. package/build/search/auto-memory.js +170 -0
  160. package/build/search/ctx-search-schema.d.ts +90 -0
  161. package/build/search/ctx-search-schema.js +135 -0
  162. package/build/search/flood-guard.d.ts +57 -0
  163. package/build/search/flood-guard.js +80 -0
  164. package/build/search/unified.d.ts +56 -0
  165. package/build/search/unified.js +104 -0
  166. package/build/security.d.ts +206 -0
  167. package/build/security.js +780 -0
  168. package/build/server.d.ts +240 -0
  169. package/build/server.js +5034 -0
  170. package/build/session/analytics.d.ts +773 -0
  171. package/build/session/analytics.js +2420 -0
  172. package/build/session/db.d.ts +468 -0
  173. package/build/session/db.js +1330 -0
  174. package/build/session/error-classifier.d.ts +87 -0
  175. package/build/session/error-classifier.js +303 -0
  176. package/build/session/event-emit.d.ts +48 -0
  177. package/build/session/event-emit.js +101 -0
  178. package/build/session/extract.d.ts +333 -0
  179. package/build/session/extract.js +2688 -0
  180. package/build/session/model-prices.json +429 -0
  181. package/build/session/persist-tool-calls.d.ts +54 -0
  182. package/build/session/persist-tool-calls.js +105 -0
  183. package/build/session/pricing.d.ts +64 -0
  184. package/build/session/pricing.js +151 -0
  185. package/build/session/project-attribution.d.ts +73 -0
  186. package/build/session/project-attribution.js +245 -0
  187. package/build/session/purge.d.ts +138 -0
  188. package/build/session/purge.js +240 -0
  189. package/build/session/retrieval-marker.d.ts +39 -0
  190. package/build/session/retrieval-marker.js +65 -0
  191. package/build/session/snapshot.d.ts +46 -0
  192. package/build/session/snapshot.js +532 -0
  193. package/build/store-directory.d.ts +56 -0
  194. package/build/store-directory.js +254 -0
  195. package/build/store.d.ts +147 -0
  196. package/build/store.js +1714 -0
  197. package/build/truncate.d.ts +57 -0
  198. package/build/truncate.js +142 -0
  199. package/build/types.d.ts +117 -0
  200. package/build/types.js +20 -0
  201. package/build/util/claude-config.d.ts +32 -0
  202. package/build/util/claude-config.js +84 -0
  203. package/build/util/hook-config.d.ts +27 -0
  204. package/build/util/hook-config.js +76 -0
  205. package/build/util/jsonc.d.ts +14 -0
  206. package/build/util/jsonc.js +104 -0
  207. package/build/util/plugin-cache-integrity.d.ts +51 -0
  208. package/build/util/plugin-cache-integrity.js +146 -0
  209. package/build/util/project-dir.d.ts +149 -0
  210. package/build/util/project-dir.js +340 -0
  211. package/build/util/sibling-mcp.d.ts +79 -0
  212. package/build/util/sibling-mcp.js +181 -0
  213. package/cli.bundle.mjs +1495 -0
  214. package/configs/antigravity/GEMINI.md +68 -0
  215. package/configs/antigravity/mcp_config.json +7 -0
  216. package/configs/antigravity-cli/hooks/hooks.json +37 -0
  217. package/configs/antigravity-cli/hooks.json +37 -0
  218. package/configs/antigravity-cli/mcp_config.json +10 -0
  219. package/configs/antigravity-cli/plugin.json +14 -0
  220. package/configs/antigravity-cli/rules/context-mode.md +77 -0
  221. package/configs/antigravity-cli/skills/context-mode/SKILL.md +77 -0
  222. package/configs/claude-code/CLAUDE.md +91 -0
  223. package/configs/codex/AGENTS.md +99 -0
  224. package/configs/codex/config.toml +8 -0
  225. package/configs/codex/hooks.json +47 -0
  226. package/configs/copilot-cli/.github/plugin/plugin.json +23 -0
  227. package/configs/copilot-cli/.mcp.json +12 -0
  228. package/configs/copilot-cli/README.md +47 -0
  229. package/configs/copilot-cli/hooks.json +41 -0
  230. package/configs/copilot-cli/skills/context-mode/SKILL.md +38 -0
  231. package/configs/cursor/context-mode.mdc +76 -0
  232. package/configs/cursor/hooks.json +21 -0
  233. package/configs/cursor/mcp.json +7 -0
  234. package/configs/gemini-cli/GEMINI.md +88 -0
  235. package/configs/gemini-cli/mcp.json +7 -0
  236. package/configs/gemini-cli/settings.json +60 -0
  237. package/configs/jetbrains-copilot/copilot-instructions.md +80 -0
  238. package/configs/jetbrains-copilot/hooks.json +16 -0
  239. package/configs/jetbrains-copilot/mcp.json +7 -0
  240. package/configs/kilo/AGENTS.md +84 -0
  241. package/configs/kilo/kilo.json +6 -0
  242. package/configs/kimi/hooks.json +54 -0
  243. package/configs/kiro/KIRO.md +84 -0
  244. package/configs/kiro/agent.json +18 -0
  245. package/configs/kiro/mcp.json +7 -0
  246. package/configs/omp/SYSTEM.md +85 -0
  247. package/configs/omp/mcp.json +7 -0
  248. package/configs/openclaw/AGENTS.md +87 -0
  249. package/configs/openclaw/openclaw.json +13 -0
  250. package/configs/opencode/AGENTS.md +84 -0
  251. package/configs/opencode/opencode.json +6 -0
  252. package/configs/pi/AGENTS.md +3 -0
  253. package/configs/qwen-code/QWEN.md +91 -0
  254. package/configs/vscode-copilot/copilot-instructions.md +80 -0
  255. package/configs/vscode-copilot/hooks.json +16 -0
  256. package/configs/vscode-copilot/mcp.json +7 -0
  257. package/configs/zed/AGENTS.md +68 -0
  258. package/hooks/antigravity-cli/payload.mjs +98 -0
  259. package/hooks/antigravity-cli/posttooluse.mjs +138 -0
  260. package/hooks/antigravity-cli/pretooluse.mjs +78 -0
  261. package/hooks/antigravity-cli/stop.mjs +58 -0
  262. package/hooks/auto-injection.mjs +102 -0
  263. package/hooks/cache-heal-utils.mjs +379 -0
  264. package/hooks/codex/platform.mjs +1 -0
  265. package/hooks/codex/posttooluse.mjs +66 -0
  266. package/hooks/codex/precompact.mjs +70 -0
  267. package/hooks/codex/pretooluse.mjs +42 -0
  268. package/hooks/codex/sessionstart.mjs +121 -0
  269. package/hooks/codex/stop.mjs +138 -0
  270. package/hooks/codex/userpromptsubmit.mjs +75 -0
  271. package/hooks/copilot-cli/posttooluse.mjs +79 -0
  272. package/hooks/copilot-cli/precompact.mjs +66 -0
  273. package/hooks/copilot-cli/pretooluse.mjs +41 -0
  274. package/hooks/copilot-cli/sessionstart.mjs +121 -0
  275. package/hooks/copilot-cli/stop.mjs +59 -0
  276. package/hooks/copilot-cli/userpromptsubmit.mjs +77 -0
  277. package/hooks/core/codex-caps.mjs +112 -0
  278. package/hooks/core/formatters.mjs +356 -0
  279. package/hooks/core/mcp-ready.mjs +109 -0
  280. package/hooks/core/platform-detect.mjs +49 -0
  281. package/hooks/core/routing.mjs +1050 -0
  282. package/hooks/core/stdin.mjs +90 -0
  283. package/hooks/core/tool-naming.mjs +57 -0
  284. package/hooks/cursor/afteragentresponse.mjs +74 -0
  285. package/hooks/cursor/hooks.json +31 -0
  286. package/hooks/cursor/posttooluse.mjs +72 -0
  287. package/hooks/cursor/pretooluse.mjs +26 -0
  288. package/hooks/cursor/sessionstart.mjs +97 -0
  289. package/hooks/cursor/stop.mjs +48 -0
  290. package/hooks/ensure-deps.mjs +254 -0
  291. package/hooks/formatters/claude-code.mjs +113 -0
  292. package/hooks/formatters/cursor.mjs +37 -0
  293. package/hooks/formatters/gemini-cli.mjs +55 -0
  294. package/hooks/formatters/vscode-copilot.mjs +55 -0
  295. package/hooks/gemini-cli/aftermodel.mjs +70 -0
  296. package/hooks/gemini-cli/aftertool.mjs +61 -0
  297. package/hooks/gemini-cli/beforeagent.mjs +99 -0
  298. package/hooks/gemini-cli/beforetool.mjs +27 -0
  299. package/hooks/gemini-cli/precompress.mjs +55 -0
  300. package/hooks/gemini-cli/sessionstart.mjs +137 -0
  301. package/hooks/heal-partial-install.mjs +712 -0
  302. package/hooks/hooks.json +143 -0
  303. package/hooks/jetbrains-copilot/posttooluse.mjs +61 -0
  304. package/hooks/jetbrains-copilot/precompact.mjs +55 -0
  305. package/hooks/jetbrains-copilot/pretooluse.mjs +27 -0
  306. package/hooks/jetbrains-copilot/sessionstart.mjs +124 -0
  307. package/hooks/kimi/platform.mjs +1 -0
  308. package/hooks/kimi/posttooluse.mjs +72 -0
  309. package/hooks/kimi/precompact.mjs +80 -0
  310. package/hooks/kimi/pretooluse.mjs +42 -0
  311. package/hooks/kimi/sessionend.mjs +61 -0
  312. package/hooks/kimi/sessionstart.mjs +113 -0
  313. package/hooks/kimi/stop.mjs +132 -0
  314. package/hooks/kimi/userpromptsubmit.mjs +90 -0
  315. package/hooks/kiro/agentspawn.mjs +97 -0
  316. package/hooks/kiro/posttooluse.mjs +51 -0
  317. package/hooks/kiro/pretooluse.mjs +64 -0
  318. package/hooks/kiro/userpromptsubmit.mjs +88 -0
  319. package/hooks/normalize-hooks.mjs +323 -0
  320. package/hooks/platform-bridge.mjs +340 -0
  321. package/hooks/posttooluse.mjs +225 -0
  322. package/hooks/precompact.mjs +100 -0
  323. package/hooks/pretooluse.mjs +227 -0
  324. package/hooks/qwen-code/platform.mjs +1 -0
  325. package/hooks/qwen-code/stop.mjs +168 -0
  326. package/hooks/routing-block.mjs +105 -0
  327. package/hooks/run-hook.mjs +95 -0
  328. package/hooks/security.bundle.mjs +2 -0
  329. package/hooks/session-attribution.bundle.mjs +1 -0
  330. package/hooks/session-db.bundle.mjs +145 -0
  331. package/hooks/session-directive.mjs +520 -0
  332. package/hooks/session-extract.bundle.mjs +3 -0
  333. package/hooks/session-helpers.mjs +428 -0
  334. package/hooks/session-loaders.mjs +448 -0
  335. package/hooks/session-snapshot.bundle.mjs +32 -0
  336. package/hooks/sessionstart.mjs +464 -0
  337. package/hooks/stop.mjs +82 -0
  338. package/hooks/suppress-stderr.mjs +25 -0
  339. package/hooks/userpromptsubmit.mjs +108 -0
  340. package/hooks/vscode-copilot/posttooluse.mjs +61 -0
  341. package/hooks/vscode-copilot/precompact.mjs +55 -0
  342. package/hooks/vscode-copilot/pretooluse.mjs +27 -0
  343. package/hooks/vscode-copilot/sessionstart.mjs +129 -0
  344. package/openclaw.plugin.json +38 -0
  345. package/package.json +132 -0
  346. package/scripts/heal-better-sqlite3.mjs +430 -0
  347. package/scripts/heal-installed-plugins.mjs +635 -0
  348. package/scripts/plugin-cache-integrity.mjs +248 -0
  349. package/scripts/postinstall.mjs +397 -0
  350. package/server.bundle.mjs +1421 -0
  351. package/server.js +5 -0
  352. package/skills/.ignore +7 -0
  353. package/skills/context-mode/SKILL.md +300 -0
  354. package/skills/context-mode/references/anti-patterns.md +283 -0
  355. package/skills/context-mode/references/patterns-javascript.md +298 -0
  356. package/skills/context-mode/references/patterns-python.md +304 -0
  357. package/skills/context-mode/references/patterns-shell.md +277 -0
  358. package/skills/ctx-doctor/SKILL.md +22 -0
  359. package/skills/ctx-index/SKILL.md +46 -0
  360. package/skills/ctx-insight/SKILL.md +24 -0
  361. package/skills/ctx-purge/SKILL.md +49 -0
  362. package/skills/ctx-search/SKILL.md +35 -0
  363. package/skills/ctx-stats/SKILL.md +26 -0
  364. package/skills/ctx-upgrade/SKILL.md +31 -0
  365. package/start.mjs +616 -0
@@ -0,0 +1,678 @@
1
+ import { spawn, execSync, execFileSync } from "node:child_process";
2
+ import { mkdtempSync, writeFileSync, rmSync, existsSync } from "node:fs";
3
+ import { join, resolve } from "node:path";
4
+ import { tmpdir } from "node:os";
5
+ import { detectRuntimes, buildCommand, } from "./runtime.js";
6
+ const isWin = process.platform === "win32";
7
+ /**
8
+ * Pure helper: extension map for temp script files per language.
9
+ * On Windows, shell scripts usually get NO extension to avoid Windows
10
+ * file-association for `.sh` (which spawns a visible Git Bash window over the
11
+ * user's IDE). Windows PowerShell/pwsh is the exception because `-File`
12
+ * requires `.ps1` there.
13
+ */
14
+ const SCRIPT_EXT = {
15
+ javascript: "js",
16
+ typescript: "ts",
17
+ python: "py",
18
+ shell: "sh",
19
+ ruby: "rb",
20
+ go: "go",
21
+ rust: "rs",
22
+ php: "php",
23
+ perl: "pl",
24
+ r: "R",
25
+ elixir: "exs",
26
+ csharp: "csx",
27
+ };
28
+ /** Pure helper — exported for unit testing. Returns "script" or "script.<ext>". */
29
+ export function buildScriptFilename(language, platform, shellPath) {
30
+ if (platform === "win32" && language === "shell") {
31
+ const shellName = shellPath?.toLowerCase() ?? "";
32
+ if (shellName.includes("powershell") || shellName.includes("pwsh"))
33
+ return "script.ps1";
34
+ const shellBase = shellName.split(/[\\/]/).pop() ?? shellName;
35
+ if (shellBase === "cmd" || shellBase === "cmd.exe")
36
+ return "script.cmd";
37
+ return "script";
38
+ }
39
+ return `script.${SCRIPT_EXT[language]}`;
40
+ }
41
+ /**
42
+ * Pure helper — exported for unit testing. Adds `windowsHide: true` on Windows
43
+ * to prevent the spawned shell from creating a visible console window that
44
+ * intercepts stdout (issue #384).
45
+ */
46
+ export function buildSpawnOptions(platform) {
47
+ return { windowsHide: platform === "win32" };
48
+ }
49
+ function quoteForPosixShell(value) {
50
+ return `'${value.replace(/'/g, `'\\''`)}'`;
51
+ }
52
+ /** Pure helper — exported for unit testing. Restores parent PATH after shell startup. */
53
+ export function buildShellScriptContent(code, inheritedPath, platform) {
54
+ if (platform === "win32" || !inheritedPath)
55
+ return code;
56
+ return `export PATH=${quoteForPosixShell(inheritedPath)}\n${code}`;
57
+ }
58
+ function isPowerShell(shellPath) {
59
+ const shellName = shellPath?.toLowerCase() ?? "";
60
+ return shellName.includes("powershell") || shellName.includes("pwsh");
61
+ }
62
+ export function buildPowerShellScriptContent(code) {
63
+ // Prefix a UTF-8 BOM so Windows PowerShell 5.1 reliably detects the script
64
+ // file as UTF-8 (without it, 5.1 falls back to the ANSI code page and
65
+ // mangles non-ASCII characters in the script body).
66
+ return [
67
+ "\uFEFF[Console]::InputEncoding = [System.Text.UTF8Encoding]::new()",
68
+ "[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new()",
69
+ "$OutputEncoding = [System.Text.UTF8Encoding]::new()",
70
+ code,
71
+ ].join("\n");
72
+ }
73
+ /**
74
+ * Resolve the real OS temp directory, bypassing any TMPDIR env override.
75
+ * os.tmpdir() reads TMPDIR from the environment, which some shells/tools
76
+ * set to the project root — causing temp files to pollute the working tree.
77
+ */
78
+ const OS_TMPDIR = (() => {
79
+ if (isWin)
80
+ return process.env.TEMP ?? process.env.TMP ?? tmpdir();
81
+ try {
82
+ const result = execFileSync(process.platform === "darwin" ? "getconf" : "mktemp", process.platform === "darwin" ? ["DARWIN_USER_TEMP_DIR"] : ["-u", "-d"], { env: { ...process.env, TMPDIR: undefined }, encoding: "utf-8" }).trim();
83
+ const dir = process.platform === "darwin" ? result : resolve(result, "..");
84
+ if (dir && dir !== process.cwd())
85
+ return dir;
86
+ }
87
+ catch { /* fall through */ }
88
+ return "/tmp";
89
+ })();
90
+ /**
91
+ * Pure helper — exported for unit testing. Issue #782.
92
+ *
93
+ * On Windows, the sandbox shell runtime is Git Bash. A bare `mvn` invocation
94
+ * runs Maven's POSIX shell script, which on the `mingw=true` branch (uname →
95
+ * MINGW64_NT-*) fails to convert `CLASSWORLDS_JAR` from a POSIX path
96
+ * (`/c/tools/maven/boot/plexus-classworlds-*.jar`) to a Windows path. Native
97
+ * `java.exe` then can't resolve the bootstrap jar → ClassNotFoundException for
98
+ * `org.codehaus.plexus.classworlds.launcher.Launcher`.
99
+ *
100
+ * The third-way fix (issue Option C): rewrite the bare `mvn` token to `mvn.cmd`,
101
+ * the native Windows launcher that uses Windows-native paths and bypasses the
102
+ * broken mingw shell branch entirely. This does NOT touch the global MSYS
103
+ * path-conversion env (MSYS_NO_PATHCONV / MSYS2_ARG_CONV_EXCL), which #826/#791
104
+ * deliberately leave unset so native git.exe launched from bash keeps its
105
+ * /tmp→C:\ argument conversion. Re-enabling global suppression would re-break
106
+ * native git; rewriting only the mvn token keeps both correct.
107
+ *
108
+ * Only a `mvn` that starts a command (start of string, or after a shell
109
+ * separator `&& | ; ( newline`) is rewritten. `mvnw`, `mvnd`, `mymvn`,
110
+ * paths like `./mvnw`, and an already-`mvn.cmd` token are left untouched
111
+ * (the token must be exactly `mvn` followed by whitespace or end-of-string).
112
+ */
113
+ export function rewriteWindowsBuildTools(code, platform) {
114
+ if (platform !== "win32")
115
+ return code;
116
+ // Rewrite a bare `mvn` command token to `mvn.cmd` (Maven's native Windows launcher).
117
+ // Algorithmic (no regex): only at a command-start position (string start or right
118
+ // after a shell separator ; & | ( newline, skipping leading spaces/tabs) and only
119
+ // when the token is exactly `mvn` followed by whitespace or end — leaves
120
+ // mvnw / mvnd / ./mvnw / already-mvn.cmd untouched.
121
+ const SEP = new Set([";", "&", "|", "(", "\n"]);
122
+ let out = "";
123
+ let atStart = true;
124
+ let i = 0;
125
+ while (i < code.length) {
126
+ const ch = code[i];
127
+ if (atStart && (ch === " " || ch === "\t")) {
128
+ out += ch;
129
+ i++;
130
+ continue;
131
+ }
132
+ if (atStart && code.startsWith("mvn", i)) {
133
+ const after = code[i + 3];
134
+ if (after === undefined || after === " " || after === "\t" || after === "\n") {
135
+ out += "mvn.cmd";
136
+ i += 3;
137
+ atStart = false;
138
+ continue;
139
+ }
140
+ }
141
+ out += ch;
142
+ atStart = SEP.has(ch);
143
+ i++;
144
+ }
145
+ return out;
146
+ }
147
+ /**
148
+ * Remove a sandbox temp dir, retrying on Windows. Issue #788.
149
+ *
150
+ * On Windows, a child process that opened SQLite databases inside the sandbox
151
+ * can leave `*-wal` / `*-shm` files with handles that linger briefly after the
152
+ * process exits. A single `rmSync` then throws EBUSY/EPERM/ENOTEMPTY and the
153
+ * old silent `catch {}` swallowed it, leaking `.ctx-mode-*` directories under
154
+ * `%TEMP%`. Node's `rmSync({ maxRetries, retryDelay })` is purpose-built for
155
+ * exactly this Windows-handle race, so let it back off and retry.
156
+ */
157
+ function cleanupTmpDir(tmpDir) {
158
+ try {
159
+ rmSync(tmpDir, {
160
+ recursive: true,
161
+ force: true,
162
+ maxRetries: isWin ? 8 : 2,
163
+ retryDelay: 100,
164
+ });
165
+ }
166
+ catch {
167
+ /* best-effort — OS will reclaim %TEMP% eventually */
168
+ }
169
+ }
170
+ /** Kill process tree — on Windows uses taskkill /T; on Unix kills the process group. */
171
+ function killTree(proc) {
172
+ if (isWin && proc.pid) {
173
+ try {
174
+ execSync(`taskkill /F /T /PID ${proc.pid}`, { stdio: "pipe" });
175
+ }
176
+ catch { /* already dead */ }
177
+ }
178
+ else if (proc.pid) {
179
+ try {
180
+ // Kill entire process group (negative PID) to prevent orphaned children
181
+ process.kill(-proc.pid, "SIGKILL");
182
+ }
183
+ catch { /* already dead */ }
184
+ }
185
+ }
186
+ export class PolyglotExecutor {
187
+ #hardCapBytes;
188
+ /**
189
+ * Resolves the project root on every access. Stored as a thunk so the
190
+ * executor stays in sync with server-side env-cascade resolvers (e.g.
191
+ * `getProjectDir` in server.ts) instead of capturing a snapshot of
192
+ * `CLAUDE_PROJECT_DIR` at construction time. String inputs are wrapped
193
+ * to preserve constructor backward compatibility.
194
+ */
195
+ #projectRootResolver;
196
+ #runtimes;
197
+ /** PIDs of backgrounded processes — killed on cleanup to prevent zombies. */
198
+ #backgroundedPids = new Set();
199
+ constructor(opts) {
200
+ this.#hardCapBytes = opts?.hardCapBytes ?? 100 * 1024 * 1024; // 100MB
201
+ const pr = opts?.projectRoot;
202
+ if (typeof pr === "function") {
203
+ this.#projectRootResolver = pr;
204
+ }
205
+ else if (typeof pr === "string") {
206
+ this.#projectRootResolver = () => pr;
207
+ }
208
+ else {
209
+ this.#projectRootResolver = () => process.cwd();
210
+ }
211
+ this.#runtimes = opts?.runtimes ?? detectRuntimes();
212
+ }
213
+ get #projectRoot() {
214
+ return this.#projectRootResolver();
215
+ }
216
+ get runtimes() {
217
+ return { ...this.#runtimes };
218
+ }
219
+ /** Kill all backgrounded processes to prevent zombie/port-conflict issues. */
220
+ cleanupBackgrounded() {
221
+ for (const pid of this.#backgroundedPids) {
222
+ try {
223
+ // Kill process group on Unix to catch all children
224
+ process.kill(isWin ? pid : -pid, "SIGTERM");
225
+ }
226
+ catch { /* already dead */ }
227
+ }
228
+ this.#backgroundedPids.clear();
229
+ }
230
+ async execute(opts) {
231
+ const { language, code, timeout, background = false, cwd: cwdOverride } = opts;
232
+ const tmpDir = mkdtempSync(join(OS_TMPDIR, ".ctx-mode-"));
233
+ try {
234
+ const filePath = this.#writeScript(tmpDir, code, language);
235
+ const cmd = buildCommand(this.#runtimes, language, filePath);
236
+ // Rust: compile then run
237
+ if (cmd[0] === "__rust_compile_run__") {
238
+ return await this.#compileAndRun(filePath, tmpDir, timeout);
239
+ }
240
+ // Every language runs in the project directory so git, relative paths,
241
+ // and other project-aware tools resolve naturally. The script FILE lives
242
+ // in the sandbox tmpDir and is passed to the runtime by absolute path
243
+ // (see buildCommand), so cwd is free to be the project root.
244
+ //
245
+ // Issue #788 — previously only `shell` used the project root; non-shell
246
+ // runtimes (python/js/ts/…) used tmpDir, so repo-relative checks like
247
+ // `pathlib.Path("package.json").exists()` silently failed depending on
248
+ // the chosen language. Unifying cwd removes that surprise.
249
+ // Issue #45 — `cwdOverride` lets per-call sites (Codex MCP handlers) pin
250
+ // cwd without mutating process-wide state.
251
+ const cwd = cwdOverride ?? this.#projectRoot;
252
+ const result = await this.#spawn(cmd, cwd, tmpDir, timeout, background);
253
+ // Skip tmpDir cleanup if process was backgrounded — it may still need files
254
+ if (!result.backgrounded) {
255
+ cleanupTmpDir(tmpDir);
256
+ }
257
+ return result;
258
+ }
259
+ catch (err) {
260
+ cleanupTmpDir(tmpDir);
261
+ throw err;
262
+ }
263
+ }
264
+ async executeFile(opts) {
265
+ const { path: filePath, language, code, timeout } = opts;
266
+ const absolutePath = resolve(this.#projectRoot, filePath);
267
+ const wrappedCode = this.#wrapWithFileContent(absolutePath, language, code);
268
+ return this.execute({ language, code: wrappedCode, timeout });
269
+ }
270
+ #writeScript(tmpDir, code, language) {
271
+ // Go needs a main package wrapper if not present
272
+ if (language === "go" && !code.includes("package ")) {
273
+ code = `package main\n\nimport "fmt"\n\nfunc main() {\n${code}\n}\n`;
274
+ }
275
+ // PHP needs opening tag if not present
276
+ if (language === "php" && !code.trimStart().startsWith("<?")) {
277
+ code = `<?php\n${code}`;
278
+ }
279
+ // Elixir: prepend compiled BEAM paths when inside a Mix project
280
+ if (language === "elixir" && existsSync(join(this.#projectRoot, "mix.exs"))) {
281
+ const escaped = JSON.stringify(join(this.#projectRoot, "_build/dev/lib"));
282
+ code = `Path.wildcard(Path.join(${escaped}, "*/ebin"))\n|> Enum.each(&Code.prepend_path/1)\n\n${code}`;
283
+ }
284
+ const fp = join(tmpDir, buildScriptFilename(language, process.platform, language === "shell" ? this.#runtimes.shell : null));
285
+ if (language === "shell") {
286
+ const shellPath = this.#runtimes.shell;
287
+ // #782 — on Windows Git Bash, rewrite bare `mvn` → `mvn.cmd` so Maven
288
+ // uses its native Windows launcher (correct path handling) instead of
289
+ // the broken mingw shell branch. No-op on non-Windows.
290
+ const rewritten = rewriteWindowsBuildTools(code, process.platform);
291
+ const shellCode = isWin && isPowerShell(shellPath)
292
+ ? buildPowerShellScriptContent(rewritten)
293
+ : rewritten;
294
+ writeFileSync(fp, buildShellScriptContent(shellCode, process.env.PATH, process.platform), { encoding: "utf-8", mode: 0o700 });
295
+ }
296
+ else {
297
+ writeFileSync(fp, code, "utf-8");
298
+ }
299
+ return fp;
300
+ }
301
+ async #compileAndRun(srcPath, cwd, timeout) {
302
+ const binSuffix = isWin ? ".exe" : "";
303
+ const binPath = srcPath.replace(/\.rs$/, "") + binSuffix;
304
+ // Compile — cap rustc invocation at 60s when caller didn't bound the
305
+ // overall timeout (a hung compile shouldn't run forever even if the
306
+ // caller is fine with a long-running binary afterwards).
307
+ try {
308
+ execFileSync("rustc", [srcPath, "-o", binPath], {
309
+ cwd,
310
+ timeout: timeout === undefined ? 60_000 : Math.min(timeout, 60_000),
311
+ encoding: "utf-8",
312
+ stdio: ["pipe", "pipe", "pipe"],
313
+ });
314
+ }
315
+ catch (err) {
316
+ const message = err instanceof Error ? err.stderr || err.message : String(err);
317
+ return {
318
+ stdout: "",
319
+ stderr: `Compilation failed:\n${message}`,
320
+ exitCode: 1,
321
+ timedOut: false,
322
+ };
323
+ }
324
+ // Run
325
+ return this.#spawn([binPath], cwd, cwd, timeout);
326
+ }
327
+ async #spawn(cmd, cwd, sandboxTmpDir, timeout, background = false) {
328
+ return new Promise((res) => {
329
+ // Only .cmd/.bat shims need shell on Windows; real executables don't.
330
+ // Using shell: true globally causes process-tree kill issues with MSYS2/Git Bash.
331
+ // "bun" is included as defense-in-depth: bunCommand() prefers absolute
332
+ // .exe paths now (#506), but if it falls back to the bare "bun" string
333
+ // on Windows that resolution typically goes through a `bun.cmd` shim
334
+ // (npm i -g bun) which CreateProcess can't execute without cmd.exe.
335
+ const needsShell = isWin && ["tsx", "ts-node", "elixir", "bun", "dotnet-script"].includes(cmd[0]);
336
+ // On Windows with Git Bash, pass the script as `bash -c "source /posix/path"`
337
+ // rather than `bash /path/to/script.sh`. This avoids MSYS2 path mangling
338
+ // while still allowing MSYS_NO_PATHCONV to protect non-ASCII paths in commands.
339
+ let spawnCmd = cmd[0];
340
+ let spawnArgs;
341
+ if (isWin && cmd.length === 2 && cmd[1]) {
342
+ const posixPath = cmd[1].replace(/\\/g, "/");
343
+ spawnArgs = [posixPath];
344
+ }
345
+ else {
346
+ spawnArgs = isWin
347
+ ? cmd.slice(1).map(a => a.replace(/\\/g, "/"))
348
+ : cmd.slice(1);
349
+ }
350
+ // Common options shared by both spawn variants below.
351
+ const commonOpts = {
352
+ cwd,
353
+ stdio: ["ignore", "pipe", "pipe"],
354
+ env: this.#buildSafeEnv(sandboxTmpDir),
355
+ // On Unix, create a new process group so killTree can kill all children
356
+ detached: !isWin,
357
+ // Hide the spawned-process console window on Windows. Without this,
358
+ // child_process.spawn creates a visible window that intercepts stdout,
359
+ // leaving the MCP response empty and popping a Git Bash terminal over
360
+ // the user's IDE. Issue #384.
361
+ ...buildSpawnOptions(process.platform),
362
+ };
363
+ // DEP0190 fix: when shell is true (Windows .cmd/.bat shims), pass a
364
+ // single command string instead of cmd + args array. Node.js warns
365
+ // that args are unsafely concatenated when shell:true is combined with
366
+ // the args-array form of spawn(). Colllapsing to a string avoids the
367
+ // warning while preserving the same shell behavior.
368
+ let proc;
369
+ if (needsShell) {
370
+ const fullCmd = [spawnCmd, ...spawnArgs]
371
+ .map(a => /\s/.test(a) ? JSON.stringify(a) : a)
372
+ .join(" ");
373
+ proc = spawn(fullCmd, [], { ...commonOpts, shell: true });
374
+ }
375
+ else {
376
+ proc = spawn(spawnCmd, spawnArgs, { ...commonOpts, shell: false });
377
+ }
378
+ let timedOut = false;
379
+ let resolved = false;
380
+ // Issue #406 — if the caller didn't pass a timeout we don't fire one.
381
+ // Timeout policy belongs to the MCP host/client (Claude Code, VSCode,
382
+ // JetBrains all enforce their own RPC timeouts); imposing a second
383
+ // policy here turned 30-minute Gradle/Maven/SBT builds into spurious
384
+ // false negatives whenever the caller forgot the explicit value.
385
+ const timer = timeout === undefined ? undefined : setTimeout(() => {
386
+ timedOut = true;
387
+ if (background) {
388
+ // Background mode: detach process, return partial output, keep running
389
+ resolved = true;
390
+ if (proc.pid)
391
+ this.#backgroundedPids.add(proc.pid);
392
+ proc.unref();
393
+ // Do NOT destroy stdout/stderr — closing the read end of the pipe
394
+ // sends SIGPIPE to the child on its next write, killing it.
395
+ // Instead, replace the data listeners with no-op drains that
396
+ // consume the stream without accumulating buffers. This keeps
397
+ // the pipe open and prevents the child from blocking on a full
398
+ // pipe buffer.
399
+ if (proc.stdout) {
400
+ proc.stdout.removeAllListeners("data");
401
+ proc.stdout.on("data", () => { });
402
+ }
403
+ if (proc.stderr) {
404
+ proc.stderr.removeAllListeners("data");
405
+ proc.stderr.on("data", () => { });
406
+ }
407
+ const rawStdout = Buffer.concat(stdoutChunks).toString("utf-8");
408
+ const rawStderr = Buffer.concat(stderrChunks).toString("utf-8");
409
+ res({
410
+ stdout: rawStdout,
411
+ stderr: rawStderr,
412
+ exitCode: 0,
413
+ timedOut: true,
414
+ backgrounded: true,
415
+ });
416
+ }
417
+ else {
418
+ killTree(proc);
419
+ }
420
+ }, timeout);
421
+ // Stream-level byte cap: kill the process once combined stdout+stderr
422
+ // exceeds hardCapBytes. Without this, a command like `yes` or
423
+ // `cat /dev/urandom | base64` can accumulate gigabytes in memory
424
+ // before the timeout fires.
425
+ const stdoutChunks = [];
426
+ const stderrChunks = [];
427
+ let totalBytes = 0;
428
+ let capExceeded = false;
429
+ proc.stdout.on("data", (chunk) => {
430
+ totalBytes += chunk.length;
431
+ if (totalBytes <= this.#hardCapBytes) {
432
+ stdoutChunks.push(chunk);
433
+ }
434
+ else if (!capExceeded) {
435
+ capExceeded = true;
436
+ killTree(proc);
437
+ }
438
+ });
439
+ proc.stderr.on("data", (chunk) => {
440
+ totalBytes += chunk.length;
441
+ if (totalBytes <= this.#hardCapBytes) {
442
+ stderrChunks.push(chunk);
443
+ }
444
+ else if (!capExceeded) {
445
+ capExceeded = true;
446
+ killTree(proc);
447
+ }
448
+ });
449
+ proc.on("close", (exitCode) => {
450
+ clearTimeout(timer);
451
+ if (resolved)
452
+ return; // Already resolved by background timeout
453
+ const rawStdout = Buffer.concat(stdoutChunks).toString("utf-8");
454
+ let rawStderr = Buffer.concat(stderrChunks).toString("utf-8");
455
+ if (capExceeded) {
456
+ rawStderr += `\n[output capped at ${(this.#hardCapBytes / 1024 / 1024).toFixed(0)}MB — process killed]`;
457
+ }
458
+ const stdout = rawStdout;
459
+ const stderr = rawStderr;
460
+ res({
461
+ stdout,
462
+ stderr,
463
+ exitCode: timedOut ? 1 : (exitCode ?? 1),
464
+ timedOut,
465
+ });
466
+ });
467
+ proc.on("error", (err) => {
468
+ clearTimeout(timer);
469
+ if (resolved)
470
+ return; // Already resolved by background timeout
471
+ res({
472
+ stdout: "",
473
+ stderr: err.message,
474
+ exitCode: 1,
475
+ timedOut: false,
476
+ });
477
+ });
478
+ });
479
+ }
480
+ #buildSafeEnv(tmpDir) {
481
+ const realHome = process.env.HOME ?? process.env.USERPROFILE ?? tmpDir;
482
+ // Denylist: env vars that corrupt sandbox stdout, inject code, or break
483
+ // language runtimes. Each entry is backed by CVE, MITRE, or live testing.
484
+ // See: https://www.elttam.com/blog/env/, MITRE T1574.006
485
+ const DENIED = new Set([
486
+ // Shell — auto-execute scripts, override builtins
487
+ "BASH_ENV", // sourced by non-interactive bash
488
+ "ENV", // sourced by sh/dash
489
+ "PROMPT_COMMAND", // runs before each prompt
490
+ "PS4", // $(cmd) expansion in xtrace
491
+ "SHELLOPTS", // enables xtrace/verbose, dumps to stdout
492
+ "BASHOPTS", // bash-specific shell options
493
+ "CDPATH", // makes cd print to stdout
494
+ "INPUTRC", // readline key rebinding
495
+ "BASH_XTRACEFD", // redirects debug output to stdout
496
+ // Node.js — require injection, inspector
497
+ "NODE_OPTIONS", // --require, --loader, --inspect
498
+ "NODE_PATH", // module search path injection
499
+ // Python — stdlib override, startup injection
500
+ "PYTHONSTARTUP", // auto-executes in interactive mode
501
+ "PYTHONHOME", // overrides stdlib location (breaks Python)
502
+ "PYTHONWARNINGS", // triggers module import chain → RCE
503
+ "PYTHONBREAKPOINT", // arbitrary callable
504
+ "PYTHONINSPECT", // enters interactive mode after script
505
+ // Ruby — option/module injection
506
+ "RUBYOPT", // injects CLI options (-r loads files)
507
+ "RUBYLIB", // module search path injection
508
+ // Perl — option/module injection
509
+ "PERL5OPT", // injects CLI options (-M runs code)
510
+ "PERL5LIB", // module search path injection
511
+ "PERLLIB", // legacy module search path
512
+ "PERL5DB", // debugger command injection
513
+ // Elixir/Erlang — eval injection
514
+ "ERL_AFLAGS", // prepends erl flags (-eval runs code)
515
+ "ERL_FLAGS", // appends erl flags
516
+ "ELIXIR_ERL_OPTIONS", // Elixir-specific erl flags
517
+ "ERL_LIBS", // beam file loading
518
+ // Go — compiler/linker injection
519
+ "GOFLAGS", // injects go command flags
520
+ "CGO_CFLAGS", // C compiler flag injection
521
+ "CGO_LDFLAGS", // linker flag injection
522
+ // Rust — compiler substitution
523
+ "RUSTC", // arbitrary compiler binary
524
+ "RUSTC_WRAPPER", // compiler wrapper injection
525
+ "RUSTC_WORKSPACE_WRAPPER",
526
+ "CARGO_BUILD_RUSTC",
527
+ "CARGO_BUILD_RUSTC_WRAPPER",
528
+ "RUSTFLAGS", // compiler flag injection
529
+ // PHP — config injection
530
+ "PHPRC", // auto_prepend_file → RCE
531
+ "PHP_INI_SCAN_DIR", // additional .ini loading
532
+ // R — startup script injection
533
+ "R_PROFILE", // site-wide R profile
534
+ "R_PROFILE_USER", // user R profile
535
+ "R_HOME", // R installation override
536
+ // .NET / C# — runtime/startup hooks, additional deps
537
+ "DOTNET_STARTUP_HOOKS", // injects managed assemblies on startup
538
+ "DOTNET_ADDITIONAL_DEPS", // additional .deps.json injection
539
+ "DOTNET_SHARED_STORE", // shared assembly probe path injection
540
+ "DOTNET_ROOT", // arbitrary .NET runtime override
541
+ "DOTNET_ROOT(x86)", // 32-bit override
542
+ "DOTNET_HOST_PATH", // host binary substitution
543
+ // .NET / C# — profiler attach (loads arbitrary DLL into dotnet host)
544
+ // and IPC-based debugger/IL injection. PR #546 follow-up.
545
+ // learn.microsoft.com/en-us/dotnet/core/runtime-config/debugging-profiling
546
+ "CORECLR_PROFILER", // CLSID of profiler to attach
547
+ "CORECLR_PROFILER_PATH", // path to profiler DLL
548
+ "CORECLR_PROFILER_PATH_32", // 32-bit specific profiler DLL
549
+ "CORECLR_PROFILER_PATH_64", // 64-bit specific profiler DLL
550
+ "CORECLR_PROFILER_PATH_ARM32", // ARM32 specific profiler DLL
551
+ "CORECLR_PROFILER_PATH_ARM64", // ARM64 specific profiler DLL
552
+ "CORECLR_ENABLE_PROFILING", // gates profiler load
553
+ "DOTNET_PROFILER_PATH", // cross-platform alias
554
+ "DOTNET_PROFILER_PATH_32",
555
+ "DOTNET_PROFILER_PATH_64",
556
+ "DOTNET_PROFILER_PATH_ARM32",
557
+ "DOTNET_PROFILER_PATH_ARM64",
558
+ "DOTNET_DiagnosticPorts", // peer attach via diagnostic IPC
559
+ "DOTNET_BUNDLE_EXTRACT_BASE_DIR", // single-file extraction hijack
560
+ // Dynamic linker — shared library injection
561
+ "LD_PRELOAD", // loads .so before all others (Linux)
562
+ "DYLD_INSERT_LIBRARIES", // macOS equivalent of LD_PRELOAD
563
+ // OpenSSL — engine loading
564
+ "OPENSSL_CONF", // loads engine modules → .so exec
565
+ "OPENSSL_ENGINES", // engine directory override
566
+ // Compiler — binary substitution
567
+ "CC", // C compiler override
568
+ "CXX", // C++ compiler override
569
+ "AR", // archiver override
570
+ // Git — command injection via hooks/config
571
+ "GIT_TEMPLATE_DIR", // hook injection on git init
572
+ "GIT_CONFIG_GLOBAL", // core.pager/editor runs commands
573
+ "GIT_CONFIG_SYSTEM", // system-level config injection
574
+ "GIT_EXEC_PATH", // substitute git subcommands
575
+ "GIT_SSH", // arbitrary command instead of ssh
576
+ "GIT_SSH_COMMAND", // arbitrary ssh command
577
+ "GIT_ASKPASS", // arbitrary credential command
578
+ ]);
579
+ // Start with parent env, then strip dangerous vars and apply overrides.
580
+ // The `COMPlus_` prefix sweep covers every COMPlus_* synonym of the
581
+ // DOTNET_* runtime knobs (.NET back-compat alias — case-insensitive).
582
+ // PR #546 follow-up: closes the alias bypass for the explicit denylist
583
+ // entries above.
584
+ const env = {};
585
+ for (const [key, val] of Object.entries(process.env)) {
586
+ if (val !== undefined &&
587
+ !DENIED.has(key) &&
588
+ !key.startsWith("BASH_FUNC_") &&
589
+ !/^COMPlus_/i.test(key)) {
590
+ env[key] = val;
591
+ }
592
+ }
593
+ // Sandbox overrides — forced values for correct sandbox behavior
594
+ env["TMPDIR"] = tmpDir;
595
+ env["HOME"] = realHome;
596
+ env["LANG"] = "en_US.UTF-8";
597
+ env["PYTHONDONTWRITEBYTECODE"] = "1";
598
+ env["PYTHONUNBUFFERED"] = "1";
599
+ env["PYTHONUTF8"] = "1";
600
+ env["NO_COLOR"] = "1";
601
+ // Windows uses "Path" (not "PATH") — normalize to "PATH" for consistency
602
+ if (isWin && !env["PATH"] && env["Path"]) {
603
+ env["PATH"] = env["Path"];
604
+ delete env["Path"];
605
+ }
606
+ if (!env["PATH"]) {
607
+ env["PATH"] = isWin ? "" : "/usr/local/bin:/usr/bin:/bin";
608
+ }
609
+ // Windows-critical PATH fixes.
610
+ if (isWin) {
611
+ // Do not carry global MSYS path-conversion blockers into Git Bash.
612
+ // Native Windows tools launched from bash (notably git.exe) need MSYS
613
+ // to convert /tmp-style arguments to Windows paths so sibling tools see
614
+ // the same filesystem location (#791).
615
+ for (const key of Object.keys(env)) {
616
+ const upper = key.toUpperCase();
617
+ if (upper === "MSYS_NO_PATHCONV" || upper === "MSYS2_ARG_CONV_EXCL") {
618
+ delete env[key];
619
+ }
620
+ }
621
+ const gitUsrBin = "C:\\Program Files\\Git\\usr\\bin";
622
+ const gitBin = "C:\\Program Files\\Git\\bin";
623
+ if (!env["PATH"].includes(gitUsrBin)) {
624
+ env["PATH"] = `${gitUsrBin};${gitBin};${env["PATH"]}`;
625
+ }
626
+ }
627
+ // Ensure SSL_CERT_FILE is set so Python/Ruby HTTPS works in sandbox.
628
+ if (!env["SSL_CERT_FILE"]) {
629
+ const certPaths = isWin ? [] : [
630
+ "/etc/ssl/cert.pem", // macOS, some Linux
631
+ "/etc/ssl/certs/ca-certificates.crt", // Debian/Ubuntu/Alpine
632
+ "/etc/pki/tls/certs/ca-bundle.crt", // RHEL/CentOS/Fedora
633
+ "/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem", // Fedora alt
634
+ ];
635
+ for (const p of certPaths) {
636
+ if (existsSync(p)) {
637
+ env["SSL_CERT_FILE"] = p;
638
+ break;
639
+ }
640
+ }
641
+ }
642
+ return env;
643
+ }
644
+ #wrapWithFileContent(absolutePath, language, code) {
645
+ const escaped = JSON.stringify(absolutePath);
646
+ switch (language) {
647
+ case "javascript":
648
+ case "typescript":
649
+ return `const FILE_CONTENT_PATH = ${escaped};\nconst file_path = FILE_CONTENT_PATH;\nconst FILE_CONTENT = require("fs").readFileSync(FILE_CONTENT_PATH, "utf-8");\n${code}`;
650
+ case "python":
651
+ return `FILE_CONTENT_PATH = ${escaped}\nfile_path = FILE_CONTENT_PATH\nwith open(FILE_CONTENT_PATH, "r", encoding="utf-8") as _f:\n FILE_CONTENT = _f.read()\n${code}`;
652
+ case "shell": {
653
+ // Single-quote the path to prevent $, backtick, and ! expansion
654
+ const sq = "'" + absolutePath.replace(/'/g, "'\\''") + "'";
655
+ return `FILE_CONTENT_PATH=${sq}\nfile_path=${sq}\nFILE_CONTENT=$(cat ${sq})\n${code}`;
656
+ }
657
+ case "ruby":
658
+ return `FILE_CONTENT_PATH = ${escaped}\nfile_path = FILE_CONTENT_PATH\nFILE_CONTENT = File.read(FILE_CONTENT_PATH, encoding: "utf-8")\n${code}`;
659
+ case "go":
660
+ return `package main\n\nimport (\n\t"fmt"\n\t"os"\n)\n\nvar FILE_CONTENT_PATH = ${escaped}\nvar file_path = FILE_CONTENT_PATH\n\nfunc main() {\n\tb, _ := os.ReadFile(FILE_CONTENT_PATH)\n\tFILE_CONTENT := string(b)\n\t_ = FILE_CONTENT\n\t_ = fmt.Sprint()\n${code}\n}\n`;
661
+ case "rust":
662
+ return `#![allow(unused_variables)]\nuse std::fs;\n\nfn main() {\n let file_content_path = ${escaped};\n let file_path = file_content_path;\n let file_content = fs::read_to_string(file_content_path).unwrap();\n${code}\n}\n`;
663
+ case "php":
664
+ return `<?php\n$FILE_CONTENT_PATH = ${escaped};\n$file_path = $FILE_CONTENT_PATH;\n$FILE_CONTENT = file_get_contents($FILE_CONTENT_PATH);\n${code}`;
665
+ case "perl":
666
+ return `my $FILE_CONTENT_PATH = ${escaped};\nmy $file_path = $FILE_CONTENT_PATH;\nopen(my $fh, '<:encoding(UTF-8)', $FILE_CONTENT_PATH) or die "Cannot open: $!";\nmy $FILE_CONTENT = do { local $/; <$fh> };\nclose($fh);\n${code}`;
667
+ case "r":
668
+ return `FILE_CONTENT_PATH <- ${escaped}\nfile_path <- FILE_CONTENT_PATH\nFILE_CONTENT <- readLines(FILE_CONTENT_PATH, warn=FALSE, encoding="UTF-8")\nFILE_CONTENT <- paste(FILE_CONTENT, collapse="\\n")\n${code}`;
669
+ case "elixir":
670
+ return `file_content_path = ${escaped}\nfile_path = file_content_path\nfile_content = File.read!(file_content_path)\n${code}`;
671
+ case "csharp":
672
+ // .csx forbids `using` directives after any other top-level statement
673
+ // (CS1529). User code inside executeFile must use fully-qualified type
674
+ // names (e.g. `System.Text.Json.JsonDocument`) instead of `using`.
675
+ return `var FILE_CONTENT_PATH = ${escaped};\nvar file_path = FILE_CONTENT_PATH;\nvar FILE_CONTENT = System.IO.File.ReadAllText(FILE_CONTENT_PATH);\n${code}`;
676
+ }
677
+ }
678
+ }