sandbox.js
"use strict";
/**
* Sandbox helpers: path validation and subprocess execution.
*
* safe() resolves a path relative to the workspace and throws if it escapes.
* run() executes a command asynchronously with a hard timeout + size cap.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.SandboxError = void 0;
exports.safe = safe;
exports.run = run;
exports.runShell = runShell;
exports.detectCxx = detectCxx;
exports.getWorkspace = getWorkspace;
exports.resolvePython = resolvePython;
const child_process_1 = require("child_process");
const path_1 = require("path");
const os_1 = require("os");
class SandboxError extends Error {
constructor(message) {
super(message);
this.name = "SandboxError";
}
}
exports.SandboxError = SandboxError;
/** Resolve path and verify it stays inside the workspace. */
function safe(workspace, requestedPath) {
const base = (0, path_1.resolve)(workspace);
const full = (0, path_1.isAbsolute)(requestedPath)
? (0, path_1.resolve)(requestedPath)
: (0, path_1.resolve)(base, requestedPath);
// Case-insensitive on macOS/Windows
if (!full.toLowerCase().startsWith(base.toLowerCase())) {
throw new SandboxError(`Path "${requestedPath}" escapes the workspace (workspace: ${base})`);
}
return full;
}
const MAX_OUTPUT = 200_000;
function trim(s) {
if (s.length > MAX_OUTPUT) {
const half = MAX_OUTPUT >> 1;
return (s.slice(0, half) +
`\n\n... [truncated ${s.length - MAX_OUTPUT} chars] ...\n\n` +
s.slice(-half));
}
return s;
}
/** Run a program (NOT via shell) with a timeout. */
function run(cmd, args, opts = {}) {
return new Promise((resolve) => {
const timeout = opts.timeout ?? 60;
const proc = (0, child_process_1.spawn)(cmd, args, {
cwd: opts.cwd,
env: opts.env ?? process.env,
stdio: ["pipe", "pipe", "pipe"],
});
let stdoutBuf = "";
let stderrBuf = "";
proc.stdout.on("data", (d) => { stdoutBuf += d.toString(); });
proc.stderr.on("data", (d) => { stderrBuf += d.toString(); });
if (opts.stdin) {
proc.stdin.write(opts.stdin);
proc.stdin.end();
}
else {
proc.stdin.end();
}
const timer = setTimeout(() => {
proc.kill("SIGKILL");
resolve({
code: -1,
stdout: trim(stdoutBuf),
stderr: `Process killed after ${timeout}s timeout`,
success: false,
});
}, timeout * 1000);
proc.on("close", (code) => {
clearTimeout(timer);
resolve({
code: code ?? -1,
stdout: trim(stdoutBuf),
stderr: trim(stderrBuf),
success: (code ?? -1) === 0,
});
});
proc.on("error", (err) => {
clearTimeout(timer);
resolve({
code: -1,
stdout: "",
stderr: `Failed to start process: ${err.message}`,
success: false,
});
});
});
}
/** Run a command via bash -c (shell string). Returns RunResult. */
function runShell(command, opts = {}) {
const shell = process.platform === "win32" ? "cmd" : "/bin/bash";
const flag = process.platform === "win32" ? "/c" : "-c";
return run(shell, [flag, command], opts);
}
/** Detect the best C++ compiler available. */
async function detectCxx(preferClang) {
const candidates = preferClang
? ["clang++", "g++", "c++"]
: ["g++", "clang++", "c++"];
for (const cxx of candidates) {
const r = await run("which", [cxx], { timeout: 5 });
if (r.success)
return r.stdout.trim();
}
return null;
}
/** Return the workspace path — use configured path or cwd. */
function getWorkspace(configured) {
return configured.trim() || process.cwd();
}
/**
* Resolve the Python binary to use.
*
* Priority:
* 1. Blank → "python3" (system default)
* 2. Contains "/" → treat as a direct binary path
* 3. Otherwise → treat as a conda env name and search common conda prefixes
*
* Returns the resolved binary string (may be a full path or "python3").
*/
async function resolvePython(configured) {
const val = configured.trim();
if (!val)
return "python3";
if (val.includes("/"))
return val; // explicit path
// Treat as conda env name — search known conda prefix directories
const condaPrefixes = [
`${(0, os_1.homedir)()}/conda_envs`,
`/Volumes/personal/conda_envs`,
`${(0, os_1.homedir)()}/miniconda3/envs`,
`${(0, os_1.homedir)()}/anaconda3/envs`,
`${(0, os_1.homedir)()}/opt/miniconda3/envs`,
`/opt/homebrew/Caskroom/miniconda/base/envs`,
`/opt/miniconda3/envs`,
`/opt/anaconda3/envs`,
];
for (const prefix of condaPrefixes) {
const candidate = (0, path_1.resolve)(prefix, val, "bin", "python");
const r = await run(candidate, ["--version"], { timeout: 5 });
if (r.success || r.stderr.startsWith("Python"))
return candidate;
}
// Last resort: ask conda itself
const condaInfo = await run("conda", ["run", "-n", val, "which", "python"], { timeout: 10 });
if (condaInfo.success)
return condaInfo.stdout.trim();
// Fall back to system python3 and warn
console.warn(`[high-perf-tools] Could not resolve conda env "${val}", falling back to python3`);
return "python3";
}