CLI
Fast command-line client for code execution and interactive sessions. 42+ languages, 30+ shells/REPLs.
Official OpenAPI Swagger Docs ↗Quick Start — Ruby
# Download + setup
curl -O https://git.unturf.com/engineering/unturf/un-inception/-/raw/main/clients/ruby/sync/src/un.rb && chmod +x un.rb && ln -sf un.rb 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.rb
Features
- 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
Integration Quickstart ⚡
Add unsandbox superpowers to your existing Ruby app:
curl -O https://git.unturf.com/engineering/unturf/un-inception/-/raw/main/clients/ruby/sync/src/un.rb
# 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 Ruby app:
require_relative 'un'
result = Un.execute_code("ruby", "puts 'Hello from Ruby running on unsandbox!'")
puts result["stdout"] # Hello from Ruby running on unsandbox!
ruby myapp.rb
09eb3b67d65adc5c31e070c845fbee0f
SHA256: 9fd0c27dc2b00b5447e81cef27ba825cae727f361e3f2b105218f22b24473678
#!/usr/bin/env ruby
# frozen_string_literal: true
# PUBLIC DOMAIN - NO LICENSE, NO WARRANTY
#
# unsandbox.com Ruby SDK (Synchronous)
#
# Library Usage:
# require_relative 'un'
#
# # Execute code synchronously
# result = Un.execute_code("python", 'print("hello")')
#
# # Execute asynchronously and get job_id
# job_id = Un.execute_async("javascript", 'console.log("hello")')
#
# # Wait for job completion with exponential backoff
# result = Un.wait_for_job(job_id)
#
# # List all jobs
# jobs = Un.list_jobs
#
# # Get supported languages (cached for 1 hour)
# languages = Un.get_languages
#
# # Snapshot operations
# snapshot_id = Un.session_snapshot(session_id)
#
# CLI Usage:
# ruby un.rb script.py # Execute Python script
# ruby un.rb -s bash 'echo hello' # Inline bash command
# ruby un.rb session --list # List sessions
# ruby un.rb service --list # List services
# ruby un.rb snapshot --list # List snapshots
# ruby un.rb key # Check API key
#
# Authentication Priority (4-tier):
# 1. Method arguments (public_key:, secret_key:)
# 2. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY)
# 3. Config file (~/.unsandbox/accounts.csv, line 0 by default)
# 4. Local directory (./accounts.csv, line 0 by default)
#
# Request Authentication (HMAC-SHA256):
# Authorization: Bearer <public_key>
# X-Timestamp: <unix_seconds>
# X-Signature: HMAC-SHA256(secret_key, "timestamp:METHOD:path:body")
#
# Languages Cache:
# - Cached in ~/.unsandbox/languages.json
# - TTL: 1 hour
# - Updated on successful API calls
require 'net/http'
require 'uri'
require 'json'
require 'openssl'
require 'fileutils'
require 'optparse'
require 'cgi'
# Unsandbox Ruby SDK module (synchronous)
module Un
# API base URL
API_BASE = 'https://api.unsandbox.com'
# Polling delays in milliseconds for exponential backoff
POLL_DELAYS_MS = [300, 450, 700, 900, 650, 1600, 2000].freeze
# Languages cache TTL in seconds (1 hour)
LANGUAGES_CACHE_TTL = 3600
# HTTP request timeout in seconds
REQUEST_TIMEOUT = 120
# Error raised when credentials cannot be found or are invalid
class CredentialsError < StandardError; end
# Error raised for API request failures
class APIError < StandardError
attr_reader :status_code, :response_body
# @param message [String] Error message
# @param status_code [Integer, nil] HTTP status code
# @param response_body [String, nil] Response body
def initialize(message, status_code: nil, response_body: nil)
super(message)
@status_code = status_code
@response_body = response_body
end
end
class << self
# Execute code synchronously (blocks until completion)
#
# @param language [String] Programming language (e.g., "python", "javascript", "go")
# @param code [String] Source code to execute
# @param public_key [String, nil] Optional API key (uses credentials resolution if not provided)
# @param secret_key [String, nil] Optional API secret (uses credentials resolution if not provided)
# @return [Hash] Response hash containing stdout, stderr, exit code, etc.
# @raise [CredentialsError] If no credentials found
# @raise [APIError] If API request fails
#
# @example
# result = Un.execute_code("python", 'print("hello")')
# puts result["stdout"] # => "hello\n"
def execute_code(language, code, public_key: nil, secret_key: nil)
pk, sk = resolve_credentials(public_key, secret_key)
response = make_request(
'POST',
'/execute',
pk,
sk,
{ language: language, code: code }
)
# If we got a job_id, poll until completion
job_id = response['job_id']
status = response['status']
if job_id && %w[pending running].include?(status)
return wait_for_job(job_id, public_key: pk, secret_key: sk)
end
response
end
# Execute code asynchronously (returns immediately with job_id)
#
# @param language [String] Programming language (e.g., "python", "javascript")
# @param code [String] Source code to execute
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @return [String] Job ID string
# @raise [CredentialsError] If no credentials found
# @raise [APIError] If API request fails
#
# @example
# job_id = Un.execute_async("python", 'import time; time.sleep(10); print("done")')
# # Later...
# result = Un.wait_for_job(job_id)
def execute_async(language, code, public_key: nil, secret_key: nil)
pk, sk = resolve_credentials(public_key, secret_key)
response = make_request(
'POST',
'/execute',
pk,
sk,
{ language: language, code: code }
)
response['job_id']
end
# Get current status/result of a job (single poll, no waiting)
#
# @param job_id [String] Job ID from execute_async
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @return [Hash] Job response hash
# @raise [CredentialsError] If no credentials found
# @raise [APIError] If API request fails
#
# @example
# job = Un.get_job(job_id)
# puts job["status"] # => "running" or "completed"
def get_job(job_id, public_key: nil, secret_key: nil)
pk, sk = resolve_credentials(public_key, secret_key)
make_request('GET', "/jobs/#{job_id}", pk, sk)
end
# Wait for job completion with exponential backoff polling
#
# Polling delays (ms): [300, 450, 700, 900, 650, 1600, 2000, ...]
# Cumulative: 300, 750, 1450, 2350, 3000, 4600, 6600ms+
#
# @param job_id [String] Job ID from execute_async
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @param timeout [Integer] Maximum time to wait in seconds (default: 3600)
# @return [Hash] Final job result when status is terminal (completed, failed, timeout, cancelled)
# @raise [CredentialsError] If no credentials found
# @raise [APIError] If API request fails or timeout reached
#
# @example
# result = Un.wait_for_job(job_id, timeout: 60)
# puts result["stdout"]
def wait_for_job(job_id, public_key: nil, secret_key: nil, timeout: 3600)
pk, sk = resolve_credentials(public_key, secret_key)
poll_count = 0
start_time = Time.now
loop do
# Check timeout
elapsed = Time.now - start_time
raise APIError, "Job wait timeout after #{timeout} seconds" if elapsed > timeout
# Sleep before polling
delay_idx = [poll_count, POLL_DELAYS_MS.length - 1].min
sleep(POLL_DELAYS_MS[delay_idx] / 1000.0)
poll_count += 1
response = get_job(job_id, public_key: pk, secret_key: sk)
status = response['status']
return response if %w[completed failed timeout cancelled].include?(status)
# Still running, continue polling
end
end
# Cancel a running job
#
# @param job_id [String] Job ID to cancel
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @return [Hash] Response hash with cancellation confirmation
# @raise [CredentialsError] If no credentials found
# @raise [APIError] If API request fails
#
# @example
# Un.cancel_job(job_id)
def cancel_job(job_id, public_key: nil, secret_key: nil)
pk, sk = resolve_credentials(public_key, secret_key)
make_request('DELETE', "/jobs/#{job_id}", pk, sk)
end
# List all jobs for the authenticated account
#
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @return [Array<Hash>] List of job hashes
# @raise [CredentialsError] If no credentials found
# @raise [APIError] If API request fails
#
# @example
# jobs = Un.list_jobs
# jobs.each { |job| puts "#{job['job_id']}: #{job['status']}" }
def list_jobs(public_key: nil, secret_key: nil)
pk, sk = resolve_credentials(public_key, secret_key)
response = make_request('GET', '/jobs', pk, sk)
response['jobs'] || []
end
# Get list of supported programming languages
#
# Results are cached for 1 hour in ~/.unsandbox/languages.json
#
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @return [Array<String>] List of language identifiers (e.g., ["python", "javascript", "go", ...])
# @raise [CredentialsError] If no credentials found
# @raise [APIError] If API request fails
#
# @example
# languages = Un.get_languages
# puts languages.join(", ")
def get_languages(public_key: nil, secret_key: nil)
# Try cache first
cached = load_languages_cache
return cached if cached
pk, sk = resolve_credentials(public_key, secret_key)
response = make_request('GET', '/languages', pk, sk)
languages = response['languages'] || []
# Cache the result
save_languages_cache(languages)
languages
end
# Detect programming language from filename extension
#
# @param filename [String] Filename to detect language from (e.g., "script.py")
# @return [String, nil] Language identifier (e.g., "python") or nil if unknown
#
# @example
# Un.detect_language("hello.py") # => "python"
# Un.detect_language("script.js") # => "javascript"
# Un.detect_language("main.go") # => "go"
# Un.detect_language("unknown") # => nil
def detect_language(filename)
return nil if filename.nil? || !filename.include?('.')
ext = filename.split('.').last&.downcase
LANGUAGE_MAP[ext]
end
# Create a snapshot of a session
#
# @param session_id [String] Session ID to snapshot
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @param name [String, nil] Optional snapshot name
# @param ephemeral [Boolean] If true, create ephemeral snapshot (default: false)
# @return [String] Snapshot ID
# @raise [CredentialsError] If no credentials found
# @raise [APIError] If API request fails
#
# @example
# snapshot_id = Un.session_snapshot(session_id, name: "my-snapshot")
def session_snapshot(session_id, public_key: nil, secret_key: nil, name: nil, ephemeral: false)
pk, sk = resolve_credentials(public_key, secret_key)
data = { session_id: session_id, hot: ephemeral }
data[:name] = name if name
response = make_request('POST', '/snapshots', pk, sk, data)
response['snapshot_id']
end
# Create a snapshot of a service
#
# @param service_id [String] Service ID to snapshot
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @param name [String, nil] Optional snapshot name
# @return [String] Snapshot ID
# @raise [CredentialsError] If no credentials found
# @raise [APIError] If API request fails
#
# @example
# snapshot_id = Un.service_snapshot(service_id, name: "production-backup")
def service_snapshot(service_id, public_key: nil, secret_key: nil, name: nil)
pk, sk = resolve_credentials(public_key, secret_key)
data = { service_id: service_id, hot: false }
data[:name] = name if name
response = make_request('POST', '/snapshots', pk, sk, data)
response['snapshot_id']
end
# List all snapshots
#
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @return [Array<Hash>] List of snapshot hashes
# @raise [CredentialsError] If no credentials found
# @raise [APIError] If API request fails
#
# @example
# snapshots = Un.list_snapshots
# snapshots.each { |s| puts "#{s['snapshot_id']}: #{s['name']}" }
def list_snapshots(public_key: nil, secret_key: nil)
pk, sk = resolve_credentials(public_key, secret_key)
response = make_request('GET', '/snapshots', pk, sk)
response['snapshots'] || []
end
# Get details of a specific snapshot
#
# @param snapshot_id [String] Snapshot ID to get details for
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @return [Hash] Snapshot details hash containing:
# - id: Snapshot ID
# - name: Snapshot name
# - type: "session" or "service"
# - source_id: Original resource ID
# - hot: Whether snapshot preserves running state
# - locked: Whether snapshot is locked
# - created_at: Creation timestamp
# - size_bytes: Size in bytes
# @raise [CredentialsError] If no credentials found
# @raise [APIError] If API request fails
#
# @example
# snapshot = Un.get_snapshot(snapshot_id)
# puts "Snapshot: #{snapshot['name']} (#{snapshot['type']})"
def get_snapshot(snapshot_id, public_key: nil, secret_key: nil)
pk, sk = resolve_credentials(public_key, secret_key)
make_request('GET', "/snapshots/#{snapshot_id}", pk, sk)
end
# Restore a snapshot
#
# @param snapshot_id [String] Snapshot ID to restore
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @return [Hash] Response hash with restored resource info
# @raise [CredentialsError] If no credentials found
# @raise [APIError] If API request fails
#
# @example
# result = Un.restore_snapshot(snapshot_id)
# puts result["session_id"] # or result["service_id"]
def restore_snapshot(snapshot_id, public_key: nil, secret_key: nil)
pk, sk = resolve_credentials(public_key, secret_key)
make_request('POST', "/snapshots/#{snapshot_id}/restore", pk, sk, {})
end
# Delete a snapshot
#
# @param snapshot_id [String] Snapshot ID to delete
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @return [Hash] Response hash with deletion confirmation
# @raise [CredentialsError] If no credentials found
# @raise [APIError] If API request fails
#
# @example
# Un.delete_snapshot(snapshot_id)
def delete_snapshot(snapshot_id, public_key: nil, secret_key: nil)
pk, sk = resolve_credentials(public_key, secret_key)
make_request_with_sudo('DELETE', "/snapshots/#{snapshot_id}", pk, sk)
end
# Lock a snapshot to prevent deletion
#
# @param snapshot_id [String] Snapshot ID to lock
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @return [Hash] Response hash with lock confirmation
# @raise [CredentialsError] If no credentials found
# @raise [APIError] If API request fails
#
# @example
# Un.lock_snapshot(snapshot_id)
def lock_snapshot(snapshot_id, public_key: nil, secret_key: nil)
pk, sk = resolve_credentials(public_key, secret_key)
make_request('POST', "/snapshots/#{snapshot_id}/lock", pk, sk, {})
end
# Unlock a snapshot to allow deletion
#
# @param snapshot_id [String] Snapshot ID to unlock
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @return [Hash] Response hash with unlock confirmation
# @raise [CredentialsError] If no credentials found
# @raise [APIError] If API request fails
#
# @example
# Un.unlock_snapshot(snapshot_id)
def unlock_snapshot(snapshot_id, public_key: nil, secret_key: nil)
pk, sk = resolve_credentials(public_key, secret_key)
make_request_with_sudo('POST', "/snapshots/#{snapshot_id}/unlock", pk, sk, {})
end
# Clone a snapshot to create a new session or service
#
# @param snapshot_id [String] Snapshot ID to clone
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @param name [String, nil] Optional name for the cloned resource
# @param type [String] Type of resource to create ("session" or "service")
# @param shell [String, nil] Optional shell for session clones
# @param ports [Array<Integer>, nil] Optional ports for service clones
# @return [Hash] Response hash with cloned resource info (session_id or service_id)
# @raise [CredentialsError] If no credentials found
# @raise [APIError] If API request fails
#
# @example Clone to session
# result = Un.clone_snapshot(snapshot_id, type: "session")
# puts result["session_id"]
#
# @example Clone to service
# result = Un.clone_snapshot(snapshot_id, type: "service", ports: [80, 443])
# puts result["service_id"]
def clone_snapshot(snapshot_id, public_key: nil, secret_key: nil, name: nil, type: 'session', shell: nil, ports: nil)
pk, sk = resolve_credentials(public_key, secret_key)
data = { type: type }
data[:name] = name if name
data[:shell] = shell if shell
data[:ports] = ports if ports
make_request('POST', "/snapshots/#{snapshot_id}/clone", pk, sk, data)
end
# ============================================================================
# Image Functions (LXD Container Images)
# ============================================================================
# Publish an LXD container image from a session or service
#
# @param source_type [String] Source type ("session" or "service")
# @param source_id [String] ID of the session or service to publish
# @param name [String, nil] Optional name for the image
# @param description [String, nil] Optional description for the image
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @return [Hash] Response hash with image_id and other metadata
# @raise [CredentialsError] If no credentials found
# @raise [APIError] If API request fails
#
# @example Publish from session
# result = Un.image_publish("session", session_id, name: "my-image")
# puts result["image_id"]
#
# @example Publish from service
# result = Un.image_publish("service", service_id, description: "Production snapshot")
# puts result["image_id"]
def image_publish(source_type, source_id, name: nil, description: nil, public_key: nil, secret_key: nil)
pk, sk = resolve_credentials(public_key, secret_key)
data = { source_type: source_type, source_id: source_id }
data[:name] = name if name
data[:description] = description if description
make_request('POST', '/images', pk, sk, data)
end
# List all images for the authenticated account
#
# @param filter_type [String, nil] Optional filter: "own", "shared", or "public"
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @return [Array<Hash>] List of image hashes
# @raise [CredentialsError] If no credentials found
# @raise [APIError] If API request fails
#
# @example List all images
# images = Un.list_images
# images.each { |img| puts "#{img['image_id']}: #{img['name']}" }
#
# @example List only owned images
# owned = Un.list_images(filter_type: "own")
#
# @example List shared images
# shared = Un.list_images(filter_type: "shared")
def list_images(filter_type: nil, public_key: nil, secret_key: nil)
pk, sk = resolve_credentials(public_key, secret_key)
path = filter_type ? "/images/#{filter_type}" : '/images'
response = make_request('GET', path, pk, sk)
response['images'] || []
end
# Get image details by ID
#
# @param image_id [String] Image ID to retrieve
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @return [Hash] Image details hash
# @raise [CredentialsError] If no credentials found
# @raise [APIError] If API request fails
#
# @example
# image = Un.get_image(image_id)
# puts "#{image['name']}: #{image['description']}"
def get_image(image_id, public_key: nil, secret_key: nil)
pk, sk = resolve_credentials(public_key, secret_key)
make_request('GET', "/images/#{image_id}", pk, sk)
end
# Delete an image
#
# @param image_id [String] Image ID to delete
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @return [Hash] Response hash with deletion confirmation
# @raise [CredentialsError] If no credentials found
# @raise [APIError] If API request fails
#
# @example
# Un.delete_image(image_id)
def delete_image(image_id, public_key: nil, secret_key: nil)
pk, sk = resolve_credentials(public_key, secret_key)
make_request_with_sudo('DELETE', "/images/#{image_id}", pk, sk)
end
# Lock an image to prevent deletion
#
# @param image_id [String] Image ID to lock
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @return [Hash] Response hash with lock confirmation
# @raise [CredentialsError] If no credentials found
# @raise [APIError] If API request fails
#
# @example
# Un.lock_image(image_id)
def lock_image(image_id, public_key: nil, secret_key: nil)
pk, sk = resolve_credentials(public_key, secret_key)
make_request('POST', "/images/#{image_id}/lock", pk, sk, {})
end
# Unlock an image to allow deletion
#
# @param image_id [String] Image ID to unlock
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @return [Hash] Response hash with unlock confirmation
# @raise [CredentialsError] If no credentials found
# @raise [APIError] If API request fails
#
# @example
# Un.unlock_image(image_id)
def unlock_image(image_id, public_key: nil, secret_key: nil)
pk, sk = resolve_credentials(public_key, secret_key)
make_request_with_sudo('POST', "/images/#{image_id}/unlock", pk, sk, {})
end
# Set image visibility (private, public, or shared)
#
# @param image_id [String] Image ID to update
# @param visibility [String] Visibility level: "private", "public", or "shared"
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @return [Hash] Response hash with visibility confirmation
# @raise [CredentialsError] If no credentials found
# @raise [APIError] If API request fails
#
# @example Make image public
# Un.set_image_visibility(image_id, "public")
#
# @example Make image private
# Un.set_image_visibility(image_id, "private")
def set_image_visibility(image_id, visibility, public_key: nil, secret_key: nil)
pk, sk = resolve_credentials(public_key, secret_key)
make_request('POST', "/images/#{image_id}/visibility", pk, sk, { visibility: visibility })
end
# Grant access to an image for another API key
#
# @param image_id [String] Image ID to share
# @param trusted_api_key [String] Public API key to grant access to
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @return [Hash] Response hash with grant confirmation
# @raise [CredentialsError] If no credentials found
# @raise [APIError] If API request fails
#
# @example
# Un.grant_image_access(image_id, "unsb-pk-xxxxx-xxxxx-xxxxx-xxxxx")
def grant_image_access(image_id, trusted_api_key, public_key: nil, secret_key: nil)
pk, sk = resolve_credentials(public_key, secret_key)
make_request('POST', "/images/#{image_id}/grant", pk, sk, { trusted_api_key: trusted_api_key })
end
# Revoke access to an image from another API key
#
# @param image_id [String] Image ID to revoke access from
# @param trusted_api_key [String] Public API key to revoke access from
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @return [Hash] Response hash with revoke confirmation
# @raise [CredentialsError] If no credentials found
# @raise [APIError] If API request fails
#
# @example
# Un.revoke_image_access(image_id, "unsb-pk-xxxxx-xxxxx-xxxxx-xxxxx")
def revoke_image_access(image_id, trusted_api_key, public_key: nil, secret_key: nil)
pk, sk = resolve_credentials(public_key, secret_key)
make_request('POST', "/images/#{image_id}/revoke", pk, sk, { trusted_api_key: trusted_api_key })
end
# List API keys with access to an image
#
# @param image_id [String] Image ID to list trusted keys for
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @return [Array<Hash>] List of trusted API key hashes
# @raise [CredentialsError] If no credentials found
# @raise [APIError] If API request fails
#
# @example
# trusted = Un.list_image_trusted(image_id)
# trusted.each { |t| puts t["api_key"] }
def list_image_trusted(image_id, public_key: nil, secret_key: nil)
pk, sk = resolve_credentials(public_key, secret_key)
response = make_request('GET', "/images/#{image_id}/trusted", pk, sk)
response['trusted'] || []
end
# Transfer image ownership to another API key
#
# @param image_id [String] Image ID to transfer
# @param to_api_key [String] Public API key to transfer ownership to
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @return [Hash] Response hash with transfer confirmation
# @raise [CredentialsError] If no credentials found
# @raise [APIError] If API request fails
#
# @example
# Un.transfer_image(image_id, "unsb-pk-xxxxx-xxxxx-xxxxx-xxxxx")
def transfer_image(image_id, to_api_key, public_key: nil, secret_key: nil)
pk, sk = resolve_credentials(public_key, secret_key)
make_request('POST', "/images/#{image_id}/transfer", pk, sk, { to_api_key: to_api_key })
end
# Spawn a new service from an image
#
# @param image_id [String] Image ID to spawn from
# @param name [String, nil] Optional name for the service
# @param ports [Array<Integer>, nil] Optional ports to expose
# @param bootstrap [String, nil] Optional bootstrap script
# @param network_mode [String] Network mode ("zerotrust" or "semitrusted", default: "zerotrust")
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @return [Hash] Response hash with service_id and other metadata
# @raise [CredentialsError] If no credentials found
# @raise [APIError] If API request fails
#
# @example Spawn with defaults
# result = Un.spawn_from_image(image_id)
# puts result["service_id"]
#
# @example Spawn with custom config
# result = Un.spawn_from_image(image_id, name: "web", ports: [80, 443], network_mode: "semitrusted")
# puts result["service_id"]
def spawn_from_image(image_id, name: nil, ports: nil, bootstrap: nil, network_mode: 'zerotrust', public_key: nil, secret_key: nil)
pk, sk = resolve_credentials(public_key, secret_key)
data = { network_mode: network_mode }
data[:name] = name if name
data[:ports] = ports if ports
data[:bootstrap] = bootstrap if bootstrap
make_request('POST', "/images/#{image_id}/spawn", pk, sk, data)
end
# Clone an image to create a new image
#
# @param image_id [String] Image ID to clone
# @param name [String, nil] Optional name for the cloned image
# @param description [String, nil] Optional description for the cloned image
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @return [Hash] Response hash with new image_id
# @raise [CredentialsError] If no credentials found
# @raise [APIError] If API request fails
#
# @example
# result = Un.clone_image(image_id, name: "my-clone", description: "Cloned from production")
# puts result["image_id"]
def clone_image(image_id, name: nil, description: nil, public_key: nil, secret_key: nil)
pk, sk = resolve_credentials(public_key, secret_key)
data = {}
data[:name] = name if name
data[:description] = description if description
make_request('POST', "/images/#{image_id}/clone", pk, sk, data)
end
# ============================================================================
# Session Functions
# ============================================================================
# List all sessions for the authenticated account
#
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @return [Array<Hash>] List of session hashes
# @raise [CredentialsError] If no credentials found
# @raise [APIError] If API request fails
#
# @example
# sessions = Un.list_sessions
# sessions.each { |s| puts "#{s['id']}: #{s['status']}" }
def list_sessions(public_key: nil, secret_key: nil)
pk, sk = resolve_credentials(public_key, secret_key)
response = make_request('GET', '/sessions', pk, sk)
response['sessions'] || []
end
# Get session details by ID
#
# @param session_id [String] Session ID to retrieve
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @return [Hash] Session details hash
# @raise [CredentialsError] If no credentials found
# @raise [APIError] If API request fails
#
# @example
# session = Un.get_session(session_id)
# puts session["status"]
def get_session(session_id, public_key: nil, secret_key: nil)
pk, sk = resolve_credentials(public_key, secret_key)
make_request('GET', "/sessions/#{session_id}", pk, sk)
end
# Create a new interactive session
#
# @param language [String] Shell or language for the session (e.g., "bash", "python3")
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @param network_mode [String] Network mode ("zerotrust" or "semitrusted")
# @param ttl [Integer] Time-to-live in seconds (default: 3600)
# @param multiplexer [String, nil] Terminal multiplexer ("tmux" or "screen")
# @param vcpu [Integer] Number of vCPUs (1-8)
# @return [Hash] Response hash with session_id and container_name
# @raise [CredentialsError] If no credentials found
# @raise [APIError] If API request fails
#
# @example
# result = Un.create_session("bash", network_mode: "semitrusted")
# puts result["session_id"]
def create_session(language, public_key: nil, secret_key: nil, network_mode: 'zerotrust', ttl: 3600, multiplexer: nil, vcpu: 1)
pk, sk = resolve_credentials(public_key, secret_key)
data = {
network_mode: network_mode,
ttl: ttl
}
data[:shell] = language if language
data[:multiplexer] = multiplexer if multiplexer
data[:vcpu] = vcpu if vcpu > 1
make_request('POST', '/sessions', pk, sk, data)
end
# Delete (terminate) a session
#
# @param session_id [String] Session ID to delete
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @return [Hash] Response hash with deletion confirmation
# @raise [CredentialsError] If no credentials found
# @raise [APIError] If API request fails
#
# @example
# Un.delete_session(session_id)
def delete_session(session_id, public_key: nil, secret_key: nil)
pk, sk = resolve_credentials(public_key, secret_key)
make_request('DELETE', "/sessions/#{session_id}", pk, sk)
end
# Freeze a session (pause execution, reduce resource usage)
#
# @param session_id [String] Session ID to freeze
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @return [Hash] Response hash with freeze confirmation
# @raise [CredentialsError] If no credentials found
# @raise [APIError] If API request fails
#
# @example
# Un.freeze_session(session_id)
def freeze_session(session_id, public_key: nil, secret_key: nil)
pk, sk = resolve_credentials(public_key, secret_key)
make_request('POST', "/sessions/#{session_id}/freeze", pk, sk, {})
end
# Unfreeze a session (resume execution)
#
# @param session_id [String] Session ID to unfreeze
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @return [Hash] Response hash with unfreeze confirmation
# @raise [CredentialsError] If no credentials found
# @raise [APIError] If API request fails
#
# @example
# Un.unfreeze_session(session_id)
def unfreeze_session(session_id, public_key: nil, secret_key: nil)
pk, sk = resolve_credentials(public_key, secret_key)
make_request('POST', "/sessions/#{session_id}/unfreeze", pk, sk, {})
end
# Boost a session (increase vCPU allocation)
#
# @param session_id [String] Session ID to boost
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @return [Hash] Response hash with boost confirmation
# @raise [CredentialsError] If no credentials found
# @raise [APIError] If API request fails
#
# @example
# Un.boost_session(session_id)
def boost_session(session_id, public_key: nil, secret_key: nil)
pk, sk = resolve_credentials(public_key, secret_key)
make_request('POST', "/sessions/#{session_id}/boost", pk, sk, {})
end
# Unboost a session (reduce vCPU allocation)
#
# @param session_id [String] Session ID to unboost
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @return [Hash] Response hash with unboost confirmation
# @raise [CredentialsError] If no credentials found
# @raise [APIError] If API request fails
#
# @example
# Un.unboost_session(session_id)
def unboost_session(session_id, public_key: nil, secret_key: nil)
pk, sk = resolve_credentials(public_key, secret_key)
make_request('POST', "/sessions/#{session_id}/unboost", pk, sk, {})
end
# Execute a shell command in an existing session
# Note: This is for non-interactive command execution, not for WebSocket shell access
#
# @param session_id [String] Session ID to execute command in
# @param command [String] Command to execute
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @return [Hash] Response hash with command output
# @raise [CredentialsError] If no credentials found
# @raise [APIError] If API request fails
#
# @example
# result = Un.shell_session(session_id, "ls -la")
# puts result["stdout"]
def shell_session(session_id, command, public_key: nil, secret_key: nil)
pk, sk = resolve_credentials(public_key, secret_key)
make_request('POST', "/sessions/#{session_id}/shell", pk, sk, { command: command })
end
# ============================================================================
# Service Functions
# ============================================================================
# List all services for the authenticated account
#
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @return [Array<Hash>] List of service hashes
# @raise [CredentialsError] If no credentials found
# @raise [APIError] If API request fails
#
# @example
# services = Un.list_services
# services.each { |s| puts "#{s['id']}: #{s['name']} (#{s['state']})" }
def list_services(public_key: nil, secret_key: nil)
pk, sk = resolve_credentials(public_key, secret_key)
response = make_request('GET', '/services', pk, sk)
response['services'] || []
end
# Create a new persistent service
#
# @param name [String] Service name
# @param ports [Array<Integer>] Ports to expose
# @param bootstrap [String] Bootstrap script content or URL
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @param network_mode [String] Network mode ("zerotrust" or "semitrusted")
# @param vcpu [Integer] Number of vCPUs (1-8)
# @param custom_domains [Array<String>, nil] Custom domains for the service
# @param service_type [String, nil] Service type for SRV records (e.g., "minecraft")
# @param unfreeze_on_demand [Boolean] If true, service will auto-wake on HTTP request (default: false)
# @return [Hash] Response hash with service_id
# @raise [CredentialsError] If no credentials found
# @raise [APIError] If API request fails
#
# @example
# result = Un.create_service("web", [80, 443], "apt install -y nginx && nginx")
# puts result["service_id"]
def create_service(name, ports, bootstrap, public_key: nil, secret_key: nil, network_mode: 'semitrusted', vcpu: 1, custom_domains: nil, service_type: nil, unfreeze_on_demand: false)
pk, sk = resolve_credentials(public_key, secret_key)
data = {
name: name,
ports: ports,
bootstrap: bootstrap,
network_mode: network_mode
}
data[:vcpu] = vcpu if vcpu > 1
data[:custom_domains] = custom_domains if custom_domains
data[:service_type] = service_type if service_type
data[:unfreeze_on_demand] = unfreeze_on_demand if unfreeze_on_demand
make_request('POST', '/services', pk, sk, data)
end
# Get service details by ID
#
# @param service_id [String] Service ID to retrieve
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @return [Hash] Service details hash
# @raise [CredentialsError] If no credentials found
# @raise [APIError] If API request fails
#
# @example
# service = Un.get_service(service_id)
# puts service["status"]
def get_service(service_id, public_key: nil, secret_key: nil)
pk, sk = resolve_credentials(public_key, secret_key)
make_request('GET', "/services/#{service_id}", pk, sk)
end
# Update a service (e.g., resize vCPU)
#
# @param service_id [String] Service ID to update
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @param vcpu [Integer, nil] New vCPU count (1-8)
# @param name [String, nil] New service name
# @return [Hash] Response hash with update confirmation
# @raise [CredentialsError] If no credentials found
# @raise [APIError] If API request fails
#
# @example
# Un.update_service(service_id, vcpu: 4)
def update_service(service_id, public_key: nil, secret_key: nil, vcpu: nil, name: nil)
pk, sk = resolve_credentials(public_key, secret_key)
data = {}
data[:vcpu] = vcpu if vcpu
data[:name] = name if name
make_request('PATCH', "/services/#{service_id}", pk, sk, data)
end
# Delete (destroy) a service
#
# @param service_id [String] Service ID to delete
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @return [Hash] Response hash with deletion confirmation
# @raise [CredentialsError] If no credentials found
# @raise [APIError] If API request fails
#
# @example
# Un.delete_service(service_id)
def delete_service(service_id, public_key: nil, secret_key: nil)
pk, sk = resolve_credentials(public_key, secret_key)
make_request_with_sudo('DELETE', "/services/#{service_id}", pk, sk)
end
# Freeze a service (stop container, reduce resource usage)
#
# @param service_id [String] Service ID to freeze
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @return [Hash] Response hash with freeze confirmation
# @raise [CredentialsError] If no credentials found
# @raise [APIError] If API request fails
#
# @example
# Un.freeze_service(service_id)
def freeze_service(service_id, public_key: nil, secret_key: nil)
pk, sk = resolve_credentials(public_key, secret_key)
make_request('POST', "/services/#{service_id}/freeze", pk, sk, {})
end
# Unfreeze a service (start container)
#
# @param service_id [String] Service ID to unfreeze
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @return [Hash] Response hash with unfreeze confirmation
# @raise [CredentialsError] If no credentials found
# @raise [APIError] If API request fails
#
# @example
# Un.unfreeze_service(service_id)
def unfreeze_service(service_id, public_key: nil, secret_key: nil)
pk, sk = resolve_credentials(public_key, secret_key)
make_request('POST', "/services/#{service_id}/unfreeze", pk, sk, {})
end
# Lock a service to prevent deletion
#
# @param service_id [String] Service ID to lock
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @return [Hash] Response hash with lock confirmation
# @raise [CredentialsError] If no credentials found
# @raise [APIError] If API request fails
#
# @example
# Un.lock_service(service_id)
def lock_service(service_id, public_key: nil, secret_key: nil)
pk, sk = resolve_credentials(public_key, secret_key)
make_request('POST', "/services/#{service_id}/lock", pk, sk, {})
end
# Unlock a service to allow deletion
#
# @param service_id [String] Service ID to unlock
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @return [Hash] Response hash with unlock confirmation
# @raise [CredentialsError] If no credentials found
# @raise [APIError] If API request fails
#
# @example
# Un.unlock_service(service_id)
def unlock_service(service_id, public_key: nil, secret_key: nil)
pk, sk = resolve_credentials(public_key, secret_key)
make_request_with_sudo('POST', "/services/#{service_id}/unlock", pk, sk, {})
end
# Set unfreeze-on-demand for a service
#
# When enabled, a frozen service will automatically wake when it receives an HTTP request.
#
# @param service_id [String] Service ID to update
# @param enabled [Boolean] Whether to enable unfreeze-on-demand
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @return [Hash] Response hash with update confirmation
# @raise [CredentialsError] If no credentials found
# @raise [APIError] If API request fails
#
# @example
# Un.set_unfreeze_on_demand(service_id, true)
def set_unfreeze_on_demand(service_id, enabled, public_key: nil, secret_key: nil)
pk, sk = resolve_credentials(public_key, secret_key)
make_request('PATCH', "/services/#{service_id}", pk, sk, { unfreeze_on_demand: enabled })
end
# Set show-freeze-page for a service
#
# When enabled, frozen services show a freeze page instead of 502 error.
#
# @param service_id [String] Service ID to update
# @param enabled [Boolean] Whether to enable show-freeze-page
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @return [Hash] Response hash with update confirmation
# @raise [CredentialsError] If no credentials found
# @raise [APIError] If API request fails
#
# @example
# Un.set_show_freeze_page(service_id, true)
def set_show_freeze_page(service_id, enabled, public_key: nil, secret_key: nil)
pk, sk = resolve_credentials(public_key, secret_key)
make_request('PATCH', "/services/#{service_id}", pk, sk, { show_freeze_page: enabled })
end
# Get service logs (bootstrap output)
#
# @param service_id [String] Service ID to get logs for
# @param all [Boolean] If true, get all logs; if false, get last 9000 lines (default: false)
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @return [Hash] Response hash with log content
# @raise [CredentialsError] If no credentials found
# @raise [APIError] If API request fails
#
# @example
# logs = Un.get_service_logs(service_id)
# puts logs["log"]
def get_service_logs(service_id, all: false, public_key: nil, secret_key: nil)
pk, sk = resolve_credentials(public_key, secret_key)
path = "/services/#{service_id}/logs"
path += '?all=true' if all
make_request('GET', path, pk, sk)
end
# Get service environment vault status
#
# @param service_id [String] Service ID to get env status for
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @return [Hash] Response hash with vault status (has_vault, count, updated_at)
# @raise [CredentialsError] If no credentials found
# @raise [APIError] If API request fails
#
# @example
# status = Un.get_service_env(service_id)
# puts "Variables: #{status['count']}"
def get_service_env(service_id, public_key: nil, secret_key: nil)
pk, sk = resolve_credentials(public_key, secret_key)
make_request('GET', "/services/#{service_id}/env", pk, sk)
end
# Set service environment variables (replaces existing vault)
#
# @param service_id [String] Service ID to set env for
# @param env [String] Environment content in .env format (KEY=VALUE per line)
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @return [Hash] Response hash with confirmation
# @raise [CredentialsError] If no credentials found
# @raise [APIError] If API request fails
#
# @example
# Un.set_service_env(service_id, "API_KEY=secret\nDEBUG=true")
def set_service_env(service_id, env, public_key: nil, secret_key: nil)
pk, sk = resolve_credentials(public_key, secret_key)
# This endpoint uses text/plain content type and PUT method
make_request_text('PUT', "/services/#{service_id}/env", pk, sk, env)
end
# Delete service environment vault
#
# @param service_id [String] Service ID to delete env for
# @param keys [Array<String>, nil] Specific keys to delete (nil = delete entire vault)
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @return [Hash] Response hash with deletion confirmation
# @raise [CredentialsError] If no credentials found
# @raise [APIError] If API request fails
#
# @example Delete entire vault
# Un.delete_service_env(service_id)
#
# @example Delete specific keys (if API supports it)
# Un.delete_service_env(service_id, keys: ["API_KEY", "DEBUG"])
def delete_service_env(service_id, keys: nil, public_key: nil, secret_key: nil)
pk, sk = resolve_credentials(public_key, secret_key)
if keys
make_request('DELETE', "/services/#{service_id}/env", pk, sk, { keys: keys })
else
make_request('DELETE', "/services/#{service_id}/env", pk, sk)
end
end
# Export service environment vault (returns .env format)
#
# @param service_id [String] Service ID to export env from
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @return [Hash] Response hash with env content
# @raise [CredentialsError] If no credentials found
# @raise [APIError] If API request fails
#
# @example
# result = Un.export_service_env(service_id)
# puts result["env"] # API_KEY=secret\nDEBUG=true
def export_service_env(service_id, public_key: nil, secret_key: nil)
pk, sk = resolve_credentials(public_key, secret_key)
make_request('POST', "/services/#{service_id}/env/export", pk, sk, {})
end
# Redeploy a service (re-run bootstrap script)
#
# @param service_id [String] Service ID to redeploy
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @param bootstrap [String, nil] New bootstrap script (optional)
# @return [Hash] Response hash with redeploy confirmation
# @raise [CredentialsError] If no credentials found
# @raise [APIError] If API request fails
#
# @example
# Un.redeploy_service(service_id)
def redeploy_service(service_id, public_key: nil, secret_key: nil, bootstrap: nil)
pk, sk = resolve_credentials(public_key, secret_key)
data = {}
data[:bootstrap] = bootstrap if bootstrap
make_request('POST', "/services/#{service_id}/redeploy", pk, sk, data)
end
# Execute a command in a running service
#
# @param service_id [String] Service ID to execute command in
# @param command [String] Command to execute
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @param timeout [Integer] Command timeout in milliseconds (default: 30000)
# @return [Hash] Response hash with command output (stdout, stderr, exit_code)
# @raise [CredentialsError] If no credentials found
# @raise [APIError] If API request fails
#
# @example
# result = Un.execute_in_service(service_id, "ls -la")
# puts result["stdout"]
def execute_in_service(service_id, command, public_key: nil, secret_key: nil, timeout: 30_000)
pk, sk = resolve_credentials(public_key, secret_key)
# Start async execution
response = make_request('POST', "/services/#{service_id}/execute", pk, sk, {
command: command,
timeout: timeout
})
job_id = response['job_id']
return response unless job_id
# Poll for completion
wait_for_job(job_id, public_key: pk, secret_key: sk, timeout: (timeout / 1000) + 10)
end
# Resize a service's vCPU allocation
#
# @param service_id [String] Service ID to resize
# @param vcpu [Integer] Number of vCPUs (1-8 typically)
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @return [Hash] Response hash with updated service info
# @raise [CredentialsError] If no credentials found
# @raise [APIError] If API request fails
#
# @example
# Un.resize_service(service_id, 4)
def resize_service(service_id, vcpu, public_key: nil, secret_key: nil)
pk, sk = resolve_credentials(public_key, secret_key)
make_request('PATCH', "/services/#{service_id}", pk, sk, { vcpu: vcpu })
end
# ============================================================================
# Key Validation
# ============================================================================
# Validate API keys
#
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @return [Hash] Response hash with validation result and account info
# @raise [CredentialsError] If no credentials found
# @raise [APIError] If API request fails or keys invalid
#
# @example
# result = Un.validate_keys
# puts result["valid"] # true or false
def validate_keys(public_key: nil, secret_key: nil)
pk, sk = resolve_credentials(public_key, secret_key)
# Note: This endpoint is on the portal, not API, but we use same auth
make_request('POST', '/keys/validate', pk, sk, {})
end
# Generate images from text prompt using AI.
#
# @param prompt [String] Text description of the image to generate
# @param model [String, nil] Model to use (optional)
# @param size [String] Image size (default: "1024x1024")
# @param quality [String] "standard" or "hd" (default: "standard")
# @param n [Integer] Number of images to generate (default: 1)
# @param public_key [String, nil] API public key
# @param secret_key [String, nil] API secret key
# @return [Hash] Result with :images array and :created_at
# @raise [CredentialsError] If no credentials found
# @raise [APIError] If API request fails
#
# @example
# result = Un.image("A sunset over mountains")
# puts result["images"] # Array of image data/URLs
def image(prompt, model: nil, size: '1024x1024', quality: 'standard', n: 1,
public_key: nil, secret_key: nil)
pk, sk = resolve_credentials(public_key, secret_key)
payload = {
prompt: prompt,
size: size,
quality: quality,
n: n
}
payload[:model] = model if model
make_request('POST', '/image', pk, sk, payload)
end
# ============================================================================
# PaaS Logs Functions
# ============================================================================
# Fetch batch logs from the PaaS platform
#
# @param source [String] Log source - "all", "api", "portal", "pool/cammy", "pool/ai"
# @param lines [Integer] Number of lines to fetch (1-10000)
# @param since [String] Time window - "1m", "5m", "1h", "1d"
# @param grep [String, nil] Optional filter pattern
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @return [Hash] Log entries
# @raise [CredentialsError] If no credentials found
# @raise [APIError] If API request fails
#
# @example
# logs = Un.logs_fetch(source: 'api', lines: 50, since: '5m')
def logs_fetch(source: 'all', lines: 100, since: '5m', grep: nil,
public_key: nil, secret_key: nil)
pk, sk = resolve_credentials(public_key, secret_key)
path = "/logs?source=#{source}&lines=#{lines}&since=#{since}"
path += "&grep=#{CGI.escape(grep)}" if grep
make_request('GET', path, pk, sk)
end
# Stream logs via Server-Sent Events
#
# Blocks until interrupted or server closes connection.
#
# @param source [String] Log source - "all", "api", "portal", "pool/cammy", "pool/ai"
# @param grep [String, nil] Optional filter pattern
# @param public_key [String, nil] Optional API key
# @param secret_key [String, nil] Optional API secret
# @yield [source, line] Called for each log line received
# @raise [CredentialsError] If no credentials found
# @raise [APIError] If API request fails
#
# @example
# Un.logs_stream(source: 'api') do |src, line|
# puts "[#{src}] #{line}"
# end
def logs_stream(source: 'all', grep: nil, public_key: nil, secret_key: nil, &block)
pk, sk = resolve_credentials(public_key, secret_key)
path = "/logs/stream?source=#{source}"
path += "&grep=#{CGI.escape(grep)}" if grep
uri = URI("#{API_BASE}#{path}")
timestamp = Time.now.to_i
signature = sign_request(sk, timestamp, 'GET', path)
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
http.read_timeout = nil
request = Net::HTTP::Get.new(uri)
request['Authorization'] = "Bearer #{pk}"
request['X-Timestamp'] = timestamp.to_s
request['X-Signature'] = signature
request['Accept'] = 'text/event-stream'
http.request(request) do |response|
response.read_body do |chunk|
chunk.split("\n").each do |line|
next unless line.start_with?('data: ')
data = line[6..]
begin
entry = JSON.parse(data)
if block_given?
yield entry['source'] || source, entry['line'] || data
else
puts "[#{entry['source'] || source}] #{entry['line'] || data}"
end
rescue JSON::ParserError
if block_given?
yield source, data
else
puts "[#{source}] #{data}"
end
end
end
end
end
end
# ============================================================================
# Utility Functions
# ============================================================================
SDK_VERSION = '4.2.0'
# Thread-local storage for last error
@last_error = nil
class << self
attr_accessor :last_error_value
end
# Get the SDK version string
#
# @return [String] Version string (e.g., "4.2.0")
#
# @example
# puts Un.version # "4.2.0"
def version
SDK_VERSION
end
# Check if the API is healthy and responding
#
# @return [Boolean] true if API is healthy, false otherwise
#
# @example
# if Un.health_check
# puts "API is healthy"
# end
def health_check
uri = URI("#{API_BASE}/health")
response = Net::HTTP.get_response(uri)
response.code == '200'
rescue StandardError => e
Un.last_error_value = "Health check failed: #{e.message}"
false
end
# Get the last error message
#
# @return [String, nil] Last error message or nil
#
# @example
# result = Un.health_check
# puts Un.last_error unless result
def last_error
Un.last_error_value
end
# Sign a message using HMAC-SHA256
#
# This is the underlying signing function used for request authentication.
# Exposed for testing and debugging purposes.
#
# @param secret_key [String] The secret key for signing
# @param message [String] The message to sign
# @return [String] 64-character lowercase hex string
#
# @example
# signature = Un.hmac_sign("secret", "message")
def hmac_sign(secret_key, message)
OpenSSL::HMAC.hexdigest('SHA256', secret_key, message)
end
private
# Language detection mapping (file extension -> language)
LANGUAGE_MAP = {
'py' => 'python',
'js' => 'javascript',
'ts' => 'typescript',
'rb' => 'ruby',
'php' => 'php',
'pl' => 'perl',
'sh' => 'bash',
'r' => 'r',
'lua' => 'lua',
'go' => 'go',
'rs' => 'rust',
'c' => 'c',
'cpp' => 'cpp',
'cc' => 'cpp',
'cxx' => 'cpp',
'java' => 'java',
'kt' => 'kotlin',
'm' => 'objc',
'cs' => 'csharp',
'fs' => 'fsharp',
'hs' => 'haskell',
'ml' => 'ocaml',
'clj' => 'clojure',
'scm' => 'scheme',
'ss' => 'scheme',
'erl' => 'erlang',
'ex' => 'elixir',
'exs' => 'elixir',
'jl' => 'julia',
'd' => 'd',
'nim' => 'nim',
'zig' => 'zig',
'v' => 'v',
'cr' => 'crystal',
'dart' => 'dart',
'groovy' => 'groovy',
'f90' => 'fortran',
'f95' => 'fortran',
'lisp' => 'commonlisp',
'lsp' => 'commonlisp',
'cob' => 'cobol',
'tcl' => 'tcl',
'raku' => 'raku',
'pro' => 'prolog',
'p' => 'prolog',
'4th' => 'forth',
'forth' => 'forth',
'fth' => 'forth'
}.freeze
# Get ~/.unsandbox directory path, creating if necessary
#
# @return [String] Path to unsandbox config directory
def unsandbox_dir
dir = File.join(Dir.home, '.unsandbox')
FileUtils.mkdir_p(dir, mode: 0o700) unless Dir.exist?(dir)
dir
end
# Load credentials from CSV file (public_key,secret_key per line)
#
# @param csv_path [String] Path to CSV file
# @param account_index [Integer] Account index (0-based)
# @return [Array<String>, nil] [public_key, secret_key] or nil if not found
def load_credentials_from_csv(csv_path, account_index = 0)
return nil unless File.exist?(csv_path)
current_index = 0
File.foreach(csv_path) do |line|
line = line.strip
next if line.empty? || line.start_with?('#')
if current_index == account_index
parts = line.split(',')
return [parts[0].strip, parts[1].strip] if parts.length >= 2
end
current_index += 1
end
nil
rescue StandardError
nil
end
# Resolve credentials from 4-tier priority system
#
# Priority:
# 1. Method arguments
# 2. Environment variables
# 3. ~/.unsandbox/accounts.csv
# 4. ./accounts.csv
#
# @param public_key [String, nil] Explicit public key
# @param secret_key [String, nil] Explicit secret key
# @param account_index [Integer, nil] Account index for CSV files
# @return [Array<String>] [public_key, secret_key]
# @raise [CredentialsError] If no credentials found
def resolve_credentials(public_key = nil, secret_key = nil, account_index = nil)
# Tier 1: Method arguments
return [public_key, secret_key] if public_key && secret_key
# Tier 2: Environment variables
env_pk = ENV['UNSANDBOX_PUBLIC_KEY']
env_sk = ENV['UNSANDBOX_SECRET_KEY']
return [env_pk, env_sk] if env_pk && env_sk
# Determine account index
account_index ||= ENV.fetch('UNSANDBOX_ACCOUNT', '0').to_i
# Tier 3: ~/.unsandbox/accounts.csv
creds = load_credentials_from_csv(File.join(unsandbox_dir, 'accounts.csv'), account_index)
return creds if creds
# Tier 4: ./accounts.csv
creds = load_credentials_from_csv('accounts.csv', account_index)
return creds if creds
raise CredentialsError, <<~MSG
No credentials found. Please provide via:
1. Method arguments (public_key:, secret_key:)
2. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY)
3. ~/.unsandbox/accounts.csv
4. ./accounts.csv
MSG
end
# Sign a request using HMAC-SHA256
#
# Message format: "timestamp:METHOD:path:body"
#
# @param secret_key [String] Secret key for signing
# @param timestamp [Integer] Unix timestamp
# @param method [String] HTTP method (GET, POST, DELETE)
# @param path [String] API path
# @param body [String, nil] Request body (JSON string)
# @return [String] 64-character lowercase hex signature
def sign_request(secret_key, timestamp, method, path, body = nil)
body_str = body || ''
message = "#{timestamp}:#{method}:#{path}:#{body_str}"
OpenSSL::HMAC.hexdigest('SHA256', secret_key, message)
end
# Make an authenticated HTTP request to the API
#
# @param method [String] HTTP method (GET, POST, DELETE)
# @param path [String] API path
# @param public_key [String] API public key
# @param secret_key [String] API secret key
# @param data [Hash, nil] Request body data
# @return [Hash] Parsed JSON response
# @raise [APIError] If request fails
def make_request(method, path, public_key, secret_key, data = nil)
uri = URI.parse("#{API_BASE}#{path}")
timestamp = Time.now.to_i
body = data ? JSON.generate(data) : ''
signature = sign_request(secret_key, timestamp, method, path, data ? body : nil)
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
http.open_timeout = REQUEST_TIMEOUT
http.read_timeout = REQUEST_TIMEOUT
headers = {
'Authorization' => "Bearer #{public_key}",
'X-Timestamp' => timestamp.to_s,
'X-Signature' => signature,
'Content-Type' => 'application/json'
}
response = case method
when 'GET'
http.get(uri.request_uri, headers)
when 'POST'
http.post(uri.request_uri, body, headers)
when 'PATCH'
http.patch(uri.request_uri, body, headers)
when 'PUT'
http.put(uri.request_uri, body, headers)
when 'DELETE'
req = Net::HTTP::Delete.new(uri.request_uri, headers)
req.body = body if data
http.request(req)
else
raise APIError, "Unsupported HTTP method: #{method}"
end
unless response.is_a?(Net::HTTPSuccess)
raise APIError.new(
"API request failed: #{response.code} #{response.message}",
status_code: response.code.to_i,
response_body: response.body
)
end
JSON.parse(response.body)
rescue JSON::ParserError => e
raise APIError, "Invalid JSON response: #{e.message}"
rescue Net::OpenTimeout, Net::ReadTimeout => e
raise APIError, "Request timeout: #{e.message}"
rescue StandardError => e
raise APIError, "Request failed: #{e.message}" unless e.is_a?(APIError)
raise
end
# Make an authenticated HTTP request with sudo OTP challenge handling.
#
# If the server returns 428 (Precondition Required), prompts for OTP
# and retries the request with X-Sudo-OTP and X-Sudo-Challenge headers.
#
# Used for destructive operations: service destroy/unlock, snapshot delete/unlock,
# image delete/unlock.
#
# @param method [String] HTTP method (GET, POST, DELETE)
# @param path [String] API path
# @param public_key [String] API public key
# @param secret_key [String] API secret key
# @param data [Hash, nil] Request body data
# @return [Hash] Parsed JSON response
# @raise [APIError] If request fails
def make_request_with_sudo(method, path, public_key, secret_key, data = nil)
uri = URI.parse("#{API_BASE}#{path}")
timestamp = Time.now.to_i
body = data ? JSON.generate(data) : ''
signature = sign_request(secret_key, timestamp, method, path, data ? body : nil)
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
http.open_timeout = REQUEST_TIMEOUT
http.read_timeout = REQUEST_TIMEOUT
headers = {
'Authorization' => "Bearer #{public_key}",
'X-Timestamp' => timestamp.to_s,
'X-Signature' => signature,
'Content-Type' => 'application/json'
}
response = case method
when 'GET'
http.get(uri.request_uri, headers)
when 'POST'
http.post(uri.request_uri, body, headers)
when 'PATCH'
http.patch(uri.request_uri, body, headers)
when 'PUT'
http.put(uri.request_uri, body, headers)
when 'DELETE'
req = Net::HTTP::Delete.new(uri.request_uri, headers)
req.body = body if data
http.request(req)
else
raise APIError, "Unsupported HTTP method: #{method}"
end
# Handle 428 sudo OTP challenge
if response.code.to_i == 428
challenge_id = ''
begin
challenge_data = JSON.parse(response.body)
challenge_id = challenge_data['challenge_id'] || ''
rescue JSON::ParserError
# Ignore JSON parse errors
end
$stderr.puts "\e[33mConfirmation required. Check your email for a one-time code.\e[0m"
$stderr.print 'Enter OTP: '
otp = $stdin.gets&.strip
raise APIError, 'Operation cancelled' if otp.nil? || otp.empty?
# Retry with sudo headers
retry_timestamp = Time.now.to_i
retry_signature = sign_request(secret_key, retry_timestamp, method, path, data ? body : nil)
retry_headers = {
'Authorization' => "Bearer #{public_key}",
'X-Timestamp' => retry_timestamp.to_s,
'X-Signature' => retry_signature,
'Content-Type' => 'application/json',
'X-Sudo-OTP' => otp,
'X-Sudo-Challenge' => challenge_id
}
response = case method
when 'GET'
http.get(uri.request_uri, retry_headers)
when 'POST'
http.post(uri.request_uri, body, retry_headers)
when 'PATCH'
http.patch(uri.request_uri, body, retry_headers)
when 'PUT'
http.put(uri.request_uri, body, retry_headers)
when 'DELETE'
req = Net::HTTP::Delete.new(uri.request_uri, retry_headers)
req.body = body if data
http.request(req)
else
raise APIError, "Unsupported HTTP method: #{method}"
end
end
unless response.is_a?(Net::HTTPSuccess)
raise APIError.new(
"API request failed: #{response.code} #{response.message}",
status_code: response.code.to_i,
response_body: response.body
)
end
JSON.parse(response.body)
rescue JSON::ParserError => e
raise APIError, "Invalid JSON response: #{e.message}"
rescue Net::OpenTimeout, Net::ReadTimeout => e
raise APIError, "Request timeout: #{e.message}"
rescue StandardError => e
raise APIError, "Request failed: #{e.message}" unless e.is_a?(APIError)
raise
end
# Make an authenticated HTTP request with text/plain content type
#
# @param method [String] HTTP method (PUT)
# @param path [String] API path
# @param public_key [String] API public key
# @param secret_key [String] API secret key
# @param body [String] Plain text request body
# @return [Hash] Parsed JSON response
# @raise [APIError] If request fails
def make_request_text(method, path, public_key, secret_key, body)
uri = URI.parse("#{API_BASE}#{path}")
timestamp = Time.now.to_i
signature = sign_request(secret_key, timestamp, method, path, body)
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
http.open_timeout = REQUEST_TIMEOUT
http.read_timeout = REQUEST_TIMEOUT
headers = {
'Authorization' => "Bearer #{public_key}",
'X-Timestamp' => timestamp.to_s,
'X-Signature' => signature,
'Content-Type' => 'text/plain'
}
response = case method
when 'PUT'
http.put(uri.request_uri, body, headers)
else
raise APIError, "Unsupported HTTP method for text: #{method}"
end
unless response.is_a?(Net::HTTPSuccess)
raise APIError.new(
"API request failed: #{response.code} #{response.message}",
status_code: response.code.to_i,
response_body: response.body
)
end
JSON.parse(response.body)
rescue JSON::ParserError => e
raise APIError, "Invalid JSON response: #{e.message}"
rescue Net::OpenTimeout, Net::ReadTimeout => e
raise APIError, "Request timeout: #{e.message}"
rescue StandardError => e
raise APIError, "Request failed: #{e.message}" unless e.is_a?(APIError)
raise
end
# Get path to languages cache file
#
# @return [String] Path to languages.json
def languages_cache_path
File.join(unsandbox_dir, 'languages.json')
end
# Load languages from cache if valid (< 1 hour old)
#
# @return [Array<String>, nil] Cached languages or nil if cache invalid/missing
def load_languages_cache
cache_path = languages_cache_path
return nil unless File.exist?(cache_path)
# Check if cache is fresh
age_seconds = Time.now - File.mtime(cache_path)
return nil if age_seconds >= LANGUAGES_CACHE_TTL
data = JSON.parse(File.read(cache_path))
data['languages']
rescue StandardError
nil
end
# Save languages to cache
#
# @param languages [Array<String>] Languages to cache
# @return [void]
def save_languages_cache(languages)
cache_path = languages_cache_path
File.write(cache_path, JSON.generate({
languages: languages,
timestamp: Time.now.to_i
}))
rescue StandardError
# Cache failures are non-fatal
nil
end
end
# ============================================================================
# CLI Implementation
# ============================================================================
# Exit codes
EXIT_SUCCESS = 0
EXIT_ERROR = 1
EXIT_INVALID_ARGS = 2
EXIT_AUTH_ERROR = 3
EXIT_API_ERROR = 4
EXIT_TIMEOUT = 5
class << self
# Main CLI entry point
def cli_main
# Global options
options = {
shell: nil,
env: [],
files: [],
file_paths: [],
public_key: nil,
secret_key: nil,
network: 'zerotrust',
vcpu: 1,
yes: false,
artifacts: false,
output: nil
}
# Check for subcommands first
if ARGV.empty?
cli_show_help
exit(EXIT_INVALID_ARGS)
end
case ARGV[0]
when 'session'
ARGV.shift
cli_session(options)
when 'service'
ARGV.shift
cli_service(options)
when 'snapshot'
ARGV.shift
cli_snapshot(options)
when 'image'
ARGV.shift
cli_image(options)
when 'key'
ARGV.shift
cli_key(options)
when 'languages'
ARGV.shift
cli_languages(options)
when '-h', '--help', 'help'
cli_show_help
exit(EXIT_SUCCESS)
else
cli_execute(options)
end
rescue CredentialsError => e
$stderr.puts "Error: #{e.message}"
exit(EXIT_AUTH_ERROR)
rescue APIError => e
$stderr.puts "Error: #{e.message}"
exit(EXIT_API_ERROR)
rescue OptionParser::InvalidOption, OptionParser::MissingArgument => e
$stderr.puts "Error: #{e.message}"
exit(EXIT_INVALID_ARGS)
rescue Interrupt
$stderr.puts "\nInterrupted"
exit(EXIT_ERROR)
end
private
# Show main help
def cli_show_help
puts <<~HELP
unsandbox.com Ruby SDK - Secure Code Execution
Usage:
ruby un.rb [options] <source_file> Execute code file
ruby un.rb [options] -s LANG 'code' Execute inline code
ruby un.rb session [options] Manage sessions
ruby un.rb service [options] Manage services
ruby un.rb snapshot [options] Manage snapshots
ruby un.rb key Check API key
ruby un.rb languages [--json] List available languages
Global Options:
-s, --shell LANG Language for inline code
-e, --env KEY=VAL Set environment variable (can repeat)
-f, --file FILE Add input file to /tmp/
-F, --file-path FILE Add input file with path preserved
-a, --artifacts Return compiled artifacts
-o, --output DIR Output directory for artifacts
-p, --public-key KEY API public key
-k, --secret-key KEY API secret key
-n, --network MODE Network mode: zerotrust or semitrusted
-v, --vcpu N vCPU count (1-8)
-y, --yes Skip confirmation prompts
-h, --help Show this help
Examples:
ruby un.rb script.py
ruby un.rb -s python 'print("hello")'
ruby un.rb -n semitrusted crawler.py
ruby un.rb session --list
ruby un.rb service --name web --ports 80 --bootstrap "python -m http.server 80"
HELP
end
# Parse global options from ARGV
def parse_global_options(options)
OptionParser.new do |opts|
opts.on('-s', '--shell LANG', 'Language for inline code') do |v|
options[:shell] = v
end
opts.on('-e', '--env KEY=VAL', 'Set environment variable') do |v|
options[:env] << v
end
opts.on('-f', '--file FILE', 'Add input file to /tmp/') do |v|
options[:files] << v
end
opts.on('-F', '--file-path FILE', 'Add input file with path preserved') do |v|
options[:file_paths] << v
end
opts.on('-a', '--artifacts', 'Return compiled artifacts') do
options[:artifacts] = true
end
opts.on('-o', '--output DIR', 'Output directory for artifacts') do |v|
options[:output] = v
end
opts.on('-p', '--public-key KEY', 'API public key') do |v|
options[:public_key] = v
end
opts.on('-k', '--secret-key KEY', 'API secret key') do |v|
options[:secret_key] = v
end
opts.on('-n', '--network MODE', 'Network mode') do |v|
options[:network] = v
end
opts.on('-v', '--vcpu N', Integer, 'vCPU count (1-8)') do |v|
options[:vcpu] = v
end
opts.on('-y', '--yes', 'Skip confirmation prompts') do
options[:yes] = true
end
opts.on('-h', '--help', 'Show help') do
yield if block_given?
exit(EXIT_SUCCESS)
end
end
end
# Execute code command
def cli_execute(options)
parser = parse_global_options(options) do
cli_show_help
end
parser.parse!(ARGV)
if ARGV.empty?
$stderr.puts 'Error: No source file or code provided'
exit(EXIT_INVALID_ARGS)
end
code = nil
language = nil
if options[:shell]
# Inline code mode: -s LANG 'code'
language = options[:shell]
code = ARGV.join(' ')
else
# File mode: script.py
filename = ARGV[0]
unless File.exist?(filename)
$stderr.puts "Error: File not found: #{filename}"
exit(EXIT_INVALID_ARGS)
end
code = File.read(filename)
language = detect_language(filename)
unless language
$stderr.puts "Error: Cannot detect language for: #{filename}"
$stderr.puts 'Use -s/--shell to specify language'
exit(EXIT_INVALID_ARGS)
end
end
# Execute the code
result = execute_code(
language,
code,
public_key: options[:public_key],
secret_key: options[:secret_key]
)
# Output results
cli_print_execute_result(result)
end
# Print execution result
def cli_print_execute_result(result)
puts result['stdout'] if result['stdout'] && !result['stdout'].empty?
$stderr.puts result['stderr'] if result['stderr'] && !result['stderr'].empty?
puts '---'
puts "Exit code: #{result['exit_code'] || 0}"
if result['execution_time_ms']
puts "Execution time: #{result['execution_time_ms']}ms"
end
end
# Session subcommand
def cli_session(options)
session_opts = {
list: false,
attach: nil,
kill: nil,
freeze: nil,
unfreeze: nil,
boost: nil,
unboost: nil,
snapshot: nil,
snapshot_name: nil,
hot: false,
tmux: false,
screen: false,
shell: 'bash',
audit: false
}
parser = parse_global_options(options) do
cli_session_help
end
parser.on('-l', '--list', 'List active sessions') do
session_opts[:list] = true
end
parser.on('--attach ID', 'Reconnect to existing session') do |v|
session_opts[:attach] = v
end
parser.on('--kill ID', 'Terminate a session') do |v|
session_opts[:kill] = v
end
parser.on('--freeze ID', 'Pause session') do |v|
session_opts[:freeze] = v
end
parser.on('--unfreeze ID', 'Resume session') do |v|
session_opts[:unfreeze] = v
end
parser.on('--boost ID', 'Add vCPUs/RAM') do |v|
session_opts[:boost] = v
end
parser.on('--unboost ID', 'Remove boost') do |v|
session_opts[:unboost] = v
end
parser.on('--snapshot ID', 'Create snapshot') do |v|
session_opts[:snapshot] = v
end
parser.on('--snapshot-name NAME', 'Name for snapshot') do |v|
session_opts[:snapshot_name] = v
end
parser.on('--hot', 'Live snapshot (no freeze)') do
session_opts[:hot] = true
end
parser.on('--tmux', 'Enable persistence with tmux') do
session_opts[:tmux] = true
end
parser.on('--screen', 'Enable persistence with screen') do
session_opts[:screen] = true
end
parser.on('--shell SHELL', 'Shell/REPL to use') do |v|
session_opts[:shell] = v
end
parser.on('--audit', 'Record session') do
session_opts[:audit] = true
end
parser.parse!(ARGV)
creds = { public_key: options[:public_key], secret_key: options[:secret_key] }
if session_opts[:list]
sessions = list_sessions(**creds)
cli_print_sessions_table(sessions)
elsif session_opts[:kill]
result = delete_session(session_opts[:kill], **creds)
puts "Session #{session_opts[:kill]} terminated"
elsif session_opts[:freeze]
result = freeze_session(session_opts[:freeze], **creds)
puts "Session #{session_opts[:freeze]} frozen"
elsif session_opts[:unfreeze]
result = unfreeze_session(session_opts[:unfreeze], **creds)
puts "Session #{session_opts[:unfreeze]} unfrozen"
elsif session_opts[:boost]
result = boost_session(session_opts[:boost], **creds)
puts "Session #{session_opts[:boost]} boosted"
elsif session_opts[:unboost]
result = unboost_session(session_opts[:unboost], **creds)
puts "Session #{session_opts[:unboost]} unboosted"
elsif session_opts[:snapshot]
snapshot_id = session_snapshot(
session_opts[:snapshot],
name: session_opts[:snapshot_name],
ephemeral: session_opts[:hot],
**creds
)
puts "Snapshot created: #{snapshot_id}"
elsif session_opts[:attach]
# Attach to existing session - show info
session = get_session(session_opts[:attach], **creds)
puts "Session: #{session['id']}"
puts "Status: #{session['status']}"
puts "WebSocket URL: #{session['websocket_url']}" if session['websocket_url']
puts "\nNote: Use a WebSocket client to connect interactively"
else
# Create new session
multiplexer = nil
multiplexer = 'tmux' if session_opts[:tmux]
multiplexer = 'screen' if session_opts[:screen]
result = create_session(
session_opts[:shell],
network_mode: options[:network],
vcpu: options[:vcpu],
multiplexer: multiplexer,
**creds
)
puts "Session created: #{result['session_id']}"
puts "Container: #{result['container_name']}" if result['container_name']
puts "WebSocket URL: #{result['websocket_url']}" if result['websocket_url']
puts "\nNote: Use a WebSocket client to connect interactively"
end
end
# Print sessions table
def cli_print_sessions_table(sessions)
if sessions.empty?
puts 'No active sessions'
return
end
# Header
puts format('%-38s %-20s %-10s %-20s', 'ID', 'NAME', 'STATUS', 'CREATED')
sessions.each do |s|
puts format('%-38s %-20s %-10s %-20s',
s['id'] || s['session_id'] || '-',
s['name'] || '-',
s['status'] || s['state'] || '-',
s['created_at'] || '-')
end
end
# Session help
def cli_session_help
puts <<~HELP
Session Management
Usage:
ruby un.rb session [options]
Options:
--shell SHELL Shell/REPL to use (default: bash)
-l, --list List active sessions
--attach ID Reconnect to existing session
--kill ID Terminate a session
--freeze ID Pause session
--unfreeze ID Resume session
--boost ID Add vCPUs/RAM
--unboost ID Remove boost
--tmux Enable persistence with tmux
--screen Enable persistence with screen
--snapshot ID Create snapshot
--snapshot-name NAME Name for snapshot
--hot Live snapshot (no freeze)
--audit Record session
Examples:
ruby un.rb session # New bash session
ruby un.rb session --shell python3 # Python REPL
ruby un.rb session --tmux # Persistent session
ruby un.rb session --list # List sessions
ruby un.rb session --kill abc123 # Kill session
HELP
end
# Service subcommand
def cli_service(options)
service_opts = {
list: false,
name: nil,
ports: nil,
domains: nil,
type: nil,
bootstrap: nil,
bootstrap_file: nil,
env_file: nil,
info: nil,
logs: nil,
tail: nil,
freeze: nil,
unfreeze: nil,
destroy: nil,
lock: nil,
unlock: nil,
resize: nil,
redeploy: nil,
execute: nil,
execute_cmd: nil,
snapshot: nil,
snapshot_name: nil
}
parser = parse_global_options(options) do
cli_service_help
end
parser.on('-l', '--list', 'List all services') do
service_opts[:list] = true
end
parser.on('--name NAME', 'Service name (creates new)') do |v|
service_opts[:name] = v
end
parser.on('--ports PORTS', 'Comma-separated ports') do |v|
service_opts[:ports] = v.split(',').map(&:to_i)
end
parser.on('--domains DOMAINS', 'Custom domains') do |v|
service_opts[:domains] = v.split(',')
end
parser.on('--type TYPE', 'Service type (minecraft, tcp, udp)') do |v|
service_opts[:type] = v
end
parser.on('--bootstrap CMD', 'Bootstrap command') do |v|
service_opts[:bootstrap] = v
end
parser.on('--bootstrap-file FILE', 'Bootstrap from file') do |v|
service_opts[:bootstrap_file] = v
end
parser.on('--env-file FILE', 'Load env from .env file') do |v|
service_opts[:env_file] = v
end
parser.on('--info ID', 'Get service details') do |v|
service_opts[:info] = v
end
parser.on('--logs ID', 'Get all logs') do |v|
service_opts[:logs] = v
end
parser.on('--tail ID', 'Get last 9000 lines') do |v|
service_opts[:tail] = v
end
parser.on('--freeze ID', 'Pause service') do |v|
service_opts[:freeze] = v
end
parser.on('--unfreeze ID', 'Resume service') do |v|
service_opts[:unfreeze] = v
end
parser.on('--destroy ID', 'Delete service') do |v|
service_opts[:destroy] = v
end
parser.on('--lock ID', 'Prevent deletion') do |v|
service_opts[:lock] = v
end
parser.on('--unlock ID', 'Allow deletion') do |v|
service_opts[:unlock] = v
end
parser.on('--resize ID', 'Resize (with --vcpu)') do |v|
service_opts[:resize] = v
end
parser.on('--redeploy ID', 'Re-run bootstrap') do |v|
service_opts[:redeploy] = v
end
parser.on('--execute ID', 'Run command in service') do |v|
service_opts[:execute] = v
end
parser.on('--snapshot ID', 'Create snapshot') do |v|
service_opts[:snapshot] = v
end
parser.on('--snapshot-name NAME', 'Name for snapshot') do |v|
service_opts[:snapshot_name] = v
end
parser.parse!(ARGV)
# Check for env subcommand
if ARGV[0] == 'env'
ARGV.shift
cli_service_env(options, service_opts)
return
end
# Get command argument for execute
service_opts[:execute_cmd] = ARGV.join(' ') if service_opts[:execute] && !ARGV.empty?
creds = { public_key: options[:public_key], secret_key: options[:secret_key] }
if service_opts[:list]
services = list_services(**creds)
cli_print_services_table(services)
elsif service_opts[:info]
service = get_service(service_opts[:info], **creds)
cli_print_service_info(service)
elsif service_opts[:logs]
result = get_service_logs(service_opts[:logs], all: true, **creds)
puts result['log'] || result['logs'] || ''
elsif service_opts[:tail]
result = get_service_logs(service_opts[:tail], all: false, **creds)
puts result['log'] || result['logs'] || ''
elsif service_opts[:freeze]
freeze_service(service_opts[:freeze], **creds)
puts "Service #{service_opts[:freeze]} frozen"
elsif service_opts[:unfreeze]
unfreeze_service(service_opts[:unfreeze], **creds)
puts "Service #{service_opts[:unfreeze]} unfrozen"
elsif service_opts[:destroy]
delete_service(service_opts[:destroy], **creds)
puts "Service #{service_opts[:destroy]} destroyed"
elsif service_opts[:lock]
lock_service(service_opts[:lock], **creds)
puts "Service #{service_opts[:lock]} locked"
elsif service_opts[:unlock]
unlock_service(service_opts[:unlock], **creds)
puts "Service #{service_opts[:unlock]} unlocked"
elsif service_opts[:resize]
update_service(service_opts[:resize], vcpu: options[:vcpu], **creds)
puts "Service #{service_opts[:resize]} resized to #{options[:vcpu]} vCPUs"
elsif service_opts[:redeploy]
bootstrap = nil
if service_opts[:bootstrap_file]
bootstrap = File.read(service_opts[:bootstrap_file])
elsif service_opts[:bootstrap]
bootstrap = service_opts[:bootstrap]
end
redeploy_service(service_opts[:redeploy], bootstrap: bootstrap, **creds)
puts "Service #{service_opts[:redeploy]} redeployed"
elsif service_opts[:execute]
cmd = service_opts[:execute_cmd]
if cmd.nil? || cmd.empty?
$stderr.puts 'Error: No command provided for --execute'
exit(EXIT_INVALID_ARGS)
end
result = execute_in_service(service_opts[:execute], cmd, **creds)
cli_print_execute_result(result)
elsif service_opts[:snapshot]
snapshot_id = service_snapshot(
service_opts[:snapshot],
name: service_opts[:snapshot_name],
**creds
)
puts "Snapshot created: #{snapshot_id}"
elsif service_opts[:name]
# Create new service
bootstrap = service_opts[:bootstrap]
if service_opts[:bootstrap_file]
bootstrap = File.read(service_opts[:bootstrap_file])
end
unless bootstrap
$stderr.puts 'Error: --bootstrap or --bootstrap-file required'
exit(EXIT_INVALID_ARGS)
end
unless service_opts[:ports]
$stderr.puts 'Error: --ports required'
exit(EXIT_INVALID_ARGS)
end
result = create_service(
service_opts[:name],
service_opts[:ports],
bootstrap,
network_mode: options[:network],
vcpu: options[:vcpu],
custom_domains: service_opts[:domains],
service_type: service_opts[:type],
**creds
)
puts "Service created: #{result['service_id']}"
puts "URL: #{result['url']}" if result['url']
else
cli_service_help
exit(EXIT_INVALID_ARGS)
end
end
# Print services table
def cli_print_services_table(services)
if services.empty?
puts 'No services'
return
end
puts format('%-38s %-20s %-10s %-20s', 'ID', 'NAME', 'STATUS', 'CREATED')
services.each do |s|
puts format('%-38s %-20s %-10s %-20s',
s['id'] || s['service_id'] || '-',
s['name'] || '-',
s['status'] || s['state'] || '-',
s['created_at'] || '-')
end
end
# Print service info
def cli_print_service_info(service)
puts "ID: #{service['id'] || service['service_id']}"
puts "Name: #{service['name']}"
puts "Status: #{service['status'] || service['state']}"
puts "URL: #{service['url']}" if service['url']
puts "Ports: #{service['ports']&.join(', ')}" if service['ports']
puts "vCPU: #{service['vcpu']}" if service['vcpu']
puts "Network: #{service['network_mode']}" if service['network_mode']
puts "Created: #{service['created_at']}" if service['created_at']
puts "Locked: #{service['locked']}" if service.key?('locked')
end
# Service help
def cli_service_help
puts <<~HELP
Service Management
Usage:
ruby un.rb service [options]
ruby un.rb service env <command> <id>
Options:
--name NAME Service name (creates new)
--ports PORTS Comma-separated ports
--domains DOMAINS Custom domains
--type TYPE Service type (minecraft, tcp, udp)
--bootstrap CMD Bootstrap command
--bootstrap-file FILE Bootstrap from file
--env-file FILE Load env from .env file
-l, --list List all services
--info ID Get service details
--logs ID Get all logs
--tail ID Get last 9000 lines
--freeze ID Pause service
--unfreeze ID Resume service
--destroy ID Delete service
--lock ID Prevent deletion
--unlock ID Allow deletion
--resize ID Resize (with --vcpu)
--redeploy ID Re-run bootstrap
--execute ID 'cmd' Run command in service
--snapshot ID Create snapshot
Env Subcommands:
ruby un.rb service env status ID Show vault status
ruby un.rb service env set ID Set from --env-file or stdin
ruby un.rb service env export ID Export to stdout
ruby un.rb service env delete ID Delete vault
Examples:
ruby un.rb service --name web --ports 80 --bootstrap "python -m http.server 80"
ruby un.rb service --list
ruby un.rb service --logs abc123
ruby un.rb service --execute abc123 'ls -la'
ruby un.rb service env status abc123
HELP
end
# Service env subcommand
def cli_service_env(options, service_opts)
if ARGV.empty?
$stderr.puts 'Error: env subcommand requires: status, set, export, or delete'
exit(EXIT_INVALID_ARGS)
end
cmd = ARGV.shift
service_id = ARGV.shift
unless service_id
$stderr.puts 'Error: Service ID required'
exit(EXIT_INVALID_ARGS)
end
creds = { public_key: options[:public_key], secret_key: options[:secret_key] }
case cmd
when 'status'
result = get_service_env(service_id, **creds)
puts "Has vault: #{result['has_vault'] || false}"
puts "Variables: #{result['count'] || 0}"
puts "Updated: #{result['updated_at']}" if result['updated_at']
when 'set'
env_content = nil
if service_opts[:env_file]
env_content = File.read(service_opts[:env_file])
elsif !$stdin.tty?
env_content = $stdin.read
else
$stderr.puts 'Error: Provide --env-file or pipe content to stdin'
exit(EXIT_INVALID_ARGS)
end
set_service_env(service_id, env_content, **creds)
puts "Environment vault updated for #{service_id}"
when 'export'
result = export_service_env(service_id, **creds)
puts result['env'] || ''
when 'delete'
delete_service_env(service_id, **creds)
puts "Environment vault deleted for #{service_id}"
else
$stderr.puts "Error: Unknown env command: #{cmd}"
exit(EXIT_INVALID_ARGS)
end
end
# Snapshot subcommand
def cli_snapshot(options)
snapshot_opts = {
list: false,
info: nil,
delete: nil,
lock: nil,
unlock: nil,
clone: nil,
clone_type: 'session',
clone_name: nil,
clone_shell: nil,
clone_ports: nil
}
parser = parse_global_options(options) do
cli_snapshot_help
end
parser.on('-l', '--list', 'List all snapshots') do
snapshot_opts[:list] = true
end
parser.on('--info ID', 'Get snapshot details') do |v|
snapshot_opts[:info] = v
end
parser.on('--delete ID', 'Delete snapshot') do |v|
snapshot_opts[:delete] = v
end
parser.on('--lock ID', 'Prevent deletion') do |v|
snapshot_opts[:lock] = v
end
parser.on('--unlock ID', 'Allow deletion') do |v|
snapshot_opts[:unlock] = v
end
parser.on('--clone ID', 'Clone snapshot') do |v|
snapshot_opts[:clone] = v
end
parser.on('--type TYPE', 'Clone type: session or service') do |v|
snapshot_opts[:clone_type] = v
end
parser.on('--name NAME', 'Name for cloned resource') do |v|
snapshot_opts[:clone_name] = v
end
parser.on('--shell SHELL', 'Shell for cloned session') do |v|
snapshot_opts[:clone_shell] = v
end
parser.on('--ports PORTS', 'Ports for cloned service') do |v|
snapshot_opts[:clone_ports] = v.split(',').map(&:to_i)
end
parser.parse!(ARGV)
creds = { public_key: options[:public_key], secret_key: options[:secret_key] }
if snapshot_opts[:list]
snapshots = list_snapshots(**creds)
cli_print_snapshots_table(snapshots)
elsif snapshot_opts[:info]
# Get snapshot details via restore endpoint or list
snapshots = list_snapshots(**creds)
snapshot = snapshots.find { |s| s['snapshot_id'] == snapshot_opts[:info] || s['id'] == snapshot_opts[:info] }
if snapshot
cli_print_snapshot_info(snapshot)
else
$stderr.puts "Error: Snapshot not found: #{snapshot_opts[:info]}"
exit(EXIT_ERROR)
end
elsif snapshot_opts[:delete]
delete_snapshot(snapshot_opts[:delete], **creds)
puts "Snapshot #{snapshot_opts[:delete]} deleted"
elsif snapshot_opts[:lock]
lock_snapshot(snapshot_opts[:lock], **creds)
puts "Snapshot #{snapshot_opts[:lock]} locked"
elsif snapshot_opts[:unlock]
unlock_snapshot(snapshot_opts[:unlock], **creds)
puts "Snapshot #{snapshot_opts[:unlock]} unlocked"
elsif snapshot_opts[:clone]
result = clone_snapshot(
snapshot_opts[:clone],
type: snapshot_opts[:clone_type],
name: snapshot_opts[:clone_name],
shell: snapshot_opts[:clone_shell],
ports: snapshot_opts[:clone_ports],
**creds
)
if result['session_id']
puts "Session created: #{result['session_id']}"
elsif result['service_id']
puts "Service created: #{result['service_id']}"
else
puts 'Clone completed'
puts JSON.pretty_generate(result)
end
else
cli_snapshot_help
exit(EXIT_INVALID_ARGS)
end
end
# Print snapshots table
def cli_print_snapshots_table(snapshots)
if snapshots.empty?
puts 'No snapshots'
return
end
puts format('%-38s %-20s %-10s %-20s', 'ID', 'NAME', 'TYPE', 'CREATED')
snapshots.each do |s|
puts format('%-38s %-20s %-10s %-20s',
s['snapshot_id'] || s['id'] || '-',
s['name'] || '-',
s['type'] || s['source_type'] || '-',
s['created_at'] || '-')
end
end
# Print snapshot info
def cli_print_snapshot_info(snapshot)
puts "ID: #{snapshot['snapshot_id'] || snapshot['id']}"
puts "Name: #{snapshot['name']}" if snapshot['name']
puts "Type: #{snapshot['type'] || snapshot['source_type']}"
puts "Source ID: #{snapshot['source_id']}" if snapshot['source_id']
puts "Size: #{snapshot['size']}" if snapshot['size']
puts "Locked: #{snapshot['locked']}" if snapshot.key?('locked')
puts "Created: #{snapshot['created_at']}" if snapshot['created_at']
end
# Snapshot help
def cli_snapshot_help
puts <<~HELP
Snapshot Management
Usage:
ruby un.rb snapshot [options]
Options:
-l, --list List all snapshots
--info ID Get snapshot details
--delete ID Delete snapshot
--lock ID Prevent deletion
--unlock ID Allow deletion
--clone ID Clone snapshot
--type TYPE Clone type: session or service
--name NAME Name for cloned resource
--shell SHELL Shell for cloned session
--ports PORTS Ports for cloned service
Examples:
ruby un.rb snapshot --list
ruby un.rb snapshot --clone abc123 --type service --name myapp --ports 80
HELP
end
# Image subcommand
def cli_image(options)
image_opts = {
list: false,
info: nil,
delete: nil,
lock: nil,
unlock: nil,
publish: nil,
source_type: nil,
visibility: nil,
visibility_mode: nil,
spawn: nil,
clone: nil,
name: nil,
ports: nil
}
parser = parse_global_options(options) do
cli_image_help
end
parser.on('-l', '--list', 'List all images') do
image_opts[:list] = true
end
parser.on('--info ID', 'Get image details') do |v|
image_opts[:info] = v
end
parser.on('--delete ID', 'Delete image') do |v|
image_opts[:delete] = v
end
parser.on('--lock ID', 'Prevent deletion') do |v|
image_opts[:lock] = v
end
parser.on('--unlock ID', 'Allow deletion') do |v|
image_opts[:unlock] = v
end
parser.on('--publish ID', 'Publish image from service/snapshot') do |v|
image_opts[:publish] = v
end
parser.on('--source-type TYPE', 'Source type: service or snapshot') do |v|
image_opts[:source_type] = v
end
parser.on('--visibility ID MODE', 'Set visibility (private, unlisted, public)') do |v|
image_opts[:visibility] = v
end
parser.on('--spawn ID', 'Spawn new service from image') do |v|
image_opts[:spawn] = v
end
parser.on('--clone ID', 'Clone an image') do |v|
image_opts[:clone] = v
end
parser.on('--name NAME', 'Name for spawned service or cloned image') do |v|
image_opts[:name] = v
end
parser.on('--ports PORTS', 'Ports for spawned service') do |v|
image_opts[:ports] = v.split(',').map(&:to_i)
end
parser.parse!(ARGV)
# Get visibility mode from remaining args if --visibility was used
if image_opts[:visibility] && ARGV.length > 0 && !ARGV[0].start_with?('-')
image_opts[:visibility_mode] = ARGV.shift
end
creds = { public_key: options[:public_key], secret_key: options[:secret_key] }
if image_opts[:list]
images = list_images(**creds)
cli_print_images_table(images)
elsif image_opts[:info]
image = get_image(image_opts[:info], **creds)
cli_print_image_info(image)
elsif image_opts[:delete]
delete_image(image_opts[:delete], **creds)
puts "Image #{image_opts[:delete]} deleted"
elsif image_opts[:lock]
lock_image(image_opts[:lock], **creds)
puts "Image #{image_opts[:lock]} locked"
elsif image_opts[:unlock]
unlock_image(image_opts[:unlock], **creds)
puts "Image #{image_opts[:unlock]} unlocked"
elsif image_opts[:publish]
unless image_opts[:source_type]
$stderr.puts 'Error: --source-type required for --publish'
exit(EXIT_INVALID_ARGS)
end
result = image_publish(
image_opts[:source_type],
image_opts[:publish],
name: image_opts[:name],
**creds
)
image_id = result['image_id'] || result['id']
puts "Image published: #{image_id}"
elsif image_opts[:visibility] && image_opts[:visibility_mode]
unless %w[private unlisted public].include?(image_opts[:visibility_mode])
$stderr.puts 'Error: visibility must be private, unlisted, or public'
exit(EXIT_INVALID_ARGS)
end
set_image_visibility(image_opts[:visibility], image_opts[:visibility_mode], **creds)
puts "Image #{image_opts[:visibility]} visibility set to #{image_opts[:visibility_mode]}"
elsif image_opts[:spawn]
unless image_opts[:name]
$stderr.puts 'Error: --name required for --spawn'
exit(EXIT_INVALID_ARGS)
end
result = spawn_from_image(
image_opts[:spawn],
name: image_opts[:name],
ports: image_opts[:ports],
**creds
)
service_id = result['service_id'] || result['id']
puts "Service spawned: #{service_id}"
elsif image_opts[:clone]
result = clone_image(
image_opts[:clone],
name: image_opts[:name],
**creds
)
image_id = result['image_id'] || result['id']
puts "Image cloned: #{image_id}"
else
cli_image_help
exit(EXIT_INVALID_ARGS)
end
end
# Print images table
def cli_print_images_table(images)
if images.empty?
puts 'No images'
return
end
puts format('%-38s %-20s %-10s %-10s %-20s', 'ID', 'NAME', 'VISIBILITY', 'SOURCE', 'CREATED')
images.each do |img|
puts format('%-38s %-20s %-10s %-10s %-20s',
img['image_id'] || img['id'] || '-',
(img['name'] || '-')[0..19],
img['visibility'] || 'private',
(img['source_type'] || '-')[0..9],
img['created_at'] || '-')
end
end
# Print image info
def cli_print_image_info(image)
puts "ID: #{image['image_id'] || image['id']}"
puts "Name: #{image['name']}" if image['name']
puts "Visibility: #{image['visibility']}" if image['visibility']
puts "Source Type: #{image['source_type']}" if image['source_type']
puts "Source ID: #{image['source_id']}" if image['source_id']
puts "Size: #{image['size']}" if image['size']
puts "Locked: #{image['locked']}" if image.key?('locked')
puts "Created: #{image['created_at']}" if image['created_at']
end
# Image help
def cli_image_help
puts <<~HELP
Image Management
Usage:
ruby un.rb image [options]
Options:
-l, --list List all images
--info ID Get image details
--delete ID Delete image
--lock ID Prevent deletion
--unlock ID Allow deletion
--publish ID Publish image (requires --source-type)
--source-type TYPE Source type: service or snapshot
--visibility ID MODE Set visibility (private, unlisted, 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
Examples:
ruby un.rb image --list
ruby un.rb image --publish svc123 --source-type service --name myimage
ruby un.rb image --spawn img123 --name myservice --ports 80,443
ruby un.rb image --visibility img123 public
HELP
end
# Key command
def cli_key(options)
parser = parse_global_options(options) do
puts 'Usage: ruby un.rb key [-p PUBLIC_KEY] [-k SECRET_KEY]'
puts
puts 'Check API key validity and show account info'
end
parser.parse!(ARGV)
creds = { public_key: options[:public_key], secret_key: options[:secret_key] }
begin
result = validate_keys(**creds)
puts "Valid: #{result['valid']}"
puts "Account: #{result['account'] || result['account_id']}" if result['account'] || result['account_id']
puts "Email: #{result['email']}" if result['email']
puts "Plan: #{result['plan']}" if result['plan']
puts "Credits: #{result['credits']}" if result['credits']
rescue APIError => e
if e.status_code == 401 || e.status_code == 403
puts 'Valid: false'
puts "Error: #{e.message}"
exit(EXIT_AUTH_ERROR)
end
raise
end
end
# Languages command - list available languages
def cli_languages(options)
json_output = false
parser = parse_global_options(options) do
puts 'Usage: ruby un.rb languages [--json]'
puts
puts 'List available programming languages'
end
parser.on('--json', 'Output as JSON array') do
json_output = true
end
parser.parse!(ARGV)
creds = { public_key: options[:public_key], secret_key: options[:secret_key] }
languages = get_languages(**creds)
if json_output
# Output as JSON array
require 'json'
puts JSON.generate(languages)
else
# Output one language per line (pipe-friendly)
languages.each { |lang| puts lang }
end
end
end
end
# CLI entry point
if __FILE__ == $0
Un.cli_main
end
Documentation clarifications
Dependencies
C Binary (un1) — requires libcurl and 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
SDK Implementations — most use stdlib only (Ruby, JS, Go, etc). Some require minimal deps:
pip install requests # Python
Execute Code
Run a Script
./un hello.py
./un app.js
./un main.rs
With Environment Variables
./un -e DEBUG=1 -e NAME=World script.py
With Input Files (teleport files into sandbox)
./un -f data.csv -f config.json process.py
Get Compiled Binary (teleport artifacts out)
./un -a -o ./bin main.c
Interactive Sessions
Start a Shell Session
# 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
Session with Network Access
./un session -n semitrusted
Session Auditing (full terminal recording)
# Record everything (including vim, interactive programs)
./un session --audit -o ./logs
# Replay session later
zcat session.log*.gz | less -R
Collect Artifacts from Session
# Files in /tmp/artifacts/ are collected on exit
./un session -a -o ./outputs
Session Persistence (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
List Active Sessions
./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
Reconnect to Existing Session
# 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
Terminate a Session
./un session --kill unsb-vm-12345
Available Shells & REPLs
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
API Key Management
Check Key Status
# Check if your API key is valid
./un key
# Output:
# Valid: key expires in 30 days
Extend Expired Key
# 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
Authentication
Credentials are loaded in priority order (highest first):
# 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
Requests are signed with HMAC-SHA256. The bearer token contains only the public key; the secret key computes the signature (never transmitted).
Resource Scaling
Set vCPU Count
# 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
Live Session Boosting
# 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
Session Freeze/Unfreeze
Freeze and Unfreeze Sessions
# 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
Persistent Services
Create a Service
# 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
Manage Services
# 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
List 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
Create Session Snapshot
# Snapshot with name
./un session --snapshot unsb-vm-12345 --name "before upgrade"
# Quick snapshot (auto-generated name)
./un session --snapshot unsb-vm-12345
Create Service Snapshot
# 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
Restore from 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
Delete Snapshot
./un snapshot --delete unsb-snapshot-a1b2-c3d4-e5f6-g7h8
Images
Images are independent, transferable container images that survive container deletion. Unlike snapshots (which live with their container), images can be shared with other users, transferred between API keys, or made public in the marketplace.
List Images
# 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
Publish Images
# 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
Create Services from Images
# 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
Image Protection
# 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
Visibility & Sharing
# 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
Transfer Ownership
# Transfer image to another API key
./un image --transfer unsb-image-xxxx-xxxx-xxxx-xxxx \
--to unsb-pk-newowner-newowner-newowner-newowner
Usage Reference
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
The UN CLI has been implemented in 42 programming languages, demonstrating that the unsandbox API can be accessed from virtually any environment.
License
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
that 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 all 42
programming languages through a unified interface, accessible to everyone. 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 its own software - with our permacomputer
monitoring itself, repairing itself, with minimal human guidance in the loop.
Our agents do their best.
Copyright 2025 TimeHexOn & foxhop & russell@unturf
https://www.timehexon.com
https://www.foxhop.net
https://www.unturf.com/software