2.5 KiB
Runner (spawn-engine child entry)
Purpose
runner.ts is the child-process entry point invoked by SpawnEngine. The parent process spawns it via node:child_process.spawn, writes a JSON RunnerInput envelope to its stdin (closing the pipe to signal end-of-input), and reads a JSON RunnerOutput envelope from its stdout. The runner runs inside a fresh OS process with a sanitised environment — that process boundary is the security guarantee, not anything the runner itself does.
The runner exports runRunner(input) as a pure function so the parent can also test the evaluation pipeline without spawning a child. The main() entry point reads stdin, parses JSON, calls runRunner, and writes stdout — exposed for tests that want to drive the full I/O surface.
Features
- Pure
runRunner(input): RunnerOutputfor in-process testing. readStdinAll()async stdin slurper.main()orchestrator.- Auto-invokes
main()when the file is launched directly (matched by filename ending inrunner.{js,ts,cjs,mjs}). stringify(value)JSON-safe fallback used by the captured console.
Usage: Basic
The runner is normally invoked by SpawnEngine; consumers do not call it
directly. To test in isolation:
import { runRunner } from "@suaveplan/sandbox/runner";
const out = runRunner({
code: "1 + 1",
context: {},
modules: [],
maxOutputSize: 1024,
});
console.log(out.result); // 2
Usage: Advanced
Drive the full stdin/stdout pipeline against the runner file by spawning a real child:
import { spawn } from "node:child_process";
const child = spawn(process.execPath, ["./dist/spawn-engine/runner.js"], {
stdio: ["pipe", "pipe", "pipe"],
});
child.stdin.write(JSON.stringify({ code: "1 + 1", context: {}, modules: [], maxOutputSize: 1024 }));
child.stdin.end();
API Reference
RunnerInput—{ code, context, modules, maxOutputSize }.RunnerOutput—{ success, result, error, logs, memoryUsed }.runRunner(input: RunnerInput): RunnerOutput.readStdinAll(): Promise<string>.main(): Promise<void>.stringify(value: unknown): string.
Implementation Notes
The runner builds a function scope via
Reflect.construct(Function, [...keys, source]) — this is the controlled
boundary the runner owns; the parent is the one that decides whether to
spawn it. Strict mode is enforced via the prologue. The
auto-invoke guard checks process.argv[1] against a filename regex so
re-importing the runner from a test never triggers the main loop.