import { spawnSync } from "node:child_process";import { fileURLToPath } from "node:url"; import type { CodeExecutor, ExecuteResult, ToolInvoker,} from "@executor/codemode-core";import * as Effect from "effect/Effect"; import { type DenoPermissions, spawnDenoWorkerProcess,} from "./deno-worker-process"; export type { DenoPermissions }; export type DenoSubprocessExecutorOptions = { /** Path to the deno binary. Falls back to DENO_BIN env, ~/.deno/bin/deno, then "deno". */ denoExecutable?: string; /** Maximum execution time in milliseconds. Defaults to 5 minutes. */ timeoutMs?: number; /** Deno permission flags. Defaults to denying everything (full sandbox). */ permissions?: DenoPermissions;}; const IPC_PREFIX = "@@executor-ipc@@";const DEFAULT_TIMEOUT_MS = 5 * 60_000; // --------------------------------------------------------------------------// IPC message types (host <-> worker)// -------------------------------------------------------------------------- type HostStartMessage = { type: "start"; code: string;}; type HostToolResultMessage = { type: "tool_result"; requestId: string; ok: boolean; value?: unknown; error?: string;}; type HostToWorkerMessage = HostStartMessage | HostToolResultMessage; type WorkerToolCallMessage = { type: "tool_call"; requestId: string; toolPath: string; args: unknown;}; type WorkerCompletedMessage = { type: "completed"; result: unknown; logs?: string[];}; type WorkerFailedMessage = { type: "failed"; error: string; logs?: string[];}; type WorkerToHostMessage = | WorkerToolCallMessage | WorkerCompletedMessage | WorkerFailedMessage; // --------------------------------------------------------------------------// Deno binary resolution// -------------------------------------------------------------------------- const defaultDenoExecutable = (): string => { const configured = process.env.DENO_BIN?.trim(); if (configured) { return configured; } const home = process.env.HOME?.trim(); if (home) { const installedPath = `${home}/.deno/bin/deno`; const installedResult = spawnSync(installedPath, ["--version"], { stdio: "ignore", timeout: 5000, }); if (installedResult.error === undefined && installedResult.status === 0) { return installedPath; } } return "deno";}; const formatDenoSpawnError = ( cause: unknown, executable: string,): string => { const code = typeof cause === "object" && cause !== null && "code" in cause ? String((cause as { code?: unknown }).code) : null; if (code === "ENOENT") { return `Failed to spawn Deno subprocess: Deno executable "${executable}" was not found. Install Deno or set DENO_BIN.`; } return `Failed to spawn Deno subprocess: ${cause instanceof Error ? cause.message : String(cause)}`;}; // --------------------------------------------------------------------------// Worker script resolution// -------------------------------------------------------------------------- const resolveWorkerScriptPath = (): string => { const moduleUrl = String(import.meta.url); if (moduleUrl.startsWith("/")) { return moduleUrl; } try { const workerUrl = new URL( "./deno-subprocess-worker.mjs", moduleUrl, ); if (workerUrl.protocol === "file:") { return fileURLToPath(workerUrl); } return workerUrl.pathname.length > 0 ? workerUrl.pathname : workerUrl.toString(); } catch { return moduleUrl; }}; let cachedWorkerScriptPath: string | undefined; const workerScriptPath = (): string => { if (!cachedWorkerScriptPath) { cachedWorkerScriptPath = resolveWorkerScriptPath(); } return cachedWorkerScriptPath;}; // --------------------------------------------------------------------------// Helpers// -------------------------------------------------------------------------- const writeMessage = ( stdin: NodeJS.WritableStream, message: HostToWorkerMessage,): void => { stdin.write(`${JSON.stringify(message)}\n`);}; const isWorkerMessage = (value: unknown): value is WorkerToHostMessage => typeof value === "object" && value !== null && "type" in value && typeof (value as Record<string, unknown>).type === "string"; // --------------------------------------------------------------------------// Core execution// -------------------------------------------------------------------------- const executeInDeno = ( code: string, toolInvoker: ToolInvoker, options: DenoSubprocessExecutorOptions,): Effect.Effect<ExecuteResult, never> => Effect.gen(function* () { const denoExecutable = options.denoExecutable ?? defaultDenoExecutable(); const timeoutMs = Math.max(100, options.timeoutMs ?? DEFAULT_TIMEOUT_MS); const result = yield* Effect.async<ExecuteResult>((resume) => { let settled = false; let stderrBuffer = ""; let worker: ReturnType<typeof spawnDenoWorkerProcess> | null = null; const finish = (executeResult: ExecuteResult) => { if (settled) { return; } settled = true; clearTimeout(timeout); worker?.dispose(); resume(Effect.succeed(executeResult)); }; const fail = ( error: string, logs?: string[], ) => { finish({ result: null, error, logs, }); }; const timeout = setTimeout(() => { fail( `Deno subprocess execution timed out after ${timeoutMs}ms`, ); }, timeoutMs); const handleStdoutLine = (rawLine: string) => { const line = rawLine.trim(); if (line.length === 0 || !line.startsWith(IPC_PREFIX)) { return; } const payload = line.slice(IPC_PREFIX.length); let message: WorkerToHostMessage; try { const parsed: unknown = JSON.parse(payload); if (!isWorkerMessage(parsed)) { fail(`Invalid worker message: ${payload}`); return; } message = parsed; } catch (cause) { fail( `Failed to decode worker message: ${payload}\n${String(cause)}`, ); return; } if (message.type === "tool_call") { if (!worker) { fail( "Deno subprocess unavailable while handling worker tool_call", ); return; } const currentWorker = worker; Effect.runPromise( Effect.match( Effect.tryPromise({ try: () => Effect.runPromise( toolInvoker.invoke({ path: message.toolPath, args: message.args, }), ), catch: (cause) => cause instanceof Error ? cause : new Error(String(cause)), }), { onSuccess: (value) => { writeMessage(currentWorker.stdin, { type: "tool_result", requestId: message.requestId, ok: true, value, }); }, onFailure: (error) => { writeMessage(currentWorker.stdin, { type: "tool_result", requestId: message.requestId, ok: false, error: error.message, }); }, }, ), ).catch((cause) => { fail( `Failed handling worker tool_call: ${String(cause)}`, ); }); return; } if (message.type === "completed") { finish({ result: message.result, logs: message.logs, }); return; } // message.type === "failed" fail(message.error, message.logs); }; try { worker = spawnDenoWorkerProcess( { executable: denoExecutable, scriptPath: workerScriptPath(), permissions: options.permissions, }, { onStdoutLine: handleStdoutLine, onStderr: (chunk) => { stderrBuffer += chunk; }, onError: (cause) => { fail(formatDenoSpawnError(cause, denoExecutable)); }, onExit: (exitCode, signal) => { if (settled) { return; } fail( `Deno subprocess exited before returning terminal message (code=${String(exitCode)} signal=${String(signal)} stderr=${stderrBuffer})`, ); }, }, ); } catch (cause) { fail(formatDenoSpawnError(cause, denoExecutable)); return; } writeMessage(worker.stdin, { type: "start", code, }); }); return result; }); // --------------------------------------------------------------------------// Public API// -------------------------------------------------------------------------- /** * Check whether the configured (or default) Deno binary is available on the system. */export const isDenoAvailable = ( executable: string = defaultDenoExecutable(),): boolean => { const result = spawnSync(executable, ["--version"], { stdio: "ignore", timeout: 5000, }); return result.error === undefined && result.status === 0;}; /** * Create a `CodeExecutor` that runs code in a sandboxed Deno subprocess. * * The subprocess is spawned with `--deny-net --deny-read --deny-write --deny-env --deny-run --deny-ffi` * by default, providing strong process-level isolation. Tool calls are proxied back to the host * via stdin/stdout IPC and resolved through the provided `ToolInvoker`. */export const makeDenoSubprocessExecutor = ( options: DenoSubprocessExecutorOptions = {},): CodeExecutor => ({ execute: (code: string, toolInvoker: ToolInvoker) => executeInDeno(code, toolInvoker, options),});
import { spawnSync } from "node:child_process";
import { fileURLToPath } from "node:url";
import type {
CodeExecutor,
ExecuteResult,
ToolInvoker,
} from "@executor/codemode-core";
import * as Effect from "effect/Effect";
import {
type DenoPermissions,
spawnDenoWorkerProcess,
} from "./deno-worker-process";
export type { DenoPermissions };
export type DenoSubprocessExecutorOptions = {
/** Path to the deno binary. Falls back to DENO_BIN env, ~/.deno/bin/deno, then "deno". */
denoExecutable?: string;
/** Maximum execution time in milliseconds. Defaults to 5 minutes. */
timeoutMs?: number;
/** Deno permission flags. Defaults to denying everything (full sandbox). */
permissions?: DenoPermissions;
};
const IPC_PREFIX = "@@executor-ipc@@";
const DEFAULT_TIMEOUT_MS = 5 * 60_000;
// --------------------------------------------------------------------------
// IPC message types (host <-> worker)
// --------------------------------------------------------------------------
type HostStartMessage = {
type: "start";
code: string;
};
type HostToolResultMessage = {
type: "tool_result";
requestId: string;
ok: boolean;
value?: unknown;
error?: string;
};
type HostToWorkerMessage = HostStartMessage | HostToolResultMessage;
type WorkerToolCallMessage = {
type: "tool_call";
requestId: string;
toolPath: string;
args: unknown;
};
type WorkerCompletedMessage = {
type: "completed";
result: unknown;
logs?: string[];
};
type WorkerFailedMessage = {
type: "failed";
error: string;
logs?: string[];
};
type WorkerToHostMessage =
| WorkerToolCallMessage
| WorkerCompletedMessage
| WorkerFailedMessage;
// --------------------------------------------------------------------------
// Deno binary resolution
// --------------------------------------------------------------------------
const defaultDenoExecutable = (): string => {
const configured = process.env.DENO_BIN?.trim();
if (configured) {
return configured;
}
const home = process.env.HOME?.trim();
if (home) {
const installedPath = `${home}/.deno/bin/deno`;
const installedResult = spawnSync(installedPath, ["--version"], {
stdio: "ignore",
timeout: 5000,
});
if (installedResult.error === undefined && installedResult.status === 0) {
return installedPath;
}
}
return "deno";
};
const formatDenoSpawnError = (
cause: unknown,
executable: string,
): string => {
const code = typeof cause === "object" && cause !== null && "code" in cause
? String((cause as { code?: unknown }).code)
: null;
if (code === "ENOENT") {
return `Failed to spawn Deno subprocess: Deno executable "${executable}" was not found. Install Deno or set DENO_BIN.`;
}
return `Failed to spawn Deno subprocess: ${cause instanceof Error ? cause.message : String(cause)}`;
};
// --------------------------------------------------------------------------
// Worker script resolution
// --------------------------------------------------------------------------
const resolveWorkerScriptPath = (): string => {
const moduleUrl = String(import.meta.url);
if (moduleUrl.startsWith("/")) {
return moduleUrl;
}
try {
const workerUrl = new URL(
"./deno-subprocess-worker.mjs",
moduleUrl,
);
if (workerUrl.protocol === "file:") {
return fileURLToPath(workerUrl);
}
return workerUrl.pathname.length > 0
? workerUrl.pathname
: workerUrl.toString();
} catch {
return moduleUrl;
}
};
let cachedWorkerScriptPath: string | undefined;
const workerScriptPath = (): string => {
if (!cachedWorkerScriptPath) {
cachedWorkerScriptPath = resolveWorkerScriptPath();
}
return cachedWorkerScriptPath;
};
// --------------------------------------------------------------------------
// Helpers
// --------------------------------------------------------------------------
const writeMessage = (
stdin: NodeJS.WritableStream,
message: HostToWorkerMessage,
): void => {
stdin.write(`${JSON.stringify(message)}\n`);
};
const isWorkerMessage = (value: unknown): value is WorkerToHostMessage =>
typeof value === "object"
&& value !== null
&& "type" in value
&& typeof (value as Record<string, unknown>).type === "string";
// --------------------------------------------------------------------------
// Core execution
// --------------------------------------------------------------------------
const executeInDeno = (
code: string,
toolInvoker: ToolInvoker,
options: DenoSubprocessExecutorOptions,
): Effect.Effect<ExecuteResult, never> =>
Effect.gen(function* () {
const denoExecutable =
options.denoExecutable ?? defaultDenoExecutable();
const timeoutMs = Math.max(100, options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
const result = yield* Effect.async<ExecuteResult>((resume) => {
let settled = false;
let stderrBuffer = "";
let worker: ReturnType<typeof spawnDenoWorkerProcess> | null =
null;
const finish = (executeResult: ExecuteResult) => {
if (settled) {
return;
}
settled = true;
clearTimeout(timeout);
worker?.dispose();
resume(Effect.succeed(executeResult));
};
const fail = (
error: string,
logs?: string[],
) => {
finish({
result: null,
error,
logs,
});
};
const timeout = setTimeout(() => {
fail(
`Deno subprocess execution timed out after ${timeoutMs}ms`,
);
}, timeoutMs);
const handleStdoutLine = (rawLine: string) => {
const line = rawLine.trim();
if (line.length === 0 || !line.startsWith(IPC_PREFIX)) {
return;
}
const payload = line.slice(IPC_PREFIX.length);
let message: WorkerToHostMessage;
try {
const parsed: unknown = JSON.parse(payload);
if (!isWorkerMessage(parsed)) {
fail(`Invalid worker message: ${payload}`);
return;
}
message = parsed;
} catch (cause) {
fail(
`Failed to decode worker message: ${payload}\n${String(cause)}`,
);
return;
}
if (message.type === "tool_call") {
if (!worker) {
fail(
"Deno subprocess unavailable while handling worker tool_call",
);
return;
}
const currentWorker = worker;
Effect.runPromise(
Effect.match(
Effect.tryPromise({
try: () =>
Effect.runPromise(
toolInvoker.invoke({
path: message.toolPath,
args: message.args,
}),
),
catch: (cause) =>
cause instanceof Error
? cause
: new Error(String(cause)),
}),
{
onSuccess: (value) => {
writeMessage(currentWorker.stdin, {
type: "tool_result",
requestId: message.requestId,
ok: true,
value,
});
},
onFailure: (error) => {
writeMessage(currentWorker.stdin, {
type: "tool_result",
requestId: message.requestId,
ok: false,
error: error.message,
});
},
},
),
).catch((cause) => {
fail(
`Failed handling worker tool_call: ${String(cause)}`,
);
});
return;
}
if (message.type === "completed") {
finish({
result: message.result,
logs: message.logs,
});
return;
}
// message.type === "failed"
fail(message.error, message.logs);
};
try {
worker = spawnDenoWorkerProcess(
{
executable: denoExecutable,
scriptPath: workerScriptPath(),
permissions: options.permissions,
},
{
onStdoutLine: handleStdoutLine,
onStderr: (chunk) => {
stderrBuffer += chunk;
},
onError: (cause) => {
fail(formatDenoSpawnError(cause, denoExecutable));
},
onExit: (exitCode, signal) => {
if (settled) {
return;
}
fail(
`Deno subprocess exited before returning terminal message (code=${String(exitCode)} signal=${String(signal)} stderr=${stderrBuffer})`,
);
},
},
);
} catch (cause) {
fail(formatDenoSpawnError(cause, denoExecutable));
return;
}
writeMessage(worker.stdin, {
type: "start",
code,
});
});
return result;
});
// --------------------------------------------------------------------------
// Public API
// --------------------------------------------------------------------------
/**
* Check whether the configured (or default) Deno binary is available on the system.
*/
export const isDenoAvailable = (
executable: string = defaultDenoExecutable(),
): boolean => {
const result = spawnSync(executable, ["--version"], {
stdio: "ignore",
timeout: 5000,
});
return result.error === undefined && result.status === 0;
};
/**
* Create a `CodeExecutor` that runs code in a sandboxed Deno subprocess.
*
* The subprocess is spawned with `--deny-net --deny-read --deny-write --deny-env --deny-run --deny-ffi`
* by default, providing strong process-level isolation. Tool calls are proxied back to the host
* via stdin/stdout IPC and resolved through the provided `ToolInvoker`.
*/
export const makeDenoSubprocessExecutor = (
options: DenoSubprocessExecutorOptions = {},
): CodeExecutor => ({
execute: (code: string, toolInvoker: ToolInvoker) =>
executeInDeno(code, toolInvoker, options),
});