Files
2026-05-27 23:29:53 +01:00

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): RunnerOutput for in-process testing.
  • readStdinAll() async stdin slurper.
  • main() orchestrator.
  • Auto-invokes main() when the file is launched directly (matched by filename ending in runner.{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.