runspace-sandbox
v0.1.0
Published
A platform-agnostic code execution framework for building secure, isolated runtime environments in the browser
Downloads
131
Maintainers
Readme
Sandbox
A modular, platform-agnostic code execution framework for building secure, isolated runtime environments in the browser. Execute user code safely while maintaining clean separation between UI, runtime, and communication layers.
Overview
Sandbox provides a complete architecture for running user code in isolated environments with real-time communication between parent applications and sandboxed contexts. Built with TypeScript and designed for extensibility, it enables developers to create interactive coding environments, educational tools, and code playgrounds.
Features
- Platform-Agnostic Runtime: Abstract runtime interface supports multiple execution environments (Pyodide, Node.js, etc.)
- Secure Isolation: iframe-based sandboxing with postMessage communication
- Real-time Streaming: Capture and stream stdout/stderr from code execution
- Output Formatting: Transform runtime output for better user experience (e.g., simplify error tracebacks)
- Modular Architecture: Clean separation of concerns across Bridge, Controller, Runtime, and Application layers
- Type-Safe: Full TypeScript support with comprehensive type definitions
- Customizable UI: Build multiple sandbox applications with different interfaces using the same core infrastructure
Architecture
The sandbox architecture consists of five core components:
1. Sandbox
The orchestrator that initializes and coordinates all components:
- Creates and configures the runtime instance
- Initializes the SandboxController
- Sets up the SandboxBridge for communication
- Injects the controller into the Sandbox Application
2. SandboxBridge
Handles all communication between the parent window and the sandbox iframe:
- Manages postMessage-based messaging
- Forwards commands and events between contexts
- Provides type-safe message handling
3. SandboxController
Manages the runtime lifecycle and provides the application API:
- Initializes and monitors runtime readiness
- Loads user code into the runtime
- Executes functions with type-safe argument handling
- Emits events for stdout, stderr, and errors
- Provides status checks for runtime and code state
4. Runtime (Interface)
Platform-agnostic interface for code execution:
initialize(): Set up the execution environmentloadCode(workspace): Load workspace with files and configurationrunFunction(name, args): Execute functions by namegetStdout()/getStderr(): Retrieve output streamsclearOutput(): Reset output buffersisReady(): Check runtime status
Implementations:
- PyodideRuntime: Python execution using Pyodide WebAssembly runtime
5. SandboxApplication
Customizable UI layer that interacts with the controller:
- Receives injected controller instance
- Implements custom rendering logic
- Calls controller methods to execute code
- Updates UI based on execution results
Installation
npm installUsage
Basic Setup
import { Sandbox, PyodideRuntime, SandboxApplication } from 'runspace-sandbox';
// Create your custom application
const myApp: SandboxApplication = {
render() {
// Implement your UI logic
const button = document.createElement('button');
button.textContent = 'Run Code';
button.onclick = async () => {
const result = await this.controller?.runFunction('myFunction', [arg1, arg2]);
console.log('Result:', result);
};
document.body.appendChild(button);
}
};
// Initialize the sandbox
const sandbox = new Sandbox({
runtime: PyodideRuntime,
app: myApp,
targetWindow: window.parent,
targetOrigin: '*'
});
await sandbox.start();Parent Application (WorkspaceHost)
For a detailed guide on implementing a Workspace Host, please refer to docs/workspace_host_guide.md.
// Create iframe for sandbox
const iframe = document.createElement('iframe');
iframe.src = '/sandbox.html';
document.body.appendChild(iframe);
// Listen for messages
window.addEventListener('message', (event) => {
const message = event.data;
switch (message.type) {
case 'READY_FOR_INIT':
// Sandbox is loaded, send initialization command
iframe.contentWindow?.postMessage({ type: 'INIT' }, '*');
break;
case 'SETUP_STREAM':
// Receive MessagePort for streaming output
const port = message.port;
port.onmessage = (e) => {
console.log('Stream output:', e.data);
};
break;
case 'INITIALIZED':
console.log('Sandbox initialized');
// Load user code
iframe.contentWindow?.postMessage({
type: 'LOAD_CODE',
payload: {
workspace: {
files: [
{ path: 'main.py', content: 'def add(a, b):\n return a + b' }
],
config: {
entryPoints: ['main.py']
}
}
}
}, '*');
break;
case 'CODE_LOADED':
console.log('Code loaded successfully');
break;
}
});Output Formatting
Transform runtime output to provide a better user experience. Output formatters are applied to both loadCode and runFunction operations:
const sandbox = new Sandbox({
runtime: PyodideRuntime,
app: myApp,
formatters: {
// Format stdout output and function results
stdout: (output: unknown) => {
// Transform output as needed
if (typeof output === 'string') {
return output.trim();
}
return output;
},
// Format stderr output and error messages
stderr: (error: string) => {
// Simplify Python tracebacks by removing internal Pyodide frames
const lines = error.split('\n');
const filteredLines: string[] = [];
let i = 0;
while (i < lines.length) {
const line = lines[i];
// Skip internal Pyodide file references and their code context
if (line.includes('/lib/python') || line.includes('_pyodide')) {
i++;
// Skip indented code context lines (start with 4+ spaces)
while (i < lines.length && lines[i].startsWith(' ')) {
i++;
}
continue;
}
// Keep this line and replace <exec> with main.py
filteredLines.push(line.replace(/File "<exec>"/g, 'File "main.py"'));
i++;
}
return filteredLines.join('\n').trim();
}
}
});Example: Simplifying Python Error Messages
Before formatting:
Traceback (most recent call last):
File "/lib/python313.zip/_pyodide/_base.py", line 597, in eval_code_async
await CodeRunner(...)
File "/lib/python313.zip/_pyodide/_base.py", line 285, in __init__
self.ast = next(self._gen)
File "<exec>", line 16
return [0]''
^^
SyntaxError: invalid syntaxAfter formatting:
Traceback (most recent call last):
File "main.py", line 16
return [0]''
^^
SyntaxError: invalid syntaxOutputFormatter Interface:
stdout?(output: unknown): unknown- Formats both stdout output and function return valuesstderr?(output: string): string- Formats both stderr output and error messages
Note: Formatters apply to both loadCode() and runFunction() operations, but not to initialize().
Creating Sandbox Applications
For a detailed guide on implementing a Sandbox Application, please refer to docs/sandbox_application_guide.md.
import { SandboxApplication, SandboxController } from 'runspace-sandbox';
class CalculatorApp implements SandboxApplication {
controller?: SandboxController;
render() {
// Create calculator UI
const display = document.createElement('div');
display.id = 'display';
const addButton = document.createElement('button');
addButton.textContent = '+';
addButton.onclick = () => this.calculate('add');
document.body.append(display, addButton);
// Listen to runtime events
this.controller?.on('stdout', (data) => {
console.log('Output:', data);
});
}
async calculate(operation: string) {
if (!this.controller) return;
const a = 5;
const b = 3;
const result = await this.controller.runFunction(operation, [a, b]);
const display = document.getElementById('display');
if (display) {
display.textContent = `Result: ${result}`;
}
}
}Project Structure
sandbox/
├── packages/ # Core library source code
│ ├── core/ # Core components
│ │ ├── Sandbox.ts # Main orchestrator
│ │ ├── SandboxBridge.ts # Communication layer
│ │ ├── SandboxController.ts # Runtime controller
│ │ ├── PyodideRuntime.ts # Pyodide runtime implementation
│ │ ├── types.ts # TypeScript type definitions
│ │ └── __tests__/ # Unit tests
│ ├── builtin/ # Built-in applications
│ │ └── apps/
│ │ └── console/ # ConsoleApp (headless, auto-runs main)
│ └── index.ts # Public API exports
├── examples/ # Example applications
│ ├── calculator/ # Calculator demo (interactive UI)
│ └── console/ # Console demo (auto-run main)
├── docs/ # Documentation
│ ├── architecture.md # Architecture overview
│ ├── sandbox_application_guide.md # Guide for Sandbox Application creation
│ ├── console_app_guide.md # Guide for ConsoleApp usage
│ ├── workspace_host_guide.md # Guide for Workspace Host creation
│ └── diagrams/ # PlantUML diagrams
├── dist/ # Compiled output
└── package.json # Package configurationDevelopment
Build
Compile TypeScript to JavaScript with type declarations:
npm run buildTest
Run the test suite:
npm testType Check
Verify TypeScript types without compilation:
npm run typecheckDevelopment Server
Run the calculator example with hot reload:
npm run devRun the console example:
npm run dev:consoleClean
Remove build artifacts:
npm run cleanMessage Protocol
The sandbox uses a typed message protocol for communication:
Message Types
- READY_FOR_INIT: Sandbox is loaded and waiting for initialization
- INIT: Command to initialize the Sandbox runtime
- SETUP_STREAM: Transfers a MessagePort for streaming output
- INITIALIZED: Sandbox initialization is complete
- LOAD_CODE: Load user code files
- CODE_LOADED: Code successfully loaded
Message Format
interface BaseMessage {
type: MessageType;
id?: string;
}
interface LoadCodeMessage extends BaseMessage {
type: 'LOAD_CODE';
payload: {
workspace: {
files: Array<{
path: string;
content: string;
}>;
config?: {
build?: Array<{ command: string; args?: string[]; }>;
run?: Array<{ command: string; args?: string[]; }>;
entryPoints?: string[];
};
};
};
}Execution Flow
Initialization Sequence
- WorkspaceHost loads sandbox iframe
- SandboxBridge sends
READY_FOR_INITto WorkspaceHost - WorkspaceHost sends
INITto SandboxBridge - SandboxBridge initializes Controller and Runtime
- Runtime signals ready state
- SandboxBridge sets up streaming and sends
SETUP_STREAM(with port) to WorkspaceHost - SandboxBridge sends
INITIALIZEDto WorkspaceHost
Code Execution Flow
- User interacts with SandboxApplication UI
- Application calls
controller.runFunction(name, args) - Controller forwards request to runtime
- Runtime executes code and returns result
- Controller returns result to application
- Runtime output (stdout/stderr) streamed via MessageChannel to WorkspaceHost
- Controller also emits events for local subscribers
Examples
Calculator Example
A fully functional calculator demonstrating:
- Custom SandboxApplication implementation
- Python code execution via Pyodide
- Real-time UI updates
- Error handling and output streaming
Run the example:
npm run devThen open your browser to the displayed URL.
API Reference
Sandbox
class Sandbox {
constructor(config: SandboxConfig);
initialize(): Promise<void>;
}
interface SandboxConfig {
runtime: new () => Runtime;
app?: SandboxApplication;
formatters?: OutputFormatter;
targetWindow?: Window;
targetOrigin?: string;
}
interface OutputFormatter {
stdout?(output: unknown): unknown;
stderr?(output: string): string;
}SandboxController
interface SandboxController {
runFunction(name: string, args: unknown[]): Promise<unknown>;
on(event: 'stdout' | 'stderr' | 'error' | 'code_loaded', handler: (message: string) => void): void;
isReady(): boolean;
isCodeLoaded(): boolean;
}Runtime
interface Runtime {
initialize(): Promise<void>;
loadCode(workspace: Workspace): Promise<void>;
runFunction(name: string, args: unknown[]): Promise<unknown>;
getStdout(): string;
getStderr(): string;
clearOutput(): void;
isReady(): boolean;
}
interface Workspace {
files: WorkspaceFile[];
config?: WorkspaceConfig;
}
interface WorkspaceFile {
path: string;
content: string;
}
interface WorkspaceConfig {
build?: PipelineStep[];
run?: PipelineStep[];
entryPoints?: string[];
}
interface PipelineStep {
command: string;
args?: string[];
}SandboxApplication
interface SandboxApplication {
controller?: SandboxController;
render(): void;
onCodeLoaded?(): void; // Optional lifecycle hook
}Testing
The project includes comprehensive test coverage:
- Unit Tests: Test individual components in isolation
- Integration Tests: Verify component interactions
- Runtime Tests: Validate code execution behavior
Test files are located in packages/__tests__/.
TypeScript Support
Full TypeScript support with exported type definitions:
import type {
Runtime,
SandboxApplication,
SandboxController,
SandboxConfig,
SandboxMessage,
Workspace,
WorkspaceFile,
WorkspaceConfig,
PipelineStep,
OutputFormatter
} from 'sandbox';Browser Compatibility
- Modern browsers with ES2020+ support
- WebAssembly support required for PyodideRuntime
- postMessage API for iframe communication
TODO
- [x] Add configurable entry point for
PyodideRuntime.loadCode()via Workspace config (build/run pipelines, entryPoints) - [x] Support console-only applications without UI rendering (pure stdin/stdout/stderr streaming)
- [x] Implement stdin streaming to sandbox runtime
- [x] Add real-time stdout streaming during function execution (currently only emitted after completion)
Contributing
This is a private project. For questions or suggestions, contact the author.
License
UNLICENSED - Private project
