Files
short-video-viral-replicator/agent-studio-mini/sandbox/manage.mjs
T

50 lines
3.5 KiB
JavaScript

import { spawnSync } from "node:child_process";
import { existsSync } from "node:fs";
import { fileURLToPath } from "node:url";
import path from "node:path";
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const [action, name = "local"] = process.argv.slice(2);
if (!["start", "stop", "verify"].includes(action) || !/^[a-z][a-z0-9-]{0,30}$/.test(name)) {
console.error("Usage: node sandbox/manage.mjs start|stop|verify instance-name"); process.exit(2);
}
function docker(args, capture = false) {
const r = spawnSync("docker", args, { cwd: root, encoding: "utf8", stdio: capture ? "pipe" : "inherit" });
if (r.error || r.status !== 0) throw new Error(r.error?.code === "ENOENT" ? "Docker is not installed. Install Docker Engine and Compose first; host execution is NOT a fallback." : "Docker operation failed; check Docker daemon/Compose access.");
return r.stdout?.trim();
}
try {
docker(["info", "--format", "{{.ServerVersion}}"], true);
docker(["compose", "version"], true);
const base = ["compose", "-p", `beryl-sb-${name}`];
if (existsSync(path.join(root, ".env"))) base.push("--env-file", ".env");
base.push("-f", "compose.sandbox.yaml");
if (process.env.PPT_RUNTIME_HOST_DIR || process.env.PPT_SKILL_HOST_DIR) base.push("-f", "compose.sandbox-ppt.yaml");
docker([...base, "config", "--quiet"]);
if (action === "start") {
docker([...base, "up", "--build", "--detach", "--wait"]);
console.log(`Sandbox instance: beryl-sb-${name}. Open http://localhost:${process.env.SANDBOX_PORT ?? 15173}/. Run verify before use.`);
} else if (action === "stop") {
docker([...base, "down"]); // Deliberately no --volumes: retain reports and requirements.
console.log("Containers stopped/removed; persistent report volume retained.");
} else {
const id = docker([...base, "ps", "-q", "agent"], true);
if (!id) throw new Error("Sandbox is not running");
const data = JSON.parse(docker(["inspect", id], true))[0];
const h = data.HostConfig;
if (data.Config.User !== "1000:1000" || !h.ReadonlyRootfs || h.Privileged || !h.CapDrop?.includes("ALL") || !h.SecurityOpt?.includes("no-new-privileges:true") || h.PidsLimit !== 128 || h.Memory <= 0 || h.NanoCpus <= 0) throw new Error("Isolation settings failed verification");
if (data.Mounts.some(m => !["/app/workspace", "/opt/ppt-runtime", "/opt/ppt-skill", "/tmp", "/home/node"].includes(m.Destination) || (m.Type === "bind" && m.RW))) throw new Error("Unexpected host mount");
const networks = Object.keys(data.NetworkSettings.Networks);
if (networks.length !== 1 || !JSON.parse(docker(["network", "inspect", networks[0]], true))[0].Internal) throw new Error("Agent must use only the internal network");
docker(["exec", id, "node", "--input-type=module", "-e", `
import fs from 'node:fs/promises';
if(process.getuid()===0) throw Error('root');
try { await fs.writeFile('/app/escape-check','x'); throw Error('rootfs writable'); } catch(e) { if(!['EROFS','EACCES'].includes(e.code)) throw e; }
await fs.writeFile('/app/workspace/.sandbox-check','ok');
await fs.unlink('/app/workspace/.sandbox-check');
const net=await import('node:net');
await new Promise((resolve,reject)=>{const s=net.connect({host:'1.1.1.1',port:443});s.setTimeout(2000,()=>{s.destroy();resolve();});s.on('error',()=>resolve());s.on('connect',()=>{s.destroy();reject(Error('direct egress bypass'));});});
console.log('PASS: non-root, read-only rootfs, workspace writable, direct network blocked');
`]);
}
} catch (e) { console.error(e.message); process.exit(1); }