@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.
- package/.claude-plugin/marketplace.json +33 -0
- package/.claude-plugin/plugin.json +31 -0
- package/.codex-plugin/hooks.json +65 -0
- package/.codex-plugin/mcp.json +13 -0
- package/.codex-plugin/plugin.json +31 -0
- package/.openclaw-plugin/index.ts +12 -0
- package/.openclaw-plugin/openclaw.plugin.json +38 -0
- package/.openclaw-plugin/package.json +28 -0
- package/LICENSE +94 -0
- package/README.md +1619 -0
- package/bin/statusline.mjs +378 -0
- package/build/adapters/antigravity/index.d.ts +51 -0
- package/build/adapters/antigravity/index.js +169 -0
- package/build/adapters/antigravity-cli/index.d.ts +51 -0
- package/build/adapters/antigravity-cli/index.js +342 -0
- package/build/adapters/base.d.ts +92 -0
- package/build/adapters/base.js +135 -0
- package/build/adapters/claude-code/config.d.ts +8 -0
- package/build/adapters/claude-code/config.js +8 -0
- package/build/adapters/claude-code/hooks.d.ts +106 -0
- package/build/adapters/claude-code/hooks.js +183 -0
- package/build/adapters/claude-code/index.d.ts +80 -0
- package/build/adapters/claude-code/index.js +569 -0
- package/build/adapters/claude-code-base.d.ts +49 -0
- package/build/adapters/claude-code-base.js +113 -0
- package/build/adapters/client-map.d.ts +10 -0
- package/build/adapters/client-map.js +42 -0
- package/build/adapters/codex/config.d.ts +8 -0
- package/build/adapters/codex/config.js +8 -0
- package/build/adapters/codex/hooks.d.ts +55 -0
- package/build/adapters/codex/hooks.js +64 -0
- package/build/adapters/codex/index.d.ts +87 -0
- package/build/adapters/codex/index.js +995 -0
- package/build/adapters/codex/paths.d.ts +1 -0
- package/build/adapters/codex/paths.js +12 -0
- package/build/adapters/codex/usage.d.ts +107 -0
- package/build/adapters/codex/usage.js +227 -0
- package/build/adapters/copilot-base.d.ts +94 -0
- package/build/adapters/copilot-base.js +297 -0
- package/build/adapters/copilot-cli/hooks.d.ts +33 -0
- package/build/adapters/copilot-cli/hooks.js +64 -0
- package/build/adapters/copilot-cli/index.d.ts +48 -0
- package/build/adapters/copilot-cli/index.js +341 -0
- package/build/adapters/cursor/config.d.ts +4 -0
- package/build/adapters/cursor/config.js +4 -0
- package/build/adapters/cursor/hooks.d.ts +55 -0
- package/build/adapters/cursor/hooks.js +82 -0
- package/build/adapters/cursor/index.d.ts +64 -0
- package/build/adapters/cursor/index.js +517 -0
- package/build/adapters/detect.d.ts +136 -0
- package/build/adapters/detect.js +623 -0
- package/build/adapters/gemini-cli/config.d.ts +8 -0
- package/build/adapters/gemini-cli/config.js +8 -0
- package/build/adapters/gemini-cli/hooks.d.ts +83 -0
- package/build/adapters/gemini-cli/hooks.js +115 -0
- package/build/adapters/gemini-cli/index.d.ts +80 -0
- package/build/adapters/gemini-cli/index.js +483 -0
- package/build/adapters/jetbrains-copilot/config.d.ts +8 -0
- package/build/adapters/jetbrains-copilot/config.js +8 -0
- package/build/adapters/jetbrains-copilot/hooks.d.ts +59 -0
- package/build/adapters/jetbrains-copilot/hooks.js +87 -0
- package/build/adapters/jetbrains-copilot/index.d.ts +31 -0
- package/build/adapters/jetbrains-copilot/index.js +131 -0
- package/build/adapters/kimi/config.d.ts +8 -0
- package/build/adapters/kimi/config.js +8 -0
- package/build/adapters/kimi/hooks.d.ts +28 -0
- package/build/adapters/kimi/hooks.js +34 -0
- package/build/adapters/kimi/index.d.ts +66 -0
- package/build/adapters/kimi/index.js +537 -0
- package/build/adapters/kimi/paths.d.ts +21 -0
- package/build/adapters/kimi/paths.js +52 -0
- package/build/adapters/kimi/usage.d.ts +82 -0
- package/build/adapters/kimi/usage.js +217 -0
- package/build/adapters/kiro/hooks.d.ts +61 -0
- package/build/adapters/kiro/hooks.js +94 -0
- package/build/adapters/kiro/index.d.ts +61 -0
- package/build/adapters/kiro/index.js +301 -0
- package/build/adapters/omp/index.d.ts +65 -0
- package/build/adapters/omp/index.js +182 -0
- package/build/adapters/omp/plugin.d.ts +81 -0
- package/build/adapters/omp/plugin.js +331 -0
- package/build/adapters/omp/usage.d.ts +49 -0
- package/build/adapters/omp/usage.js +110 -0
- package/build/adapters/openclaw/config.d.ts +8 -0
- package/build/adapters/openclaw/config.js +8 -0
- package/build/adapters/openclaw/hooks.d.ts +50 -0
- package/build/adapters/openclaw/hooks.js +61 -0
- package/build/adapters/openclaw/index.d.ts +74 -0
- package/build/adapters/openclaw/index.js +439 -0
- package/build/adapters/openclaw/mcp-tools.d.ts +54 -0
- package/build/adapters/openclaw/mcp-tools.js +207 -0
- package/build/adapters/openclaw/plugin.d.ts +141 -0
- package/build/adapters/openclaw/plugin.js +818 -0
- package/build/adapters/openclaw/session-db.d.ts +55 -0
- package/build/adapters/openclaw/session-db.js +88 -0
- package/build/adapters/openclaw/usage.d.ts +34 -0
- package/build/adapters/openclaw/usage.js +52 -0
- package/build/adapters/openclaw/workspace-router.d.ts +29 -0
- package/build/adapters/openclaw/workspace-router.js +64 -0
- package/build/adapters/opencode/config.d.ts +8 -0
- package/build/adapters/opencode/config.js +8 -0
- package/build/adapters/opencode/hooks.d.ts +38 -0
- package/build/adapters/opencode/hooks.js +50 -0
- package/build/adapters/opencode/index.d.ts +144 -0
- package/build/adapters/opencode/index.js +638 -0
- package/build/adapters/opencode/plugin.d.ts +249 -0
- package/build/adapters/opencode/plugin.js +1536 -0
- package/build/adapters/opencode/v2.d.ts +117 -0
- package/build/adapters/opencode/v2.js +224 -0
- package/build/adapters/opencode/zod3tov4.d.ts +1 -0
- package/build/adapters/opencode/zod3tov4.js +132 -0
- package/build/adapters/pi/extension.d.ts +81 -0
- package/build/adapters/pi/extension.js +861 -0
- package/build/adapters/pi/index.d.ts +57 -0
- package/build/adapters/pi/index.js +173 -0
- package/build/adapters/pi/mcp-bridge.d.ts +268 -0
- package/build/adapters/pi/mcp-bridge.js +894 -0
- package/build/adapters/qwen-code/hooks.d.ts +26 -0
- package/build/adapters/qwen-code/hooks.js +29 -0
- package/build/adapters/qwen-code/index.d.ts +40 -0
- package/build/adapters/qwen-code/index.js +361 -0
- package/build/adapters/qwen-code/usage.d.ts +90 -0
- package/build/adapters/qwen-code/usage.js +222 -0
- package/build/adapters/types.d.ts +360 -0
- package/build/adapters/types.js +118 -0
- package/build/adapters/vscode-copilot/config.d.ts +8 -0
- package/build/adapters/vscode-copilot/config.js +8 -0
- package/build/adapters/vscode-copilot/hooks.d.ts +71 -0
- package/build/adapters/vscode-copilot/hooks.js +98 -0
- package/build/adapters/vscode-copilot/index.d.ts +32 -0
- package/build/adapters/vscode-copilot/index.js +222 -0
- package/build/adapters/zed/index.d.ts +40 -0
- package/build/adapters/zed/index.js +163 -0
- package/build/cli.d.ts +34 -0
- package/build/cli.js +1828 -0
- package/build/db-base.d.ts +250 -0
- package/build/db-base.js +756 -0
- package/build/executor.d.ts +72 -0
- package/build/executor.js +678 -0
- package/build/exit-classify.d.ts +19 -0
- package/build/exit-classify.js +12 -0
- package/build/fetch/blocks.d.ts +145 -0
- package/build/fetch/blocks.js +244 -0
- package/build/fetch/extract.d.ts +68 -0
- package/build/fetch/extract.js +123 -0
- package/build/fetch/page-store.d.ts +66 -0
- package/build/fetch/page-store.js +174 -0
- package/build/fetch-cache.d.ts +13 -0
- package/build/fetch-cache.js +15 -0
- package/build/lifecycle.d.ts +124 -0
- package/build/lifecycle.js +278 -0
- package/build/package-identity.d.ts +26 -0
- package/build/package-identity.js +28 -0
- package/build/runPool.d.ts +36 -0
- package/build/runPool.js +51 -0
- package/build/runtime.d.ts +71 -0
- package/build/runtime.js +659 -0
- package/build/search/auto-memory.d.ts +47 -0
- package/build/search/auto-memory.js +170 -0
- package/build/search/ctx-search-schema.d.ts +90 -0
- package/build/search/ctx-search-schema.js +135 -0
- package/build/search/flood-guard.d.ts +57 -0
- package/build/search/flood-guard.js +80 -0
- package/build/search/unified.d.ts +56 -0
- package/build/search/unified.js +104 -0
- package/build/security.d.ts +206 -0
- package/build/security.js +780 -0
- package/build/server.d.ts +240 -0
- package/build/server.js +5034 -0
- package/build/session/analytics.d.ts +773 -0
- package/build/session/analytics.js +2420 -0
- package/build/session/db.d.ts +468 -0
- package/build/session/db.js +1330 -0
- package/build/session/error-classifier.d.ts +87 -0
- package/build/session/error-classifier.js +303 -0
- package/build/session/event-emit.d.ts +48 -0
- package/build/session/event-emit.js +101 -0
- package/build/session/extract.d.ts +333 -0
- package/build/session/extract.js +2688 -0
- package/build/session/model-prices.json +429 -0
- package/build/session/persist-tool-calls.d.ts +54 -0
- package/build/session/persist-tool-calls.js +105 -0
- package/build/session/pricing.d.ts +64 -0
- package/build/session/pricing.js +151 -0
- package/build/session/project-attribution.d.ts +73 -0
- package/build/session/project-attribution.js +245 -0
- package/build/session/purge.d.ts +138 -0
- package/build/session/purge.js +240 -0
- package/build/session/retrieval-marker.d.ts +39 -0
- package/build/session/retrieval-marker.js +65 -0
- package/build/session/snapshot.d.ts +46 -0
- package/build/session/snapshot.js +532 -0
- package/build/store-directory.d.ts +56 -0
- package/build/store-directory.js +254 -0
- package/build/store.d.ts +147 -0
- package/build/store.js +1714 -0
- package/build/truncate.d.ts +57 -0
- package/build/truncate.js +142 -0
- package/build/types.d.ts +117 -0
- package/build/types.js +20 -0
- package/build/util/claude-config.d.ts +32 -0
- package/build/util/claude-config.js +84 -0
- package/build/util/hook-config.d.ts +27 -0
- package/build/util/hook-config.js +76 -0
- package/build/util/jsonc.d.ts +14 -0
- package/build/util/jsonc.js +104 -0
- package/build/util/plugin-cache-integrity.d.ts +51 -0
- package/build/util/plugin-cache-integrity.js +146 -0
- package/build/util/project-dir.d.ts +149 -0
- package/build/util/project-dir.js +340 -0
- package/build/util/sibling-mcp.d.ts +79 -0
- package/build/util/sibling-mcp.js +181 -0
- package/cli.bundle.mjs +1495 -0
- package/configs/antigravity/GEMINI.md +68 -0
- package/configs/antigravity/mcp_config.json +7 -0
- package/configs/antigravity-cli/hooks/hooks.json +37 -0
- package/configs/antigravity-cli/hooks.json +37 -0
- package/configs/antigravity-cli/mcp_config.json +10 -0
- package/configs/antigravity-cli/plugin.json +14 -0
- package/configs/antigravity-cli/rules/context-mode.md +77 -0
- package/configs/antigravity-cli/skills/context-mode/SKILL.md +77 -0
- package/configs/claude-code/CLAUDE.md +91 -0
- package/configs/codex/AGENTS.md +99 -0
- package/configs/codex/config.toml +8 -0
- package/configs/codex/hooks.json +47 -0
- package/configs/copilot-cli/.github/plugin/plugin.json +23 -0
- package/configs/copilot-cli/.mcp.json +12 -0
- package/configs/copilot-cli/README.md +47 -0
- package/configs/copilot-cli/hooks.json +41 -0
- package/configs/copilot-cli/skills/context-mode/SKILL.md +38 -0
- package/configs/cursor/context-mode.mdc +76 -0
- package/configs/cursor/hooks.json +21 -0
- package/configs/cursor/mcp.json +7 -0
- package/configs/gemini-cli/GEMINI.md +88 -0
- package/configs/gemini-cli/mcp.json +7 -0
- package/configs/gemini-cli/settings.json +60 -0
- package/configs/jetbrains-copilot/copilot-instructions.md +80 -0
- package/configs/jetbrains-copilot/hooks.json +16 -0
- package/configs/jetbrains-copilot/mcp.json +7 -0
- package/configs/kilo/AGENTS.md +84 -0
- package/configs/kilo/kilo.json +6 -0
- package/configs/kimi/hooks.json +54 -0
- package/configs/kiro/KIRO.md +84 -0
- package/configs/kiro/agent.json +18 -0
- package/configs/kiro/mcp.json +7 -0
- package/configs/omp/SYSTEM.md +85 -0
- package/configs/omp/mcp.json +7 -0
- package/configs/openclaw/AGENTS.md +87 -0
- package/configs/openclaw/openclaw.json +13 -0
- package/configs/opencode/AGENTS.md +84 -0
- package/configs/opencode/opencode.json +6 -0
- package/configs/pi/AGENTS.md +3 -0
- package/configs/qwen-code/QWEN.md +91 -0
- package/configs/vscode-copilot/copilot-instructions.md +80 -0
- package/configs/vscode-copilot/hooks.json +16 -0
- package/configs/vscode-copilot/mcp.json +7 -0
- package/configs/zed/AGENTS.md +68 -0
- package/hooks/antigravity-cli/payload.mjs +98 -0
- package/hooks/antigravity-cli/posttooluse.mjs +138 -0
- package/hooks/antigravity-cli/pretooluse.mjs +78 -0
- package/hooks/antigravity-cli/stop.mjs +58 -0
- package/hooks/auto-injection.mjs +102 -0
- package/hooks/cache-heal-utils.mjs +379 -0
- package/hooks/codex/platform.mjs +1 -0
- package/hooks/codex/posttooluse.mjs +66 -0
- package/hooks/codex/precompact.mjs +70 -0
- package/hooks/codex/pretooluse.mjs +42 -0
- package/hooks/codex/sessionstart.mjs +121 -0
- package/hooks/codex/stop.mjs +138 -0
- package/hooks/codex/userpromptsubmit.mjs +75 -0
- package/hooks/copilot-cli/posttooluse.mjs +79 -0
- package/hooks/copilot-cli/precompact.mjs +66 -0
- package/hooks/copilot-cli/pretooluse.mjs +41 -0
- package/hooks/copilot-cli/sessionstart.mjs +121 -0
- package/hooks/copilot-cli/stop.mjs +59 -0
- package/hooks/copilot-cli/userpromptsubmit.mjs +77 -0
- package/hooks/core/codex-caps.mjs +112 -0
- package/hooks/core/formatters.mjs +356 -0
- package/hooks/core/mcp-ready.mjs +109 -0
- package/hooks/core/platform-detect.mjs +49 -0
- package/hooks/core/routing.mjs +1050 -0
- package/hooks/core/stdin.mjs +90 -0
- package/hooks/core/tool-naming.mjs +57 -0
- package/hooks/cursor/afteragentresponse.mjs +74 -0
- package/hooks/cursor/hooks.json +31 -0
- package/hooks/cursor/posttooluse.mjs +72 -0
- package/hooks/cursor/pretooluse.mjs +26 -0
- package/hooks/cursor/sessionstart.mjs +97 -0
- package/hooks/cursor/stop.mjs +48 -0
- package/hooks/ensure-deps.mjs +254 -0
- package/hooks/formatters/claude-code.mjs +113 -0
- package/hooks/formatters/cursor.mjs +37 -0
- package/hooks/formatters/gemini-cli.mjs +55 -0
- package/hooks/formatters/vscode-copilot.mjs +55 -0
- package/hooks/gemini-cli/aftermodel.mjs +70 -0
- package/hooks/gemini-cli/aftertool.mjs +61 -0
- package/hooks/gemini-cli/beforeagent.mjs +99 -0
- package/hooks/gemini-cli/beforetool.mjs +27 -0
- package/hooks/gemini-cli/precompress.mjs +55 -0
- package/hooks/gemini-cli/sessionstart.mjs +137 -0
- package/hooks/heal-partial-install.mjs +712 -0
- package/hooks/hooks.json +143 -0
- package/hooks/jetbrains-copilot/posttooluse.mjs +61 -0
- package/hooks/jetbrains-copilot/precompact.mjs +55 -0
- package/hooks/jetbrains-copilot/pretooluse.mjs +27 -0
- package/hooks/jetbrains-copilot/sessionstart.mjs +124 -0
- package/hooks/kimi/platform.mjs +1 -0
- package/hooks/kimi/posttooluse.mjs +72 -0
- package/hooks/kimi/precompact.mjs +80 -0
- package/hooks/kimi/pretooluse.mjs +42 -0
- package/hooks/kimi/sessionend.mjs +61 -0
- package/hooks/kimi/sessionstart.mjs +113 -0
- package/hooks/kimi/stop.mjs +132 -0
- package/hooks/kimi/userpromptsubmit.mjs +90 -0
- package/hooks/kiro/agentspawn.mjs +97 -0
- package/hooks/kiro/posttooluse.mjs +51 -0
- package/hooks/kiro/pretooluse.mjs +64 -0
- package/hooks/kiro/userpromptsubmit.mjs +88 -0
- package/hooks/normalize-hooks.mjs +323 -0
- package/hooks/platform-bridge.mjs +340 -0
- package/hooks/posttooluse.mjs +225 -0
- package/hooks/precompact.mjs +100 -0
- package/hooks/pretooluse.mjs +227 -0
- package/hooks/qwen-code/platform.mjs +1 -0
- package/hooks/qwen-code/stop.mjs +168 -0
- package/hooks/routing-block.mjs +105 -0
- package/hooks/run-hook.mjs +95 -0
- package/hooks/security.bundle.mjs +2 -0
- package/hooks/session-attribution.bundle.mjs +1 -0
- package/hooks/session-db.bundle.mjs +145 -0
- package/hooks/session-directive.mjs +520 -0
- package/hooks/session-extract.bundle.mjs +3 -0
- package/hooks/session-helpers.mjs +428 -0
- package/hooks/session-loaders.mjs +448 -0
- package/hooks/session-snapshot.bundle.mjs +32 -0
- package/hooks/sessionstart.mjs +464 -0
- package/hooks/stop.mjs +82 -0
- package/hooks/suppress-stderr.mjs +25 -0
- package/hooks/userpromptsubmit.mjs +108 -0
- package/hooks/vscode-copilot/posttooluse.mjs +61 -0
- package/hooks/vscode-copilot/precompact.mjs +55 -0
- package/hooks/vscode-copilot/pretooluse.mjs +27 -0
- package/hooks/vscode-copilot/sessionstart.mjs +129 -0
- package/openclaw.plugin.json +38 -0
- package/package.json +132 -0
- package/scripts/heal-better-sqlite3.mjs +430 -0
- package/scripts/heal-installed-plugins.mjs +635 -0
- package/scripts/plugin-cache-integrity.mjs +248 -0
- package/scripts/postinstall.mjs +397 -0
- package/server.bundle.mjs +1421 -0
- package/server.js +5 -0
- package/skills/.ignore +7 -0
- package/skills/context-mode/SKILL.md +300 -0
- package/skills/context-mode/references/anti-patterns.md +283 -0
- package/skills/context-mode/references/patterns-javascript.md +298 -0
- package/skills/context-mode/references/patterns-python.md +304 -0
- package/skills/context-mode/references/patterns-shell.md +277 -0
- package/skills/ctx-doctor/SKILL.md +22 -0
- package/skills/ctx-index/SKILL.md +46 -0
- package/skills/ctx-insight/SKILL.md +24 -0
- package/skills/ctx-purge/SKILL.md +49 -0
- package/skills/ctx-search/SKILL.md +35 -0
- package/skills/ctx-stats/SKILL.md +26 -0
- package/skills/ctx-upgrade/SKILL.md +31 -0
- 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
|
+
}
|