mirror of
https://github.com/andrewyng/openworker.git
synced 2026-11-03 21:06:51 +00:00
151 lines
4.0 KiB
JavaScript
151 lines
4.0 KiB
JavaScript
// Container-level coding-tool smoke. Not an OpenShell/kernel-isolation claim.
|
|
import assert from "node:assert/strict";
|
|
import { spawn } from "node:child_process";
|
|
import { createInterface } from "node:readline";
|
|
const image = process.argv[2];
|
|
assert(
|
|
image && /^[A-Za-z0-9./_:@-]+$/.test(image),
|
|
"Pass a trusted local image tag or immutable digest",
|
|
);
|
|
const child = spawn(
|
|
"docker",
|
|
[
|
|
"run",
|
|
"--rm",
|
|
"-i",
|
|
"--network",
|
|
"none",
|
|
"--read-only",
|
|
"--cap-drop",
|
|
"ALL",
|
|
"--security-opt",
|
|
"no-new-privileges",
|
|
"--pids-limit",
|
|
"64",
|
|
"--memory",
|
|
"512m",
|
|
"--tmpfs",
|
|
"/sandbox:rw,size=64m",
|
|
"--tmpfs",
|
|
"/tmp:rw,size=64m",
|
|
"--entrypoint",
|
|
"/opt/openworker/openworker",
|
|
image,
|
|
"mcp-coding",
|
|
"--workspace",
|
|
"/sandbox",
|
|
],
|
|
{ stdio: ["pipe", "pipe", "pipe"] },
|
|
);
|
|
const pending = new Map();
|
|
let id = 0,
|
|
stderr = "",
|
|
exited = false;
|
|
child.stderr.on("data", (b) => {
|
|
stderr = (stderr + b).slice(-8000);
|
|
});
|
|
child.on("error", (e) => {
|
|
for (const p of pending.values()) p.reject(e);
|
|
});
|
|
child.on("exit", (code) => {
|
|
exited = true;
|
|
for (const p of pending.values())
|
|
p.reject(Error(`Container exited ${code}: ${stderr}`));
|
|
});
|
|
const lines = createInterface({ input: child.stdout });
|
|
lines.on("line", (line) => {
|
|
try {
|
|
const r = JSON.parse(line);
|
|
const p = pending.get(r.id);
|
|
if (!p) return;
|
|
pending.delete(r.id);
|
|
clearTimeout(p.timer);
|
|
r.error ? p.reject(Error("MCP protocol error")) : p.resolve(r.result);
|
|
} catch (e) {
|
|
for (const p of pending.values()) p.reject(e);
|
|
}
|
|
});
|
|
function rpc(method, params = {}) {
|
|
const n = ++id;
|
|
return new Promise((resolve, reject) => {
|
|
const timer = setTimeout(() => {
|
|
pending.delete(n);
|
|
reject(Error("Coding MCP deadline"));
|
|
}, 20000);
|
|
pending.set(n, { resolve, reject, timer });
|
|
child.stdin.write(
|
|
JSON.stringify({ jsonrpc: "2.0", id: n, method, params }) + "\n",
|
|
);
|
|
});
|
|
}
|
|
async function tool(name, args) {
|
|
const r = await rpc("tools/call", { name, arguments: args });
|
|
assert(!r.isError, JSON.stringify(r.content));
|
|
return JSON.parse(r.content[0].text);
|
|
}
|
|
try {
|
|
await rpc("initialize", {
|
|
protocolVersion: "2025-11-25",
|
|
capabilities: {},
|
|
clientInfo: { name: "coding-qualification", version: "1" },
|
|
});
|
|
const catalog = await rpc("tools/list");
|
|
for (const name of [
|
|
"read_file",
|
|
"write_file",
|
|
"apply_patch",
|
|
"process_start",
|
|
"process_logs",
|
|
"process_status",
|
|
"process_stop",
|
|
"git_status",
|
|
])
|
|
assert(
|
|
catalog.tools.some((t) => t.name === name),
|
|
`Missing ${name}`,
|
|
);
|
|
await tool("write_file", {
|
|
path: "smoke.txt",
|
|
content: "coding runner smoke\n",
|
|
create_only: true,
|
|
});
|
|
const file = await tool("read_file", { path: "smoke.txt" });
|
|
assert(JSON.stringify(file).includes("coding runner smoke"));
|
|
const shell = await tool("run_shell", {
|
|
command:
|
|
"node --version && python3 --version && git --version && curl --version | head -n 1",
|
|
yield_ms: 10000,
|
|
timeout_seconds: 20,
|
|
});
|
|
assert.equal(shell.exit_code, 0);
|
|
assert(shell.stdout.includes("v24."));
|
|
const process = await tool("process_start", {
|
|
command: 'node -e "setInterval(() => {}, 1000)"',
|
|
timeout_seconds: 30,
|
|
});
|
|
assert(process.process_id && process.status === "running");
|
|
const status = await tool("process_status", {
|
|
process_id: process.process_id,
|
|
});
|
|
assert.equal(status.status, "running");
|
|
await tool("process_stop", { process_id: process.process_id });
|
|
console.log(
|
|
JSON.stringify({
|
|
passed: true,
|
|
scope: "network-disabled read-only container; no OpenShell claim",
|
|
tools: catalog.tools.length,
|
|
node24: true,
|
|
managedProcessSurvivedToolCalls: true,
|
|
hostCredentialsPassed: false,
|
|
}),
|
|
);
|
|
} finally {
|
|
child.stdin.end();
|
|
for (const p of pending.values()) clearTimeout(p.timer);
|
|
lines.close();
|
|
const timer = setTimeout(() => {
|
|
if (!exited) child.kill("SIGKILL");
|
|
}, 3000);
|
|
timer.unref();
|
|
}
|