un-inception/un.js
russell@unturf.com e433bc28c8 Add snapshot endpoints to Python and JavaScript SDKs
- session_snapshot(session_id): Create snapshot of session state
- service_snapshot(service_id): Create snapshot of service state
- list_snapshots(): List all available snapshots
- restore_snapshot(snapshot_id): Restore from snapshot
- delete_snapshot(snapshot_id): Delete a snapshot

All snapshot functions added to:
- Module-level exports (Python __all__, JS module.exports)
- Client class methods (for ease of use with stored credentials)
- Full JSDoc/docstring documentation

Agents should now include snapshot functions in all SDK refactoring work.
2026-01-15 15:37:23 -05:00

1195 lines
38 KiB
JavaScript

#!/usr/bin/env node
// PUBLIC DOMAIN - NO LICENSE, NO WARRANTY
//
// This is free public domain software for the public good of a permacomputer hosted
// at permacomputer.com - an always-on computer by the people, for the people. One
// which is durable, easy to repair, and distributed like tap water for machine
// learning intelligence.
//
// The permacomputer is community-owned infrastructure optimized around four values:
//
// TRUTH - First principles, math & science, open source code freely distributed
// FREEDOM - Voluntary partnerships, freedom from tyranny & corporate control
// HARMONY - Minimal waste, self-renewing systems with diverse thriving connections
// LOVE - Be yourself without hurting others, cooperation through natural law
//
// This software contributes to that vision by enabling code execution across 42+
// programming languages through a unified interface, accessible to all. Code is
// seeds to sprout on any abandoned technology.
//
// Learn more: https://www.permacomputer.com
//
// Anyone is free to copy, modify, publish, use, compile, sell, or distribute this
// software, either in source code form or as a compiled binary, for any purpose,
// commercial or non-commercial, and by any means.
//
// NO WARRANTY. THE SOFTWARE IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND.
//
// That said, our permacomputer's digital membrane stratum continuously runs unit,
// integration, and functional tests on all of it's own software - with our
// permacomputer monitoring itself, repairing itself, with minimal human in the
// loop guidance. Our agents do their best.
//
// Copyright 2025 TimeHexOn & foxhop & russell@unturf
// https://www.timehexon.com
// https://www.foxhop.net
// https://www.unturf.com/software
//
// unsandbox SDK for JavaScript/Node.js - Execute code in secure sandboxes
// https://unsandbox.com | https://api.unsandbox.com/openapi
//
// Library Usage:
// const un = require('./un.js');
// const result = await un.execute("javascript", 'console.log("Hello")');
// const job = await un.executeAsync("javascript", code);
// const result = await un.wait(job.job_id);
//
// CLI Usage:
// node un.js script.js
// node un.js -s javascript 'console.log("Hello")'
// node un.js session --shell node
//
// Authentication (in priority order):
// 1. Function arguments: execute(..., { publicKey, secretKey })
// 2. Environment variables: UNSANDBOX_PUBLIC_KEY + UNSANDBOX_SECRET_KEY
// 3. Config file: ~/.unsandbox/accounts.csv (public_key,secret_key per line)
/**
* unsandbox - Secure Code Execution SDK for JavaScript
*
* @example Simple execution
* const un = require('./un.js');
* const result = await un.execute("javascript", 'console.log("Hello World")');
* console.log(result.stdout);
*
* @example Async execution
* const job = await un.executeAsync("javascript", longRunningCode);
* const result = await un.wait(job.job_id);
*
* @example Client class
* const client = new un.Client({ publicKey: "unsb-pk-...", secretKey: "unsb-sk-..." });
* const result = await client.execute("javascript", code);
*/
const crypto = require('crypto');
const https = require('https');
const fs = require('fs');
const path = require('path');
const os = require('os');
// ============================================================================
// Configuration
// ============================================================================
const API_BASE = "https://api.unsandbox.com";
const PORTAL_BASE = "https://unsandbox.com";
const DEFAULT_TIMEOUT = 300000; // 5 minutes in ms
const DEFAULT_TTL = 60; // 1 minute execution limit
// Polling delays (ms) - exponential backoff matching un.c
const POLL_DELAYS = [300, 450, 700, 900, 650, 1600, 2000];
// Extension to language mapping
const EXT_MAP = {
".py": "python", ".js": "javascript", ".ts": "typescript",
".rb": "ruby", ".php": "php", ".pl": "perl", ".lua": "lua",
".sh": "bash", ".go": "go", ".rs": "rust", ".c": "c",
".cpp": "cpp", ".cc": "cpp", ".cxx": "cpp",
".java": "java", ".kt": "kotlin", ".cs": "csharp", ".fs": "fsharp",
".hs": "haskell", ".ml": "ocaml", ".clj": "clojure", ".scm": "scheme",
".lisp": "commonlisp", ".erl": "erlang", ".ex": "elixir", ".exs": "elixir",
".jl": "julia", ".r": "r", ".R": "r", ".cr": "crystal",
".d": "d", ".nim": "nim", ".zig": "zig", ".v": "v",
".dart": "dart", ".groovy": "groovy", ".scala": "scala",
".f90": "fortran", ".f95": "fortran", ".cob": "cobol",
".pro": "prolog", ".forth": "forth", ".4th": "forth",
".tcl": "tcl", ".raku": "raku", ".m": "objc", ".awk": "awk",
};
// ANSI colors
const BLUE = "\x1b[34m";
const RED = "\x1b[31m";
const GREEN = "\x1b[32m";
const YELLOW = "\x1b[33m";
const RESET = "\x1b[0m";
// ============================================================================
// Exceptions
// ============================================================================
class UnsandboxError extends Error {
constructor(message) {
super(message);
this.name = 'UnsandboxError';
}
}
class AuthenticationError extends UnsandboxError {
constructor(message) {
super(message);
this.name = 'AuthenticationError';
}
}
class ExecutionError extends UnsandboxError {
constructor(message, exitCode = null, stderr = null) {
super(message);
this.name = 'ExecutionError';
this.exitCode = exitCode;
this.stderr = stderr;
}
}
class APIError extends UnsandboxError {
constructor(message, statusCode = null, response = null) {
super(message);
this.name = 'APIError';
this.statusCode = statusCode;
this.response = response;
}
}
class TimeoutError extends UnsandboxError {
constructor(message) {
super(message);
this.name = 'TimeoutError';
}
}
// ============================================================================
// HMAC Authentication
// ============================================================================
/**
* Generate HMAC-SHA256 signature for API request.
* Signature = HMAC-SHA256(secretKey, "timestamp:METHOD:path:body")
*/
function signRequest(secretKey, timestamp, method, path, body = "") {
const message = `${timestamp}:${method}:${path}:${body}`;
return crypto.createHmac('sha256', secretKey)
.update(message)
.digest('hex');
}
/**
* Load credentials from accounts.csv file.
* @param {string} filepath - Path to accounts.csv
* @param {number} accountIndex - Account index (0-based)
* @returns {Object|null} { publicKey, secretKey } or null
*/
function loadAccountsCsv(filepath, accountIndex = 0) {
if (!fs.existsSync(filepath)) return null;
try {
const lines = fs.readFileSync(filepath, 'utf-8').trim().split('\n');
const validAccounts = [];
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith('#')) continue;
if (trimmed.includes(',')) {
const [pk, sk] = trimmed.split(',', 2);
if (pk.startsWith('unsb-pk-') && sk.startsWith('unsb-sk-')) {
validAccounts.push({ publicKey: pk, secretKey: sk });
}
}
}
if (validAccounts.length > accountIndex) {
return validAccounts[accountIndex];
}
} catch (e) {
// Ignore file read errors
}
return null;
}
/**
* Get API credentials in priority order:
* 1. Function arguments
* 2. Environment variables
* 3. ~/.unsandbox/accounts.csv
* 4. ./accounts.csv (same directory as this SDK)
*/
function getCredentials(publicKey = null, secretKey = null, accountIndex = 0) {
// Priority 1: Function arguments
if (publicKey && secretKey) {
return { publicKey, secretKey };
}
// Priority 2: Environment variables
const envPk = process.env.UNSANDBOX_PUBLIC_KEY;
const envSk = process.env.UNSANDBOX_SECRET_KEY;
if (envPk && envSk) {
return { publicKey: envPk, secretKey: envSk };
}
// Priority 3: ~/.unsandbox/accounts.csv
const homeAccounts = path.join(os.homedir(), '.unsandbox', 'accounts.csv');
let result = loadAccountsCsv(homeAccounts, accountIndex);
if (result) return result;
// Priority 4: ./accounts.csv (same directory as SDK)
const localAccounts = path.join(__dirname, 'accounts.csv');
result = loadAccountsCsv(localAccounts, accountIndex);
if (result) return result;
throw new AuthenticationError(
"No credentials found. Set UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY, " +
"or create ~/.unsandbox/accounts.csv or ./accounts.csv, or pass credentials to function."
);
}
// ============================================================================
// HTTP Client
// ============================================================================
/**
* Make authenticated API request with HMAC signature.
*/
function apiRequest(endpoint, options = {}) {
return new Promise((resolve, reject) => {
const {
method = "GET",
data = null,
bodyText = null,
contentType = "application/json",
publicKey = null,
secretKey = null,
timeout = DEFAULT_TIMEOUT,
} = options;
const creds = getCredentials(publicKey, secretKey);
const url = new URL(API_BASE + endpoint);
// Prepare body
let body = "";
if (bodyText !== null) {
body = bodyText;
} else if (data !== null) {
body = JSON.stringify(data);
}
// Generate signature
const timestamp = Math.floor(Date.now() / 1000);
const signature = signRequest(creds.secretKey, timestamp, method, endpoint, body);
const reqOptions = {
hostname: url.hostname,
port: 443,
path: url.pathname + url.search,
method: method,
headers: {
'Authorization': `Bearer ${creds.publicKey}`,
'X-Timestamp': timestamp.toString(),
'X-Signature': signature,
'Content-Type': contentType,
},
timeout: timeout,
};
const req = https.request(reqOptions, (res) => {
let responseBody = '';
res.on('data', chunk => responseBody += chunk);
res.on('end', () => {
if (res.statusCode >= 200 && res.statusCode < 300) {
try {
resolve(responseBody ? JSON.parse(responseBody) : {});
} catch (e) {
resolve(responseBody);
}
} else if (res.statusCode === 401) {
if (responseBody.toLowerCase().includes('timestamp')) {
reject(new AuthenticationError(
"Request timestamp expired. Your system clock may be out of sync. " +
"Run: sudo ntpdate -s time.nist.gov"
));
} else {
reject(new AuthenticationError(`Authentication failed: ${responseBody}`));
}
} else if (res.statusCode === 429) {
reject(new APIError(`Rate limit exceeded: ${responseBody}`, res.statusCode, responseBody));
} else {
reject(new APIError(`HTTP ${res.statusCode}: ${responseBody}`, res.statusCode, responseBody));
}
});
});
req.on('error', (e) => {
reject(new APIError(`Connection failed: ${e.message}`));
});
req.on('timeout', () => {
req.destroy();
reject(new TimeoutError('Request timeout'));
});
if (body) {
req.write(body);
}
req.end();
});
}
// ============================================================================
// Core Execution Functions
// ============================================================================
/**
* Execute code synchronously and return results.
*
* @param {string} language - Programming language (python, javascript, go, rust, etc.)
* @param {string} code - Source code to execute
* @param {Object} options - Optional parameters
* @param {Object} options.env - Environment variables dict
* @param {Array} options.inputFiles - List of {filename, content} or {filename, contentBase64}
* @param {string} options.networkMode - "zerotrust" (no network) or "semitrusted" (internet access)
* @param {number} options.ttl - Execution timeout in seconds (1-900, default 60)
* @param {number} options.vcpu - Virtual CPUs (1-8, default 1)
* @param {boolean} options.returnArtifact - Return compiled binary
* @param {boolean} options.returnWasmArtifact - Compile to WebAssembly
* @param {string} options.publicKey - API public key
* @param {string} options.secretKey - API secret key
* @param {number} options.timeout - HTTP request timeout in ms
* @returns {Promise<Object>} Result with stdout, stderr, exit_code, etc.
*
* @example
* const result = await un.execute("javascript", 'console.log("Hello World")');
* console.log(result.stdout);
*/
async function execute(language, code, options = {}) {
const {
env = null,
inputFiles = null,
networkMode = "zerotrust",
ttl = DEFAULT_TTL,
vcpu = 1,
returnArtifact = false,
returnWasmArtifact = false,
publicKey = null,
secretKey = null,
timeout = DEFAULT_TIMEOUT,
} = options;
const payload = {
language,
code,
network_mode: networkMode,
ttl,
vcpu,
};
if (env) payload.env = env;
if (inputFiles) {
payload.input_files = inputFiles.map(f => {
if (f.contentBase64 || f.content_base64) {
return { filename: f.filename, content_base64: f.contentBase64 || f.content_base64 };
} else if (f.content) {
return {
filename: f.filename,
content_base64: Buffer.from(f.content).toString('base64')
};
}
return f;
});
}
if (returnArtifact) payload.return_artifact = true;
if (returnWasmArtifact) payload.return_wasm_artifact = true;
return apiRequest("/execute", {
method: "POST",
data: payload,
publicKey,
secretKey,
timeout,
});
}
/**
* Execute code asynchronously. Returns immediately with job_id for polling.
*
* @param {string} language - Programming language
* @param {string} code - Source code to execute
* @param {Object} options - Same options as execute()
* @returns {Promise<Object>} Result with job_id, status ("pending")
*
* @example
* const job = await un.executeAsync("javascript", longRunningCode);
* console.log(`Job submitted: ${job.job_id}`);
* const result = await un.wait(job.job_id);
*/
async function executeAsync(language, code, options = {}) {
const {
env = null,
inputFiles = null,
networkMode = "zerotrust",
ttl = DEFAULT_TTL,
vcpu = 1,
returnArtifact = false,
returnWasmArtifact = false,
publicKey = null,
secretKey = null,
} = options;
const payload = {
language,
code,
network_mode: networkMode,
ttl,
vcpu,
};
if (env) payload.env = env;
if (inputFiles) {
payload.input_files = inputFiles.map(f => {
if (f.contentBase64 || f.content_base64) {
return { filename: f.filename, content_base64: f.contentBase64 || f.content_base64 };
} else if (f.content) {
return {
filename: f.filename,
content_base64: Buffer.from(f.content).toString('base64')
};
}
return f;
});
}
if (returnArtifact) payload.return_artifact = true;
if (returnWasmArtifact) payload.return_wasm_artifact = true;
return apiRequest("/execute/async", {
method: "POST",
data: payload,
publicKey,
secretKey,
});
}
/**
* Execute code with automatic language detection from shebang.
*
* @param {string} code - Source code with shebang (e.g., #!/usr/bin/env node)
* @param {Object} options - Optional parameters
* @returns {Promise<Object>} Result with detected_language, stdout, stderr, etc.
*
* @example
* const code = '#!/usr/bin/env node\nconsole.log("Auto-detected!")';
* const result = await un.run(code);
* console.log(result.detected_language); // "javascript"
*/
async function run(code, options = {}) {
const {
env = null,
networkMode = "zerotrust",
ttl = DEFAULT_TTL,
publicKey = null,
secretKey = null,
timeout = DEFAULT_TIMEOUT,
} = options;
let endpoint = `/run?ttl=${ttl}&network_mode=${networkMode}`;
if (env) {
endpoint += `&env=${encodeURIComponent(JSON.stringify(env))}`;
}
return apiRequest(endpoint, {
method: "POST",
bodyText: code,
contentType: "text/plain",
publicKey,
secretKey,
timeout,
});
}
/**
* Execute code asynchronously with automatic language detection.
*
* @param {string} code - Source code with shebang
* @param {Object} options - Optional parameters
* @returns {Promise<Object>} Result with job_id, detected_language, status ("pending")
*/
async function runAsync(code, options = {}) {
const {
env = null,
networkMode = "zerotrust",
ttl = DEFAULT_TTL,
publicKey = null,
secretKey = null,
} = options;
let endpoint = `/run/async?ttl=${ttl}&network_mode=${networkMode}`;
if (env) {
endpoint += `&env=${encodeURIComponent(JSON.stringify(env))}`;
}
return apiRequest(endpoint, {
method: "POST",
bodyText: code,
contentType: "text/plain",
publicKey,
secretKey,
});
}
// ============================================================================
// Job Management
// ============================================================================
/**
* Get job status and results.
*
* @param {string} jobId - Job ID from executeAsync or runAsync
* @param {Object} options - Optional parameters
* @returns {Promise<Object>} Job status with keys: job_id, status, result (if completed)
*/
async function getJob(jobId, options = {}) {
const { publicKey = null, secretKey = null } = options;
return apiRequest(`/jobs/${jobId}`, {
method: "GET",
publicKey,
secretKey,
});
}
/**
* Wait for job completion with exponential backoff polling.
*
* @param {string} jobId - Job ID from executeAsync or runAsync
* @param {Object} options - Optional parameters
* @param {number} options.maxPolls - Maximum number of poll attempts (default 100)
* @returns {Promise<Object>} Final job result
*
* @example
* const job = await un.executeAsync("javascript", code);
* const result = await un.wait(job.job_id);
* console.log(result.stdout);
*/
async function wait(jobId, options = {}) {
const {
maxPolls = 100,
publicKey = null,
secretKey = null,
} = options;
const terminalStates = new Set(['completed', 'failed', 'timeout', 'cancelled']);
for (let i = 0; i < maxPolls; i++) {
// Exponential backoff delay
const delayIdx = Math.min(i, POLL_DELAYS.length - 1);
await new Promise(resolve => setTimeout(resolve, POLL_DELAYS[delayIdx]));
const result = await getJob(jobId, { publicKey, secretKey });
const status = result.status || "";
if (terminalStates.has(status)) {
if (status === 'failed') {
throw new ExecutionError(
`Job failed: ${result.error || 'Unknown error'}`,
result.exit_code,
result.stderr
);
}
if (status === 'timeout') {
throw new TimeoutError(`Job timed out: ${jobId}`);
}
return result;
}
}
throw new TimeoutError(`Max polls (${maxPolls}) exceeded for job ${jobId}`);
}
/**
* Cancel a running job.
*
* @param {string} jobId - Job ID to cancel
* @param {Object} options - Optional parameters
* @returns {Promise<Object>} Partial output and artifacts collected before cancellation
*/
async function cancelJob(jobId, options = {}) {
const { publicKey = null, secretKey = null } = options;
return apiRequest(`/jobs/${jobId}`, {
method: "DELETE",
publicKey,
secretKey,
});
}
/**
* List all active jobs for this API key.
*
* @param {Object} options - Optional parameters
* @returns {Promise<Array>} List of job summaries
*/
async function listJobs(options = {}) {
const { publicKey = null, secretKey = null } = options;
const result = await apiRequest("/jobs", {
method: "GET",
publicKey,
secretKey,
});
return result.jobs || [];
}
// ============================================================================
// Image Generation
// ============================================================================
/**
* Generate images from text prompt.
*
* @param {string} prompt - Text description of the image to generate
* @param {Object} options - Optional parameters
* @param {string} options.model - Model to use (optional)
* @param {string} options.size - Image size (e.g., "1024x1024")
* @param {string} options.quality - "standard" or "hd"
* @param {number} options.n - Number of images to generate
* @returns {Promise<Object>} Result with images array
*
* @example
* const result = await un.image("A sunset over mountains");
* console.log(result.images[0]);
*/
async function image(prompt, options = {}) {
const {
model = null,
size = "1024x1024",
quality = "standard",
n = 1,
publicKey = null,
secretKey = null,
} = options;
const payload = { prompt, size, quality, n };
if (model) payload.model = model;
return apiRequest("/image", {
method: "POST",
data: payload,
publicKey,
secretKey,
});
}
// ============================================================================
// Utility Functions
// ============================================================================
/**
* Get list of supported programming languages.
*
* Results are cached in ~/.unsandbox/languages.json for 1 hour.
*
* @param {Object} options - Optional parameters
* @param {boolean} options.forceRefresh - Bypass cache and fetch fresh data
* @returns {Promise<Object>} Result with languages array, count, aliases
*/
async function languages(options = {}) {
const { publicKey = null, secretKey = null, forceRefresh = false } = options;
const cachePath = path.join(os.homedir(), '.unsandbox', 'languages.json');
const cacheMaxAge = 3600 * 1000; // 1 hour in ms
// Check cache unless force refresh
if (!forceRefresh && fs.existsSync(cachePath)) {
try {
const stat = fs.statSync(cachePath);
if (Date.now() - stat.mtimeMs < cacheMaxAge) {
return JSON.parse(fs.readFileSync(cachePath, 'utf-8'));
}
} catch (e) {
// Cache read failed, fetch from API
}
}
// Fetch from API
const result = await apiRequest("/languages", {
method: "GET",
publicKey,
secretKey,
});
// Save to cache
try {
const cacheDir = path.dirname(cachePath);
if (!fs.existsSync(cacheDir)) {
fs.mkdirSync(cacheDir, { recursive: true });
}
fs.writeFileSync(cachePath, JSON.stringify(result));
} catch (e) {
// Cache write failed, continue anyway
}
return result;
}
/**
* Detect programming language from file extension or shebang.
*
* @param {string} filename - File path
* @returns {string|null} Language name or null if undetected
*/
function detectLanguage(filename) {
const ext = path.extname(filename).toLowerCase();
if (EXT_MAP[ext]) return EXT_MAP[ext];
// Try shebang
try {
const content = fs.readFileSync(filename, 'utf-8');
const firstLine = content.split('\n')[0];
if (firstLine.startsWith('#!')) {
if (firstLine.includes('python')) return 'python';
if (firstLine.includes('node')) return 'javascript';
if (firstLine.includes('ruby')) return 'ruby';
if (firstLine.includes('perl')) return 'perl';
if (firstLine.includes('bash') || firstLine.includes('/sh')) return 'bash';
if (firstLine.includes('lua')) return 'lua';
if (firstLine.includes('php')) return 'php';
}
} catch (e) {
// File read failed
}
return null;
}
// ============================================================================
// Snapshots (Save/Restore Session & Service State)
// ============================================================================
/**
* Create a snapshot of a session's current state.
*
* @param {string} sessionId - ID of the session to snapshot
* @param {Object} options - Optional parameters
* @returns {Promise<Object>} Result with snapshot_id, created_at, status
*/
async function sessionSnapshot(sessionId, options = {}) {
const { publicKey = null, secretKey = null } = options;
return apiRequest(`/sessions/${sessionId}/snapshot`, {
method: "POST",
data: {},
publicKey,
secretKey,
});
}
/**
* Create a snapshot of a service's current state.
*
* @param {string} serviceId - ID of the service to snapshot
* @param {Object} options - Optional parameters
* @returns {Promise<Object>} Result with snapshot_id, created_at, status
*/
async function serviceSnapshot(serviceId, options = {}) {
const { publicKey = null, secretKey = null } = options;
return apiRequest(`/services/${serviceId}/snapshot`, {
method: "POST",
data: {},
publicKey,
secretKey,
});
}
/**
* List all available snapshots.
*
* @param {Object} options - Optional parameters
* @returns {Promise<Object>} Result with snapshots array, count
*/
async function listSnapshots(options = {}) {
const { publicKey = null, secretKey = null } = options;
return apiRequest("/snapshots", {
method: "GET",
publicKey,
secretKey,
});
}
/**
* Restore a session or service from a snapshot.
*
* @param {string} snapshotId - ID of the snapshot to restore
* @param {Object} options - Optional parameters
* @returns {Promise<Object>} Result with restored_id, status
*/
async function restoreSnapshot(snapshotId, options = {}) {
const { publicKey = null, secretKey = null } = options;
return apiRequest(`/snapshots/${snapshotId}/restore`, {
method: "POST",
data: {},
publicKey,
secretKey,
});
}
/**
* Delete a snapshot.
*
* @param {string} snapshotId - ID of the snapshot to delete
* @param {Object} options - Optional parameters
* @returns {Promise<Object>} Result with status
*/
async function deleteSnapshot(snapshotId, options = {}) {
const { publicKey = null, secretKey = null } = options;
return apiRequest(`/snapshots/${snapshotId}`, {
method: "DELETE",
publicKey,
secretKey,
});
}
// ============================================================================
// Client Class
// ============================================================================
/**
* Unsandbox API client with stored credentials.
*
* @example
* const client = new un.Client({ publicKey: "unsb-pk-...", secretKey: "unsb-sk-..." });
* const result = await client.execute("javascript", 'console.log("Hello")');
*
* // Or load from environment/config automatically:
* const client = new un.Client();
* const result = await client.execute("javascript", code);
*/
class Client {
/**
* Initialize client with credentials.
*
* @param {Object} options - Optional parameters
* @param {string} options.publicKey - API public key (unsb-pk-...)
* @param {string} options.secretKey - API secret key (unsb-sk-...)
* @param {number} options.accountIndex - Account index in ~/.unsandbox/accounts.csv
*/
constructor(options = {}) {
const { publicKey = null, secretKey = null, accountIndex = 0 } = options;
const creds = getCredentials(publicKey, secretKey, accountIndex);
this.publicKey = creds.publicKey;
this.secretKey = creds.secretKey;
}
async execute(language, code, options = {}) {
return execute(language, code, {
...options,
publicKey: this.publicKey,
secretKey: this.secretKey,
});
}
async executeAsync(language, code, options = {}) {
return executeAsync(language, code, {
...options,
publicKey: this.publicKey,
secretKey: this.secretKey,
});
}
async run(code, options = {}) {
return run(code, {
...options,
publicKey: this.publicKey,
secretKey: this.secretKey,
});
}
async runAsync(code, options = {}) {
return runAsync(code, {
...options,
publicKey: this.publicKey,
secretKey: this.secretKey,
});
}
async getJob(jobId) {
return getJob(jobId, { publicKey: this.publicKey, secretKey: this.secretKey });
}
async wait(jobId, options = {}) {
return wait(jobId, {
...options,
publicKey: this.publicKey,
secretKey: this.secretKey,
});
}
async cancelJob(jobId) {
return cancelJob(jobId, { publicKey: this.publicKey, secretKey: this.secretKey });
}
async listJobs() {
return listJobs({ publicKey: this.publicKey, secretKey: this.secretKey });
}
async image(prompt, options = {}) {
return image(prompt, {
...options,
publicKey: this.publicKey,
secretKey: this.secretKey,
});
}
async languages(options = {}) {
return languages({ ...options, publicKey: this.publicKey, secretKey: this.secretKey });
}
async sessionSnapshot(sessionId) {
return sessionSnapshot(sessionId, { publicKey: this.publicKey, secretKey: this.secretKey });
}
async serviceSnapshot(serviceId) {
return serviceSnapshot(serviceId, { publicKey: this.publicKey, secretKey: this.secretKey });
}
async listSnapshots() {
return listSnapshots({ publicKey: this.publicKey, secretKey: this.secretKey });
}
async restoreSnapshot(snapshotId) {
return restoreSnapshot(snapshotId, { publicKey: this.publicKey, secretKey: this.secretKey });
}
async deleteSnapshot(snapshotId) {
return deleteSnapshot(snapshotId, { publicKey: this.publicKey, secretKey: this.secretKey });
}
}
// ============================================================================
// CLI Interface
// ============================================================================
async function cliMain() {
const args = process.argv.slice(2);
if (args.length === 0 || args[0] === '-h' || args[0] === '--help') {
console.log(`unsandbox - Execute code in secure sandboxes
Usage:
node un.js [options] <source_file>
node un.js -s <language> '<code>'
Options:
-s, --shell LANG Execute inline code with specified language
-e KEY=VALUE Set environment variable (multiple allowed)
-f FILE Add input file (multiple allowed)
-n MODE Network mode: zerotrust (default) or semitrusted
-v N vCPU count (1-8, default 1)
--ttl N Execution timeout in seconds (default 60)
-a, --artifacts Return artifacts
-o DIR Output directory for artifacts
-p KEY API public key
-k KEY API secret key
--async Execute asynchronously
Examples:
node un.js script.js Execute JavaScript file
node un.js -s python 'print("Hello")' Execute inline Python
node un.js -e DEBUG=1 script.js With environment variable
node un.js -n semitrusted script.js With network access
`);
process.exit(args.length === 0 ? 1 : 0);
}
// Parse arguments
let source = null;
let inlineLang = null;
const env = {};
const files = [];
let networkMode = "zerotrust";
let vcpu = 1;
let ttl = 60;
let artifacts = false;
let outputDir = null;
let publicKey = null;
let secretKey = null;
let asyncMode = false;
for (let i = 0; i < args.length; i++) {
const arg = args[i];
if (arg === '-s' || arg === '--shell') {
inlineLang = args[++i];
} else if (arg === '-e') {
const [k, v] = args[++i].split('=', 2);
env[k] = v || '';
} else if (arg === '-f') {
files.push(args[++i]);
} else if (arg === '-n' || arg === '--network') {
networkMode = args[++i];
} else if (arg === '-v' || arg === '--vcpu') {
vcpu = parseInt(args[++i]);
} else if (arg === '--ttl') {
ttl = parseInt(args[++i]);
} else if (arg === '-a' || arg === '--artifacts') {
artifacts = true;
} else if (arg === '-o' || arg === '--output') {
outputDir = args[++i];
} else if (arg === '-p' || arg === '--public-key') {
publicKey = args[++i];
} else if (arg === '-k' || arg === '--secret-key') {
secretKey = args[++i];
} else if (arg === '--async') {
asyncMode = true;
} else if (!arg.startsWith('-')) {
source = arg;
}
}
try {
// Determine language and code
let language, code;
if (inlineLang) {
language = inlineLang;
code = source || "";
} else if (!source) {
console.error(`${RED}Error: No source file or code provided${RESET}`);
process.exit(1);
} else if (!fs.existsSync(source)) {
// Treat as inline bash
language = "bash";
code = source;
} else {
language = detectLanguage(source);
if (!language) {
console.error(`${RED}Error: Cannot detect language for ${source}${RESET}`);
process.exit(1);
}
code = fs.readFileSync(source, 'utf-8');
}
// Load input files
const inputFiles = files.map(filepath => {
if (!fs.existsSync(filepath)) {
console.error(`${RED}Error: File not found: ${filepath}${RESET}`);
process.exit(1);
}
return {
filename: path.basename(filepath),
contentBase64: fs.readFileSync(filepath).toString('base64')
};
});
// Execute
if (asyncMode) {
const result = await executeAsync(language, code, {
env: Object.keys(env).length ? env : null,
inputFiles: inputFiles.length ? inputFiles : null,
networkMode,
ttl,
vcpu,
returnArtifact: artifacts,
publicKey,
secretKey,
});
console.log(`${GREEN}Job submitted: ${result.job_id}${RESET}`);
console.log(`Status: ${result.status}`);
console.log(`\nPoll with: node un.js job ${result.job_id}`);
} else {
const result = await execute(language, code, {
env: Object.keys(env).length ? env : null,
inputFiles: inputFiles.length ? inputFiles : null,
networkMode,
ttl,
vcpu,
returnArtifact: artifacts,
publicKey,
secretKey,
});
// Print output
if (result.stdout) process.stdout.write(result.stdout);
if (result.stderr) process.stderr.write(`${RED}${result.stderr}${RESET}`);
// Save artifacts
if (artifacts && result.artifacts) {
const outDir = outputDir || '.';
if (!fs.existsSync(outDir)) fs.mkdirSync(outDir, { recursive: true });
for (const artifact of result.artifacts) {
const filename = artifact.filename || 'artifact';
const content = Buffer.from(artifact.content_base64, 'base64');
const filepath = path.join(outDir, filename);
fs.writeFileSync(filepath, content);
fs.chmodSync(filepath, 0o755);
console.error(`${GREEN}Saved: ${filepath}${RESET}`);
}
}
process.exit(result.exit_code || 0);
}
} catch (e) {
if (e instanceof AuthenticationError) {
console.error(`${RED}Authentication error: ${e.message}${RESET}`);
} else if (e instanceof ExecutionError) {
console.error(`${RED}Execution error: ${e.message}${RESET}`);
if (e.stderr) console.error(`${RED}${e.stderr}${RESET}`);
} else if (e instanceof APIError) {
console.error(`${RED}API error: ${e.message}${RESET}`);
} else if (e instanceof TimeoutError) {
console.error(`${RED}Timeout: ${e.message}${RESET}`);
process.exit(124);
} else {
console.error(`${RED}Error: ${e.message}${RESET}`);
}
process.exit(1);
}
}
// ============================================================================
// Module Exports
// ============================================================================
module.exports = {
// Core execution
execute,
executeAsync,
run,
runAsync,
// Job management
getJob,
wait,
cancelJob,
listJobs,
// Image generation
image,
// Snapshots
sessionSnapshot,
serviceSnapshot,
listSnapshots,
restoreSnapshot,
deleteSnapshot,
// Utilities
languages,
detectLanguage,
// Client class
Client,
// Exceptions
UnsandboxError,
AuthenticationError,
ExecutionError,
APIError,
TimeoutError,
// Constants
API_BASE,
PORTAL_BASE,
EXT_MAP,
// Internal functions (for testing/library usage)
_signRequest: signRequest,
_getCredentials: getCredentials,
_apiRequest: apiRequest,
};
// Run CLI if called directly
if (require.main === module) {
cliMain().catch(e => {
console.error(`${RED}${e.message}${RESET}`);
process.exit(1);
});
}