Skip to content

VtrixAI/sandbox-node

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

19 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

@vtrixai/sandbox

Node.js / TypeScript SDK for Vtrix sandbox — run commands and manage files in isolated Linux environments over a persistent WebSocket connection.

Installation

npm install @vtrixai/sandbox

Requires Node.js 18+

Quick Start

import { Client } from '@vtrixai/sandbox';

const client = new Client({
  baseURL:   'http://your-hermes-host:8080',
  token:     'your-token',
  projectID: 'your-project-id',
});

// Create a sandbox and wait for it to become active
const sb = await client.create({ user_id: 'user-123' });

// Run a command and get the result
const result = await sb.runCommand('echo hello && uname -a');
console.log(`exit_code=${result.exitCode}`);
console.log(result.output);

sb.close();

Core classes

Class What it does
Client Creates and manages sandbox instances
Sandbox Runs commands and manages files in an isolated environment
Command Handles a running or completed process
CommandFinished Result after a command completes — extends Command with exitCode and output

Client

new Client(opts: ClientOptions)

Creates a new client. The client is reusable and safe for concurrent use across multiple sandbox sessions.

Field Type Required Description
baseURL string Yes Hermes gateway URL (e.g. http://host:8080).
token string No Bearer token for authentication.
projectID string No Value sent as X-Project-ID header.
const client = new Client({
  baseURL:   'http://your-hermes-host:8080',
  token:     'your-token',
  projectID: 'your-project-id',
});

await client.create(opts) → Sandbox

Use client.create() to launch a new sandbox, poll until it is active, and open a WebSocket connection. This is the primary entry point for starting a sandbox session. Pass env to set default environment variables that all commands in this sandbox will inherit.

Returns: Promise<Sandbox>

Parameter Type Required Description
opts.user_id string Yes Owner of the sandbox.
opts.spec Spec No Resource spec (cpu, memory, image).
opts.labels Record<string, string> No Arbitrary key-value metadata attached to the sandbox.
opts.payloads Payload[] No Initialisation calls sent to the pod after creation.
opts.ttl_hours number No Sandbox lifetime in hours. Uses the server default when 0.
opts.env Record<string, string> No Default environment variables inherited by all commands. Per-command RunOptions.env values override these.
const sb = await client.create({
  user_id: 'user-123',
  spec: { cpu: '2', memory: '4Gi' },
  ttl_hours: 2,
  env: { NODE_ENV: 'production' },
});

await client.attach(sandboxId) → Sandbox

Use client.attach() to connect to an existing sandbox without creating a new one. Use this to resume a session after a restart or to connect from a different process. Auth uses the client-level token and project ID.

Returns: Promise<Sandbox>

Parameter Type Required Description
sandboxId string Yes ID of the sandbox to connect to.
const sb = await client.attach('sandbox-id-abc');

await client.list(opts?) → ListResult

Use client.list() to enumerate sandboxes visible to the current credentials. Filter by user_id or status to scope results.

Returns: Promise<ListResult>.items is SandboxInfo[], .pagination has total, limit, offset, has_more.

Parameter Type Required Description
opts.user_id string No Return only sandboxes owned by this user.
opts.status string No Filter by status: "active", "stopped", etc.
opts.limit number No Maximum number of results.
opts.offset number No Pagination offset.
const { items, pagination } = await client.list({ user_id: 'user-123', status: 'active' });
console.log(`Found ${pagination.total} sandboxes`);

await client.get(sandboxId) → SandboxInfo

Use client.get() to fetch metadata for a sandbox by ID without opening a WebSocket connection.

Returns: Promise<SandboxInfo>

const info = await client.get('sandbox-id-abc');
console.log(info.status);

await client.delete(sandboxId)

Call client.delete() to permanently delete a sandbox. This cannot be undone.

Returns: Promise<void>

await client.delete('sandbox-id-abc');

Sandbox

A Sandbox instance gives you full control over an isolated environment. You receive one from client.create() or client.attach().

Properties

sandbox.createdAt → Date

The createdAt property returns the sandbox creation time parsed from info.created_at. Returns a new Date object on each access.

Returns: Date

console.log(sb.createdAt.toISOString());

sandbox.status → string

The status property reports the cached lifecycle state of the sandbox. Call await sandbox.refresh() first if you need a live value.

Returns: string"active", "stopped", "destroying", etc.

console.log(sb.status);

sandbox.expireAt → string

The expireAt property returns the cached expiry timestamp. Call await sandbox.refresh() first for an accurate value.

Returns: string — RFC 3339 timestamp.

console.log(sb.expireAt);

sandbox.timeout → number

The timeout property returns the remaining sandbox lifetime in milliseconds based on the cached expireAt. Returns 0 if the sandbox has already expired.

Returns: number — milliseconds remaining; 0 if expired.

if (sb.timeout < 60_000) {
  await sb.extend(3600); // extend by 3600 seconds (1 hour); Atlas API uses seconds
}

Running Commands

await sandbox.runCommand(cmd, args?, opts?) → CommandFinished

sandbox.runCommand() executes a command inside the sandbox and blocks until it finishes.

Set opts.stdout or opts.stderr to receive output in real time while still blocking — useful for progress logging.

Returns: Promise<CommandFinished>

Parameter Type Required Description
cmd string Yes Shell command to run.
args string[] No Arguments shell-quoted and appended to cmd. Prevents injection.
opts.working_dir string No Working directory inside the sandbox.
opts.timeout_sec number No Kill the command after this many seconds.
opts.env Record<string, string> No Per-command environment variables. Merges with sandbox defaults.
opts.sudo boolean No Prepend sudo -E to the command.
opts.stdin string No Data written to the command's stdin before reading output.
opts.stdout NodeJS.WritableStream No Receives stdout chunks as they arrive.
opts.stderr NodeJS.WritableStream No Receives stderr chunks as they arrive.
const result = await sb.runCommand('npm install', undefined, {
  working_dir: '/app',
  stdout: process.stdout,
  stderr: process.stderr,
});
console.log(`exit_code=${result.exitCode}`);

await sandbox.runCommandDetached(cmd, args?, opts?) → Command

Use sandbox.runCommandDetached() to start a command in the background and return immediately. Use this for long-running processes such as servers where you want to do other work while the command runs, then call cmd.wait() when you need the result.

Returns: Promise<Command>

const cmd = await sb.runCommandDetached('node server.js', undefined, {
  working_dir: '/app',
  env: { PORT: '8080' },
});
// ... do other work ...
const finished = await cmd.wait();

for await (const ev of sandbox.runCommandStream(cmd, args?, opts?)) → AsyncGenerator<ExecEvent>

Use sandbox.runCommandStream() to run a command and stream ExecEvent values in real time. Use this instead of runCommand when you need to process stdout and stderr as separate, typed events — for example, to display them with different colours or route them to different log streams.

Returns: AsyncGenerator<ExecEvent>

ev.type Meaning
"start" Command has started executing.
"stdout" A chunk of standard output. Read from ev.data.
"stderr" A chunk of standard error. Read from ev.data.
"done" Command has finished.
for await (const ev of sb.runCommandStream('make build')) {
  if (ev.type === 'stdout') process.stdout.write(ev.data!);
  if (ev.type === 'stderr') process.stderr.write(ev.data!);
}

for await (const ev of sandbox.execLogs(cmdId)) → AsyncGenerator<ExecEvent>

Use sandbox.execLogs() to attach to a running or completed command and stream its output. It replays buffered output first (up to 512 KB), then streams live events for commands still running. Use this to replay logs from a detached command or to attach a second observer.

Returns: AsyncGenerator<ExecEvent>

Parameter Type Required Description
cmdId string Yes ID of the command to attach to.
for await (const ev of sb.execLogs(cmd.cmdId)) {
  console.log(`[${ev.type}] ${ev.data}`);
}

sandbox.getCommand(cmdId) → Command

Use sandbox.getCommand() to reconstruct a Command handle from a known cmdId. Use this to reconnect to a command started in a previous call without going through runCommandDetached again.

Returns: Command

Parameter Type Required Description
cmdId string Yes ID of the command to retrieve.
const cmd = sb.getCommand('cmd-id-abc');
const result = await cmd.wait();

await sandbox.kill(cmdId, signal?)

Call sandbox.kill() to send a signal to a running command by ID. The signal is sent to the entire process group, so child processes are also terminated. Send SIGTERM for graceful shutdown or SIGKILL for immediate termination.

Returns: Promise<void>

Parameter Type Required Description
cmdId string Yes ID of the command to signal.
signal string No Signal name: "SIGTERM" (default), "SIGKILL", "SIGINT", "SIGHUP".
await sb.kill(cmd.cmdId, 'SIGTERM');

Command

A Command represents a running or completed process. You receive one from sandbox.runCommandDetached() or sandbox.getCommand(). CommandFinished extends Command and adds exitCode and output.

Properties

Property Type Description
cmdId string Unique identifier for this command execution.
pid number Process ID inside the sandbox.
cwd string Working directory where the command is executing.
startedAt Date Timestamp when the command started.
exitCode number | null Exit status. null while the command is still running.

await command.wait() → CommandFinished

Use command.wait() to block until a detached command finishes and get the resulting CommandFinished object.

Returns: Promise<CommandFinished>exitCode, output, cmdId.

const cmd = await sb.runCommandDetached('node server.js');
// ... do other work ...
const result = await cmd.wait();
if (result.exitCode !== 0) {
  console.error('Command failed:', result.output);
}

for await (const ev of command.logs()) → AsyncGenerator<LogEvent>

Call command.logs() to stream structured log entries as they arrive. Each LogEvent has stream ("stdout" or "stderr") and data. Use this instead of sandbox.execLogs() when you already have a Command handle.

Returns: AsyncGenerator<LogEvent>

for await (const ev of cmd.logs()) {
  if (ev.stream === 'stdout') process.stdout.write(ev.data);
  else process.stderr.write(ev.data);
}

await command.stdout() → string

Use command.stdout() to collect the full standard output as a string.

Returns: Promise<string>

const output = await cmd.stdout();
const data = JSON.parse(output);

await command.stderr() → string

Use command.stderr() to collect the full standard error output as a string.

Returns: Promise<string>

const errors = await cmd.stderr();
if (errors) console.error('Command errors:', errors);

await command.collectOutput(stream) → string

Use command.collectOutput() to collect stdout, stderr, or both as a single string.

Returns: Promise<string>

Parameter Type Required Description
stream "stdout" | "stderr" | "both" Yes The output stream to collect.
const combined = await cmd.collectOutput('both');

await command.kill(signal?)

Call command.kill() to send a signal to this command. See sandbox.kill() for valid signal names.

Returns: Promise<void>

Parameter Type Required Description
signal string No Signal name: "SIGTERM" (default), "SIGKILL", "SIGINT", "SIGHUP".
await cmd.kill('SIGKILL');

File Operations

await sandbox.read(path) → ReadResult

Use sandbox.read() to read a file from the sandbox. Text files up to 200 KB are returned in full; larger files are truncated (truncated: true). Image files are detected automatically and returned as base64-encoded data with a MIME type. Throws if the file does not exist.

Returns: Promise<ReadResult>

Field Type Description
type "text" | "image" Type of the file.
content string File content (text files).
truncated boolean true if the file was larger than 200 KB. Use readStream for the full content.
data string Base64-encoded bytes (image files).
mime_type string MIME type (image files, e.g. "image/png").
const result = await sb.read('/app/config.json');
if (result.truncated) {
  // use readStream for the full file
}
console.log(result.content);

await sandbox.write(path, content) → WriteResult

Use sandbox.write() to write a text string to a file. Creates parent directories automatically. Returns the number of bytes written.

Returns: Promise<WriteResult>.bytes_written.

Parameter Type Required Description
path string Yes Destination path inside the sandbox.
content string Yes Text content to write.
const result = await sb.write('/app/config.json', JSON.stringify(config));
console.log(`Wrote ${result.bytes_written} bytes`);

await sandbox.edit(path, oldText, newText) → EditResult

Use sandbox.edit() to replace an exact occurrence of oldText with newText inside a file. Throws if oldText appears zero times or more than once — ensuring the edit is unambiguous.

Returns: Promise<EditResult>.message.

Parameter Type Required Description
path string Yes Path to the file inside the sandbox.
oldText string Yes The exact text to find and replace.
newText string Yes The text to substitute in its place.
await sb.edit('/app/config.json', '"port": 3000', '"port": 8080');

await sandbox.writeFiles(files)

Use sandbox.writeFiles() to upload one or more binary files in a single round trip. Creates parent directories automatically. Use this for uploading compiled binaries, images, or executable scripts.

Returns: Promise<void>

Parameter Type Required Description
files[].path string Yes Destination path inside the sandbox.
files[].content Buffer | Uint8Array Yes Raw file bytes.
files[].mode number No Unix permission bits (e.g. 0o755 for executable). Uses server default when omitted.
await sb.writeFiles([
  { path: '/app/run.sh', content: Buffer.from(script), mode: 0o755 },
  { path: '/app/data.bin', content: dataBuffer },
]);

await sandbox.readToBuffer(path) → Buffer | null

Use sandbox.readToBuffer() to read a file into memory as a Buffer. Returns null (not an error) when the file does not exist, making it easy to check for optional files without try/catch.

Returns: Promise<Buffer | null>null if the file does not exist.

Parameter Type Required Description
path string Yes File path inside the sandbox.
const buf = await sb.readToBuffer('/app/output.bin');
if (buf !== null) {
  process(buf);
}

for await (const chunk of sandbox.readStream(path, chunkSize?)) → AsyncGenerator<Buffer>

Use sandbox.readStream() to read a large file in chunks. Use this instead of read when the file exceeds 200 KB or you need complete binary content without truncation. Each chunk is already decoded (base64 decoding is handled internally).

Returns: AsyncGenerator<Buffer>

Parameter Type Required Description
path string Yes File path inside the sandbox.
chunkSize number No Bytes per chunk. Defaults to 65536.
import { createWriteStream } from 'fs';
const out = createWriteStream('large.csv');
for await (const chunk of sb.readStream('/data/large.csv')) {
  out.write(chunk);
}
out.end();

await sandbox.mkDir(path)

Use sandbox.mkDir() to create a directory and all parent directories. Safe to call on paths that already exist.

Returns: Promise<void>

Parameter Type Required Description
path string Yes Directory to create.
await sb.mkDir('/app/logs');

await sandbox.listFiles(path) → FileEntry[]

Use sandbox.listFiles() to list the contents of a directory. Throws if the path does not exist or is not a directory.

Returns: Promise<FileEntry[]> — each entry has name, path, size, is_dir, modified_at (RFC 3339 string or undefined).

Parameter Type Required Description
path string Yes Directory path inside the sandbox.
const entries = await sb.listFiles('/app');
for (const entry of entries) {
  console.log(`${entry.is_dir ? 'd' : 'f'} ${entry.name}`);
}

await sandbox.stat(path) → FileInfo

Use sandbox.stat() to get metadata for a path. Unlike most operations, this does not throw when the path does not exist — check info.exists instead.

Returns: Promise<FileInfo>

Field Type Description
exists boolean false when the path does not exist.
is_file boolean true for regular files.
is_dir boolean true for directories.
size number File size in bytes.
modified_at string | undefined RFC 3339 timestamp, or undefined.
const info = await sb.stat('/app/config.json');
if (!info.exists) {
  await sb.write('/app/config.json', '{}');
}

await sandbox.exists(path) → boolean

Use sandbox.exists() to check whether a path exists. A convenient shorthand for stat when you only need the existence check.

Returns: Promise<boolean>

Parameter Type Required Description
path string Yes Path to check.
if (await sb.exists('/app/config.json')) {
  // ...
}

await sandbox.uploadFile(localPath, sandboxPath, opts?)

Use sandbox.uploadFile() to upload a file from the local filesystem into the sandbox.

Returns: Promise<void>

Parameter Type Required Description
localPath string Yes Absolute path on the local machine.
sandboxPath string Yes Destination path inside the sandbox.
opts.mkdirRecursive boolean No Create parent directories on the sandbox side if they do not exist.
await sb.uploadFile('/local/model.bin', '/app/model.bin', { mkdirRecursive: true });

await sandbox.downloadFile(sandboxPath, localPath, opts?) → string | null

Use sandbox.downloadFile() to download a file from the sandbox to the local filesystem. Returns the absolute local path on success, or null when the sandbox file does not exist.

Returns: Promise<string | null>null if the file does not exist.

Parameter Type Required Description
sandboxPath string Yes Path to the file inside the sandbox.
localPath string Yes Destination path on the local machine.
opts.mkdirRecursive boolean No Create local parent directories if they do not exist.
const dst = await sb.downloadFile('/app/output.json', '/tmp/output.json');
if (dst !== null) {
  console.log(`Saved to ${dst}`);
}

await sandbox.downloadFiles(entries) → Map<string, string | null>

Use sandbox.downloadFiles() to download multiple files in one call (up to 8 concurrent). Returns a Map of sandbox path → local path for each file.

Returns: Promise<Map<string, string | null>>

const results = await sb.downloadFiles([
  { sandboxPath: '/app/out.json', localPath: '/tmp/out.json' },
  { sandboxPath: '/app/log.txt', localPath: '/tmp/log.txt' },
]);

sandbox.domain(port) → string

Use sandbox.domain() to get the publicly accessible URL for an exposed port. The sandbox must be created with this port declared.

Returns: string

Parameter Type Required Description
port number Yes Port number to resolve.
const url = sb.domain(3000);
console.log(`App running at ${url}`);

Lifecycle

await sandbox.refresh()

Call sandbox.refresh() to re-fetch sandbox metadata from the server and update the cached values. Call this before reading sandbox.status or sandbox.expireAt if you need current values.

Returns: Promise<void>

await sb.refresh();
console.log(sb.status);

await sandbox.stop(opts?)

Call sandbox.stop() to pause the sandbox without deleting it. Set opts.blocking: true to wait until the sandbox reaches "stopped" or "failed" status before returning.

Returns: Promise<void>

Parameter Type Required Description
opts.blocking boolean No Poll until the sandbox has stopped.
opts.pollIntervalMs number No How often to poll in milliseconds. Defaults to 2000.
opts.timeoutMs number No Maximum time to wait in milliseconds. Defaults to 300000.
await sb.stop({ blocking: true });

await sandbox.start()

Use sandbox.start() to resume a stopped sandbox.

Returns: Promise<void>

await sb.start();

await sandbox.restart()

Use sandbox.restart() to stop and restart the sandbox.

Returns: Promise<void>

await sb.restart();

await sandbox.extend(seconds)

Use sandbox.extend() to extend the sandbox TTL by seconds (Atlas POST .../extend, body field seconds). Must be in (0, MAX_EXTEND_SECONDS] (86400).

Returns: Promise<void>

Parameter Type Required Description
seconds number Yes Seconds to add (1–86400).
import { MAX_EXTEND_SECONDS } from '@vtrixai/sandbox';
await sb.extend(2 * 3600); // extend by 2 hours

await sandbox.extendTimeout(seconds)

Use sandbox.extendTimeout() to extend the TTL and immediately refresh the cached info in one call.

Returns: Promise<void>

await sb.extendTimeout(3600); // +3600s, then refresh

await sandbox.update(opts)

Use sandbox.update() to change the sandbox spec or image only (Atlas PATCH /api/v1/sandbox/:id). For payloads, use configure().

Returns: Promise<void>

Parameter Type Required Description
opts.spec Spec No New resource spec.
opts.image string No New container image tag.
await sb.update({ spec: { cpu: '4', memory: '8Gi' } });

await sandbox.configure(payloads?)

Call sandbox.configure() to immediately apply the current configuration to the running pod. Optionally override the stored payloads for this apply only.

Returns: Promise<void>

await sb.configure();

await sandbox.delete()

Call sandbox.delete() to permanently delete the sandbox. This cannot be undone.

Returns: Promise<void>

await sb.delete();

sandbox.close()

Call sandbox.close() to close the WebSocket connection. Call this when you are done with the sandbox to free the connection.

sb.close();

Examples

File Description
examples/basic.ts Create a sandbox, run commands, use detached execution
examples/stream.ts Real-time streaming, exec_logs replay, Command.logs/stdout
examples/attach.ts Reconnect to an existing sandbox by ID
examples/files.ts Read, write, edit, upload, download, and stream files
examples/lifecycle.ts Stop, start, extend, update, and delete sandboxes
npx ts-node examples/basic.ts

License

MIT — see LICENSE.

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors