The Sandbox class is the recommended way to use Lifo. It provides a high-level, ergonomic API that orchestrates the Kernel, VFS, Shell, and Command Registry.
Quick Start
import { Sandbox } from '@lifo-sh/core';
const sandbox = await Sandbox.create();
// Run commands
const { stdout } = await sandbox.commands.run('ls -la');
console.log(stdout);
// Filesystem operations
await sandbox.fs.writeFile('/data.json', '{"x":42}');
const data = await sandbox.fs.readFile('/data.json', 'utf8');
// Access internals
sandbox.kernel.vfs.stat('/data.json');
sandbox.shell.getCwd();
Class Definition
export class Sandbox {
readonly commands: SandboxCommands; // Command execution API
readonly fs: SandboxFs; // Filesystem API
readonly env: Record<string, string>; // Environment variables
// Power-user escape hatches
readonly kernel: Kernel;
readonly shell: Shell;
// Current working directory
get cwd(): string;
set cwd(path: string);
static async create(options?: SandboxOptions): Promise<Sandbox>
// Native filesystem mounting (Node.js only)
mountNative(virtualPath: string, hostPath: string, options?: {...}): void
unmountNative(virtualPath: string): void
// Visual mode (attach to terminal)
attach(terminal: ITerminal): void
detach(): void
// Snapshots
async exportSnapshot(): Promise<Uint8Array>
async importSnapshot(data: Uint8Array): Promise<void>
destroy(): void
}
Location: src/sandbox/Sandbox.ts:24
Creation Options
interface SandboxOptions {
// Initial working directory
cwd?: string;
// Environment variables (merged with defaults)
env?: Record<string, string>;
// Pre-populate files
files?: Record<string, string>;
// Enable persistence (default: false for programmatic use)
persist?: boolean;
// Visual mode: provide a terminal instance
terminal?: ITerminal;
// Mount native filesystem directories (Node.js only)
mounts?: Array<{
virtualPath: string;
hostPath: string;
readOnly?: boolean;
fsModule?: NativeFsModule;
}>;
}
See src/sandbox/types.ts:1-18.
Example: Headless Mode
const sandbox = await Sandbox.create({
cwd: '/home/user/project',
files: {
'/home/user/project/README.md': '# My Project',
'/home/user/project/package.json': JSON.stringify({
name: 'my-app',
version: '1.0.0',
}),
},
env: {
NODE_ENV: 'development',
},
});
Example: Visual Mode
import { Terminal } from 'xterm';
import 'xterm/css/xterm.css';
const terminal = new Terminal();
terminal.open(document.getElementById('terminal'));
const sandbox = await Sandbox.create({
terminal,
persist: true, // Save filesystem to IndexedDB
});
// The terminal is now interactive, user can type commands
Example: Native Filesystem Mounts
// Node.js only
import * as fs from 'node:fs';
const sandbox = await Sandbox.create({
mounts: [
{
virtualPath: '/mnt/project',
hostPath: '/home/user/code/my-project',
readOnly: false,
fsModule: fs, // Explicit fs module (required for bundlers)
},
],
});
// Now all VFS operations under /mnt/project delegate to the host filesystem
const content = await sandbox.fs.readFile('/mnt/project/package.json', 'utf8');
See Native Filesystem Mounts for details.
Boot Sequence
When you call Sandbox.create(), the following happens:
Step 1: Create and Boot Kernel
const kernel = new Kernel();
await kernel.boot({ persist: options?.persist ?? false });
- Creates VFS
- Loads persisted state (if enabled)
- Initializes standard directories (
/bin, /etc, /home, etc.)
- Mounts virtual providers (
/proc, /dev)
- Sets up persistence watchers
See src/sandbox/Sandbox.ts:67-68.
Step 2: Create Command Registry
const registry = createDefaultRegistry();
bootLifoPackages(kernel.vfs, registry);
Registers all built-in commands and scans /usr/share/pkg/node_modules for installed packages.
See src/sandbox/Sandbox.ts:71-72.
Step 3: Pre-populate Files
if (options?.files) {
for (const [path, content] of Object.entries(options.files)) {
ensureParentDirs(kernel.vfs, path);
kernel.vfs.writeFile(path, content);
}
}
Creates directories as needed.
See src/sandbox/Sandbox.ts:75-80.
Step 4: Setup Environment
const defaultEnv = kernel.getDefaultEnv();
const env = { ...defaultEnv, ...options?.env };
if (options?.cwd) {
env.PWD = options.cwd;
}
Merges default environment with user overrides.
See src/sandbox/Sandbox.ts:83-87.
Step 5: Create Terminal
let shellTerminal: ITerminal;
if (options?.terminal) {
shellTerminal = options.terminal; // Visual mode
} else {
shellTerminal = new HeadlessTerminal(); // Programmatic mode
}
Headless terminal silently captures output.
See src/sandbox/Sandbox.ts:90-104.
Step 6: Create Shell
const shell = new Shell(shellTerminal, kernel.vfs, registry, env);
See src/sandbox/Sandbox.ts:107.
Step 7: Register Factory Commands
Some commands need access to runtime state:
registry.register('ps', createPsCommand(jobTable));
registry.register('top', createTopCommand(jobTable));
registry.register('kill', createKillCommand(jobTable));
registry.register('watch', createWatchCommand(registry));
registry.register('help', createHelpCommand(registry));
registry.register('node', createNodeCommand(kernel.portRegistry));
registry.register('curl', createCurlCommand(kernel.portRegistry));
registry.register('npm', createNpmCommand(registry, shellExecute));
See src/sandbox/Sandbox.ts:110-130.
Step 8: Source Configuration Files
await shell.sourceFile('/etc/profile');
await shell.sourceFile(env.HOME + '/.bashrc');
Runs shell initialization scripts.
See src/sandbox/Sandbox.ts:133-134.
Step 9: Start Shell (Visual Mode Only)
if (isVisual) {
shell.start(); // Enable interactive input handling
shellTerminal.focus();
}
See src/sandbox/Sandbox.ts:142-145.
Step 10: Mount Native Filesystems
if (options?.mounts) {
for (const mount of options.mounts) {
sandbox.mountNative(mount.virtualPath, mount.hostPath, {...});
}
}
See src/sandbox/Sandbox.ts:155-162.
Commands API
The sandbox.commands object provides command execution:
interface SandboxCommands {
// Run a command and capture output
run(command: string, options?: RunOptions): Promise<RunResult>;
// Check if a command exists
exists(name: string): Promise<boolean>;
// List all available commands
list(): string[];
}
interface RunOptions {
cwd?: string;
env?: Record<string, string>;
stdin?: string;
}
interface RunResult {
stdout: string;
stderr: string;
exitCode: number;
}
See src/sandbox/types.ts:20-31 and src/sandbox/SandboxCommands.ts.
Example: Running Commands
// Simple execution
const { stdout, exitCode } = await sandbox.commands.run('ls -la');
// With options
const result = await sandbox.commands.run('node script.js', {
cwd: '/home/user/project',
env: { NODE_ENV: 'production' },
stdin: JSON.stringify({ input: 'data' }),
});
if (result.exitCode !== 0) {
console.error('Command failed:', result.stderr);
}
Example: Checking Command Existence
if (await sandbox.commands.exists('git')) {
await sandbox.commands.run('git status');
}
Example: Listing Commands
const commands = sandbox.commands.list();
// ['ls', 'cat', 'grep', 'sed', 'awk', 'git', 'npm', 'node', ...]
Filesystem API
The sandbox.fs object provides filesystem operations:
interface SandboxFs {
// File operations
readFile(path: string): Promise<Uint8Array>;
readFile(path: string, encoding: 'utf8'): Promise<string>;
writeFile(path: string, content: string | Uint8Array): Promise<void>;
appendFile(path: string, content: string | Uint8Array): Promise<void>;
unlink(path: string): Promise<void>;
rename(oldPath: string, newPath: string): Promise<void>;
exists(path: string): Promise<boolean>;
stat(path: string): Promise<Stat>;
// Directory operations
mkdir(path: string, options?: { recursive?: boolean }): Promise<void>;
rmdir(path: string, options?: { recursive?: boolean }): Promise<void>;
readdir(path: string): Promise<Array<{ name: string; type: FileType }>>;
// Snapshots
exportSnapshot(): Promise<Uint8Array>;
importSnapshot(data: Uint8Array): Promise<void>;
}
See src/sandbox/types.ts:33-52 and src/sandbox/SandboxFs.ts.
Note: All methods are async (return promises) even though the underlying VFS is synchronous. This provides a consistent API and allows for future async implementations.
Example: Reading Files
// Binary read
const bytes = await sandbox.fs.readFile('/image.png');
// Text read
const text = await sandbox.fs.readFile('/README.md', 'utf8');
// JSON read
const data = JSON.parse(
await sandbox.fs.readFile('/data.json', 'utf8')
);
Example: Writing Files
// Text write
await sandbox.fs.writeFile('/hello.txt', 'Hello, world!');
// Binary write
await sandbox.fs.writeFile('/data.bin', new Uint8Array([0x00, 0xFF]));
// JSON write
await sandbox.fs.writeFile(
'/config.json',
JSON.stringify({ theme: 'dark' }, null, 2)
);
Example: Directory Operations
// Create directory (recursive)
await sandbox.fs.mkdir('/path/to/nested/dir', { recursive: true });
// List directory
const entries = await sandbox.fs.readdir('/home/user');
for (const entry of entries) {
console.log(entry.name, entry.type);
}
// Remove directory (recursive)
await sandbox.fs.rmdir('/old/dir', { recursive: true });
Example: Checking Existence
if (await sandbox.fs.exists('/config.json')) {
const config = await sandbox.fs.readFile('/config.json', 'utf8');
// ...
}
Example: Getting Stats
const stat = await sandbox.fs.stat('/data.json');
console.log('Size:', stat.size);
console.log('Modified:', new Date(stat.mtime));
console.log('Type:', stat.type); // 'file' | 'directory'
Environment Variables
// Read
console.log(sandbox.env.HOME); // /home/user
// Write
sandbox.env.DEBUG = '1';
// Used by commands
const { stdout } = await sandbox.commands.run('echo $DEBUG');
// stdout: "1\n"
Changes to sandbox.env affect subsequent command execution.
Current Working Directory
// Read
console.log(sandbox.cwd); // /home/user
// Write
sandbox.cwd = '/tmp';
// Used by commands
await sandbox.commands.run('pwd');
// stdout: "/tmp\n"
Native Filesystem Mounts
Sandbox supports mounting real host directories into the VFS (Node.js/Deno/Bun only):
sandbox.mountNative('/mnt/project', '/home/user/code/my-project', {
readOnly: false,
});
// Now all VFS operations delegate to the host filesystem
const pkg = await sandbox.fs.readFile('/mnt/project/package.json', 'utf8');
await sandbox.fs.writeFile('/mnt/project/output.txt', 'result');
// Commands also work
await sandbox.commands.run('ls /mnt/project');
await sandbox.commands.run('git status', { cwd: '/mnt/project' });
See src/sandbox/Sandbox.ts:179-209.
How It Works
mountNative() creates a NativeFsProvider
- The provider wraps Node.js
fs module
- All VFS operations under
/mnt/project delegate to the provider
- The provider translates VFS calls to
fs.readFileSync(), etc.
- Paths are resolved relative to the host path
See src/kernel/vfs/providers/NativeFsProvider.ts.
Read-Only Mounts
sandbox.mountNative('/data', '/host/readonly-data', {
readOnly: true,
});
// Reads work
const data = await sandbox.fs.readFile('/data/file.txt', 'utf8');
// Writes fail
await sandbox.fs.writeFile('/data/output.txt', 'test');
// Error: read-only filesystem
Custom fs Module (for Bundlers)
Bundlers (webpack, esbuild) don’t handle dynamic require(). Provide the fs module explicitly:
import * as fs from 'node:fs';
sandbox.mountNative('/project', '/host/path', {
fsModule: fs,
});
Unmounting
sandbox.unmountNative('/mnt/project');
// Now /mnt/project is back to the in-memory VFS
Snapshots
Snapshots allow exporting/importing the entire filesystem as a tar.gz archive:
Exporting
const snapshot = await sandbox.exportSnapshot();
// Uint8Array (gzip-compressed tar)
// Save to file (Node.js)
fs.writeFileSync('snapshot.tar.gz', snapshot);
// Download in browser
const blob = new Blob([snapshot], { type: 'application/gzip' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'lifo-snapshot.tar.gz';
a.click();
Importing
// Load from file (Node.js)
const snapshot = fs.readFileSync('snapshot.tar.gz');
await sandbox.importSnapshot(snapshot);
// Upload in browser
const input = document.createElement('input');
input.type = 'file';
input.onchange = async (e) => {
const file = e.target.files[0];
const buffer = await file.arrayBuffer();
await sandbox.importSnapshot(new Uint8Array(buffer));
};
input.click();
See src/sandbox/SandboxFs.ts:150-167.
Snapshots are useful for distributing pre-configured environments, backing up work, or migrating between devices.
Visual Mode (Terminal Attachment)
A headless sandbox can be attached to a terminal later:
// Create headless
const sandbox = await Sandbox.create();
// Later: attach to terminal
const terminal = new Terminal();
terminal.open(document.getElementById('terminal'));
sandbox.attach(terminal);
// The terminal is now interactive
See src/sandbox/Sandbox.ts:224-230.
Current limitations:
attach() displays the MOTD but doesn’t fully integrate with the shell
- Interactive input requires creating with
terminal option
detach() is a no-op placeholder
Full hot-attach/detach is planned for a future release.
Lifecycle Management
Destroying a Sandbox
sandbox.destroy();
// Future operations will throw
try {
await sandbox.commands.run('ls');
} catch (e) {
console.error('Sandbox is destroyed');
}
See src/sandbox/Sandbox.ts:256-258.
What destroy() does:
- Sets internal
_destroyed flag
- Future operations throw errors
- Does NOT clean up kernel/VFS (manual cleanup if needed)
Resource Cleanup
For long-running applications, consider:
// Clear command history
sandbox.shell.getJobTable().list().forEach(job => {
job.abortController.abort();
});
// Unmount native filesystems
sandbox.unmountNative('/mnt/project');
// Destroy sandbox
sandbox.destroy();
Advanced: Direct Kernel/Shell Access
For power users, the Sandbox exposes the underlying components:
// Direct VFS access
sandbox.kernel.vfs.writeFile('/file.txt', 'data');
const root = sandbox.kernel.vfs.getRoot();
// Direct shell access
sandbox.shell.tokenize('ls -la');
const cwd = sandbox.shell.getCwd();
// Port registry
sandbox.kernel.portRegistry.set(8080, (req, res) => {
res.statusCode = 200;
res.body = 'Hello!';
});
Direct access bypasses the promise-based Sandbox API. Only use for advanced use cases.
Error Handling
Command Errors
const result = await sandbox.commands.run('invalid-command');
if (result.exitCode !== 0) {
console.error('Command failed:', result.stderr);
// stderr: "invalid-command: command not found\n"
}
Filesystem Errors
try {
await sandbox.fs.readFile('/nonexistent.txt', 'utf8');
} catch (e) {
if (e instanceof VFSError && e.code === 'ENOENT') {
console.error('File not found');
}
}
Mount Errors
try {
sandbox.mountNative('/mnt/project', '/invalid/path');
} catch (e) {
console.error('Mount failed:', e.message);
}
Command Execution Overhead
Each sandbox.commands.run() call:
- Parses the command (~1ms)
- Sets up streams (~0.5ms)
- Executes command (varies)
- Captures output (negligible)
Tip: For many simple operations, use sandbox.fs instead:
// ❌ Slower
for (const file of files) {
await sandbox.commands.run(`cat ${file}`);
}
// ✅ Faster
for (const file of files) {
await sandbox.fs.readFile(file, 'utf8');
}
The VFS is in-memory, so operations are fast:
- File read/write: ~0.01-0.1ms
- Directory listing: ~0.1ms
- Large files (≥1MB): ~1-10ms (chunking overhead)
Tip: For large batch operations, use the underlying VFS directly:
const vfs = sandbox.kernel.vfs;
for (let i = 0; i < 10000; i++) {
vfs.writeFile(`/file${i}.txt`, `data ${i}`);
}
Complete Example
import { Sandbox } from '@lifo-sh/core';
import * as fs from 'node:fs';
async function main() {
// Create sandbox with native mount
const sandbox = await Sandbox.create({
cwd: '/project',
files: {
'/project/README.md': '# My Project\nBuilt with Lifo',
},
mounts: [
{
virtualPath: '/host',
hostPath: process.cwd(),
fsModule: fs,
},
],
});
// Run commands
console.log('\n=== Project Files ===');
const { stdout } = await sandbox.commands.run('ls -la /project');
console.log(stdout);
// Filesystem operations
console.log('\n=== README Content ===');
const readme = await sandbox.fs.readFile('/project/README.md', 'utf8');
console.log(readme);
// Create a script and run it
await sandbox.fs.writeFile('/project/hello.sh', `
#!/bin/sh
echo "Hello from Lifo!"
echo "CWD: $(pwd)"
echo "Files: $(ls -1 | wc -l)"
`.trim());
console.log('\n=== Running Script ===');
const result = await sandbox.commands.run('sh /project/hello.sh');
console.log(result.stdout);
// Export snapshot
console.log('\n=== Creating Snapshot ===');
const snapshot = await sandbox.exportSnapshot();
console.log(`Snapshot size: ${snapshot.length} bytes`);
fs.writeFileSync('snapshot.tar.gz', snapshot);
// Cleanup
sandbox.destroy();
}
main().catch(console.error);