/** * localCodeRunner — run code in a child process, on this machine. * * Pattern: Adapter (GoF) over `node:child_process`. * Role: the dev default behind the {@link CodeRunner} port. `agentCoreCodeRunner` * is the production swap; the tool code does not change. * Emits: nothing. The tool that owns the session emits. * * ── ISOLATION, NOT A SANDBOX. Read this before deploying it. ──────────────── * The name is `localCodeRunner`, never `sandboxedCodeRunner`, and the * difference is not modesty — it is the whole security posture. * * What a child process DOES give you: * • a separate process and heap — a crash or an OOM takes the child, not you; * • kill on timeout, so runaway code has a ceiling; * • no inherited stdin, so nothing can block waiting for a terminal; * • an environment ALLOWLIST — `process.env` is not inherited (only `PATH`, * so the interpreter can be found), so an `AWS_SECRET_ACCESS_KEY` in your * shell is not in the model's reach; * • a working directory, by convention. * * What it does NOT give you, at all: * • a filesystem jail — the code can read and write anywhere this process can; * • a network jail — it can call out; * • CPU or memory limits beyond the timeout; * • any protection from code that deletes something outside `cwd`. * * ── It stages inputs; it does NOT report outputs (a decision, not a gap) ──── * `stageInputs` puts declared artifact payloads INTO the session, and the code * reads them from the manifest. Nothing comes back the same way: this runner * leaves `CodeResult.artifacts` ABSENT — never `[]`, which would claim the code * produced nothing — because it has no output location to collect from. The * child's working directory is the CALLER'S OWN cwd, so "files the code wrote" * is not a set this adapter can identify without guessing which of a developer's * files were meant, and a dev-loop runner that quietly uploaded whatever * appeared beside your source would be the worse failure. Producing outputs * needs three things this adapter does not have yet: a declared output * directory the model is told about, a bounded read-back of what landed in it, * and a size policy for what is too big to carry in-band. A runner that has * them (a managed sandbox that returns file contents) fills the field, and * `codeRunnerTool` mints every data-carrying entry with no further wiring. * * So: a development loop, a trusted-input pipeline, a machine you would be * relaxed about a shell script running on. NOT arbitrary model-written code * from an untrusted user, on a host with anything on it. For that, run a real * sandbox behind the same port — `agentCoreCodeRunner`, a gVisor container, a * Firecracker VM — and keep the tool identical. * * **In-process `eval` / `node:vm` is refused outright.** Node's own * documentation says `vm` is not a security mechanism ("do not use it to run * untrusted code"), so shipping it as one would be theater: the same code * reaching the same globals, wearing a word that makes a reader stop checking. * A subprocess is genuinely a boundary; it is just a smaller one than the word * "sandbox" implies. The library teaches refusals for things that are WRONG and * honest names for things that are merely LIMITED — running local code in a dev * loop is not wrong, and calling it a sandbox is. * * @example * const agent = Agent.create({ provider }) * .tool(codeRunnerTool({ runner: localCodeRunner() })) * .build(); */ import type { CodeRunner } from '../types.js'; export interface LocalCodeRunnerOptions { /** * How to run a snippet, as `[command, ...args]` — the code is appended as the * final argument. Defaults by language: `['node', '-e']` for javascript, * `['python3', '-c']` for python. */ readonly command?: readonly string[]; /** Working directory for the child. Defaults to `process.cwd()`. */ readonly cwd?: string; /** * The child's environment. An ALLOWLIST, never a merge: `process.env` is NOT * inherited, so a credential sitting in this process's environment is not * handed to model-written code by default. Pass exactly what the code needs. * * ONE exception, stated because an unstated one is a hole: `PATH` is passed * through, because without it the operating system cannot find the * interpreter and nothing runs at all. `PATH` names directories, not secrets. * Set `env: { PATH: '/usr/bin' }` to narrow it, or `env: { PATH: '' }` to * refuse even that — anything you supply here WINS over the inherited value. */ readonly env?: Readonly>; /** Per-execution ceiling; the child is killed past it. Default 30s. */ readonly timeoutMs?: number; /** * Per-stream output ceiling, in characters. Default 8000. * * Cutting is allowed; cutting SILENTLY is not — anything cut is reported on * `CodeResult.truncated`, and `codeRunnerTool` renders that as a visible * marker in the tool result. The model has to know the table it is about to * reason over is a fragment. */ readonly maxOutputChars?: number; /** Stable id (default `'local-code-runner'`). Rides the session events. */ readonly id?: string; /** @internal Test seam — the `node:child_process` module. */ readonly _childProcess?: ChildProcessModuleLike; /** @internal Test seam — the `node:fs` slice staging uses. */ readonly _fs?: FsModuleLike; /** @internal Test seam — where staging directories are created. Defaults to * the OS temp directory. */ readonly _stagingRoot?: string; } /** The slice of `node:fs` staging touches. Loaded lazily, and ONLY when a * caller actually stages something — a session that never stages never asks * this runtime for a filesystem. */ export interface FsModuleLike { readonly mkdtempSync?: (prefix: string) => string; readonly writeFileSync?: (path: string, data: string | Uint8Array) => void; readonly rmSync?: (path: string, options?: { recursive?: boolean; force?: boolean; }) => void; } /** The slice of `node:child_process` this adapter touches. */ export interface ChildProcessModuleLike { readonly execFile?: (file: string, args: readonly string[], options: { cwd?: string; env?: Record; timeout?: number; maxBuffer?: number; signal?: AbortSignal; }, callback: (error: (Error & { code?: number | string; killed?: boolean; }) | null, stdout: string, stderr: string) => void) => unknown; } export declare function localCodeRunner(options?: LocalCodeRunnerOptions): CodeRunner;