CLI
Cliente de linha de comando rápido para execução de código e sessões interativas. Mais de 42 linguagens, mais de 30 shells/REPLs.
Documentação Oficial OpenAPI Swagger ↗Início Rápido — 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
Baixar
Guia de Instalação →Características:
- 42+ languages - Python, JS, Go, Rust, C++, Java...
- Sessions - 30+ shells/REPLs, tmux persistence
- Files - Upload files, collect artifacts
- Services - Persistent containers with domains
- Snapshots - Point-in-time backups
- Images - Publish, share, transfer
Início Rápido de Integração ⚡
Adicione superpoderes unsandbox ao seu app Ruby existente:
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
5a1ce26e79f64aca5ff3b36b110bfb27
SHA256: 57433d00d31f64fbcb55f38d050bfcb679ba831d187a3fbfa85c57f4e7c6008c
#!/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. --account N flag (accounts.csv row N, overrides env vars)
# 3. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY)
# 4. Config file (~/.unsandbox/accounts.csv, line 0 by default)
# 5. 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'
require 'base64'
# 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)
# @param input_files [Array<Hash>, nil] Optional list of hashes with "filename" and "content" (base64)
# @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, input_files: nil)
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
data[:input_files] = input_files if input_files && !input_files.empty?
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)
# @param input_files [Array<Hash>, nil] Optional list of hashes with "filename" and "content" (base64)
# @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, input_files: nil)
pk, sk = resolve_credentials(public_key, secret_key)
data = {}
data[:bootstrap] = bootstrap if bootstrap
data[:input_files] = input_files if input_files && !input_files.empty?
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
# CLI-set account index (-1 means not specified). When >= 0, takes
# priority over env vars in resolve_credentials.
attr_accessor :cli_account_index
end
@cli_account_index = -1
# 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. account_index >= 0 (explicit --account N flag) → CSV row N
# 3. Environment variables
# 4. Default CSV lookup (account 0 or UNSANDBOX_ACCOUNT env)
#
# @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 (-1 means not specified)
# @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
# Resolve effective account_index: parameter takes precedence, then CLI-set value
effective_index = account_index
effective_index = Un.cli_account_index if effective_index.nil?
# Tier 2: Explicit account_index (--account N) → load from CSV row N before env vars
if effective_index && effective_index >= 0
creds = load_credentials_from_csv(File.join(unsandbox_dir, 'accounts.csv'), effective_index)
return creds if creds
creds = load_credentials_from_csv('accounts.csv', effective_index)
return creds if creds
end
# Tier 3: Environment variables
env_pk = ENV['UNSANDBOX_PUBLIC_KEY']
env_sk = ENV['UNSANDBOX_SECRET_KEY']
return [env_pk, env_sk] if env_pk && env_sk
# Tier 4: Default CSV lookup (UNSANDBOX_ACCOUNT env or row 0)
default_index = ENV.fetch('UNSANDBOX_ACCOUNT', '0').to_i
creds = load_credentials_from_csv(File.join(unsandbox_dir, 'accounts.csv'), default_index)
return creds if creds
creds = load_credentials_from_csv('accounts.csv', default_index)
return creds if creds
raise CredentialsError, <<~MSG
No credentials found. Please provide via:
1. Method arguments (public_key:, secret_key:)
2. --account N flag (CSV row N)
3. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY)
4. ~/.unsandbox/accounts.csv
5. ./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,
account_index: -1,
network: 'zerotrust',
vcpu: 1,
yes: false,
artifacts: false,
output: nil
}
# Pre-scan for --account N before subcommand dispatch so it works in any position.
# The flag will also be consumed by parse_global_options inside each sub-handler.
ARGV.each_with_index do |arg, i|
if arg == '--account' && ARGV[i + 1] =~ /\A-?\d+\z/
options[:account_index] = ARGV[i + 1].to_i
break
elsif arg =~ /\A--account=(-?\d+)\z/
options[:account_index] = Regexp.last_match(1).to_i
break
end
end
Un.cli_account_index = options[:account_index]
# 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
--account N Use accounts.csv row N (overrides env vars)
-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('--account N', Integer, 'Use credentials from accounts.csv row N') do |v|
options[:account_index] = 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
# Build input_files from -f args
service_input_files = nil
unless options[:files].empty?
service_input_files = options[:files].map do |fpath|
{
filename: File.basename(fpath),
content: Base64.strict_encode64(File.binread(fpath))
}
end
end
redeploy_service(service_opts[:redeploy], bootstrap: bootstrap, input_files: service_input_files, **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
# Build input_files from -f args
service_input_files = nil
unless options[:files].empty?
service_input_files = options[:files].map do |fpath|
{
filename: File.basename(fpath),
content: Base64.strict_encode64(File.binread(fpath))
}
end
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],
input_files: service_input_files,
**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
Esclarecimentos de documentação
Dependências
C Binary (un1) — requer libcurl e libwebsockets:
sudo apt install build-essential libcurl4-openssl-dev libwebsockets-dev
wget unsandbox.com/downloads/un.c && gcc -O2 -o un un.c -lcurl -lwebsockets
Implementações SDK — a maioria usa apenas stdlib (Ruby, JS, Go, etc). Alguns requerem dependências mínimas:
pip install requests # Python
Executar Código
Executar um script
./un hello.py
./un app.js
./un main.rs
Com variáveis de ambiente
./un -e DEBUG=1 -e NAME=World script.py
Com arquivos de entrada (teletransportar arquivos para sandbox)
./un -f data.csv -f config.json process.py
Obter binário compilado
./un -a -o ./bin main.c
Sessões interativas
Iniciar uma sessão de shell
# Default bash shell
./un session
# Choose your shell
./un session --shell zsh
./un session --shell fish
# Jump into a REPL
./un session --shell python3
./un session --shell node
./un session --shell julia
Sessão com acesso à rede
./un session -n semitrusted
Auditoria de sessão (gravação completa do terminal)
# Record everything (including vim, interactive programs)
./un session --audit -o ./logs
# Replay session later
zcat session.log*.gz | less -R
Coletar artefatos da sessão
# Files in /tmp/artifacts/ are collected on exit
./un session -a -o ./outputs
Persistência de sessão (tmux/screen)
# Default: session terminates on disconnect (clean exit)
./un session
# With tmux: session persists, can reconnect later
./un session --tmux
# Press Ctrl+b then d to detach
# With screen: alternative multiplexer
./un session --screen
# Press Ctrl+a then d to detach
Listar Trabalhos Ativos
./un session --list
# Output:
# Active sessions: 2
#
# SESSION ID CONTAINER SHELL TTL STATUS
# abc123... unsb-vm-12345 python3 45m30s active
# def456... unsb-vm-67890 bash 1h2m active
Reconectar à sessão existente
# Reconnect by container name (requires --tmux or --screen)
./un session --attach unsb-vm-12345
# Use exit to terminate session, or detach to keep it running
Encerrar uma sessão
./un session --kill unsb-vm-12345
Shells e REPLs disponíveis
Shells: bash, dash, sh, zsh, fish, ksh, tcsh, csh, elvish, xonsh, ash
REPLs: python3, bpython, ipython # Python
node # JavaScript
ruby, irb # Ruby
lua # Lua
php # PHP
perl # Perl
guile, scheme # Scheme
ghci # Haskell
erl, iex # Erlang/Elixir
sbcl, clisp # Common Lisp
r # R
julia # Julia
clojure # Clojure
Gerenciamento de Chave API
Verificar Status do Pagamento
# Check if your API key is valid
./un key
# Output:
# Valid: key expires in 30 days
Estender Chave Expirada
# Open the portal to extend an expired key
./un key --extend
# This opens the unsandbox.com portal where you can
# add more credits to extend your key's expiration
Autenticação
As credenciais são carregadas em ordem de prioridade (maior primeiro):
# 1. CLI flags (highest priority)
./un -p unsb-pk-xxxx -k unsb-sk-xxxxx script.py
# 2. Environment variables
export UNSANDBOX_PUBLIC_KEY=unsb-pk-xxxx-xxxx-xxxx-xxxx
export UNSANDBOX_SECRET_KEY=unsb-sk-xxxxx-xxxxx-xxxxx-xxxxx
./un script.py
# 3. Config file (lowest priority)
# ~/.unsandbox/accounts.csv format: public_key,secret_key
mkdir -p ~/.unsandbox
echo "unsb-pk-xxxx-xxxx-xxxx-xxxx,unsb-sk-xxxxx-xxxxx-xxxxx-xxxxx" > ~/.unsandbox/accounts.csv
./un script.py
As requisições são assinadas com HMAC-SHA256. O token bearer contém apenas a chave pública; a chave secreta calcula a assinatura (nunca é transmitida).
Escalonamento de Recursos
Definir Quantidade de vCPU
# Default: 1 vCPU, 2GB RAM
./un script.py
# Scale up: 4 vCPUs, 8GB RAM
./un -v 4 script.py
# Maximum: 8 vCPUs, 16GB RAM
./un --vcpu 8 heavy_compute.py
Reforço de Sessão Ao Vivo
# Boost a running session to 2 vCPU, 4GB RAM
./un session --boost sandbox-abc
# Boost to specific vCPU count (4 vCPU, 8GB RAM)
./un session --boost sandbox-abc --boost-vcpu 4
# Return to base resources (1 vCPU, 2GB RAM)
./un session --unboost sandbox-abc
Congelar/Descongelar Sessão
Congelar e Descongelar Sessões
# Freeze a session (stop billing, preserve state)
./un session --freeze sandbox-abc
# Unfreeze a frozen session
./un session --unfreeze sandbox-abc
# Note: Requires --tmux or --screen for persistence
Serviços Persistentes
Criar um Serviço
# Web server with ports
./un service --name web --ports 80,443 --bootstrap "python -m http.server 80"
# With custom domains
./un service --name blog --ports 8000 --domains blog.example.com
# Game server with SRV records
./un service --name mc --type minecraft --bootstrap ./setup.sh
# Deploy app tarball with bootstrap script
./un service --name app --ports 8000 -f app.tar.gz --bootstrap-file ./setup.sh
# setup.sh: cd /tmp && tar xzf app.tar.gz && ./app/start.sh
Gerenciar Serviços
# List all services
./un service --list
# Get service details
./un service --info abc123
# View bootstrap logs
./un service --logs abc123
./un service --tail abc123 # last 9000 lines
# Execute command in running service
./un service --execute abc123 'journalctl -u myapp -n 50'
# Dump bootstrap script (for migrations)
./un service --dump-bootstrap abc123
./un service --dump-bootstrap abc123 backup.sh
# Freeze/unfreeze service
./un service --freeze abc123
./un service --unfreeze abc123
# Service settings (auto-wake, freeze page display)
./un service --auto-unfreeze abc123 # enable auto-wake on HTTP
./un service --no-auto-unfreeze abc123 # disable auto-wake
./un service --show-freeze-page abc123 # show HTML payment page (default)
./un service --no-show-freeze-page abc123 # return JSON error instead
# Redeploy with new bootstrap
./un service --redeploy abc123 --bootstrap ./new-setup.sh
# Destroy service
./un service --destroy abc123
Snapshots
Listar Snapshots
./un snapshot --list
# Output:
# Snapshots: 3
#
# SNAPSHOT ID NAME SOURCE SIZE CREATED
# unsb-snapshot-a1b2-c3d4-e5f6-g7h8 before-upgrade session 512 MB 2h ago
# unsb-snapshot-i9j0-k1l2-m3n4-o5p6 stable-v1.0 service 1.2 GB 1d ago
Criar Snapshot da Sessão
# Snapshot with name
./un session --snapshot unsb-vm-12345 --name "before upgrade"
# Quick snapshot (auto-generated name)
./un session --snapshot unsb-vm-12345
Criar Snapshot do Serviço
# Standard snapshot (pauses container briefly)
./un service --snapshot unsb-service-abc123 --name "stable v1.0"
# Hot snapshot (no pause, may be inconsistent)
./un service --snapshot unsb-service-abc123 --hot
Restaurar a partir do Snapshot
# Restore session from snapshot
./un session --restore unsb-snapshot-a1b2-c3d4-e5f6-g7h8
# Restore service from snapshot
./un service --restore unsb-snapshot-i9j0-k1l2-m3n4-o5p6
Excluir Snapshot
./un snapshot --delete unsb-snapshot-a1b2-c3d4-e5f6-g7h8
Imagens
Imagens são imagens de container independentes e transferíveis que sobrevivem à exclusão do container. Diferente dos snapshots (que permanecem com seu container), imagens podem ser compartilhadas com outros usuários, transferidas entre chaves de API ou tornadas públicas no marketplace.
Listar Imagens
# List all images (owned + shared + public)
./un image --list
# List only your images
./un image --list owned
# List images shared with you
./un image --list shared
# List public marketplace images
./un image --list public
# Get image details
./un image --info unsb-image-xxxx-xxxx-xxxx-xxxx
Publicar Imagens
# Publish from a stopped or frozen service
./un image --publish-service unsb-service-abc123 \
--name "My App v1.0" --description "Production snapshot"
# Publish from a snapshot
./un image --publish-snapshot unsb-snapshot-xxxx-xxxx-xxxx-xxxx \
--name "Stable Release"
# Note: Cannot publish from running containers - stop or freeze first
Criar Serviços a partir de Imagens
# Spawn a new service from an image
./un image --spawn unsb-image-xxxx-xxxx-xxxx-xxxx \
--name new-service --ports 80,443
# Clone an image (creates a copy you own)
./un image --clone unsb-image-xxxx-xxxx-xxxx-xxxx
Proteção de Imagem
# Lock image to prevent accidental deletion
./un image --lock unsb-image-xxxx-xxxx-xxxx-xxxx
# Unlock image to allow deletion
./un image --unlock unsb-image-xxxx-xxxx-xxxx-xxxx
# Delete image (must be unlocked)
./un image --delete unsb-image-xxxx-xxxx-xxxx-xxxx
Visibilidade e Compartilhamento
# Set visibility level
./un image --visibility unsb-image-xxxx-xxxx-xxxx-xxxx private # owner only (default)
./un image --visibility unsb-image-xxxx-xxxx-xxxx-xxxx unlisted # can be shared
./un image --visibility unsb-image-xxxx-xxxx-xxxx-xxxx public # marketplace
# Share with specific user
./un image --grant unsb-image-xxxx-xxxx-xxxx-xxxx \
--key unsb-pk-friend-friend-friend-friend
# Revoke access
./un image --revoke unsb-image-xxxx-xxxx-xxxx-xxxx \
--key unsb-pk-friend-friend-friend-friend
# List who has access
./un image --trusted unsb-image-xxxx-xxxx-xxxx-xxxx
Transferir Propriedade
# Transfer image to another API key
./un image --transfer unsb-image-xxxx-xxxx-xxxx-xxxx \
--to unsb-pk-newowner-newowner-newowner-newowner
Referência de uso
Usage: ./un [options] <source_file>
./un session [options]
./un service [options]
./un snapshot [options]
./un image [options]
./un key
Commands:
(default) Execute source file in sandbox
session Open interactive shell/REPL session
service Manage persistent services
snapshot Manage container snapshots
image Manage container images (publish, share, transfer)
key Check API key validity and expiration
Options:
-e KEY=VALUE Set environment variable (can use multiple times)
-f FILE Add input file (can use multiple times)
-a Return and save artifacts from /tmp/artifacts/
-o DIR Output directory for artifacts (default: current dir)
-p KEY Public key (or set UNSANDBOX_PUBLIC_KEY env var)
-k KEY Secret key (or set UNSANDBOX_SECRET_KEY env var)
-n MODE Network mode: zerotrust (default) or semitrusted
-v N, --vcpu N vCPU count 1-8, each vCPU gets 2GB RAM (default: 1)
-y Skip confirmation for large uploads (>1GB)
-h Show this help
Authentication (priority order):
1. -p and -k flags (public and secret key)
2. UNSANDBOX_PUBLIC_KEY + UNSANDBOX_SECRET_KEY env vars
3. ~/.unsandbox/accounts.csv (format: public_key,secret_key per line)
Session options:
-s, --shell SHELL Shell/REPL to use (default: bash)
-l, --list List active sessions
--attach ID Reconnect to existing session (ID or container name)
--kill ID Terminate a session (ID or container name)
--freeze ID Freeze a session (requires --tmux/--screen)
--unfreeze ID Unfreeze a frozen session
--boost ID Boost session resources (2 vCPU, 4GB RAM)
--boost-vcpu N Specify vCPU count for boost (1-8)
--unboost ID Return to base resources
--audit Record full session for auditing
--tmux Enable session persistence with tmux (allows reconnect)
--screen Enable session persistence with screen (allows reconnect)
Service options:
--name NAME Service name (creates new service)
--ports PORTS Comma-separated ports (e.g., 80,443)
--domains DOMAINS Custom domains (e.g., example.com,www.example.com)
--type TYPE Service type: minecraft, mumble, teamspeak, source, tcp, udp
--bootstrap CMD Bootstrap command/file/URL to run on startup
-f FILE Upload file to /tmp/ (can use multiple times)
-l, --list List all services
--info ID Get service details
--tail ID Get last 9000 lines of bootstrap logs
--logs ID Get all bootstrap logs
--freeze ID Freeze a service
--unfreeze ID Unfreeze a service
--auto-unfreeze ID Enable auto-wake on HTTP request
--no-auto-unfreeze ID Disable auto-wake on HTTP request
--show-freeze-page ID Show HTML payment page when frozen (default)
--no-show-freeze-page ID Return JSON error when frozen
--destroy ID Destroy a service
--redeploy ID Re-run bootstrap script (requires --bootstrap)
--execute ID CMD Run a command in a running service
--dump-bootstrap ID [FILE] Dump bootstrap script (for migrations)
--snapshot ID Create snapshot of session or service
--snapshot-name User-friendly name for snapshot
--hot Create snapshot without pausing (may be inconsistent)
--restore ID Restore session/service from snapshot ID
Snapshot options:
-l, --list List all snapshots
--info ID Get snapshot details
--delete ID Delete a snapshot permanently
Image options:
-l, --list [owned|shared|public] List images (all, owned, shared, or public)
--info ID Get image details
--publish-service ID Publish image from stopped/frozen service
--publish-snapshot ID Publish image from snapshot
--name NAME Name for published image
--description DESC Description for published image
--delete ID Delete image (must be unlocked)
--clone ID Clone image (creates copy you own)
--spawn ID Create service from image (requires --name)
--lock ID Lock image to prevent deletion
--unlock ID Unlock image to allow deletion
--visibility ID LEVEL Set visibility (private|unlisted|public)
--grant ID --key KEY Grant access to another API key
--revoke ID --key KEY Revoke access from API key
--transfer ID --to KEY Transfer ownership to API key
--trusted ID List API keys with access
Key options:
(no options) Check API key validity
--extend Open portal to extend an expired key
Examples:
./un script.py # execute Python script
./un -e DEBUG=1 script.py # with environment variable
./un -f data.csv process.py # with input file
./un -a -o ./bin main.c # save compiled artifacts
./un -v 4 heavy.py # with 4 vCPUs, 8GB RAM
./un session # interactive bash session
./un session --tmux # bash with reconnect support
./un session --list # list active sessions
./un session --attach unsb-vm-12345 # reconnect to session
./un session --kill unsb-vm-12345 # terminate a session
./un session --freeze unsb-vm-12345 # freeze session
./un session --unfreeze unsb-vm-12345 # unfreeze session
./un session --boost unsb-vm-12345 # boost resources
./un session --unboost unsb-vm-12345 # return to base
./un session --shell python3 # Python REPL
./un session --shell node # Node.js REPL
./un session -n semitrusted # session with network access
./un session --audit -o ./logs # record session for auditing
./un service --name web --ports 80 # create web service
./un service --list # list all services
./un service --logs abc123 # view bootstrap logs
./un key # check API key
./un key --extend # extend expired key
./un snapshot --list # list all snapshots
./un session --snapshot unsb-vm-123 # snapshot a session
./un service --snapshot abc123 # snapshot a service
./un session --restore unsb-snapshot-xxxx # restore from snapshot
./un image --list # list all images
./un image --list owned # list your images
./un image --publish-service abc # publish image from service
./un image --spawn img123 --name x # create service from image
./un image --grant img --key pk # share image with user
CLI Inception
O UN CLI foi implementado em 42 linguagens de programação, demonstrando que a API do unsandbox pode ser acessada de praticamente qualquer ambiente.
Ver Todas as 42 Implementações →
Licença
DOMÍNIO PÚBLICO - SEM LICENÇA, SEM GARANTIA
Este é software gratuito de domínio público para o bem público de um permacomputador hospedado
em permacomputer.com - um computador sempre ativo pelo povo, para o povo. Um que é
durável, fácil de reparar e distribuído como água da torneira para inteligência de
aprendizado de máquina.
O permacomputador é infraestrutura de propriedade comunitária otimizada em torno de quatro valores:
VERDADE - Primeiros princípios, matemática & ciência, código aberto distribuído livremente
LIBERDADE - Parcerias voluntárias, liberdade da tirania e controle corporativo
HARMONIA - Desperdício mínimo, sistemas auto-renováveis com diversas conexões prósperas
AMOR - Seja você mesmo sem ferir os outros, cooperação através da lei natural
Este software contribui para essa visão ao permitir a execução de código em mais de 42
linguagens de programação através de uma interface unificada, acessível a todos. Código são
sementes que brotam em qualquer tecnologia abandonada.
Saiba mais: https://www.permacomputer.com
Qualquer pessoa é livre para copiar, modificar, publicar, usar, compilar, vender ou distribuir
este software, seja em forma de código-fonte ou como binário compilado, para qualquer propósito,
comercial ou não comercial, e por qualquer meio.
SEM GARANTIA. O SOFTWARE É FORNECIDO "COMO ESTÁ" SEM GARANTIA DE QUALQUER TIPO.
Dito isso, a camada de membrana digital do nosso permacomputador executa continuamente testes
unitários, de integração e funcionais em todo o seu próprio software - com nosso permacomputador
monitorando a si mesmo, reparando a si mesmo, com orientação humana mínima no ciclo.
Nossos agentes fazem o seu melhor.
Copyright 2025 TimeHexOn & foxhop & russell@unturf
https://www.timehexon.com
https://www.foxhop.net
https://www.unturf.com/software