CLI
Cliente de linha de comando rápido para execução de código e sessões interativas. Mais de 42 linguagens, mais de 30 shells/REPLs.
Documentação Oficial OpenAPI Swagger ↗Início Rápido — Groovy
# Download + setup
curl -O https://git.unturf.com/engineering/unturf/un-inception/-/raw/main/clients/groovy/sync/src/un.groovy && chmod +x un.groovy && ln -sf un.groovy un
export UNSANDBOX_PUBLIC_KEY="unsb-pk-xxxx-xxxx-xxxx-xxxx"
export UNSANDBOX_SECRET_KEY="unsb-sk-xxxxx-xxxxx-xxxxx-xxxxx"
# Run code
./un script.groovy
Baixar
Guia de Instalação →Características:
- 42+ languages - Python, JS, Go, Rust, C++, Java...
- Sessions - 30+ shells/REPLs, tmux persistence
- Files - Upload files, collect artifacts
- Services - Persistent containers with domains
- Snapshots - Point-in-time backups
- Images - Publish, share, transfer
Início Rápido de Integração ⚡
Adicione superpoderes unsandbox ao seu app Groovy existente:
curl -O https://git.unturf.com/engineering/unturf/un-inception/-/raw/main/clients/groovy/sync/src/un.groovy
# Option A: Environment variables
export UNSANDBOX_PUBLIC_KEY="unsb-pk-xxxx-xxxx-xxxx-xxxx"
export UNSANDBOX_SECRET_KEY="unsb-sk-xxxxx-xxxxx-xxxxx-xxxxx"
# Option B: Config file (persistent)
mkdir -p ~/.unsandbox
echo "unsb-pk-xxxx-xxxx-xxxx-xxxx,unsb-sk-xxxxx-xxxxx-xxxxx-xxxxx" > ~/.unsandbox/accounts.csv
// In your Groovy app:
import static Un.*
def result = executeCode("groovy", "println 'Hello from Groovy running on unsandbox!'")
println result.stdout // Hello from Groovy running on unsandbox!
groovy myapp.groovy
f0b8bb7cc226d1070e4a526e3e111d75
SHA256: 4f7292b19fb15c68b3ccb56847010d221099a03a3644f9a92ca923bc2b980aa7
#!/usr/bin/env groovy
// 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
#!/usr/bin/env groovy
/**
* unsandbox SDK for Groovy - Execute code in secure sandboxes
* https://unsandbox.com | https://api.unsandbox.com/openapi
*
* <h2>Library Usage:</h2>
* <pre>{@code
* import un
*
* // Simple execution
* def result = un.execute("python", 'print("Hello")')
* println result.stdout
*
* // Async execution
* def job = un.executeAsync("python", longCode)
* def result = un.wait(job.job_id)
*
* // Using Client class
* def client = new un.Client(publicKey: "unsb-pk-...", secretKey: "unsb-sk-...")
* def result = client.execute("python", code)
* }</pre>
*
* <h2>CLI Usage:</h2>
* <pre>
* groovy un.groovy script.py
* groovy un.groovy -s python 'print("Hello")'
* groovy un.groovy session --shell python3
* </pre>
*
* <h2>Authentication (in priority order):</h2>
* <ol>
* <li>Function arguments: execute(..., publicKey: "...", secretKey: "...")</li>
* <li>Environment variables: UNSANDBOX_PUBLIC_KEY + UNSANDBOX_SECRET_KEY</li>
* <li>Config file: ~/.unsandbox/accounts.csv (public_key,secret_key per line)</li>
* </ol>
*
* @author Permacomputer Project
* @version 4.3.4
*/
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec
import groovy.json.JsonSlurper
import groovy.json.JsonOutput
// ============================================================================
// Configuration
// ============================================================================
/** API base URL for unsandbox */
def API_BASE = 'https://api.unsandbox.com'
/** Portal base URL for unsandbox */
def PORTAL_BASE = 'https://unsandbox.com'
/** Default execution timeout in seconds */
def DEFAULT_TIMEOUT = 300
/** Default TTL for code execution */
def DEFAULT_TTL = 60
/** Maximum vault content size (64KB) */
def MAX_ENV_CONTENT_SIZE = 65536
/** Polling delays (ms) - exponential backoff */
def POLL_DELAYS = [300, 450, 700, 900, 650, 1600, 2000]
// ANSI colors
def BLUE = '\033[34m'
def RED = '\033[31m'
def GREEN = '\033[32m'
def YELLOW = '\033[33m'
def RESET = '\033[0m'
/** Extension to language mapping */
def EXT_MAP = [
'.java': 'java', '.kt': 'kotlin', '.cs': 'csharp', '.fs': 'fsharp',
'.groovy': 'groovy', '.dart': 'dart', '.scala': 'scala',
'.py': 'python', '.js': 'javascript', '.ts': 'typescript',
'.rb': 'ruby', '.go': 'go', '.rs': 'rust', '.cpp': 'cpp', '.c': 'c',
'.sh': 'bash', '.pl': 'perl', '.lua': 'lua', '.php': 'php',
'.hs': 'haskell', '.ml': 'ocaml', '.clj': 'clojure', '.scm': 'scheme',
'.lisp': 'commonlisp', '.erl': 'erlang', '.ex': 'elixir',
'.jl': 'julia', '.r': 'r', '.cr': 'crystal', '.f90': 'fortran',
'.cob': 'cobol', '.pro': 'prolog', '.forth': 'forth', '.tcl': 'tcl',
'.raku': 'raku', '.d': 'd', '.nim': 'nim', '.zig': 'zig', '.v': 'v',
'.awk': 'awk', '.m': 'objc'
]
// ============================================================================
// Exceptions
// ============================================================================
/**
* Base exception for unsandbox errors.
*/
class UnsandboxError extends Exception {
UnsandboxError(String message) {
super(message)
}
}
/**
* Authentication failed - invalid or missing credentials.
*/
class AuthenticationError extends UnsandboxError {
AuthenticationError(String message) {
super(message)
}
}
/**
* Code execution failed.
*/
class ExecutionError extends UnsandboxError {
Integer exitCode
String stderr
ExecutionError(String message, Integer exitCode = null, String stderr = null) {
super(message)
this.exitCode = exitCode
this.stderr = stderr
}
}
/**
* API request failed.
*/
class APIError extends UnsandboxError {
Integer statusCode
String response
APIError(String message, Integer statusCode = null, String response = null) {
super(message)
this.statusCode = statusCode
this.response = response
}
}
/**
* Execution timed out.
*/
class TimeoutError extends UnsandboxError {
TimeoutError(String message) {
super(message)
}
}
// ============================================================================
// HMAC Authentication
// ============================================================================
/**
* Generate HMAC-SHA256 signature for API request.
*
* <p>Signature format: HMAC-SHA256(secretKey, "timestamp:METHOD:path:body")</p>
*
* @param secretKey The secret key for HMAC
* @param timestamp Unix timestamp
* @param method HTTP method (GET, POST, etc.)
* @param path API endpoint path
* @param body Request body (empty string if none)
* @return Hex-encoded signature
*/
def signRequest(String secretKey, long timestamp, String method, String path, String body = "") {
def message = "${timestamp}:${method}:${path}:${body}"
def mac = Mac.getInstance("HmacSHA256")
mac.init(new SecretKeySpec(secretKey.getBytes("UTF-8"), "HmacSHA256"))
return mac.doFinal(message.getBytes("UTF-8")).encodeHex().toString()
}
/**
* Get API credentials in priority order.
*
* <ol>
* <li>Function arguments</li>
* <li>Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY)</li>
* <li>Config file (~/.unsandbox/accounts.csv)</li>
* </ol>
*
* @param publicKey Optional public key argument
* @param secretKey Optional secret key argument
* @param accountIndex Account index in config file (default 0)
* @return Tuple of [publicKey, secretKey]
* @throws AuthenticationError if no credentials found
*/
def loadAccountsFromCsv(File path) {
def validAccounts = []
if (!path.exists()) return validAccounts
try {
def lines = path.text.trim().split('\n')
lines.each { line ->
def trimmed = line.trim()
if (!trimmed || trimmed.startsWith('#')) return
if (trimmed.contains(',')) {
def parts = trimmed.split(',', 2)
def pk = parts[0].trim()
def sk = parts[1].trim()
if (pk.startsWith('unsb-pk-') && sk.startsWith('unsb-sk-')) {
validAccounts << [pk, sk]
}
}
}
} catch (Exception e) {
// Ignore file read errors
}
return validAccounts
}
def getCredentials(String publicKey = null, String secretKey = null, int accountIndex = -1) {
// Priority 1: Function arguments
if (publicKey && secretKey) {
return [publicKey, secretKey]
}
// Priority 2: --account N => accounts.csv row N (bypasses env vars)
if (accountIndex >= 0) {
def searchPaths = [
new File(System.getProperty('user.home'), '.unsandbox/accounts.csv'),
new File('accounts.csv')
]
for (path in searchPaths) {
def accts = loadAccountsFromCsv(path)
if (accts && accountIndex < accts.size()) {
return accts[accountIndex]
}
}
throw new AuthenticationError("No account at index ${accountIndex} in accounts.csv")
}
// Priority 3: Environment variables
def envPk = System.getenv('UNSANDBOX_PUBLIC_KEY')
def envSk = System.getenv('UNSANDBOX_SECRET_KEY')
if (envPk && envSk) {
return [envPk, envSk]
}
// Priority 4: ~/.unsandbox/accounts.csv row 0 (or UNSANDBOX_ACCOUNT env)
def defaultIdx = (System.getenv('UNSANDBOX_ACCOUNT') ?: '0').toInteger()
def searchPaths = [
new File(System.getProperty('user.home'), '.unsandbox/accounts.csv'),
new File('accounts.csv')
]
for (path in searchPaths) {
def accts = loadAccountsFromCsv(path)
if (accts && defaultIdx < accts.size()) {
return accts[defaultIdx]
}
}
throw new AuthenticationError(
"No credentials found. Set UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY, " +
"or create ~/.unsandbox/accounts.csv, or pass credentials to function."
)
}
// Legacy compatibility - now delegates to getCredentials for proper priority
def getApiKeys(argsKey, int accountIndex = -1) {
try {
return getCredentials(argsKey ?: null, null, accountIndex)
} catch (AuthenticationError e) {
System.err.println("${RED}Error: ${e.message}${RESET}")
System.exit(1)
}
}
// ============================================================================
// HTTP Client
// ============================================================================
/**
* Make authenticated API request with HMAC signature.
*
* @param endpoint API endpoint path
* @param method HTTP method
* @param data Request body data (will be JSON-encoded if Map)
* @param publicKey API public key
* @param secretKey API secret key
* @param timeout Request timeout in seconds
* @param contentType Content-Type header
* @return Parsed JSON response as Map
* @throws APIError on request failure
*/
def apiRequest(String endpoint, String method, data, String publicKey, String secretKey,
int timeout = DEFAULT_TIMEOUT, String contentType = 'application/json') {
def tempFile = File.createTempFile('un_request_', '.json')
try {
def body = ""
if (data) {
body = data instanceof Map ? JsonOutput.toJson(data) : data.toString()
tempFile.text = body
}
def curlCmd = ['curl', '-s', '-X', method, "${API_BASE}${endpoint}",
'-H', "Content-Type: ${contentType}"]
// Add HMAC authentication headers if secretKey is provided
if (secretKey) {
def timestamp = (System.currentTimeMillis() / 1000) as long
def signature = signRequest(secretKey, timestamp, method, endpoint, body)
curlCmd += ['-H', "Authorization: Bearer ${publicKey}"]
curlCmd += ['-H', "X-Timestamp: ${timestamp}"]
curlCmd += ['-H', "X-Signature: ${signature}"]
} else {
curlCmd += ['-H', "Authorization: Bearer ${publicKey}"]
}
if (data) {
curlCmd += ['-d', "@${tempFile.absolutePath}"]
}
def proc = curlCmd.execute()
def output = proc.text
proc.waitFor()
if (proc.exitValue() != 0) {
throw new APIError("curl failed with exit code ${proc.exitValue()}")
}
// Check for timestamp authentication errors
if (output.toLowerCase().contains('timestamp') &&
(output.contains('401') || output.toLowerCase().contains('expired') || output.toLowerCase().contains('invalid'))) {
throw new AuthenticationError(
"Request timestamp expired. Your system clock may be out of sync. " +
"Run: sudo ntpdate -s time.nist.gov"
)
}
try {
return new JsonSlurper().parseText(output)
} catch (Exception e) {
return [raw: output]
}
} finally {
tempFile.delete()
}
}
def apiRequestPatch(endpoint, data, publicKey, secretKey) {
return apiRequest(endpoint, 'PATCH', data, publicKey, secretKey)
}
/**
* Exception for 428 Sudo Challenge requiring OTP confirmation.
*/
class SudoChallengeError extends UnsandboxError {
String challengeId
String responseBody
SudoChallengeError(String challengeId, String responseBody) {
super("Sudo challenge required")
this.challengeId = challengeId
this.responseBody = responseBody
}
}
/**
* Make API request for destructive operations with 428 handling.
* Uses curl with -w to capture HTTP status code.
*/
def apiRequestDestructive(String endpoint, String method, data, String publicKey, String secretKey) {
def tempFile = File.createTempFile('un_request_', '.json')
def statusFile = File.createTempFile('un_status_', '.txt')
try {
def body = ""
if (data) {
body = data instanceof Map ? JsonOutput.toJson(data) : data.toString()
tempFile.text = body
}
def timestamp = (System.currentTimeMillis() / 1000) as long
def signature = signRequest(secretKey, timestamp, method, endpoint, body)
def curlCmd = ['curl', '-s', '-X', method, "${API_BASE}${endpoint}",
'-H', "Content-Type: application/json",
'-H', "Authorization: Bearer ${publicKey}",
'-H', "X-Timestamp: ${timestamp}",
'-H', "X-Signature: ${signature}",
'-w', '\\n%{http_code}',
'-o', statusFile.absolutePath]
if (data) {
curlCmd += ['-d', "@${tempFile.absolutePath}"]
}
def proc = curlCmd.execute()
def statusOutput = proc.text.trim()
proc.waitFor()
def responseBody = statusFile.exists() ? statusFile.text : ""
def httpCode = 0
try {
httpCode = statusOutput.toInteger()
} catch (Exception e) {
// Failed to parse status code
}
if (httpCode == 428) {
// Extract challenge_id from response
def challengeId = null
try {
def parsed = new JsonSlurper().parseText(responseBody)
challengeId = parsed?.challenge_id
} catch (Exception e) {
// Ignore parse errors
}
throw new SudoChallengeError(challengeId, responseBody)
}
if (httpCode < 200 || httpCode >= 300) {
throw new APIError("HTTP ${httpCode} - ${responseBody}", httpCode, responseBody)
}
try {
return new JsonSlurper().parseText(responseBody)
} catch (Exception e) {
return [raw: responseBody]
}
} finally {
tempFile.delete()
statusFile.delete()
}
}
/**
* Make API request with sudo OTP headers.
*/
def apiRequestWithSudo(String endpoint, String method, data, String publicKey, String secretKey, String otp, String challengeId) {
def tempFile = File.createTempFile('un_request_', '.json')
def statusFile = File.createTempFile('un_status_', '.txt')
try {
def body = ""
if (data) {
body = data instanceof Map ? JsonOutput.toJson(data) : data.toString()
tempFile.text = body
}
def timestamp = (System.currentTimeMillis() / 1000) as long
def signature = signRequest(secretKey, timestamp, method, endpoint, body)
def curlCmd = ['curl', '-s', '-X', method, "${API_BASE}${endpoint}",
'-H', "Content-Type: application/json",
'-H', "Authorization: Bearer ${publicKey}",
'-H', "X-Timestamp: ${timestamp}",
'-H', "X-Signature: ${signature}",
'-H', "X-Sudo-OTP: ${otp}",
'-w', '\\n%{http_code}',
'-o', statusFile.absolutePath]
if (challengeId) {
curlCmd += ['-H', "X-Sudo-Challenge: ${challengeId}"]
}
if (data) {
curlCmd += ['-d', "@${tempFile.absolutePath}"]
}
def proc = curlCmd.execute()
def statusOutput = proc.text.trim()
proc.waitFor()
def responseBody = statusFile.exists() ? statusFile.text : ""
def httpCode = 0
try {
httpCode = statusOutput.toInteger()
} catch (Exception e) {
// Failed to parse status code
}
if (httpCode < 200 || httpCode >= 300) {
throw new APIError("HTTP ${httpCode} - ${responseBody}", httpCode, responseBody)
}
try {
return new JsonSlurper().parseText(responseBody)
} catch (Exception e) {
return [raw: responseBody]
}
} finally {
tempFile.delete()
statusFile.delete()
}
}
/**
* Handle sudo challenge by prompting for OTP and retrying.
*/
def handleSudoChallenge(String challengeId, String method, String endpoint, data, String publicKey, String secretKey) {
System.err.println("${YELLOW}Confirmation required. Check your email for a one-time code.${RESET}")
System.err.print("Enter OTP: ")
System.err.flush()
def reader = new BufferedReader(new InputStreamReader(System.in))
def otp = reader.readLine()?.trim()
if (!otp) {
throw new RuntimeException("Operation cancelled - no OTP provided")
}
return apiRequestWithSudo(endpoint, method, data, publicKey, secretKey, otp, challengeId)
}
/**
* Execute a destructive operation with 428 sudo challenge handling.
*/
def executeDestructive(String endpoint, String method, data, String publicKey, String secretKey) {
try {
return apiRequestDestructive(endpoint, method, data, publicKey, secretKey)
} catch (SudoChallengeError e) {
return handleSudoChallenge(e.challengeId, method, endpoint, data, publicKey, secretKey)
}
}
def apiRequestText(endpoint, method, body, publicKey, secretKey) {
def tempFile = File.createTempFile('un_env_', '.txt')
try {
if (body) {
tempFile.text = body
}
def curlCmd = ['curl', '-s', '-X', method, "${API_BASE}${endpoint}",
'-H', 'Content-Type: text/plain']
if (secretKey) {
def timestamp = (System.currentTimeMillis() / 1000) as long
def message = "${timestamp}:${method}:${endpoint}:${body ?: ''}"
def mac = Mac.getInstance("HmacSHA256")
mac.init(new SecretKeySpec(secretKey.getBytes("UTF-8"), "HmacSHA256"))
def signature = mac.doFinal(message.getBytes("UTF-8")).encodeHex().toString()
curlCmd += ['-H', "Authorization: Bearer ${publicKey}"]
curlCmd += ['-H', "X-Timestamp: ${timestamp}"]
curlCmd += ['-H', "X-Signature: ${signature}"]
} else {
curlCmd += ['-H', "Authorization: Bearer ${publicKey}"]
}
if (body) {
curlCmd += ['--data-binary', "@${tempFile.absolutePath}"]
}
def proc = curlCmd.execute()
def output = proc.text
proc.waitFor()
return proc.exitValue() == 0
} finally {
tempFile.delete()
}
}
// ============================================================================
// Core Library Functions
// ============================================================================
/**
* Execute code synchronously and return results.
*
* @param language Programming language (python, javascript, go, rust, etc.)
* @param code Source code to execute
* @param options Optional parameters:
* <ul>
* <li>env: Map of environment variables</li>
* <li>inputFiles: List of [filename: "...", content: "..."] or [filename: "...", contentBase64: "..."]</li>
* <li>networkMode: "zerotrust" (no network) or "semitrusted" (internet access)</li>
* <li>ttl: Execution timeout in seconds (1-900, default 60)</li>
* <li>vcpu: Virtual CPUs (1-8, default 1)</li>
* <li>returnArtifact: Return compiled binary</li>
* <li>returnWasmArtifact: Compile to WebAssembly</li>
* <li>publicKey: API public key</li>
* <li>secretKey: API secret key</li>
* </ul>
* @return Map with keys: success, stdout, stderr, exit_code, language, job_id, total_time_ms, network_mode, artifacts
* @throws AuthenticationError Invalid or missing credentials
* @throws ExecutionError Code execution failed
* @throws APIError API request failed
*
* <pre>{@code
* def result = un.execute("python", 'print("Hello World")')
* println result.stdout // "Hello World\n"
* }</pre>
*/
def execute(String language, String code, Map options = [:]) {
def (publicKey, secretKey) = getCredentials(
options.publicKey,
options.secretKey,
options.accountIndex != null ? options.accountIndex : -1
)
def payload = [
language: language,
code: code,
network_mode: options.networkMode ?: 'zerotrust',
ttl: options.ttl ?: DEFAULT_TTL,
vcpu: options.vcpu ?: 1
]
if (options.env) {
payload.env = options.env
}
if (options.inputFiles) {
payload.input_files = options.inputFiles.collect { 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: f.content.bytes.encodeBase64().toString()]
}
return f
}
}
if (options.returnArtifact) payload.return_artifact = true
if (options.returnWasmArtifact) payload.return_wasm_artifact = true
return apiRequest('/execute', 'POST', payload, publicKey, secretKey)
}
/**
* Execute code asynchronously. Returns immediately with job_id for polling.
*
* @param language Programming language
* @param code Source code to execute
* @param options Same options as execute()
* @return Map with keys: job_id, status ("pending")
*
* <pre>{@code
* def job = un.executeAsync("python", longRunningCode)
* println "Job submitted: ${job.job_id}"
* def result = un.wait(job.job_id)
* }</pre>
*/
def executeAsync(String language, String code, Map options = [:]) {
def (publicKey, secretKey) = getCredentials(
options.publicKey,
options.secretKey,
options.accountIndex != null ? options.accountIndex : -1
)
def payload = [
language: language,
code: code,
network_mode: options.networkMode ?: 'zerotrust',
ttl: options.ttl ?: DEFAULT_TTL,
vcpu: options.vcpu ?: 1
]
if (options.env) payload.env = options.env
if (options.inputFiles) {
payload.input_files = options.inputFiles.collect { 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: f.content.bytes.encodeBase64().toString()]
}
return f
}
}
if (options.returnArtifact) payload.return_artifact = true
if (options.returnWasmArtifact) payload.return_wasm_artifact = true
return apiRequest('/execute/async', 'POST', payload, publicKey, secretKey)
}
/**
* Execute code with automatic language detection from shebang.
*
* @param code Source code with shebang (e.g., #!/usr/bin/env python3)
* @param options Optional parameters (env, networkMode, ttl, publicKey, secretKey)
* @return Map with keys: success, stdout, stderr, exit_code, detected_language, ...
*
* <pre>{@code
* def code = '''#!/usr/bin/env python3
* print("Auto-detected!")
* '''
* def result = un.run(code)
* println result.detected_language // "python"
* }</pre>
*/
def run(String code, Map options = [:]) {
def (publicKey, secretKey) = getCredentials(
options.publicKey,
options.secretKey,
options.accountIndex != null ? options.accountIndex : -1
)
def ttl = options.ttl ?: DEFAULT_TTL
def networkMode = options.networkMode ?: 'zerotrust'
def endpoint = "/run?ttl=${ttl}&network_mode=${networkMode}"
if (options.env) {
endpoint += "&env=${URLEncoder.encode(JsonOutput.toJson(options.env), 'UTF-8')}"
}
return apiRequest(endpoint, 'POST', code, publicKey, secretKey, DEFAULT_TIMEOUT, 'text/plain')
}
/**
* Execute code asynchronously with automatic language detection.
*
* @param code Source code with shebang
* @param options Optional parameters
* @return Map with keys: job_id, detected_language, status ("pending")
*/
def runAsync(String code, Map options = [:]) {
def (publicKey, secretKey) = getCredentials(
options.publicKey,
options.secretKey,
options.accountIndex != null ? options.accountIndex : -1
)
def ttl = options.ttl ?: DEFAULT_TTL
def networkMode = options.networkMode ?: 'zerotrust'
def endpoint = "/run/async?ttl=${ttl}&network_mode=${networkMode}"
if (options.env) {
endpoint += "&env=${URLEncoder.encode(JsonOutput.toJson(options.env), 'UTF-8')}"
}
return apiRequest(endpoint, 'POST', code, publicKey, secretKey, DEFAULT_TIMEOUT, 'text/plain')
}
// ============================================================================
// Job Management
// ============================================================================
/**
* Get job status and results.
*
* @param jobId Job ID from executeAsync or runAsync
* @param options Optional parameters (publicKey, secretKey)
* @return Map with keys: job_id, status, result (if completed), timestamps
*
* <p>Status values: pending, running, completed, failed, timeout, cancelled</p>
*/
def getJob(String jobId, Map options = [:]) {
def (publicKey, secretKey) = getCredentials(options.publicKey, options.secretKey)
return apiRequest("/jobs/${jobId}", 'GET', null, publicKey, secretKey)
}
/**
* Wait for job completion with exponential backoff polling.
*
* @param jobId Job ID from executeAsync or runAsync
* @param options Optional parameters:
* <ul>
* <li>maxPolls: Maximum number of poll attempts (default 100)</li>
* <li>publicKey: API public key</li>
* <li>secretKey: API secret key</li>
* </ul>
* @return Final job result Map
* @throws TimeoutError Max polls exceeded
* @throws ExecutionError Job failed
*
* <pre>{@code
* def job = un.executeAsync("python", code)
* def result = un.wait(job.job_id)
* println result.stdout
* }</pre>
*/
def wait(String jobId, Map options = [:]) {
def maxPolls = options.maxPolls ?: 100
def terminalStates = ['completed', 'failed', 'timeout', 'cancelled'] as Set
for (int i = 0; i < maxPolls; i++) {
// Exponential backoff delay
def delayIdx = Math.min(i, POLL_DELAYS.size() - 1)
Thread.sleep(POLL_DELAYS[delayIdx])
def result = getJob(jobId, options)
def status = result.status ?: ''
if (status in terminalStates) {
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 jobId Job ID to cancel
* @param options Optional parameters (publicKey, secretKey)
* @return Partial output and artifacts collected before cancellation
*/
def cancelJob(String jobId, Map options = [:]) {
def (publicKey, secretKey) = getCredentials(options.publicKey, options.secretKey)
return apiRequest("/jobs/${jobId}", 'DELETE', null, publicKey, secretKey)
}
/**
* List all active jobs for this API key.
*
* @param options Optional parameters (publicKey, secretKey)
* @return List of job summary Maps with keys: job_id, language, status, submitted_at
*/
def listJobs(Map options = [:]) {
def (publicKey, secretKey) = getCredentials(options.publicKey, options.secretKey)
def result = apiRequest('/jobs', 'GET', null, publicKey, secretKey)
return result.jobs ?: []
}
// ============================================================================
// Image Generation
// ============================================================================
/**
* Generate images from text prompt.
*
* @param prompt Text description of the image to generate
* @param options Optional parameters:
* <ul>
* <li>model: Model to use (optional, uses default)</li>
* <li>size: Image size (e.g., "1024x1024", "512x512")</li>
* <li>quality: "standard" or "hd"</li>
* <li>n: Number of images to generate</li>
* <li>publicKey: API public key</li>
* <li>secretKey: API secret key</li>
* </ul>
* @return Map with keys: images (list of base64 or URLs), created_at
*
* <pre>{@code
* def result = un.image("A sunset over mountains")
* println result.images[0]
* }</pre>
*/
def image(String prompt, Map options = [:]) {
def (publicKey, secretKey) = getCredentials(options.publicKey, options.secretKey)
def payload = [
prompt: prompt,
size: options.size ?: '1024x1024',
quality: options.quality ?: 'standard',
n: options.n ?: 1
]
if (options.model) payload.model = options.model
return apiRequest('/image', 'POST', payload, publicKey, secretKey)
}
// ============================================================================
// Utility Functions
// ============================================================================
/** Cache max age for languages (1 hour in milliseconds) */
def LANGUAGES_CACHE_MAX_AGE = 3600000
/**
* Get list of supported programming languages.
*
* <p>Results are cached in ~/.unsandbox/languages.json for 1 hour.</p>
*
* @param options Optional parameters:
* <ul>
* <li>forceRefresh: Bypass cache and fetch fresh data</li>
* <li>publicKey: API public key</li>
* <li>secretKey: API secret key</li>
* </ul>
* @return Map with keys: languages (list), count, aliases (map)
*/
def languages(Map options = [:]) {
def cachePath = new File(System.getProperty('user.home'), '.unsandbox/languages.json')
// Check cache unless force refresh
if (!options.forceRefresh && cachePath.exists()) {
try {
def cacheAge = System.currentTimeMillis() - cachePath.lastModified()
if (cacheAge < LANGUAGES_CACHE_MAX_AGE) {
return new JsonSlurper().parseText(cachePath.text)
}
} catch (Exception e) {
// Cache read failed, fetch from API
}
}
// Fetch from API
def (publicKey, secretKey) = getCredentials(options.publicKey, options.secretKey)
def result = apiRequest('/languages', 'GET', null, publicKey, secretKey)
// Save to cache
try {
cachePath.parentFile.mkdirs()
cachePath.text = JsonOutput.toJson(result)
} catch (Exception e) {
// Cache write failed, continue anyway
}
return result
}
// ============================================================================
// Utility Functions
// ============================================================================
/**
* Get SDK version string.
*/
def version() {
return "4.2.0"
}
/**
* Check API health status.
*/
def healthCheck() {
try {
def url = new URL("${API_BASE}/health")
def connection = url.openConnection() as java.net.HttpURLConnection
connection.requestMethod = "GET"
connection.connectTimeout = 5000
connection.readTimeout = 5000
return connection.responseCode == 200
} catch (Exception e) {
return false
}
}
/**
* Generate HMAC-SHA256 signature.
*/
def hmacSign(String secretKey, String message) {
def mac = Mac.getInstance("HmacSHA256")
mac.init(new SecretKeySpec(secretKey.getBytes("UTF-8"), "HmacSHA256"))
return mac.doFinal(message.getBytes("UTF-8")).encodeHex().toString()
}
// ============================================================================
// Session Functions
// ============================================================================
/**
* List all sessions.
*/
def sessionList(Map options = [:]) {
def (publicKey, secretKey) = getCredentials(options.publicKey, options.secretKey)
def result = apiRequest('/sessions', 'GET', null, publicKey, secretKey)
return result.sessions ?: []
}
/**
* Get session details.
*/
def sessionGet(String sessionId, Map options = [:]) {
def (publicKey, secretKey) = getCredentials(options.publicKey, options.secretKey)
return apiRequest("/sessions/${sessionId}", 'GET', null, publicKey, secretKey)
}
/**
* Create a new session.
*/
def sessionCreate(Map options = [:]) {
def (publicKey, secretKey) = getCredentials(options.publicKey, options.secretKey)
def payload = [
network_mode: options.networkMode ?: 'zerotrust',
shell: options.shell ?: 'bash'
]
if (options.vcpu) payload.vcpu = options.vcpu
return apiRequest('/sessions', 'POST', payload, publicKey, secretKey)
}
/**
* Destroy a session.
*/
def sessionDestroy(String sessionId, Map options = [:]) {
def (publicKey, secretKey) = getCredentials(options.publicKey, options.secretKey)
return apiRequest("/sessions/${sessionId}", 'DELETE', null, publicKey, secretKey)
}
/**
* Freeze a session.
*/
def sessionFreeze(String sessionId, Map options = [:]) {
def (publicKey, secretKey) = getCredentials(options.publicKey, options.secretKey)
return apiRequest("/sessions/${sessionId}/freeze", 'POST', null, publicKey, secretKey)
}
/**
* Unfreeze a session.
*/
def sessionUnfreeze(String sessionId, Map options = [:]) {
def (publicKey, secretKey) = getCredentials(options.publicKey, options.secretKey)
return apiRequest("/sessions/${sessionId}/unfreeze", 'POST', null, publicKey, secretKey)
}
/**
* Boost a session.
*/
def sessionBoost(String sessionId, int vcpu = 2, Map options = [:]) {
def (publicKey, secretKey) = getCredentials(options.publicKey, options.secretKey)
return apiRequest("/sessions/${sessionId}/boost", 'POST', [vcpu: vcpu], publicKey, secretKey)
}
/**
* Unboost a session.
*/
def sessionUnboost(String sessionId, Map options = [:]) {
def (publicKey, secretKey) = getCredentials(options.publicKey, options.secretKey)
return apiRequest("/sessions/${sessionId}/unboost", 'POST', null, publicKey, secretKey)
}
/**
* Execute command in a session.
*/
def sessionExecute(String sessionId, String command, Map options = [:]) {
def (publicKey, secretKey) = getCredentials(options.publicKey, options.secretKey)
return apiRequest("/sessions/${sessionId}/shell", 'POST', [command: command], publicKey, secretKey)
}
// ============================================================================
// Service Functions
// ============================================================================
/**
* List all services.
*/
def serviceList(Map options = [:]) {
def (publicKey, secretKey) = getCredentials(options.publicKey, options.secretKey)
def result = apiRequest('/services', 'GET', null, publicKey, secretKey)
return result.services ?: []
}
/**
* Get service details.
*/
def serviceGet(String serviceId, Map options = [:]) {
def (publicKey, secretKey) = getCredentials(options.publicKey, options.secretKey)
return apiRequest("/services/${serviceId}", 'GET', null, publicKey, secretKey)
}
/**
* Create a new service.
*/
def serviceCreate(String name, Map options = [:]) {
def (publicKey, secretKey) = getCredentials(options.publicKey, options.secretKey)
def payload = [name: name]
if (options.ports) payload.ports = options.ports.split(',').collect { it.trim().toInteger() }
if (options.domains) payload.domains = options.domains
if (options.bootstrap) payload.bootstrap = options.bootstrap
if (options.networkMode) payload.network_mode = options.networkMode
def result = apiRequest('/services', 'POST', payload, publicKey, secretKey)
return result.id
}
/**
* Destroy a service.
*/
def serviceDestroy(String serviceId, Map options = [:]) {
def (publicKey, secretKey) = getCredentials(options.publicKey, options.secretKey)
return executeDestructive("/services/${serviceId}", 'DELETE', null, publicKey, secretKey)
}
/**
* Freeze a service.
*/
def serviceFreeze(String serviceId, Map options = [:]) {
def (publicKey, secretKey) = getCredentials(options.publicKey, options.secretKey)
return apiRequest("/services/${serviceId}/freeze", 'POST', null, publicKey, secretKey)
}
/**
* Unfreeze a service.
*/
def serviceUnfreeze(String serviceId, Map options = [:]) {
def (publicKey, secretKey) = getCredentials(options.publicKey, options.secretKey)
return apiRequest("/services/${serviceId}/unfreeze", 'POST', null, publicKey, secretKey)
}
/**
* Lock a service.
*/
def serviceLock(String serviceId, Map options = [:]) {
def (publicKey, secretKey) = getCredentials(options.publicKey, options.secretKey)
return apiRequest("/services/${serviceId}/lock", 'POST', null, publicKey, secretKey)
}
/**
* Unlock a service.
*/
def serviceUnlock(String serviceId, Map options = [:]) {
def (publicKey, secretKey) = getCredentials(options.publicKey, options.secretKey)
return executeDestructive("/services/${serviceId}/unlock", 'POST', null, publicKey, secretKey)
}
/**
* Set unfreeze on demand for a service.
*/
def serviceSetUnfreezeOnDemand(String serviceId, boolean enabled, Map options = [:]) {
def (publicKey, secretKey) = getCredentials(options.publicKey, options.secretKey)
return apiRequestPatch("/services/${serviceId}", [unfreeze_on_demand: enabled], publicKey, secretKey)
}
/**
* Redeploy a service.
*/
def serviceRedeploy(String serviceId, String bootstrap = null, Map options = [:]) {
def (publicKey, secretKey) = getCredentials(options.publicKey, options.secretKey)
def payload = bootstrap ? [bootstrap: bootstrap] : [:]
return apiRequest("/services/${serviceId}/redeploy", 'POST', payload, publicKey, secretKey)
}
/**
* Get service logs.
*/
def serviceLogs(String serviceId, boolean allLogs = false, Map options = [:]) {
def (publicKey, secretKey) = getCredentials(options.publicKey, options.secretKey)
def path = allLogs ? "/services/${serviceId}/logs?all=true" : "/services/${serviceId}/logs"
def result = apiRequest(path, 'GET', null, publicKey, secretKey)
return result.logs
}
/**
* Execute command in a service.
*/
def serviceExecute(String serviceId, String command, int timeoutMs = 0, Map options = [:]) {
def (publicKey, secretKey) = getCredentials(options.publicKey, options.secretKey)
def payload = [command: command]
if (timeoutMs > 0) payload.timeout = timeoutMs
return apiRequest("/services/${serviceId}/execute", 'POST', payload, publicKey, secretKey)
}
/**
* Get service environment vault status.
*/
def serviceEnvGet(String serviceId, Map options = [:]) {
def (publicKey, secretKey) = getCredentials(options.publicKey, options.secretKey)
return apiRequest("/services/${serviceId}/env", 'GET', null, publicKey, secretKey)
}
/**
* Set service environment vault.
*/
def serviceEnvSet(String serviceId, String envContent, Map options = [:]) {
def (publicKey, secretKey) = getCredentials(options.publicKey, options.secretKey)
return apiRequestText("/services/${serviceId}/env", 'PUT', envContent, publicKey, secretKey)
}
/**
* Delete service environment vault.
*/
def serviceEnvDelete(String serviceId, Map options = [:]) {
def (publicKey, secretKey) = getCredentials(options.publicKey, options.secretKey)
return apiRequest("/services/${serviceId}/env", 'DELETE', null, publicKey, secretKey)
}
/**
* Export service environment vault.
*/
def serviceEnvExport(String serviceId, Map options = [:]) {
def (publicKey, secretKey) = getCredentials(options.publicKey, options.secretKey)
return apiRequest("/services/${serviceId}/env/export", 'POST', [:], publicKey, secretKey)
}
/**
* Resize a service.
*/
def serviceResize(String serviceId, int vcpu, Map options = [:]) {
def (publicKey, secretKey) = getCredentials(options.publicKey, options.secretKey)
return apiRequestPatch("/services/${serviceId}", [vcpu: vcpu], publicKey, secretKey)
}
// ============================================================================
// Snapshot Functions
// ============================================================================
/**
* List all snapshots.
*/
def snapshotList(Map options = [:]) {
def (publicKey, secretKey) = getCredentials(options.publicKey, options.secretKey)
def result = apiRequest('/snapshots', 'GET', null, publicKey, secretKey)
return result.snapshots ?: []
}
/**
* Get snapshot details.
*/
def snapshotGet(String snapshotId, Map options = [:]) {
def (publicKey, secretKey) = getCredentials(options.publicKey, options.secretKey)
return apiRequest("/snapshots/${snapshotId}", 'GET', null, publicKey, secretKey)
}
/**
* Create snapshot from session.
*/
def snapshotSession(String sessionId, String name = null, boolean hot = false, Map options = [:]) {
def (publicKey, secretKey) = getCredentials(options.publicKey, options.secretKey)
def payload = [session_id: sessionId, hot: hot]
if (name) payload.name = name
def result = apiRequest('/snapshots', 'POST', payload, publicKey, secretKey)
return result.snapshot_id
}
/**
* Create snapshot from service.
*/
def snapshotService(String serviceId, String name = null, boolean hot = false, Map options = [:]) {
def (publicKey, secretKey) = getCredentials(options.publicKey, options.secretKey)
def payload = [service_id: serviceId, hot: hot]
if (name) payload.name = name
def result = apiRequest('/snapshots', 'POST', payload, publicKey, secretKey)
return result.snapshot_id
}
/**
* Restore a snapshot.
*/
def snapshotRestore(String snapshotId, Map options = [:]) {
def (publicKey, secretKey) = getCredentials(options.publicKey, options.secretKey)
return apiRequest("/snapshots/${snapshotId}/restore", 'POST', [:], publicKey, secretKey)
}
/**
* Delete a snapshot.
*/
def snapshotDelete(String snapshotId, Map options = [:]) {
def (publicKey, secretKey) = getCredentials(options.publicKey, options.secretKey)
return executeDestructive("/snapshots/${snapshotId}", 'DELETE', null, publicKey, secretKey)
}
/**
* Lock a snapshot.
*/
def snapshotLock(String snapshotId, Map options = [:]) {
def (publicKey, secretKey) = getCredentials(options.publicKey, options.secretKey)
return apiRequest("/snapshots/${snapshotId}/lock", 'POST', null, publicKey, secretKey)
}
/**
* Unlock a snapshot.
*/
def snapshotUnlock(String snapshotId, Map options = [:]) {
def (publicKey, secretKey) = getCredentials(options.publicKey, options.secretKey)
return executeDestructive("/snapshots/${snapshotId}/unlock", 'POST', null, publicKey, secretKey)
}
/**
* Clone a snapshot.
*/
def snapshotClone(String snapshotId, String cloneType, String name = null, String ports = null, String shell = null, Map options = [:]) {
def (publicKey, secretKey) = getCredentials(options.publicKey, options.secretKey)
def payload = [type: cloneType]
if (name) payload.name = name
if (ports) payload.ports = ports.split(',').collect { it.trim().toInteger() }
if (shell) payload.shell = shell
def result = apiRequest("/snapshots/${snapshotId}/clone", 'POST', payload, publicKey, secretKey)
return result.session_id ?: result.service_id
}
// ============================================================================
// Image Functions
// ============================================================================
/**
* List all images.
*/
def imageList(String filter = null, Map options = [:]) {
def (publicKey, secretKey) = getCredentials(options.publicKey, options.secretKey)
def path = filter ? "/images/${filter}" : '/images'
def result = apiRequest(path, 'GET', null, publicKey, secretKey)
return result.images ?: []
}
/**
* Get image details.
*/
def imageGet(String imageId, Map options = [:]) {
def (publicKey, secretKey) = getCredentials(options.publicKey, options.secretKey)
return apiRequest("/images/${imageId}", 'GET', null, publicKey, secretKey)
}
/**
* Publish an image.
*/
def imagePublish(String sourceType, String sourceId, String name = null, String description = null, Map options = [:]) {
def (publicKey, secretKey) = getCredentials(options.publicKey, options.secretKey)
def payload = [source_type: sourceType, source_id: sourceId]
if (name) payload.name = name
if (description) payload.description = description
def result = apiRequest('/images', 'POST', payload, publicKey, secretKey)
return result.image_id
}
/**
* Delete an image.
*/
def imageDelete(String imageId, Map options = [:]) {
def (publicKey, secretKey) = getCredentials(options.publicKey, options.secretKey)
return executeDestructive("/images/${imageId}", 'DELETE', null, publicKey, secretKey)
}
/**
* Lock an image.
*/
def imageLock(String imageId, Map options = [:]) {
def (publicKey, secretKey) = getCredentials(options.publicKey, options.secretKey)
return apiRequest("/images/${imageId}/lock", 'POST', null, publicKey, secretKey)
}
/**
* Unlock an image.
*/
def imageUnlock(String imageId, Map options = [:]) {
def (publicKey, secretKey) = getCredentials(options.publicKey, options.secretKey)
return executeDestructive("/images/${imageId}/unlock", 'POST', null, publicKey, secretKey)
}
/**
* Set image visibility.
*/
def imageSetVisibility(String imageId, String visibility, Map options = [:]) {
def (publicKey, secretKey) = getCredentials(options.publicKey, options.secretKey)
return apiRequest("/images/${imageId}/visibility", 'POST', [visibility: visibility], publicKey, secretKey)
}
/**
* Grant access to an image.
*/
def imageGrantAccess(String imageId, String trustedApiKey, Map options = [:]) {
def (publicKey, secretKey) = getCredentials(options.publicKey, options.secretKey)
return apiRequest("/images/${imageId}/grant", 'POST', [trusted_api_key: trustedApiKey], publicKey, secretKey)
}
/**
* Revoke access to an image.
*/
def imageRevokeAccess(String imageId, String trustedApiKey, Map options = [:]) {
def (publicKey, secretKey) = getCredentials(options.publicKey, options.secretKey)
return apiRequest("/images/${imageId}/revoke", 'POST', [trusted_api_key: trustedApiKey], publicKey, secretKey)
}
/**
* List trusted keys for an image.
*/
def imageListTrusted(String imageId, Map options = [:]) {
def (publicKey, secretKey) = getCredentials(options.publicKey, options.secretKey)
def result = apiRequest("/images/${imageId}/trusted", 'GET', null, publicKey, secretKey)
return result.trusted ?: []
}
/**
* Transfer image ownership.
*/
def imageTransfer(String imageId, String toApiKey, Map options = [:]) {
def (publicKey, secretKey) = getCredentials(options.publicKey, options.secretKey)
return apiRequest("/images/${imageId}/transfer", 'POST', [to_api_key: toApiKey], publicKey, secretKey)
}
/**
* Spawn a service from an image.
*/
def imageSpawn(String imageId, String name = null, String ports = null, String bootstrap = null, String networkMode = null, Map options = [:]) {
def (publicKey, secretKey) = getCredentials(options.publicKey, options.secretKey)
def payload = [:]
if (name) payload.name = name
if (ports) payload.ports = ports.split(',').collect { it.trim().toInteger() }
if (bootstrap) payload.bootstrap = bootstrap
if (networkMode) payload.network_mode = networkMode
def result = apiRequest("/images/${imageId}/spawn", 'POST', payload, publicKey, secretKey)
return result.service_id
}
/**
* Clone an image.
*/
def imageClone(String imageId, String name = null, String description = null, Map options = [:]) {
def (publicKey, secretKey) = getCredentials(options.publicKey, options.secretKey)
def payload = [:]
if (name) payload.name = name
if (description) payload.description = description
def result = apiRequest("/images/${imageId}/clone", 'POST', payload, publicKey, secretKey)
return result.image_id
}
// ============================================================================
// PaaS Logs Functions
// ============================================================================
/**
* Fetch batch logs.
*/
def logsFetch(String source = 'all', int lines = 100, String since = null, String grep = null, Map options = [:]) {
def (publicKey, secretKey) = getCredentials(options.publicKey, options.secretKey)
def params = ["source=${source}", "lines=${lines}"]
if (since) params << "since=${since}"
if (grep) params << "grep=${URLEncoder.encode(grep, 'UTF-8')}"
return apiRequest("/paas/logs?${params.join('&')}", 'GET', null, publicKey, secretKey)
}
/**
* Callback interface for log streaming.
*/
interface LogCallback {
void onLogLine(String source, String line)
}
/**
* Stream logs via SSE. Blocks until interrupted or server closes.
*
* @param source Log source ('all', 'api', 'portal', 'pool/cammy', 'pool/ai')
* @param grep Optional filter pattern
* @param callback Callback for each log line
* @param options Optional parameters (publicKey, secretKey)
* @return true on clean shutdown, false on error
*/
def logsStream(String source = 'all', String grep = null, LogCallback callback, Map options = [:]) {
def (publicKey, secretKey) = getCredentials(options.publicKey, options.secretKey)
def path = "/paas/logs/stream?source=${source ?: 'all'}"
if (grep) {
path += "&grep=${URLEncoder.encode(grep, 'UTF-8')}"
}
def timestamp = (System.currentTimeMillis() / 1000) as long
def signature = signRequest(secretKey, timestamp, 'GET', path, '')
def url = new URL("${API_BASE}${path}")
def connection = url.openConnection() as java.net.HttpURLConnection
connection.requestMethod = 'GET'
connection.setRequestProperty('Authorization', "Bearer ${publicKey}")
connection.setRequestProperty('X-Timestamp', timestamp.toString())
connection.setRequestProperty('X-Signature', signature)
connection.setRequestProperty('Accept', 'text/event-stream')
connection.connectTimeout = 30000
connection.readTimeout = 0 // No timeout for streaming
if (connection.responseCode != 200) {
return false
}
try {
def reader = new BufferedReader(new InputStreamReader(connection.inputStream, 'UTF-8'))
def currentSource = source ?: 'all'
def line
while ((line = reader.readLine()) != null) {
if (line.startsWith('data: ')) {
def data = line.substring(6)
if (callback) {
callback.onLogLine(currentSource, data)
}
} else if (line.startsWith('event: ')) {
currentSource = line.substring(7)
}
}
return true
} catch (Exception e) {
return false
}
}
/**
* Validate API keys.
*/
def validateKeys(Map options = [:]) {
def (publicKey, secretKey) = getCredentials(options.publicKey, options.secretKey)
def timestamp = (System.currentTimeMillis() / 1000) as long
def message = "${timestamp}:POST:/keys/validate:{}"
def signature = signRequest(secretKey, timestamp, 'POST', '/keys/validate', '{}')
def url = new URL("${PORTAL_BASE}/keys/validate")
def connection = url.openConnection() as java.net.HttpURLConnection
connection.requestMethod = 'POST'
connection.setRequestProperty('Authorization', "Bearer ${publicKey}")
connection.setRequestProperty('X-Timestamp', timestamp.toString())
connection.setRequestProperty('X-Signature', signature)
connection.setRequestProperty('Content-Type', 'application/json')
connection.connectTimeout = 30000
connection.readTimeout = 30000
connection.doOutput = true
connection.outputStream.withWriter { it.write('{}') }
if (connection.responseCode !in 200..299) {
throw new APIError("HTTP ${connection.responseCode}")
}
return new JsonSlurper().parseText(connection.inputStream.text)
}
/**
* Detect programming language from file extension or shebang.
*
* @param filename File path
* @return Language name or null if undetected
*/
def detectLanguage(String filename) {
def dotIndex = filename.lastIndexOf('.')
if (dotIndex == -1) return null
def ext = filename.substring(dotIndex)
def language = EXT_MAP[ext]
if (language) return language
// Try shebang
try {
def file = new File(filename)
if (file.exists()) {
def firstLine = file.readLines()[0]
if (firstLine?.startsWith('#!')) {
if (firstLine.contains('python')) return 'python'
if (firstLine.contains('node')) return 'javascript'
if (firstLine.contains('ruby')) return 'ruby'
if (firstLine.contains('perl')) return 'perl'
if (firstLine.contains('bash') || firstLine.contains('/sh')) return 'bash'
if (firstLine.contains('lua')) return 'lua'
if (firstLine.contains('php')) return 'php'
}
}
} catch (Exception e) {
// Ignore file read errors
}
return null
}
// ============================================================================
// Client Class
// ============================================================================
/**
* Unsandbox API client with stored credentials.
*
* <p>Use the Client class when making multiple API calls to avoid
* repeated credential resolution.</p>
*
* <pre>{@code
* // With explicit credentials
* def client = new un.Client(publicKey: "unsb-pk-...", secretKey: "unsb-sk-...")
* def result = client.execute("python", 'print("Hello")')
*
* // Or load from environment/config automatically
* def client = new un.Client()
* def result = client.execute("python", code)
* }</pre>
*
* @author Permacomputer Project
*/
class Client {
String publicKey
String secretKey
/**
* Initialize client with credentials.
*
* @param options Optional parameters:
* <ul>
* <li>publicKey: API public key (unsb-pk-...)</li>
* <li>secretKey: API secret key (unsb-sk-...)</li>
* <li>accountIndex: Account index in ~/.unsandbox/accounts.csv (default 0)</li>
* </ul>
*/
Client(Map options = [:]) {
def creds = getCredentialsStatic(
options.publicKey,
options.secretKey,
options.accountIndex != null ? options.accountIndex : -1
)
this.publicKey = creds[0]
this.secretKey = creds[1]
}
private static loadCsvAccounts(File path) {
def accounts = []
if (!path.exists()) return accounts
try {
path.text.trim().split('\n').each { line ->
def trimmed = line.trim()
if (!trimmed || trimmed.startsWith('#')) return
if (trimmed.contains(',')) {
def parts = trimmed.split(',', 2)
def pk = parts[0].trim()
def sk = parts[1].trim()
if (pk.startsWith('unsb-pk-') && sk.startsWith('unsb-sk-')) {
accounts << [pk, sk]
}
}
}
} catch (Exception e) {
// Ignore
}
return accounts
}
private static getCredentialsStatic(String publicKey, String secretKey, int accountIndex) {
// Priority 1: explicit arguments
if (publicKey && secretKey) {
return [publicKey, secretKey]
}
// Priority 2: --account N => CSV row N (bypasses env vars)
if (accountIndex >= 0) {
def searchPaths = [
new File(System.getProperty('user.home'), '.unsandbox/accounts.csv'),
new File('accounts.csv')
]
for (path in searchPaths) {
def accts = loadCsvAccounts(path)
if (accts && accountIndex < accts.size()) {
return accts[accountIndex]
}
}
throw new AuthenticationError("No account at index ${accountIndex} in accounts.csv")
}
// Priority 3: Environment variables
def envPk = System.getenv('UNSANDBOX_PUBLIC_KEY')
def envSk = System.getenv('UNSANDBOX_SECRET_KEY')
if (envPk && envSk) {
return [envPk, envSk]
}
// Priority 4: ~/.unsandbox/accounts.csv row 0 (or UNSANDBOX_ACCOUNT env)
def defaultIdx = (System.getenv('UNSANDBOX_ACCOUNT') ?: '0').toInteger()
def searchPaths = [
new File(System.getProperty('user.home'), '.unsandbox/accounts.csv'),
new File('accounts.csv')
]
for (path in searchPaths) {
def accts = loadCsvAccounts(path)
if (accts && defaultIdx < accts.size()) {
return accts[defaultIdx]
}
}
throw new AuthenticationError(
"No credentials found. Set UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY."
)
}
/**
* Execute code synchronously.
* @see #execute(String, String, Map)
*/
def execute(String language, String code, Map options = [:]) {
options.publicKey = this.publicKey
options.secretKey = this.secretKey
return binding.execute(language, code, options)
}
/**
* Execute code asynchronously.
* @see #executeAsync(String, String, Map)
*/
def executeAsync(String language, String code, Map options = [:]) {
options.publicKey = this.publicKey
options.secretKey = this.secretKey
return binding.executeAsync(language, code, options)
}
/**
* Execute with auto-detect.
* @see #run(String, Map)
*/
def run(String code, Map options = [:]) {
options.publicKey = this.publicKey
options.secretKey = this.secretKey
return binding.run(code, options)
}
/**
* Execute async with auto-detect.
* @see #runAsync(String, Map)
*/
def runAsync(String code, Map options = [:]) {
options.publicKey = this.publicKey
options.secretKey = this.secretKey
return binding.runAsync(code, options)
}
/**
* Get job status.
* @see #getJob(String, Map)
*/
def getJob(String jobId) {
return binding.getJob(jobId, [publicKey: this.publicKey, secretKey: this.secretKey])
}
/**
* Wait for job completion.
* @see #wait(String, Map)
*/
def wait(String jobId, Map options = [:]) {
options.publicKey = this.publicKey
options.secretKey = this.secretKey
return binding.wait(jobId, options)
}
/**
* Cancel a job.
* @see #cancelJob(String, Map)
*/
def cancelJob(String jobId) {
return binding.cancelJob(jobId, [publicKey: this.publicKey, secretKey: this.secretKey])
}
/**
* List active jobs.
* @see #listJobs(Map)
*/
def listJobs() {
return binding.listJobs([publicKey: this.publicKey, secretKey: this.secretKey])
}
/**
* Generate image.
* @see #image(String, Map)
*/
def image(String prompt, Map options = [:]) {
options.publicKey = this.publicKey
options.secretKey = this.secretKey
return binding.image(prompt, options)
}
/**
* Get supported languages.
* @see #languages(Map)
*/
def languages() {
return binding.languages([publicKey: this.publicKey, secretKey: this.secretKey])
}
}
// ============================================================================
// CLI Support Classes and Functions
// ============================================================================
class Args {
String command = null
String sourceFile = null
String inlineLang = null
String apiKey = null
Integer accountIndex = -1
String network = null
Integer vcpu = 0
List<String> env = []
List<String> files = []
Boolean artifacts = false
String outputDir = null
Boolean sessionList = false
String sessionShell = null
String sessionKill = null
String sessionSnapshot = null
String sessionRestore = null
String sessionFrom = null
String sessionSnapshotName = null
Boolean sessionHot = false
Boolean serviceList = false
String serviceName = null
String servicePorts = null
String serviceType = null
String serviceBootstrap = null
String serviceBootstrapFile = null
String serviceInfo = null
String serviceLogs = null
String serviceTail = null
String serviceSleep = null
String serviceWake = null
String serviceDestroy = null
String serviceExecute = null
String serviceCommand = null
String serviceDumpBootstrap = null
String serviceDumpFile = null
String serviceResize = null
String serviceSetUnfreezeOnDemand = null
String serviceUnfreezeOnDemandValue = null
String serviceSnapshot = null
String serviceRestore = null
String serviceFrom = null
String serviceSnapshotName = null
Boolean serviceHot = false
Boolean snapshotList = false
String snapshotInfo = null
String snapshotDelete = null
String snapshotClone = null
String snapshotType = null
String snapshotName = null
String snapshotShell = null
String snapshotPorts = null
Boolean keyExtend = false
Boolean imageList = false
String imageInfo = null
String imageDelete = null
String imageLock = null
String imageUnlock = null
String imagePublish = null
String imageSourceType = null
String imageVisibility = null
String imageVisibilityMode = null
String imageSpawn = null
String imageClone = null
String imageName = null
String imagePorts = null
List<String> svcEnvs = []
String svcEnvFile = null
String envAction = null
String envTarget = null
Boolean jsonOutput = false
}
def readEnvFile(filename) {
def file = new File(filename)
if (!file.exists()) {
System.err.println("${RED}Error: Cannot read env file: ${filename}${RESET}")
return ''
}
return file.text
}
def buildEnvContent(envs, envFile) {
def result = new StringBuilder()
envs.each { env ->
result.append(env).append('\n')
}
if (envFile) {
def content = readEnvFile(envFile)
content.split('\n').each { line ->
def trimmed = line.trim()
if (trimmed && !trimmed.startsWith('#')) {
result.append(trimmed).append('\n')
}
}
}
return result.toString()
}
def serviceEnvSet(serviceId, content, publicKey, secretKey) {
return apiRequestText("/services/${serviceId}/env", 'PUT', content, publicKey, secretKey)
}
def cmdServiceEnv(args) {
def (publicKey, secretKey) = getApiKeys(args.apiKey, args.accountIndex ?: -1)
switch (args.envAction) {
case 'status':
def output = apiRequest("/services/${args.envTarget}/env", 'GET', null, publicKey, secretKey)
println(JsonOutput.prettyPrint(JsonOutput.toJson(output)))
break
case 'set':
if (!args.svcEnvs && !args.svcEnvFile) {
System.err.println("${RED}Error: No environment variables specified. Use -e KEY=VALUE or --env-file FILE${RESET}")
return
}
def content = buildEnvContent(args.svcEnvs, args.svcEnvFile)
if (content.length() > MAX_ENV_CONTENT_SIZE) {
System.err.println("${RED}Error: Environment content exceeds 64KB limit${RESET}")
return
}
if (serviceEnvSet(args.envTarget, content, publicKey, secretKey)) {
println("${GREEN}Vault updated for service ${args.envTarget}${RESET}")
}
break
case 'export':
def output = apiRequest("/services/${args.envTarget}/env/export", 'POST', null, publicKey, secretKey)
println(JsonOutput.prettyPrint(JsonOutput.toJson(output)))
break
case 'delete':
apiRequest("/services/${args.envTarget}/env", 'DELETE', null, publicKey, secretKey)
println("${GREEN}Vault deleted for service ${args.envTarget}${RESET}")
break
default:
System.err.println("${RED}Error: Unknown env action: ${args.envAction}${RESET}")
System.err.println("Usage: un service env <status|set|export|delete> <service_id>")
}
}
def cmdExecute(args) {
def (publicKey, secretKey) = getApiKeys(args.apiKey, args.accountIndex ?: -1)
String code
String language
if (args.inlineLang) {
language = args.inlineLang
code = args.sourceFile ?: ""
} else {
def file = new File(args.sourceFile)
if (!file.exists()) {
System.err.println("${RED}Error: File not found: ${args.sourceFile}${RESET}")
System.exit(1)
}
code = file.text
language = detectLanguage(args.sourceFile)
if (!language) {
System.err.println("${RED}Error: Cannot detect language for ${args.sourceFile}${RESET}")
System.exit(1)
}
}
def options = [
networkMode: args.network ?: 'zerotrust',
vcpu: args.vcpu > 0 ? args.vcpu : 1,
publicKey: publicKey,
secretKey: secretKey
]
if (args.env) {
def envMap = [:]
args.env.each { e ->
def parts = e.split('=', 2)
if (parts.size() == 2) {
envMap[parts[0]] = parts[1]
}
}
if (envMap) options.env = envMap
}
if (args.files) {
options.inputFiles = args.files.collect { filepath ->
def f = new File(filepath)
if (!f.exists()) {
System.err.println("${RED}Error: Input file not found: ${filepath}${RESET}")
System.exit(1)
}
return [filename: f.name, contentBase64: f.bytes.encodeBase64().toString()]
}
}
if (args.artifacts) {
options.returnArtifact = true
}
def result = execute(language, code, options)
if (result.stdout) {
print("${BLUE}${result.stdout}${RESET}")
}
if (result.stderr) {
System.err.print("${RED}${result.stderr}${RESET}")
}
if (args.artifacts && result.artifacts) {
def outDir = args.outputDir ?: '.'
new File(outDir).mkdirs()
result.artifacts.each { artifact ->
def filename = artifact.filename ?: 'artifact'
def content = artifact.content_base64.decodeBase64()
def filepath = new File(outDir, filename)
filepath.bytes = content
"chmod 755 ${filepath.absolutePath}".execute().waitFor()
System.err.println("${GREEN}Saved: ${filepath.absolutePath}${RESET}")
}
}
System.exit(result.exit_code ?: 0)
}
def cmdSession(args) {
def (publicKey, secretKey) = getApiKeys(args.apiKey, args.accountIndex ?: -1)
if (args.sessionSnapshot) {
def payload = [:]
if (args.sessionSnapshotName) payload.name = args.sessionSnapshotName
if (args.sessionHot) payload.hot = true
def output = apiRequest("/sessions/${args.sessionSnapshot}/snapshot", 'POST', payload, publicKey, secretKey)
println("${GREEN}Snapshot created${RESET}")
println(JsonOutput.prettyPrint(JsonOutput.toJson(output)))
return
}
if (args.sessionRestore) {
def output = apiRequest("/snapshots/${args.sessionRestore}/restore", 'POST', [:], publicKey, secretKey)
println("${GREEN}Session restored from snapshot${RESET}")
println(JsonOutput.prettyPrint(JsonOutput.toJson(output)))
return
}
if (args.sessionList) {
def output = apiRequest('/sessions', 'GET', null, publicKey, secretKey)
def sessions = output.sessions ?: []
if (sessions.isEmpty()) {
println("No active sessions")
} else {
println(String.format("%-40s %-10s %-10s %s", "ID", "Shell", "Status", "Created"))
sessions.each { s ->
println(String.format("%-40s %-10s %-10s %s",
s.id ?: '', s.shell ?: '', s.status ?: '', s.created_at ?: ''))
}
}
return
}
if (args.sessionKill) {
apiRequest("/sessions/${args.sessionKill}", 'DELETE', null, publicKey, secretKey)
println("${GREEN}Session terminated: ${args.sessionKill}${RESET}")
return
}
def payload = [shell: args.sessionShell ?: 'bash']
if (args.network) payload.network = args.network
if (args.vcpu > 0) payload.vcpu = args.vcpu
if (args.files) {
payload.input_files = args.files.collect { filepath ->
def f = new File(filepath)
if (!f.exists()) {
System.err.println("${RED}Error: Input file not found: ${filepath}${RESET}")
System.exit(1)
}
return [filename: f.name, content_base64: f.bytes.encodeBase64().toString()]
}
}
println("${YELLOW}Creating session...${RESET}")
def output = apiRequest('/sessions', 'POST', payload, publicKey, secretKey)
println("${GREEN}Session created: ${output.id ?: 'unknown'}${RESET}")
println("${YELLOW}(Interactive sessions require WebSocket - use un2 for full support)${RESET}")
}
def openBrowser(url) {
def osName = System.getProperty('os.name').toLowerCase()
try {
if (osName.contains('linux')) {
Runtime.runtime.exec(['xdg-open', url] as String[])
} else if (osName.contains('mac')) {
Runtime.runtime.exec(['open', url] as String[])
} else if (osName.contains('win')) {
Runtime.runtime.exec(['cmd', '/c', 'start', url] as String[])
}
} catch (Exception e) {
System.err.println("${RED}Error opening browser: ${e.message}${RESET}")
}
}
def cmdSnapshot(args) {
def (publicKey, secretKey) = getApiKeys(args.apiKey, args.accountIndex ?: -1)
if (args.snapshotList) {
def output = apiRequest('/snapshots', 'GET', null, publicKey, secretKey)
println(JsonOutput.prettyPrint(JsonOutput.toJson(output)))
return
}
if (args.snapshotInfo) {
def output = apiRequest("/snapshots/${args.snapshotInfo}", 'GET', null, publicKey, secretKey)
println(JsonOutput.prettyPrint(JsonOutput.toJson(output)))
return
}
if (args.snapshotDelete) {
executeDestructive("/snapshots/${args.snapshotDelete}", 'DELETE', null, publicKey, secretKey)
println("${GREEN}Snapshot deleted: ${args.snapshotDelete}${RESET}")
return
}
if (args.snapshotClone) {
if (!args.snapshotType) {
System.err.println("${RED}Error: --type required (session or service)${RESET}")
System.exit(1)
}
def payload = [type: args.snapshotType]
if (args.snapshotName) payload.name = args.snapshotName
if (args.snapshotShell) payload.shell = args.snapshotShell
if (args.snapshotPorts) payload.ports = args.snapshotPorts.split(',').collect { it.trim().toInteger() }
def output = apiRequest("/snapshots/${args.snapshotClone}/clone", 'POST', payload, publicKey, secretKey)
println("${GREEN}Created from snapshot${RESET}")
println(JsonOutput.prettyPrint(JsonOutput.toJson(output)))
return
}
System.err.println("Error: Use --list, --info ID, --delete ID, or --clone ID --type TYPE")
System.exit(1)
}
def cmdImage(args) {
def (publicKey, secretKey) = getApiKeys(args.apiKey, args.accountIndex ?: -1)
if (args.imageList) {
def output = apiRequest('/images', 'GET', null, publicKey, secretKey)
println(JsonOutput.prettyPrint(JsonOutput.toJson(output)))
return
}
if (args.imageInfo) {
def output = apiRequest("/images/${args.imageInfo}", 'GET', null, publicKey, secretKey)
println(JsonOutput.prettyPrint(JsonOutput.toJson(output)))
return
}
if (args.imageDelete) {
executeDestructive("/images/${args.imageDelete}", 'DELETE', null, publicKey, secretKey)
println("${GREEN}Image deleted: ${args.imageDelete}${RESET}")
return
}
if (args.imageLock) {
apiRequest("/images/${args.imageLock}/lock", 'POST', null, publicKey, secretKey)
println("${GREEN}Image locked: ${args.imageLock}${RESET}")
return
}
if (args.imageUnlock) {
executeDestructive("/images/${args.imageUnlock}/unlock", 'POST', null, publicKey, secretKey)
println("${GREEN}Image unlocked: ${args.imageUnlock}${RESET}")
return
}
if (args.imagePublish) {
if (!args.imageSourceType) {
System.err.println("${RED}Error: --source-type required (service or snapshot)${RESET}")
System.exit(1)
}
def payload = [source_type: args.imageSourceType, source_id: args.imagePublish]
if (args.imageName) payload.name = args.imageName
def output = apiRequest("/images/publish", 'POST', payload, publicKey, secretKey)
println("${GREEN}Image published${RESET}")
println(JsonOutput.prettyPrint(JsonOutput.toJson(output)))
return
}
if (args.imageVisibility) {
if (!args.imageVisibilityMode) {
System.err.println("${RED}Error: --visibility requires MODE (private, unlisted, or public)${RESET}")
System.exit(1)
}
def payload = [visibility: args.imageVisibilityMode]
apiRequest("/images/${args.imageVisibility}/visibility", 'POST', payload, publicKey, secretKey)
println("${GREEN}Image visibility set to ${args.imageVisibilityMode}: ${args.imageVisibility}${RESET}")
return
}
if (args.imageSpawn) {
def payload = [:]
if (args.imageName) payload.name = args.imageName
if (args.imagePorts) payload.ports = args.imagePorts.split(',').collect { it.trim().toInteger() }
def output = apiRequest("/images/${args.imageSpawn}/spawn", 'POST', payload, publicKey, secretKey)
println("${GREEN}Service spawned from image${RESET}")
println(JsonOutput.prettyPrint(JsonOutput.toJson(output)))
return
}
if (args.imageClone) {
def payload = [:]
if (args.imageName) payload.name = args.imageName
def output = apiRequest("/images/${args.imageClone}/clone", 'POST', payload, publicKey, secretKey)
println("${GREEN}Image cloned${RESET}")
println(JsonOutput.prettyPrint(JsonOutput.toJson(output)))
return
}
System.err.println("${RED}Error: Use --list, --info ID, --delete ID, --lock ID, --unlock ID, --publish ID, --visibility ID MODE, --spawn ID, or --clone ID${RESET}")
System.exit(1)
}
def cmdLanguages(args) {
def (publicKey, secretKey) = getApiKeys(args.apiKey, args.accountIndex ?: -1)
def result = languages([publicKey: publicKey, secretKey: secretKey, forceRefresh: true])
def langList = result.languages ?: []
if (args.jsonOutput) {
println(JsonOutput.toJson(langList))
} else {
langList.each { lang ->
println(lang)
}
}
}
def cmdKey(args) {
def (publicKey, secretKey) = getApiKeys(args.apiKey, args.accountIndex ?: -1)
def curlCmd = ['curl', '-s', '-X', 'POST', "${PORTAL_BASE}/keys/validate",
'-H', 'Content-Type: application/json']
if (secretKey) {
def timestamp = (System.currentTimeMillis() / 1000) as long
def message = "${timestamp}:POST:/keys/validate:{}"
def mac = Mac.getInstance("HmacSHA256")
mac.init(new SecretKeySpec(secretKey.getBytes("UTF-8"), "HmacSHA256"))
def signature = mac.doFinal(message.getBytes("UTF-8")).encodeHex().toString()
curlCmd += ['-H', "Authorization: Bearer ${publicKey}"]
curlCmd += ['-H', "X-Timestamp: ${timestamp}"]
curlCmd += ['-H', "X-Signature: ${signature}"]
} else {
curlCmd += ['-H', "Authorization: Bearer ${publicKey}"]
}
curlCmd += ['-d', '{}']
def proc = curlCmd.execute()
def output = proc.text
proc.waitFor()
if (proc.exitValue() != 0) {
println("${RED}Invalid${RESET}")
System.err.println("${RED}Error: Failed to validate key${RESET}")
System.exit(1)
}
def result = new JsonSlurper().parseText(output)
def fetchedPublicKey = result.public_key ?: 'N/A'
def tier = result.tier ?: 'N/A'
def status = result.status ?: 'N/A'
def expiresAt = result.expires_at ?: 'N/A'
def timeRemaining = result.time_remaining ?: 'N/A'
def rateLimit = result.rate_limit ?: 'N/A'
def burst = result.burst ?: 'N/A'
def concurrency = result.concurrency ?: 'N/A'
def expired = result.expired ?: false
if (args.keyExtend && fetchedPublicKey != 'N/A') {
def extendUrl = "${PORTAL_BASE}/keys/extend?pk=${fetchedPublicKey}"
println("${BLUE}Opening browser to extend key...${RESET}")
openBrowser(extendUrl)
return
}
if (expired) {
println("${RED}Expired${RESET}")
println("Public Key: ${fetchedPublicKey}")
println("Tier: ${tier}")
println("Expired: ${expiresAt}")
println("${YELLOW}To renew: Visit https://unsandbox.com/keys/extend${RESET}")
System.exit(1)
}
println("${GREEN}Valid${RESET}")
println("Public Key: ${fetchedPublicKey}")
println("Tier: ${tier}")
println("Status: ${status}")
println("Expires: ${expiresAt}")
println("Time Remaining: ${timeRemaining}")
println("Rate Limit: ${rateLimit}")
println("Burst: ${burst}")
println("Concurrency: ${concurrency}")
}
def cmdService(args) {
def (publicKey, secretKey) = getApiKeys(args.apiKey, args.accountIndex ?: -1)
if (args.serviceSnapshot) {
def payload = [:]
if (args.serviceSnapshotName) payload.name = args.serviceSnapshotName
if (args.serviceHot) payload.hot = true
def output = apiRequest("/services/${args.serviceSnapshot}/snapshot", 'POST', payload, publicKey, secretKey)
println("${GREEN}Snapshot created${RESET}")
println(JsonOutput.prettyPrint(JsonOutput.toJson(output)))
return
}
if (args.serviceRestore) {
def output = apiRequest("/snapshots/${args.serviceRestore}/restore", 'POST', [:], publicKey, secretKey)
println("${GREEN}Service restored from snapshot${RESET}")
println(JsonOutput.prettyPrint(JsonOutput.toJson(output)))
return
}
if (args.serviceList) {
def output = apiRequest('/services', 'GET', null, publicKey, secretKey)
def services = output.services ?: []
if (services.isEmpty()) {
println("No services")
} else {
println(String.format("%-20s %-15s %-10s %-15s %s", "ID", "Name", "Status", "Ports", "Domains"))
services.each { s ->
def ports = (s.ports ?: []).join(',')
def domains = (s.domains ?: []).join(',')
println(String.format("%-20s %-15s %-10s %-15s %s",
s.id ?: '', s.name ?: '', s.status ?: '', ports, domains))
}
}
return
}
if (args.serviceInfo) {
def output = apiRequest("/services/${args.serviceInfo}", 'GET', null, publicKey, secretKey)
println(JsonOutput.prettyPrint(JsonOutput.toJson(output)))
return
}
if (args.serviceLogs) {
def output = apiRequest("/services/${args.serviceLogs}/logs", 'GET', null, publicKey, secretKey)
println(output.logs ?: '')
return
}
if (args.serviceTail) {
def output = apiRequest("/services/${args.serviceTail}/logs?lines=9000", 'GET', null, publicKey, secretKey)
println(output.logs ?: '')
return
}
if (args.serviceSleep) {
apiRequest("/services/${args.serviceSleep}/freeze", 'POST', null, publicKey, secretKey)
println("${GREEN}Service frozen: ${args.serviceSleep}${RESET}")
return
}
if (args.serviceWake) {
apiRequest("/services/${args.serviceWake}/unfreeze", 'POST', null, publicKey, secretKey)
println("${GREEN}Service unfreezing: ${args.serviceWake}${RESET}")
return
}
if (args.serviceDestroy) {
executeDestructive("/services/${args.serviceDestroy}", 'DELETE', null, publicKey, secretKey)
println("${GREEN}Service destroyed: ${args.serviceDestroy}${RESET}")
return
}
if (args.serviceResize) {
if (args.vcpu <= 0) {
System.err.println("${RED}Error: --resize requires --vcpu N (1-8)${RESET}")
System.exit(1)
}
apiRequestPatch("/services/${args.serviceResize}", [vcpu: args.vcpu], publicKey, secretKey)
def ram = args.vcpu * 2
println("${GREEN}Service resized to ${args.vcpu} vCPU, ${ram} GB RAM${RESET}")
return
}
if (args.serviceSetUnfreezeOnDemand) {
def enabledStr = (args.serviceUnfreezeOnDemandValue ?: 'false').toLowerCase()
def enabled = enabledStr in ['true', '1', 'yes', 'on']
apiRequestPatch("/services/${args.serviceSetUnfreezeOnDemand}", [unfreeze_on_demand: enabled], publicKey, secretKey)
println("${GREEN}Service unfreeze_on_demand set to ${enabled}: ${args.serviceSetUnfreezeOnDemand}${RESET}")
return
}
if (args.serviceExecute) {
def output = apiRequest("/services/${args.serviceExecute}/execute", 'POST',
[command: args.serviceCommand], publicKey, secretKey)
if (output.stdout) print("${BLUE}${output.stdout}${RESET}")
if (output.stderr) System.err.print("${RED}${output.stderr}${RESET}")
return
}
if (args.serviceDumpBootstrap) {
System.err.println("Fetching bootstrap script from ${args.serviceDumpBootstrap}...")
def output = apiRequest("/services/${args.serviceDumpBootstrap}/execute", 'POST',
[command: 'cat /tmp/bootstrap.sh'], publicKey, secretKey)
if (output.stdout) {
if (args.serviceDumpFile) {
try {
new File(args.serviceDumpFile).text = output.stdout
"chmod 755 ${args.serviceDumpFile}".execute().waitFor()
println("Bootstrap saved to ${args.serviceDumpFile}")
} catch (Exception e) {
System.err.println("${RED}Error: Could not write to ${args.serviceDumpFile}: ${e.message}${RESET}")
System.exit(1)
}
} else {
print(output.stdout)
}
} else {
System.err.println("${RED}Error: Failed to fetch bootstrap (service not running or no bootstrap file)${RESET}")
System.exit(1)
}
return
}
if (args.serviceName) {
def payload = [name: args.serviceName]
if (args.servicePorts) {
payload.ports = args.servicePorts.split(',').collect { it.trim().toInteger() }
}
if (args.serviceType) payload.service_type = args.serviceType
if (args.serviceBootstrap) payload.bootstrap = args.serviceBootstrap
if (args.serviceBootstrapFile) {
def file = new File(args.serviceBootstrapFile)
if (file.exists()) {
payload.bootstrap_content = file.text
} else {
System.err.println("${RED}Error: Bootstrap file not found: ${args.serviceBootstrapFile}${RESET}")
System.exit(1)
}
}
if (args.network) payload.network = args.network
if (args.vcpu > 0) payload.vcpu = args.vcpu
if (args.files) {
payload.input_files = args.files.collect { filepath ->
def f = new File(filepath)
if (!f.exists()) {
System.err.println("${RED}Error: Input file not found: ${filepath}${RESET}")
System.exit(1)
}
return [filename: f.name, content_base64: f.bytes.encodeBase64().toString()]
}
}
def output = apiRequest('/services', 'POST', payload, publicKey, secretKey)
def serviceId = output.id
println("${GREEN}Service created: ${serviceId ?: 'unknown'}${RESET}")
println("Name: ${output.name ?: ''}")
if (output.url) println("URL: ${output.url}")
// Auto-set vault if -e or --env-file provided
if (serviceId && (args.svcEnvs || args.svcEnvFile)) {
def envContent = buildEnvContent(args.svcEnvs, args.svcEnvFile)
if (envContent) {
if (serviceEnvSet(serviceId, envContent, publicKey, secretKey)) {
println("${GREEN}Vault configured for service ${serviceId}${RESET}")
}
}
}
return
}
System.err.println("${RED}Error: Specify --name to create a service, or use --list, --info, etc.${RESET}")
System.exit(1)
}
def parseArgs(argv) {
def args = new Args()
def i = 0
while (i < argv.size()) {
switch (argv[i]) {
case 'languages':
args.command = 'languages'
break
case 'session':
args.command = 'session'
break
case 'service':
args.command = 'service'
break
case 'env':
if (args.command == 'service' && i + 2 < argv.size()) {
args.envAction = argv[++i]
args.envTarget = argv[++i]
}
break
case 'snapshot':
args.command = 'snapshot'
break
case 'image':
args.command = 'image'
break
case 'key':
args.command = 'key'
break
case '-s':
args.inlineLang = argv[++i]
break
case '-k':
case '--api-key':
args.apiKey = argv[++i]
break
case '-p':
case '--public-key':
args.apiKey = argv[++i] // For compatibility
break
case '--account':
args.accountIndex = argv[++i].toInteger()
break
case '-n':
case '--network':
args.network = argv[++i]
break
case '-v':
case '--vcpu':
args.vcpu = argv[++i].toInteger()
break
case '-e':
case '--env':
def envVal = argv[++i]
args.env << envVal
if (args.command == 'service') {
args.svcEnvs << envVal
}
break
case '--env-file':
args.svcEnvFile = argv[++i]
break
case '-f':
case '--files':
args.files << argv[++i]
break
case '-a':
case '--artifacts':
args.artifacts = true
break
case '-o':
case '--output-dir':
args.outputDir = argv[++i]
break
case '-l':
case '--list':
if (args.command == 'session') args.sessionList = true
else if (args.command == 'service') args.serviceList = true
else if (args.command == 'snapshot') args.snapshotList = true
else if (args.command == 'image') args.imageList = true
break
case '--shell':
if (args.command == 'snapshot') args.snapshotShell = argv[++i]
else args.sessionShell = argv[++i]
break
case '--kill':
args.sessionKill = argv[++i]
break
case '--snapshot':
if (args.command == 'session') args.sessionSnapshot = argv[++i]
else if (args.command == 'service') args.serviceSnapshot = argv[++i]
break
case '--restore':
if (args.command == 'session') args.sessionRestore = argv[++i]
else if (args.command == 'service') args.serviceRestore = argv[++i]
break
case '--from':
if (args.command == 'session') args.sessionFrom = argv[++i]
else if (args.command == 'service') args.serviceFrom = argv[++i]
break
case '--snapshot-name':
if (args.command == 'session') args.sessionSnapshotName = argv[++i]
else if (args.command == 'service') args.serviceSnapshotName = argv[++i]
break
case '--hot':
if (args.command == 'session') args.sessionHot = true
else if (args.command == 'service') args.serviceHot = true
break
case '--info':
if (args.command == 'snapshot') args.snapshotInfo = argv[++i]
else if (args.command == 'image') args.imageInfo = argv[++i]
else args.serviceInfo = argv[++i]
break
case '--delete':
if (args.command == 'snapshot') args.snapshotDelete = argv[++i]
else if (args.command == 'image') args.imageDelete = argv[++i]
break
case '--clone':
if (args.command == 'image') args.imageClone = argv[++i]
else args.snapshotClone = argv[++i]
break
case '--lock':
if (args.command == 'image') args.imageLock = argv[++i]
break
case '--unlock':
if (args.command == 'image') args.imageUnlock = argv[++i]
break
case '--publish':
if (args.command == 'image') args.imagePublish = argv[++i]
break
case '--source-type':
args.imageSourceType = argv[++i]
break
case '--visibility':
if (args.command == 'image') {
args.imageVisibility = argv[++i]
if (i + 1 < argv.size() && !argv[i + 1].startsWith('-')) {
args.imageVisibilityMode = argv[++i]
}
}
break
case '--spawn':
if (args.command == 'image') args.imageSpawn = argv[++i]
break
case '--type':
if (args.command == 'snapshot') args.snapshotType = argv[++i]
else args.serviceType = argv[++i]
break
case '--name':
if (args.command == 'snapshot') args.snapshotName = argv[++i]
else if (args.command == 'image') args.imageName = argv[++i]
else args.serviceName = argv[++i]
break
case '--ports':
if (args.command == 'snapshot') args.snapshotPorts = argv[++i]
else if (args.command == 'image') args.imagePorts = argv[++i]
else args.servicePorts = argv[++i]
break
case '--bootstrap':
args.serviceBootstrap = argv[++i]
break
case '--bootstrap-file':
args.serviceBootstrapFile = argv[++i]
break
case '--logs':
args.serviceLogs = argv[++i]
break
case '--tail':
args.serviceTail = argv[++i]
break
case '--freeze':
args.serviceSleep = argv[++i]
break
case '--unfreeze':
args.serviceWake = argv[++i]
break
case '--destroy':
args.serviceDestroy = argv[++i]
break
case '--resize':
args.serviceResize = argv[++i]
break
case '--set-unfreeze-on-demand':
args.serviceSetUnfreezeOnDemand = argv[++i]
if (i + 1 < argv.size() && !argv[i + 1].startsWith('-')) {
args.serviceUnfreezeOnDemandValue = argv[++i]
}
break
case '--execute':
args.serviceExecute = argv[++i]
break
case '--command':
args.serviceCommand = argv[++i]
break
case '--dump-bootstrap':
args.serviceDumpBootstrap = argv[++i]
break
case '--dump-file':
args.serviceDumpFile = argv[++i]
break
case '--extend':
args.keyExtend = true
break
case '--json':
args.jsonOutput = true
break
default:
if (argv[i].startsWith('-')) {
System.err.println("${RED}Unknown option: ${argv[i]}${RESET}")
System.exit(1)
} else {
args.sourceFile = argv[i]
}
}
i++
}
return args
}
def printHelp() {
println '''unsandbox SDK for Groovy - Execute code in secure sandboxes
https://unsandbox.com | https://api.unsandbox.com/openapi
Usage: groovy un.groovy [options] <source_file>
groovy un.groovy -s <language> '<code>'
groovy un.groovy session [options]
groovy un.groovy service [options]
groovy un.groovy service env <action> <service_id> [options]
groovy un.groovy image [options]
groovy un.groovy languages [--json]
groovy un.groovy key [options]
Execute options:
-s LANG Execute inline code with specified language
-e KEY=VALUE Set environment variable
-f FILE Add input file
-a Return artifacts
-o DIR Output directory for artifacts
-n MODE Network mode (zerotrust/semitrusted)
-v N vCPU count (1-8)
-k KEY API key (legacy)
-p KEY Public key
Session options:
--list List active sessions
--shell NAME Shell/REPL to use
--kill ID Terminate session
--snapshot ID Create snapshot of session
--restore ID Restore session from snapshot
Service options:
--list List services
--name NAME Service name (creates service)
--ports PORTS Comma-separated ports
--type TYPE Service type
--bootstrap CMD Bootstrap command
-e KEY=VALUE Set env var in vault (when creating)
--env-file FILE Load env vars from file
--info ID Get service details
--logs ID Get all logs
--tail ID Get last 9000 lines
--freeze ID Freeze service
--unfreeze ID Unfreeze service
--destroy ID Destroy service
--resize ID Resize service (requires --vcpu N)
--set-unfreeze-on-demand ID true|false
Set unfreeze_on_demand for service
--execute ID Execute command in service
--command CMD Command to execute (with --execute)
--dump-bootstrap ID Dump bootstrap script
--dump-file FILE File to save bootstrap
Vault commands:
service env status <id> Check vault status
service env set <id> Set vault (-e KEY=VAL or --env-file FILE)
service env export <id> Export vault contents
service env delete <id> Delete vault
Key options:
--extend Open browser to extend key
Image options:
--list List images
--info ID Get image details
--delete ID Delete an image
--lock ID Lock image to prevent deletion
--unlock ID Unlock image
--publish ID Publish image from service/snapshot (requires --source-type)
--source-type TYPE Source type: service or snapshot
--visibility ID MODE Set visibility: private, unlisted, or public
--spawn ID Spawn new service from image
--clone ID Clone an image
--name NAME Name for spawned service or cloned image
--ports PORTS Ports for spawned service
Languages options:
--json Output as JSON array
Library Usage:
import un
def result = un.execute("python", 'print("Hello")')
def client = new un.Client(publicKey: "unsb-pk-...", secretKey: "unsb-sk-...")
'''
}
// ============================================================================
// Main Execution (CLI)
// ============================================================================
try {
def args = parseArgs(this.args as List)
if (args.command == 'languages') {
cmdLanguages(args)
} else if (args.command == 'session') {
cmdSession(args)
} else if (args.command == 'service') {
if (args.envAction && args.envTarget) {
cmdServiceEnv(args)
} else {
cmdService(args)
}
} else if (args.command == 'snapshot') {
cmdSnapshot(args)
} else if (args.command == 'image') {
cmdImage(args)
} else if (args.command == 'key') {
cmdKey(args)
} else if (args.sourceFile || args.inlineLang) {
cmdExecute(args)
} else {
printHelp()
System.exit(1)
}
} catch (UnsandboxError e) {
System.err.println("${RED}Error: ${e.message}${RESET}")
System.exit(1)
} catch (Exception e) {
System.err.println("${RED}Error: ${e.message}${RESET}")
System.exit(1)
}
Esclarecimentos de documentação
Dependências
C Binary (un1) — requer libcurl e libwebsockets:
sudo apt install build-essential libcurl4-openssl-dev libwebsockets-dev
wget unsandbox.com/downloads/un.c && gcc -O2 -o un un.c -lcurl -lwebsockets
Implementações SDK — a maioria usa apenas stdlib (Ruby, JS, Go, etc). Alguns requerem dependências mínimas:
pip install requests # Python
Executar Código
Executar um script
./un hello.py
./un app.js
./un main.rs
Com variáveis de ambiente
./un -e DEBUG=1 -e NAME=World script.py
Com arquivos de entrada (teletransportar arquivos para sandbox)
./un -f data.csv -f config.json process.py
Obter binário compilado
./un -a -o ./bin main.c
Sessões interativas
Iniciar uma sessão de shell
# Default bash shell
./un session
# Choose your shell
./un session --shell zsh
./un session --shell fish
# Jump into a REPL
./un session --shell python3
./un session --shell node
./un session --shell julia
Sessão com acesso à rede
./un session -n semitrusted
Auditoria de sessão (gravação completa do terminal)
# Record everything (including vim, interactive programs)
./un session --audit -o ./logs
# Replay session later
zcat session.log*.gz | less -R
Coletar artefatos da sessão
# Files in /tmp/artifacts/ are collected on exit
./un session -a -o ./outputs
Persistência de sessão (tmux/screen)
# Default: session terminates on disconnect (clean exit)
./un session
# With tmux: session persists, can reconnect later
./un session --tmux
# Press Ctrl+b then d to detach
# With screen: alternative multiplexer
./un session --screen
# Press Ctrl+a then d to detach
Listar Trabalhos Ativos
./un session --list
# Output:
# Active sessions: 2
#
# SESSION ID CONTAINER SHELL TTL STATUS
# abc123... unsb-vm-12345 python3 45m30s active
# def456... unsb-vm-67890 bash 1h2m active
Reconectar à sessão existente
# Reconnect by container name (requires --tmux or --screen)
./un session --attach unsb-vm-12345
# Use exit to terminate session, or detach to keep it running
Encerrar uma sessão
./un session --kill unsb-vm-12345
Shells e REPLs disponíveis
Shells: bash, dash, sh, zsh, fish, ksh, tcsh, csh, elvish, xonsh, ash
REPLs: python3, bpython, ipython # Python
node # JavaScript
ruby, irb # Ruby
lua # Lua
php # PHP
perl # Perl
guile, scheme # Scheme
ghci # Haskell
erl, iex # Erlang/Elixir
sbcl, clisp # Common Lisp
r # R
julia # Julia
clojure # Clojure
Gerenciamento de Chave API
Verificar Status do Pagamento
# Check if your API key is valid
./un key
# Output:
# Valid: key expires in 30 days
Estender Chave Expirada
# Open the portal to extend an expired key
./un key --extend
# This opens the unsandbox.com portal where you can
# add more credits to extend your key's expiration
Autenticação
As credenciais são carregadas em ordem de prioridade (maior primeiro):
# 1. CLI flags (highest priority)
./un -p unsb-pk-xxxx -k unsb-sk-xxxxx script.py
# 2. Environment variables
export UNSANDBOX_PUBLIC_KEY=unsb-pk-xxxx-xxxx-xxxx-xxxx
export UNSANDBOX_SECRET_KEY=unsb-sk-xxxxx-xxxxx-xxxxx-xxxxx
./un script.py
# 3. Config file (lowest priority)
# ~/.unsandbox/accounts.csv format: public_key,secret_key
mkdir -p ~/.unsandbox
echo "unsb-pk-xxxx-xxxx-xxxx-xxxx,unsb-sk-xxxxx-xxxxx-xxxxx-xxxxx" > ~/.unsandbox/accounts.csv
./un script.py
As requisições são assinadas com HMAC-SHA256. O token bearer contém apenas a chave pública; a chave secreta calcula a assinatura (nunca é transmitida).
Escalonamento de Recursos
Definir Quantidade de vCPU
# Default: 1 vCPU, 2GB RAM
./un script.py
# Scale up: 4 vCPUs, 8GB RAM
./un -v 4 script.py
# Maximum: 8 vCPUs, 16GB RAM
./un --vcpu 8 heavy_compute.py
Reforço de Sessão Ao Vivo
# Boost a running session to 2 vCPU, 4GB RAM
./un session --boost sandbox-abc
# Boost to specific vCPU count (4 vCPU, 8GB RAM)
./un session --boost sandbox-abc --boost-vcpu 4
# Return to base resources (1 vCPU, 2GB RAM)
./un session --unboost sandbox-abc
Congelar/Descongelar Sessão
Congelar e Descongelar Sessões
# Freeze a session (stop billing, preserve state)
./un session --freeze sandbox-abc
# Unfreeze a frozen session
./un session --unfreeze sandbox-abc
# Note: Requires --tmux or --screen for persistence
Serviços Persistentes
Criar um Serviço
# Web server with ports
./un service --name web --ports 80,443 --bootstrap "python -m http.server 80"
# With custom domains
./un service --name blog --ports 8000 --domains blog.example.com
# Game server with SRV records
./un service --name mc --type minecraft --bootstrap ./setup.sh
# Deploy app tarball with bootstrap script
./un service --name app --ports 8000 -f app.tar.gz --bootstrap-file ./setup.sh
# setup.sh: cd /tmp && tar xzf app.tar.gz && ./app/start.sh
Gerenciar Serviços
# List all services
./un service --list
# Get service details
./un service --info abc123
# View bootstrap logs
./un service --logs abc123
./un service --tail abc123 # last 9000 lines
# Execute command in running service
./un service --execute abc123 'journalctl -u myapp -n 50'
# Dump bootstrap script (for migrations)
./un service --dump-bootstrap abc123
./un service --dump-bootstrap abc123 backup.sh
# Freeze/unfreeze service
./un service --freeze abc123
./un service --unfreeze abc123
# Service settings (auto-wake, freeze page display)
./un service --auto-unfreeze abc123 # enable auto-wake on HTTP
./un service --no-auto-unfreeze abc123 # disable auto-wake
./un service --show-freeze-page abc123 # show HTML payment page (default)
./un service --no-show-freeze-page abc123 # return JSON error instead
# Redeploy with new bootstrap
./un service --redeploy abc123 --bootstrap ./new-setup.sh
# Destroy service
./un service --destroy abc123
Snapshots
Listar Snapshots
./un snapshot --list
# Output:
# Snapshots: 3
#
# SNAPSHOT ID NAME SOURCE SIZE CREATED
# unsb-snapshot-a1b2-c3d4-e5f6-g7h8 before-upgrade session 512 MB 2h ago
# unsb-snapshot-i9j0-k1l2-m3n4-o5p6 stable-v1.0 service 1.2 GB 1d ago
Criar Snapshot da Sessão
# Snapshot with name
./un session --snapshot unsb-vm-12345 --name "before upgrade"
# Quick snapshot (auto-generated name)
./un session --snapshot unsb-vm-12345
Criar Snapshot do Serviço
# Standard snapshot (pauses container briefly)
./un service --snapshot unsb-service-abc123 --name "stable v1.0"
# Hot snapshot (no pause, may be inconsistent)
./un service --snapshot unsb-service-abc123 --hot
Restaurar a partir do Snapshot
# Restore session from snapshot
./un session --restore unsb-snapshot-a1b2-c3d4-e5f6-g7h8
# Restore service from snapshot
./un service --restore unsb-snapshot-i9j0-k1l2-m3n4-o5p6
Excluir Snapshot
./un snapshot --delete unsb-snapshot-a1b2-c3d4-e5f6-g7h8
Imagens
Imagens são imagens de container independentes e transferíveis que sobrevivem à exclusão do container. Diferente dos snapshots (que permanecem com seu container), imagens podem ser compartilhadas com outros usuários, transferidas entre chaves de API ou tornadas públicas no marketplace.
Listar Imagens
# List all images (owned + shared + public)
./un image --list
# List only your images
./un image --list owned
# List images shared with you
./un image --list shared
# List public marketplace images
./un image --list public
# Get image details
./un image --info unsb-image-xxxx-xxxx-xxxx-xxxx
Publicar Imagens
# Publish from a stopped or frozen service
./un image --publish-service unsb-service-abc123 \
--name "My App v1.0" --description "Production snapshot"
# Publish from a snapshot
./un image --publish-snapshot unsb-snapshot-xxxx-xxxx-xxxx-xxxx \
--name "Stable Release"
# Note: Cannot publish from running containers - stop or freeze first
Criar Serviços a partir de Imagens
# Spawn a new service from an image
./un image --spawn unsb-image-xxxx-xxxx-xxxx-xxxx \
--name new-service --ports 80,443
# Clone an image (creates a copy you own)
./un image --clone unsb-image-xxxx-xxxx-xxxx-xxxx
Proteção de Imagem
# Lock image to prevent accidental deletion
./un image --lock unsb-image-xxxx-xxxx-xxxx-xxxx
# Unlock image to allow deletion
./un image --unlock unsb-image-xxxx-xxxx-xxxx-xxxx
# Delete image (must be unlocked)
./un image --delete unsb-image-xxxx-xxxx-xxxx-xxxx
Visibilidade e Compartilhamento
# Set visibility level
./un image --visibility unsb-image-xxxx-xxxx-xxxx-xxxx private # owner only (default)
./un image --visibility unsb-image-xxxx-xxxx-xxxx-xxxx unlisted # can be shared
./un image --visibility unsb-image-xxxx-xxxx-xxxx-xxxx public # marketplace
# Share with specific user
./un image --grant unsb-image-xxxx-xxxx-xxxx-xxxx \
--key unsb-pk-friend-friend-friend-friend
# Revoke access
./un image --revoke unsb-image-xxxx-xxxx-xxxx-xxxx \
--key unsb-pk-friend-friend-friend-friend
# List who has access
./un image --trusted unsb-image-xxxx-xxxx-xxxx-xxxx
Transferir Propriedade
# Transfer image to another API key
./un image --transfer unsb-image-xxxx-xxxx-xxxx-xxxx \
--to unsb-pk-newowner-newowner-newowner-newowner
Referência de uso
Usage: ./un [options] <source_file>
./un session [options]
./un service [options]
./un snapshot [options]
./un image [options]
./un key
Commands:
(default) Execute source file in sandbox
session Open interactive shell/REPL session
service Manage persistent services
snapshot Manage container snapshots
image Manage container images (publish, share, transfer)
key Check API key validity and expiration
Options:
-e KEY=VALUE Set environment variable (can use multiple times)
-f FILE Add input file (can use multiple times)
-a Return and save artifacts from /tmp/artifacts/
-o DIR Output directory for artifacts (default: current dir)
-p KEY Public key (or set UNSANDBOX_PUBLIC_KEY env var)
-k KEY Secret key (or set UNSANDBOX_SECRET_KEY env var)
-n MODE Network mode: zerotrust (default) or semitrusted
-v N, --vcpu N vCPU count 1-8, each vCPU gets 2GB RAM (default: 1)
-y Skip confirmation for large uploads (>1GB)
-h Show this help
Authentication (priority order):
1. -p and -k flags (public and secret key)
2. UNSANDBOX_PUBLIC_KEY + UNSANDBOX_SECRET_KEY env vars
3. ~/.unsandbox/accounts.csv (format: public_key,secret_key per line)
Session options:
-s, --shell SHELL Shell/REPL to use (default: bash)
-l, --list List active sessions
--attach ID Reconnect to existing session (ID or container name)
--kill ID Terminate a session (ID or container name)
--freeze ID Freeze a session (requires --tmux/--screen)
--unfreeze ID Unfreeze a frozen session
--boost ID Boost session resources (2 vCPU, 4GB RAM)
--boost-vcpu N Specify vCPU count for boost (1-8)
--unboost ID Return to base resources
--audit Record full session for auditing
--tmux Enable session persistence with tmux (allows reconnect)
--screen Enable session persistence with screen (allows reconnect)
Service options:
--name NAME Service name (creates new service)
--ports PORTS Comma-separated ports (e.g., 80,443)
--domains DOMAINS Custom domains (e.g., example.com,www.example.com)
--type TYPE Service type: minecraft, mumble, teamspeak, source, tcp, udp
--bootstrap CMD Bootstrap command/file/URL to run on startup
-f FILE Upload file to /tmp/ (can use multiple times)
-l, --list List all services
--info ID Get service details
--tail ID Get last 9000 lines of bootstrap logs
--logs ID Get all bootstrap logs
--freeze ID Freeze a service
--unfreeze ID Unfreeze a service
--auto-unfreeze ID Enable auto-wake on HTTP request
--no-auto-unfreeze ID Disable auto-wake on HTTP request
--show-freeze-page ID Show HTML payment page when frozen (default)
--no-show-freeze-page ID Return JSON error when frozen
--destroy ID Destroy a service
--redeploy ID Re-run bootstrap script (requires --bootstrap)
--execute ID CMD Run a command in a running service
--dump-bootstrap ID [FILE] Dump bootstrap script (for migrations)
--snapshot ID Create snapshot of session or service
--snapshot-name User-friendly name for snapshot
--hot Create snapshot without pausing (may be inconsistent)
--restore ID Restore session/service from snapshot ID
Snapshot options:
-l, --list List all snapshots
--info ID Get snapshot details
--delete ID Delete a snapshot permanently
Image options:
-l, --list [owned|shared|public] List images (all, owned, shared, or public)
--info ID Get image details
--publish-service ID Publish image from stopped/frozen service
--publish-snapshot ID Publish image from snapshot
--name NAME Name for published image
--description DESC Description for published image
--delete ID Delete image (must be unlocked)
--clone ID Clone image (creates copy you own)
--spawn ID Create service from image (requires --name)
--lock ID Lock image to prevent deletion
--unlock ID Unlock image to allow deletion
--visibility ID LEVEL Set visibility (private|unlisted|public)
--grant ID --key KEY Grant access to another API key
--revoke ID --key KEY Revoke access from API key
--transfer ID --to KEY Transfer ownership to API key
--trusted ID List API keys with access
Key options:
(no options) Check API key validity
--extend Open portal to extend an expired key
Examples:
./un script.py # execute Python script
./un -e DEBUG=1 script.py # with environment variable
./un -f data.csv process.py # with input file
./un -a -o ./bin main.c # save compiled artifacts
./un -v 4 heavy.py # with 4 vCPUs, 8GB RAM
./un session # interactive bash session
./un session --tmux # bash with reconnect support
./un session --list # list active sessions
./un session --attach unsb-vm-12345 # reconnect to session
./un session --kill unsb-vm-12345 # terminate a session
./un session --freeze unsb-vm-12345 # freeze session
./un session --unfreeze unsb-vm-12345 # unfreeze session
./un session --boost unsb-vm-12345 # boost resources
./un session --unboost unsb-vm-12345 # return to base
./un session --shell python3 # Python REPL
./un session --shell node # Node.js REPL
./un session -n semitrusted # session with network access
./un session --audit -o ./logs # record session for auditing
./un service --name web --ports 80 # create web service
./un service --list # list all services
./un service --logs abc123 # view bootstrap logs
./un key # check API key
./un key --extend # extend expired key
./un snapshot --list # list all snapshots
./un session --snapshot unsb-vm-123 # snapshot a session
./un service --snapshot abc123 # snapshot a service
./un session --restore unsb-snapshot-xxxx # restore from snapshot
./un image --list # list all images
./un image --list owned # list your images
./un image --publish-service abc # publish image from service
./un image --spawn img123 --name x # create service from image
./un image --grant img --key pk # share image with user
CLI Inception
O UN CLI foi implementado em 42 linguagens de programação, demonstrando que a API do unsandbox pode ser acessada de praticamente qualquer ambiente.
Ver Todas as 42 Implementações →
Licença
DOMÍNIO PÚBLICO - SEM LICENÇA, SEM GARANTIA
Este é software gratuito de domínio público para o bem público de um permacomputador hospedado
em permacomputer.com - um computador sempre ativo pelo povo, para o povo. Um que é
durável, fácil de reparar e distribuído como água da torneira para inteligência de
aprendizado de máquina.
O permacomputador é infraestrutura de propriedade comunitária otimizada em torno de quatro valores:
VERDADE - Primeiros princípios, matemática & ciência, código aberto distribuído livremente
LIBERDADE - Parcerias voluntárias, liberdade da tirania e controle corporativo
HARMONIA - Desperdício mínimo, sistemas auto-renováveis com diversas conexões prósperas
AMOR - Seja você mesmo sem ferir os outros, cooperação através da lei natural
Este software contribui para essa visão ao permitir a execução de código em mais de 42
linguagens de programação através de uma interface unificada, acessível a todos. Código são
sementes que brotam em qualquer tecnologia abandonada.
Saiba mais: https://www.permacomputer.com
Qualquer pessoa é livre para copiar, modificar, publicar, usar, compilar, vender ou distribuir
este software, seja em forma de código-fonte ou como binário compilado, para qualquer propósito,
comercial ou não comercial, e por qualquer meio.
SEM GARANTIA. O SOFTWARE É FORNECIDO "COMO ESTÁ" SEM GARANTIA DE QUALQUER TIPO.
Dito isso, a camada de membrana digital do nosso permacomputador executa continuamente testes
unitários, de integração e funcionais em todo o seu próprio software - com nosso permacomputador
monitorando a si mesmo, reparando a si mesmo, com orientação humana mínima no ciclo.
Nossos agentes fazem o seu melhor.
Copyright 2025 TimeHexOn & foxhop & russell@unturf
https://www.timehexon.com
https://www.foxhop.net
https://www.unturf.com/software