openclaw/secure/sandbox.ts
Claude a44d683dd7
feat: wire up sandbox/scheduler commands + complete AssureBot rebrand
- Add /sandbox, /schedule, /tasks, /deltask commands to telegram bot
- Wire sandbox and scheduler dependencies through to telegram handler
- Complete AssureBot rebrand across all files (audit, config, webhooks, etc.)
- Update secure/README.md with correct branding
- Update X-AssureBot-Token header for webhooks
- Update ASSUREBOT_GATEWAY_TOKEN env var

https://claude.ai/code/session_015VqJ7gN4vaxtYfYc92UjLs
2026-01-30 07:04:49 +00:00

268 lines
6.3 KiB
TypeScript

/**
* AssureBot - Sandbox Execution
*
* Isolated Docker container for code/script execution.
* Security-first: no network, read-only root, resource limits.
*/
import { spawn } from "node:child_process";
import type { SecureConfig } from "./config.js";
import type { AuditLogger } from "./audit.js";
export type SandboxResult = {
exitCode: number;
stdout: string;
stderr: string;
timedOut: boolean;
durationMs: number;
};
export type SandboxRunner = {
run: (command: string, stdin?: string) => Promise<SandboxResult>;
isAvailable: () => Promise<boolean>;
};
/**
* Check if Docker is available
*/
async function checkDocker(): Promise<boolean> {
return new Promise((resolve) => {
const proc = spawn("docker", ["version"], {
stdio: ["ignore", "ignore", "ignore"],
});
proc.on("error", () => resolve(false));
proc.on("close", (code) => resolve(code === 0));
});
}
/**
* Build Docker run arguments for secure execution
*/
function buildDockerArgs(config: SecureConfig["sandbox"], command: string): string[] {
const args: string[] = [
"run",
"--rm", // Remove container after exit
"-i", // Interactive (for stdin)
// Security: No network by default
`--network=${config.network}`,
// Security: Read-only root filesystem
"--read-only",
// Security: tmpfs for writable areas
"--tmpfs=/tmp:rw,noexec,nosuid,size=64m",
"--tmpfs=/var/tmp:rw,noexec,nosuid,size=64m",
// Security: Drop all capabilities
"--cap-drop=ALL",
// Security: No new privileges
"--security-opt=no-new-privileges",
// Resource limits
`--memory=${config.memory}`,
`--cpus=${config.cpus}`,
"--pids-limit=100",
// Timeout handled externally, but set a ulimit too
"--ulimit=cpu=60:60",
// Working directory
"--workdir=/workspace",
// Image
config.image,
// Command (via shell for flexibility)
"sh",
"-c",
command,
];
return args;
}
export function createSandboxRunner(config: SecureConfig, audit: AuditLogger): SandboxRunner {
const sandboxConfig = config.sandbox;
return {
async isAvailable(): Promise<boolean> {
if (!sandboxConfig.enabled) return false;
return checkDocker();
},
async run(command: string, stdin?: string): Promise<SandboxResult> {
const startTime = Date.now();
if (!sandboxConfig.enabled) {
return {
exitCode: 1,
stdout: "",
stderr: "Sandbox is disabled",
timedOut: false,
durationMs: 0,
};
}
return new Promise((resolve) => {
const args = buildDockerArgs(sandboxConfig, command);
const proc = spawn("docker", args, {
stdio: ["pipe", "pipe", "pipe"],
});
let stdout = "";
let stderr = "";
let timedOut = false;
let resolved = false;
const finish = (exitCode: number) => {
if (resolved) return;
resolved = true;
const durationMs = Date.now() - startTime;
audit.sandbox({
command,
exitCode,
durationMs,
});
resolve({
exitCode,
stdout: stdout.slice(0, 10000), // Limit output size
stderr: stderr.slice(0, 10000),
timedOut,
durationMs,
});
};
// Timeout
const timeout = setTimeout(() => {
timedOut = true;
proc.kill("SIGKILL");
}, sandboxConfig.timeoutMs);
proc.stdout?.on("data", (data: Buffer) => {
stdout += data.toString();
// Prevent memory exhaustion
if (stdout.length > 100000) {
proc.kill("SIGKILL");
}
});
proc.stderr?.on("data", (data: Buffer) => {
stderr += data.toString();
if (stderr.length > 100000) {
proc.kill("SIGKILL");
}
});
proc.on("error", (err) => {
clearTimeout(timeout);
stderr += `\nProcess error: ${err.message}`;
finish(1);
});
proc.on("close", (code) => {
clearTimeout(timeout);
finish(code ?? 1);
});
// Write stdin if provided
if (stdin && proc.stdin) {
proc.stdin.write(stdin);
proc.stdin.end();
} else {
proc.stdin?.end();
}
});
},
};
}
/**
* Parse sandbox command from user message
* Returns null if message doesn't request code execution
*/
export function parseSandboxRequest(text: string): {
language: string;
code: string;
} | null {
// Match code blocks with language
const codeBlockMatch = text.match(/```(\w+)?\n([\s\S]*?)```/);
if (codeBlockMatch) {
const language = codeBlockMatch[1] || "sh";
const code = codeBlockMatch[2].trim();
return { language, code };
}
// Match /run command
const runMatch = text.match(/^\/run\s+(.+)$/s);
if (runMatch) {
return { language: "sh", code: runMatch[1].trim() };
}
// Match /python command
const pythonMatch = text.match(/^\/python\s+(.+)$/s);
if (pythonMatch) {
return { language: "python", code: pythonMatch[1].trim() };
}
return null;
}
/**
* Build execution command for language
*/
export function buildCommand(language: string, code: string): string {
switch (language.toLowerCase()) {
case "python":
case "py":
// Write code to temp file and execute
return `python3 -c ${JSON.stringify(code)}`;
case "javascript":
case "js":
case "node":
return `node -e ${JSON.stringify(code)}`;
case "bash":
case "sh":
case "shell":
return code;
default:
// Default to shell
return code;
}
}
/**
* Format sandbox result for display
*/
export function formatSandboxResult(result: SandboxResult): string {
let output = "";
if (result.timedOut) {
output += "**Timed out**\n\n";
}
if (result.stdout) {
output += "**Output:**\n```\n" + result.stdout.trim() + "\n```\n";
}
if (result.stderr) {
output += "**Errors:**\n```\n" + result.stderr.trim() + "\n```\n";
}
if (!result.stdout && !result.stderr) {
output += result.exitCode === 0 ? "Command completed (no output)" : "Command failed (no output)";
}
output += `\n_Exit code: ${result.exitCode}, Duration: ${result.durationMs}ms_`;
return output;
}