/** * The one place chant starts a sandboxed child process. * * Extracted from `./run.ts` by chant #1113, which added a SECOND thing that * has to run behind the same boundary (`chant.config.ts` evaluation, see * `./config-run.ts`). Both callers must get the identical `--permission` * profile and the identical environment scrub — if the two drifted, the * weaker one would silently become the boundary. Keeping the spawn itself in * one function is the cheapest way to make "same profile" a fact rather than * a claim. * * chant #1148 — this is also the one place chant forwards a sandboxed * child's own stdout/stderr, so `./run.ts`, `./config-run.ts` and * `./policy-run.ts` cannot drift on whether project output vanishes. See * {@link SandboxForkOptions.outputPrefix}. * * Isolation mechanics (verified on Node v24.13.1 — see the chant#1045 PR * description for the full write-up): * - `--permission --allow-fs-read=,[,]` — no filesystem write, no child-process, no * worker-thread access. Bundling with esbuild first (`./bundle.ts`) means * the child needs NO TypeScript loader (no `tsx`, so no `--allow-worker` * and no writable temp dir either). * - The env is a spawn-time scrub, not `--permission`: Node's Permission * Model does not gate `process.env` at all (confirmed: every key stays * readable even under `--permission`). See {@link SandboxForkOptions.env}. * - Network egress is NOT addressed — Node has no flag for it. See * `docs/.../architecture/sandbox.mdx` for the residual-risk statement. */ export interface SandboxForkOptions { /** Absolute, realpath'd path to the bundled ESM entry file to run. */ bundlePath: string; /** Absolute, realpath'd directory holding {@link bundlePath} — granted `--allow-fs-read`. */ bundleDir: string; /** Absolute, realpath'd project directory — granted `--allow-fs-read`. */ projectRealpath: string; /** Additional directories to grant `--allow-fs-read` (the resolved locations of `./bundle.ts`'s deliberately-unbundled trusted packages). */ externalReadPaths: readonly string[]; /** * The child's ENTIRE environment. Callers pass an explicit, closed set — * never a spread of `process.env`. `./run.ts` passes `PATH` only; * `./config-run.ts` adds `CHANT_ENV` (see its doc for why that one * variable, and only that one, is forwarded). */ env: Record; /** How long to wait for the child's one IPC message before killing it. */ timeoutMs: number; /** What timed out / exited early, for the error message (e.g. `"sandboxed run"`). */ label: string; /** * chant #1148 — prepended to every line the child writes on EITHER stdout * or stderr before it is relayed, line-buffered, to this process's own * stderr (e.g. `"[sandbox:run]"`, `"[sandbox:config]"`, * `"[policy:org.ts]"`). * * A sandboxed child's `console.log`/`console.error` used to go nowhere: its * stdout was piped but never read, and its stderr was captured only into * {@link stderrBuf}'s error-message use, never surfaced on a successful * run. Diagnostics crossing as data (the whole point of the boundary) is * not the same thing as incidental output being silently dropped — chant's * stance is that nothing the project prints vanishes, sandboxed or not. * * This is forwarding, not a second capture: {@link stderrBuf} still * accumulates the child's raw stderr for `classifyChildError`/the * exited-before-reporting message exactly as before. Both read the same * `data` events; one buffers for classification, this one relays for a * human to see. */ outputPrefix: string; /** * chant #1131 — an optional payload sent to the child over the SAME IPC * channel its response comes back on, immediately after the fork. * * The run and config children are fully described by their generated driver * source, so they need nothing inbound. The policy child does: its input is * the finished build result, which exists only after the parent has merged * and serialized, long after the bundle was built. Sending it rather than * baking it into a source literal keeps the bundle small (esbuild would * otherwise parse a multi-megabyte literal) and keeps it off disk. * * Safe to send before the child has booted: `child.send` writes to the IPC * pipe and Node queues the message until the child's channel is read, and * the driver registers its `process.on("message", …)` synchronously at module * top level — before the event loop can deliver anything. This is NOT a * second protocol: same channel, same JSON, same one-message-back response. */ send?: Record; } /** * Fork `bundlePath` under `--permission` with a scrubbed environment, and * resolve with the first IPC message that satisfies `isResponse` (or reject * on crash / timeout / fork error). */ export declare function forkSandboxed(options: SandboxForkOptions, isResponse: (value: unknown) => value is T): Promise; //# sourceMappingURL=fork.d.ts.map