Skip to content

mcpc-tech/handle-sandbox

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

14 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

@mcpc/handle-sandbox

JSR

Simple and secure JavaScript execution in an isolated Deno subprocess. Register async host functions, run untrusted code with explicit permissions, and optionally stream logs and stderr back to the host in real time.

Features

  • Secure sandboxing with Deno permissions
  • JSON-RPC IPC between host and sandbox process
  • Async host handlers exposed directly inside sandboxed code
  • Streaming logs via onLog(text, level) before execute() resolves
  • Streaming stderr via onStderr(text)
  • Process controls for timeout, memory, permissions, cwd, env, and extra Deno CLI args
  • Zero-config runtime discovery from the bundled deno package

Installation

# npm
npm install @mcpc-tech/handle-sandbox

# jsr
npx jsr add @mcpc/handle-sandbox
deno add @mcpc/handle-sandbox --allow-scripts=npm:deno

Quick Start

Basic usage

import { Sandbox } from "@mcpc/handle-sandbox";

const sandbox = new Sandbox();
sandbox.start();

const result = await sandbox.execute(`
  console.log("Hello from sandbox!");
  return 1 + 1;
`);

console.log(result.logs); // ["Hello from sandbox!"]
console.log(result.result); // 2
console.log(result.error); // undefined

sandbox.stop();

Register host handlers

Registered handlers are available inside sandboxed code by the same name.

import { Sandbox } from "@mcpc/handle-sandbox";

const sandbox = new Sandbox();

sandbox.registerHandler("fetchUser", async (userId) => {
  return { id: userId, name: "Alice" };
});

sandbox.registerHandler("saveAuditLog", async (payload) => {
  return { ok: true, payload };
});

sandbox.start();

const result = await sandbox.execute(`
  const user = await fetchUser(123);
  await saveAuditLog({ action: "login", userId: user.id });
  console.log("User:", user.name);
  return user;
`);

console.log(result.result); // { id: 123, name: "Alice" }
console.log(result.logs); // ["User: Alice"]

sandbox.stop();

Stream logs and stderr

import { type LogLevel, Sandbox } from "@mcpc/handle-sandbox";

const sandbox = new Sandbox({
  onLog: (text: string, level: LogLevel) => {
    console.log(`[sandbox:${level}]`, text);
  },
  onStderr: (text) => {
    process.stderr.write(`[sandbox:stderr] ${text}`);
  },
});

sandbox.start();

const result = await sandbox.execute(`
  console.log("step 1");
  console.warn("step 2");
  await Deno.stderr.write(new TextEncoder().encode("warning from stderr\n"));
  return "done";
`);

console.log(result.result); // "done"

sandbox.stop();

Configure permissions and process options

import { Sandbox } from "@mcpc/handle-sandbox";

const sandbox = new Sandbox({
  timeout: 5_000,
  memoryLimit: 256,
  permissions: ["--allow-net=api.example.com", "--allow-env=API_TOKEN"],
  cwd: process.cwd(),
  env: {
    ...process.env,
    API_TOKEN: "example-token",
  },
  extraArgs: ["--quiet"],
});

sandbox.start();

const result = await sandbox.execute(`
  return {
    cwd: Deno.cwd(),
    tokenAvailable: Boolean(Deno.env.get("API_TOKEN")),
  };
`);

console.log(result.result);

sandbox.stop();

API

new Sandbox(config?)

Create a sandbox instance.

Config options

  • timeout?: number - Execution timeout in milliseconds. Default: 30000
  • memoryLimit?: number - V8 max old space size in MB
  • permissions?: string[] - Deno permission flags such as "--allow-net=api.example.com"
  • extraArgs?: string[] - Additional Deno CLI args such as "--quiet"
  • cwd?: string - Working directory for the sandbox subprocess
  • env?: Record<string, string | undefined> - Environment variables for the sandbox subprocess
  • onLog?: (text: string, level: LogLevel) => void - Streaming console output callback
  • onStderr?: (text: string) => void - Streaming stderr callback

sandbox.registerHandler(name, handler)

Register an async host function that sandboxed code can call directly by name.

sandbox.registerHandler("double", async (value) => {
  return Number(value) * 2;
});

const result = await sandbox.execute(`
  return await double(21);
`);

sandbox.start()

Start the Deno subprocess. Call this before execute().

sandbox.execute(code, context?)

Execute JavaScript code inside the sandbox.

Parameters

  • code: string - JavaScript source to run
  • context?: Record<string, unknown> - Optional execution context

Returns

Promise<{
  logs: string[];
  result?: unknown;
  error?: string;
}>;

sandbox.stop()

Stop the sandbox process and clean up resources.

LogLevel

type LogLevel = "log" | "error" | "warn" | "info";

How it works

  1. The host starts a Deno subprocess.
  2. The host sends user code to the subprocess over JSON-RPC.
  3. The sandbox executes the code in an isolated environment.
  4. Registered handlers are exposed as async functions inside the sandbox.
  5. Console output is returned in the final result and can also be streamed via onLog.
  6. The subprocess stderr stream can be observed via onStderr.

Security model

The sandbox runs with minimal Deno permissions by default. Grant only the permissions your use case needs.

// No permissions by default
new Sandbox();

// Allow network access to specific domains
new Sandbox({
  permissions: ["--allow-net=github.com,api.example.com"],
});

// Allow reading specific directories
new Sandbox({
  permissions: ["--allow-read=/tmp,/var/log"],
});

// Allow access to selected environment variables
new Sandbox({
  permissions: ["--allow-env=API_TOKEN"],
});

Without the corresponding Deno permission flags, operations like file access, network access, and reading environment variables will fail.

Development

# format
deno fmt

# test
deno test --allow-all tests/

License

MIT

About

Deno sandbox with injectable async handlers

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors