Console Playground

CLI

Fast command-line client for code execution and interactive sessions. 42+ languages, 30+ shells/REPLs.

Official OpenAPI Swagger Docs ↗

Quick Start — Go

# Download + setup
curl -O https://git.unturf.com/engineering/unturf/un-inception/-/raw/main/clients/go/sync/src/un.go
export UNSANDBOX_PUBLIC_KEY="unsb-pk-xxxx-xxxx-xxxx-xxxx"
export UNSANDBOX_SECRET_KEY="unsb-sk-xxxxx-xxxxx-xxxxx-xxxxx"
# Compile first
go build -o un un.go

# Run code
./un script.go

Downloads

Install Guide →
Static Binary
Linux x86_64 (5.3MB)
un
Go SDK
un.go (85.5 KB)
Download

Features

  • 42+ languages - Python, JS, Go, Rust, C++, Java...
  • Sessions - 30+ shells/REPLs, tmux persistence
  • Files - Upload files, collect artifacts
  • Services - Persistent containers with domains
  • Snapshots - Point-in-time backups
  • Images - Publish, share, transfer

Integration Quickstart ⚡

Add unsandbox superpowers to your existing Go app:

1
Download
curl -O https://git.unturf.com/engineering/unturf/un-inception/-/raw/main/clients/go/sync/src/un.go
2
Set API Keys
# 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
3
Hello World
// In your Go app:
package main

import "un"

func main() {
    result, _ := un.ExecuteCode("go", `fmt.Println("Hello from Go running on unsandbox!")`)
    fmt.Println(result.Stdout)  // Hello from Go running on unsandbox!
}
Demo cooldown: s
stdout:

                      
JSON Response:

                      
4
Compile & Run
go build -o myapp main.go && ./myapp
Source Code 📄 (3304 lines)
MD5: 9fcb43511dee8c4f8d75a15fe01a91bf SHA256: 2fb2130dcefba667d1d810023faf7c360ca944e037e26c43a434968da036433b
/*
PUBLIC DOMAIN - NO LICENSE, NO WARRANTY

unsandbox.com Go SDK (Synchronous)

Library Usage:
    import "un"

    // Create credentials
    creds, err := un.ResolveCredentials("", "")
    if err != nil {
        log.Fatal(err)
    }

    // Execute code synchronously
    result, err := un.ExecuteCode(creds, "python", `print("hello")`)
    if err != nil {
        log.Fatal(err)
    }

    // Execute asynchronously
    jobID, err := un.ExecuteAsync(creds, "javascript", `console.log("hello")`)
    if err != nil {
        log.Fatal(err)
    }

    // Wait for job completion with exponential backoff
    result, err := un.WaitForJob(creds, jobID)
    if err != nil {
        log.Fatal(err)
    }

    // List all jobs
    jobs, err := un.ListJobs(creds)
    if err != nil {
        log.Fatal(err)
    }

    // Get supported languages
    languages, err := un.GetLanguages(creds)
    if err != nil {
        log.Fatal(err)
    }

    // Detect language from filename
    lang := un.DetectLanguage("script.py")  // Returns "python"

    // Snapshot operations (NEW)
    snapshotID, err := un.SessionSnapshot(creds, sessionID, "my_snapshot", false)
    snapshots, err := un.ListSnapshots(creds)
    result, err := un.RestoreSnapshot(creds, snapshotID)
    err = un.DeleteSnapshot(creds, snapshotID)

Authentication Priority (4-tier):
    1. Function arguments (publicKey, secretKey)
    2. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY)
    3. Config file (~/.unsandbox/accounts.csv, line 0 by default)
    4. Local directory (./accounts.csv, line 0 by default)

    Format: public_key,secret_key (one per line)
    Account selection: UNSANDBOX_ACCOUNT=N env var (0-based index)

Request Authentication (HMAC-SHA256):
    Authorization: Bearer <public_key>                  (identifies account)
    X-Timestamp: <unix_seconds>                         (replay prevention)
    X-Signature: HMAC-SHA256(secret_key, msg)           (proves secret + body integrity)

    Message format: "timestamp:METHOD:path:body"
    - timestamp: seconds since epoch
    - METHOD: GET, POST, DELETE, etc. (uppercase)
    - path: e.g., "/execute", "/jobs/123"
    - body: JSON payload (empty string for GET/DELETE)

Languages Cache:
    - Cached in ~/.unsandbox/languages.json
    - TTL: 1 hour
    - Updated on successful API calls
*/

package un

import (
	"bufio"
	"bytes"
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"os"
	"os/user"
	"path/filepath"
	"strconv"
	"strings"
	"time"
)

const (
	APIBase              = "https://api.unsandbox.com"
	LanguagesCacheTTL    = 3600 // 1 hour
)

var (
	PollDelaysMs = []int{300, 450, 700, 900, 650, 1600, 2000}

	// Language detection mapping (file extension -> language)
	LanguageMap = map[string]string{
		"py":     "python",
		"js":     "javascript",
		"ts":     "typescript",
		"rb":     "ruby",
		"php":    "php",
		"pl":     "perl",
		"sh":     "bash",
		"r":      "r",
		"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",
	}
)

// CredentialsError is raised when credentials cannot be found or are invalid
type CredentialsError struct {
	Message string
}

func (e *CredentialsError) Error() string {
	return e.Message
}

// Credentials holds the public and secret API keys
type Credentials struct {
	PublicKey string
	SecretKey string
}

// getUnsandboxDir returns ~/.unsandbox directory path, creating if necessary
func getUnsandboxDir() (string, error) {
	user, err := user.Current()
	if err != nil {
		return "", err
	}

	dir := filepath.Join(user.HomeDir, ".unsandbox")
	if err := os.MkdirAll(dir, 0700); err != nil {
		return "", err
	}
	return dir, nil
}

// loadCredentialsFromCsv loads credentials from CSV file (public_key,secret_key per line)
func loadCredentialsFromCsv(csvPath string, accountIndex int) *Credentials {
	data, err := os.ReadFile(csvPath)
	if err != nil {
		return nil
	}

	lines := strings.Split(string(data), "\n")
	currentIndex := 0

	for _, line := range lines {
		line = strings.TrimSpace(line)
		if line == "" || strings.HasPrefix(line, "#") {
			continue
		}

		if currentIndex == accountIndex {
			parts := strings.Split(line, ",")
			if len(parts) >= 2 {
				return &Credentials{
					PublicKey: strings.TrimSpace(parts[0]),
					SecretKey: strings.TrimSpace(parts[1]),
				}
			}
		}
		currentIndex++
	}

	return nil
}

// ResolveCredentials resolves credentials from 4-tier priority system.
//
// Priority:
//   1. Function arguments (publicKey, secretKey non-empty)
//   2. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY)
//   3. ~/.unsandbox/accounts.csv
//   4. ./accounts.csv
func ResolveCredentials(publicKey, secretKey string) (*Credentials, error) {
	// Tier 1: Function arguments
	if publicKey != "" && secretKey != "" {
		return &Credentials{
			PublicKey: publicKey,
			SecretKey: secretKey,
		}, nil
	}

	// Tier 2: Environment variables
	envPk := os.Getenv("UNSANDBOX_PUBLIC_KEY")
	envSk := os.Getenv("UNSANDBOX_SECRET_KEY")
	if envPk != "" && envSk != "" {
		return &Credentials{
			PublicKey: envPk,
			SecretKey: envSk,
		}, nil
	}

	// Determine account index
	accountIndex := 0
	if envAccount := os.Getenv("UNSANDBOX_ACCOUNT"); envAccount != "" {
		var err error
		accountIndex, err = strconv.Atoi(envAccount)
		if err != nil {
			accountIndex = 0
		}
	}

	// Tier 3: ~/.unsandbox/accounts.csv
	unsandboxDir, err := getUnsandboxDir()
	if err == nil {
		if creds := loadCredentialsFromCsv(filepath.Join(unsandboxDir, "accounts.csv"), accountIndex); creds != nil {
			return creds, nil
		}
	}

	// Tier 4: ./accounts.csv
	if creds := loadCredentialsFromCsv("accounts.csv", accountIndex); creds != nil {
		return creds, nil
	}

	return nil, &CredentialsError{
		Message: "No credentials found. Please provide via:\n" +
			"  1. Function arguments (publicKey, secretKey)\n" +
			"  2. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY)\n" +
			"  3. ~/.unsandbox/accounts.csv\n" +
			"  4. ./accounts.csv",
	}
}

// signRequest signs a request using HMAC-SHA256
//
// Message format: "timestamp:METHOD:path:body"
// Returns: 64-character hex string
func signRequest(secretKey string, timestamp int64, method, path string, body []byte) string {
	bodyStr := ""
	if body != nil {
		bodyStr = string(body)
	}
	message := fmt.Sprintf("%d:%s:%s:%s", timestamp, method, path, bodyStr)

	h := hmac.New(sha256.New, []byte(secretKey))
	h.Write([]byte(message))
	return hex.EncodeToString(h.Sum(nil))
}

// makeRequest makes an authenticated HTTP request to the API
func makeRequest(method, path string, creds *Credentials, data interface{}) (map[string]interface{}, error) {
	url := APIBase + path
	timestamp := time.Now().Unix()

	var body []byte
	var err error
	if data != nil {
		body, err = json.Marshal(data)
		if err != nil {
			return nil, err
		}
	}

	signature := signRequest(creds.SecretKey, timestamp, method, path, body)

	req, err := http.NewRequest(method, url, bytes.NewReader(body))
	if err != nil {
		return nil, err
	}

	req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", creds.PublicKey))
	req.Header.Set("X-Timestamp", fmt.Sprintf("%d", timestamp))
	req.Header.Set("X-Signature", signature)
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("User-Agent", "un-go/2.0")

	client := &http.Client{Timeout: 120 * time.Second}
	resp, err := client.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	respBody, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, err
	}

	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
		return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(respBody))
	}

	var result map[string]interface{}
	if err := json.Unmarshal(respBody, &result); err != nil {
		return nil, fmt.Errorf("failed to parse response: %w", err)
	}

	return result, nil
}

// SudoChallengeError represents a 428 response requiring OTP confirmation
type SudoChallengeError struct {
	ChallengeID string
	Message     string
	StatusCode  int
	Body        []byte
}

func (e *SudoChallengeError) Error() string {
	return fmt.Sprintf("HTTP 428: sudo challenge required (challenge_id: %s)", e.ChallengeID)
}

// makeRequestWithSudo makes an authenticated HTTP request with optional sudo headers
func makeRequestWithSudo(method, path string, creds *Credentials, data interface{}, sudoOTP, sudoChallengeID string) (map[string]interface{}, int, []byte, error) {
	url := APIBase + path
	timestamp := time.Now().Unix()

	var body []byte
	var err error
	if data != nil {
		body, err = json.Marshal(data)
		if err != nil {
			return nil, 0, nil, err
		}
	}

	signature := signRequest(creds.SecretKey, timestamp, method, path, body)

	req, err := http.NewRequest(method, url, bytes.NewReader(body))
	if err != nil {
		return nil, 0, nil, err
	}

	req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", creds.PublicKey))
	req.Header.Set("X-Timestamp", fmt.Sprintf("%d", timestamp))
	req.Header.Set("X-Signature", signature)
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("User-Agent", "un-go/2.0")

	// Add sudo headers if provided
	if sudoOTP != "" {
		req.Header.Set("X-Sudo-OTP", sudoOTP)
	}
	if sudoChallengeID != "" {
		req.Header.Set("X-Sudo-Challenge", sudoChallengeID)
	}

	client := &http.Client{Timeout: 120 * time.Second}
	resp, err := client.Do(req)
	if err != nil {
		return nil, 0, nil, err
	}
	defer resp.Body.Close()

	respBody, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, resp.StatusCode, nil, err
	}

	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
		return nil, resp.StatusCode, respBody, fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(respBody))
	}

	var result map[string]interface{}
	if err := json.Unmarshal(respBody, &result); err != nil {
		return nil, resp.StatusCode, respBody, fmt.Errorf("failed to parse response: %w", err)
	}

	return result, resp.StatusCode, respBody, nil
}

// handleSudoChallenge handles 428 sudo OTP challenge - prompts user for OTP and retries request
func handleSudoChallenge(method, path string, creds *Credentials, data interface{}, responseBody []byte) (map[string]interface{}, error) {
	// Extract challenge_id from response
	var resp map[string]interface{}
	if err := json.Unmarshal(responseBody, &resp); err != nil {
		return nil, fmt.Errorf("failed to parse 428 response: %w", err)
	}

	challengeID, _ := resp["challenge_id"].(string)

	// Prompt user for OTP
	fmt.Fprintf(os.Stderr, "\033[33mConfirmation required. Check your email for a one-time code.\033[0m\n")
	fmt.Fprintf(os.Stderr, "Enter OTP: ")

	reader := bufio.NewReader(os.Stdin)
	otp, err := reader.ReadString('\n')
	if err != nil {
		return nil, fmt.Errorf("failed to read OTP: %w", err)
	}
	otp = strings.TrimSpace(otp)

	if otp == "" {
		return nil, fmt.Errorf("operation cancelled")
	}

	// Retry the request with sudo headers
	result, statusCode, retryBody, err := makeRequestWithSudo(method, path, creds, data, otp, challengeID)
	if err != nil {
		if statusCode >= 200 && statusCode < 300 {
			return result, nil
		}
		// Extract error message from response if available
		if retryBody != nil {
			var errResp map[string]interface{}
			if json.Unmarshal(retryBody, &errResp) == nil {
				if errMsg, ok := errResp["error"].(string); ok {
					return nil, fmt.Errorf("%s", errMsg)
				}
			}
		}
		return nil, err
	}

	fmt.Fprintf(os.Stderr, "\033[32mOperation completed successfully\033[0m\n")
	return result, nil
}

// makeDestructiveRequest makes a request that may require sudo OTP confirmation (for 428 responses)
func makeDestructiveRequest(method, path string, creds *Credentials, data interface{}) (map[string]interface{}, error) {
	result, statusCode, respBody, err := makeRequestWithSudo(method, path, creds, data, "", "")
	if statusCode == 428 {
		return handleSudoChallenge(method, path, creds, data, respBody)
	}
	if err != nil {
		return nil, err
	}
	return result, nil
}

// getLanguagesCachePath returns path to languages cache file
func getLanguagesCachePath() (string, error) {
	unsandboxDir, err := getUnsandboxDir()
	if err != nil {
		return "", err
	}
	return filepath.Join(unsandboxDir, "languages.json"), nil
}

// loadLanguagesCache loads languages from cache if valid (< 1 hour old)
func loadLanguagesCache() ([]string, bool) {
	cachePath, err := getLanguagesCachePath()
	if err != nil {
		return nil, false
	}

	data, err := os.ReadFile(cachePath)
	if err != nil {
		return nil, false
	}

	stat, err := os.Stat(cachePath)
	if err != nil {
		return nil, false
	}

	ageSeconds := int(time.Since(stat.ModTime()).Seconds())
	if ageSeconds >= LanguagesCacheTTL {
		return nil, false
	}

	var cacheData map[string]interface{}
	if err := json.Unmarshal(data, &cacheData); err != nil {
		return nil, false
	}

	if langs, ok := cacheData["languages"].([]interface{}); ok {
		result := make([]string, len(langs))
		for i, lang := range langs {
			if s, ok := lang.(string); ok {
				result[i] = s
			}
		}
		return result, true
	}

	return nil, false
}

// saveLanguagesCache saves languages to cache
func saveLanguagesCache(languages []string) {
	cachePath, err := getLanguagesCachePath()
	if err != nil {
		return
	}

	cacheData := map[string]interface{}{
		"languages": languages,
		"timestamp": time.Now().Unix(),
	}

	data, err := json.MarshalIndent(cacheData, "", "  ")
	if err != nil {
		return
	}

	_ = os.WriteFile(cachePath, data, 0600)
}

// ExecuteCode executes code synchronously (blocks until completion)
func ExecuteCode(creds *Credentials, language, code string) (map[string]interface{}, error) {
	data := map[string]string{
		"language": language,
		"code":     code,
	}

	response, err := makeRequest("POST", "/execute", creds, data)
	if err != nil {
		return nil, err
	}

	// If we got a job_id, poll until completion
	if jobID, ok := response["job_id"].(string); ok {
		if status, ok := response["status"].(string); ok && (status == "pending" || status == "running") {
			return WaitForJob(creds, jobID)
		}
	}

	return response, nil
}

// ExecuteAsync executes code asynchronously (returns immediately with job_id)
func ExecuteAsync(creds *Credentials, language, code string) (string, error) {
	data := map[string]string{
		"language": language,
		"code":     code,
	}

	response, err := makeRequest("POST", "/execute", creds, data)
	if err != nil {
		return "", err
	}

	if jobID, ok := response["job_id"].(string); ok {
		return jobID, nil
	}

	return "", fmt.Errorf("no job_id in response")
}

// GetJob gets current status/result of a job (single poll, no waiting)
func GetJob(creds *Credentials, jobID string) (map[string]interface{}, error) {
	return makeRequest("GET", fmt.Sprintf("/jobs/%s", jobID), creds, nil)
}

// WaitForJob waits 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+
func WaitForJob(creds *Credentials, jobID string) (map[string]interface{}, error) {
	pollCount := 0

	for {
		// Sleep before polling
		delayIdx := pollCount
		if delayIdx >= len(PollDelaysMs) {
			delayIdx = len(PollDelaysMs) - 1
		}
		time.Sleep(time.Duration(PollDelaysMs[delayIdx]) * time.Millisecond)
		pollCount++

		response, err := GetJob(creds, jobID)
		if err != nil {
			return nil, err
		}

		if status, ok := response["status"].(string); ok {
			if status == "completed" || status == "failed" || status == "timeout" || status == "cancelled" {
				return response, nil
			}
		}

		// Still running, continue polling
	}
}

// CancelJob cancels a running job
func CancelJob(creds *Credentials, jobID string) (map[string]interface{}, error) {
	return makeRequest("DELETE", fmt.Sprintf("/jobs/%s", jobID), creds, nil)
}

// ListJobs lists all jobs for the authenticated account
func ListJobs(creds *Credentials) ([]map[string]interface{}, error) {
	response, err := makeRequest("GET", "/jobs", creds, nil)
	if err != nil {
		return nil, err
	}

	if jobs, ok := response["jobs"].([]interface{}); ok {
		result := make([]map[string]interface{}, len(jobs))
		for i, job := range jobs {
			if m, ok := job.(map[string]interface{}); ok {
				result[i] = m
			}
		}
		return result, nil
	}

	return []map[string]interface{}{}, nil
}

// GetLanguages gets list of supported programming languages
//
// Results are cached for 1 hour in ~/.unsandbox/languages.json
func GetLanguages(creds *Credentials) ([]string, error) {
	// Try cache first
	if cached, ok := loadLanguagesCache(); ok {
		return cached, nil
	}

	response, err := makeRequest("GET", "/languages", creds, nil)
	if err != nil {
		return nil, err
	}

	var languages []string
	if langs, ok := response["languages"].([]interface{}); ok {
		for _, lang := range langs {
			if s, ok := lang.(string); ok {
				languages = append(languages, s)
			}
		}
	}

	// Cache the result
	saveLanguagesCache(languages)
	return languages, nil
}

// DetectLanguage detects programming language from filename extension
//
// Args:
//   filename: Filename to detect language from (e.g., "script.py")
//
// Returns:
//   Language identifier (e.g., "python") or empty string if unknown
//
// Examples:
//   DetectLanguage("hello.py")   // -> "python"
//   DetectLanguage("script.js")  // -> "javascript"
//   DetectLanguage("main.go")    // -> "go"
//   DetectLanguage("unknown")    // -> ""
func DetectLanguage(filename string) string {
	if !strings.Contains(filename, ".") {
		return ""
	}

	parts := strings.Split(filename, ".")
	ext := parts[len(parts)-1]

	// Try exact match first (for case-sensitive extensions like .R)
	if lang, ok := LanguageMap[ext]; ok {
		return lang
	}

	// Try lowercase match
	if lang, ok := LanguageMap[strings.ToLower(ext)]; ok {
		return lang
	}

	return ""
}

// SessionSnapshot creates a snapshot of a session (NEW)
func SessionSnapshot(creds *Credentials, sessionID, name string, hot bool) (string, error) {
	data := map[string]interface{}{
		"session_id": sessionID,
		"hot":        hot,
	}
	if name != "" {
		data["name"] = name
	}

	response, err := makeRequest("POST", "/snapshots", creds, data)
	if err != nil {
		return "", err
	}

	if snapshotID, ok := response["snapshot_id"].(string); ok {
		return snapshotID, nil
	}

	return "", fmt.Errorf("no snapshot_id in response")
}

// ServiceSnapshot creates a snapshot of a service (NEW)
func ServiceSnapshot(creds *Credentials, serviceID, name string, hot bool) (string, error) {
	data := map[string]interface{}{
		"service_id": serviceID,
		"hot":        hot,
	}
	if name != "" {
		data["name"] = name
	}

	response, err := makeRequest("POST", "/snapshots", creds, data)
	if err != nil {
		return "", err
	}

	if snapshotID, ok := response["snapshot_id"].(string); ok {
		return snapshotID, nil
	}

	return "", fmt.Errorf("no snapshot_id in response")
}

// ListSnapshots lists all snapshots (NEW)
func ListSnapshots(creds *Credentials) ([]map[string]interface{}, error) {
	response, err := makeRequest("GET", "/snapshots", creds, nil)
	if err != nil {
		return nil, err
	}

	if snapshots, ok := response["snapshots"].([]interface{}); ok {
		result := make([]map[string]interface{}, len(snapshots))
		for i, snapshot := range snapshots {
			if m, ok := snapshot.(map[string]interface{}); ok {
				result[i] = m
			}
		}
		return result, nil
	}

	return []map[string]interface{}{}, nil
}

// RestoreSnapshot restores a snapshot (NEW)
func RestoreSnapshot(creds *Credentials, snapshotID string) (map[string]interface{}, error) {
	return makeRequest("POST", fmt.Sprintf("/snapshots/%s/restore", snapshotID), creds, map[string]interface{}{})
}

// DeleteSnapshot deletes a snapshot (NEW)
// This operation may require sudo OTP confirmation (428 response handling)
func DeleteSnapshot(creds *Credentials, snapshotID string) (map[string]interface{}, error) {
	return makeDestructiveRequest("DELETE", fmt.Sprintf("/snapshots/%s", snapshotID), creds, nil)
}

// ============================================================================
// Session Operations
// ============================================================================

// SessionOptions contains optional parameters for session creation.
type SessionOptions struct {
	NetworkMode string // "zerotrust" (default) or "semitrusted"
	Shell       string // Shell to use (e.g., "bash", "python3")
	TTL         int    // Time-to-live in seconds (default: 3600)
	VCPU        int    // Number of virtual CPUs (default: 1)
	Multiplexer string // Multiplexer to use (e.g., "tmux")
}

// ListSessions lists all active sessions for the authenticated account.
func ListSessions(creds *Credentials) ([]map[string]interface{}, error) {
	response, err := makeRequest("GET", "/sessions", creds, nil)
	if err != nil {
		return nil, err
	}

	if sessions, ok := response["sessions"].([]interface{}); ok {
		result := make([]map[string]interface{}, len(sessions))
		for i, session := range sessions {
			if m, ok := session.(map[string]interface{}); ok {
				result[i] = m
			}
		}
		return result, nil
	}

	return []map[string]interface{}{}, nil
}

// GetSession gets details of a specific session.
func GetSession(creds *Credentials, sessionID string) (map[string]interface{}, error) {
	return makeRequest("GET", fmt.Sprintf("/sessions/%s", sessionID), creds, nil)
}

// CreateSession creates a new interactive session.
//
// Args:
//
//	creds: API credentials
//	opts: Optional session configuration (can be nil for defaults)
//
// Returns:
//
//	Session info including session_id and container_name
func CreateSession(creds *Credentials, opts *SessionOptions) (map[string]interface{}, error) {
	data := make(map[string]interface{})

	if opts != nil {
		if opts.NetworkMode != "" {
			data["network_mode"] = opts.NetworkMode
		}
		if opts.Shell != "" {
			data["shell"] = opts.Shell
		}
		if opts.TTL > 0 {
			data["ttl"] = opts.TTL
		}
		if opts.VCPU > 0 {
			data["vcpu"] = opts.VCPU
		}
		if opts.Multiplexer != "" {
			data["multiplexer"] = opts.Multiplexer
		}
	}

	return makeRequest("POST", "/sessions", creds, data)
}

// DeleteSession terminates a session.
func DeleteSession(creds *Credentials, sessionID string) (map[string]interface{}, error) {
	return makeRequest("DELETE", fmt.Sprintf("/sessions/%s", sessionID), creds, nil)
}

// FreezeSession freezes a session (pauses execution, preserves state).
func FreezeSession(creds *Credentials, sessionID string) (map[string]interface{}, error) {
	return makeRequest("POST", fmt.Sprintf("/sessions/%s/freeze", sessionID), creds, map[string]interface{}{})
}

// UnfreezeSession unfreezes a previously frozen session.
func UnfreezeSession(creds *Credentials, sessionID string) (map[string]interface{}, error) {
	return makeRequest("POST", fmt.Sprintf("/sessions/%s/unfreeze", sessionID), creds, map[string]interface{}{})
}

// BoostSession increases the vCPU allocation for a session.
//
// Args:
//
//	creds: API credentials
//	sessionID: Session ID to boost
//	vcpu: Number of vCPUs (2, 4, 8, etc.)
func BoostSession(creds *Credentials, sessionID string, vcpu int) (map[string]interface{}, error) {
	data := map[string]interface{}{
		"vcpu": vcpu,
	}
	return makeRequest("POST", fmt.Sprintf("/sessions/%s/boost", sessionID), creds, data)
}

// UnboostSession resets the vCPU allocation for a session to default.
func UnboostSession(creds *Credentials, sessionID string) (map[string]interface{}, error) {
	return makeRequest("POST", fmt.Sprintf("/sessions/%s/unboost", sessionID), creds, map[string]interface{}{})
}

// ShellSession executes a command in a session's shell.
//
// Note: For interactive shell access, use the WebSocket-based shell endpoint.
// This function is for executing single commands.
func ShellSession(creds *Credentials, sessionID, command string) (map[string]interface{}, error) {
	data := map[string]interface{}{
		"command": command,
	}
	return makeRequest("POST", fmt.Sprintf("/sessions/%s/shell", sessionID), creds, data)
}

// ============================================================================
// Service Operations
// ============================================================================

// ServiceOptions contains optional parameters for service creation.
type ServiceOptions struct {
	NetworkMode      string // "zerotrust" (default) or "semitrusted"
	Shell            string // Shell to use for bootstrap
	VCPU             int    // Number of virtual CPUs
	UnfreezeOnDemand bool   // Enable automatic unfreezing on HTTP request
}

// ServiceUpdateOptions contains optional parameters for service updates.
type ServiceUpdateOptions struct {
	VCPU int // Number of virtual CPUs
}

// ListServices lists all services for the authenticated account.
func ListServices(creds *Credentials) ([]map[string]interface{}, error) {
	response, err := makeRequest("GET", "/services", creds, nil)
	if err != nil {
		return nil, err
	}

	if services, ok := response["services"].([]interface{}); ok {
		result := make([]map[string]interface{}, len(services))
		for i, service := range services {
			if m, ok := service.(map[string]interface{}); ok {
				result[i] = m
			}
		}
		return result, nil
	}

	return []map[string]interface{}{}, nil
}

// CreateService creates a new persistent service.
//
// Args:
//
//	creds: API credentials
//	name: Service name
//	ports: Array of port numbers to expose
//	bootstrap: Bootstrap script to run on service start
//	opts: Optional service configuration (can be nil for defaults)
func CreateService(creds *Credentials, name string, ports []int, bootstrap string, opts *ServiceOptions) (map[string]interface{}, error) {
	data := map[string]interface{}{
		"name":      name,
		"ports":     ports,
		"bootstrap": bootstrap,
	}

	if opts != nil {
		if opts.NetworkMode != "" {
			data["network_mode"] = opts.NetworkMode
		}
		if opts.Shell != "" {
			data["shell"] = opts.Shell
		}
		if opts.VCPU > 0 {
			data["vcpu"] = opts.VCPU
		}
		if opts.UnfreezeOnDemand {
			data["unfreeze_on_demand"] = true
		}
	}

	return makeRequest("POST", "/services", creds, data)
}

// GetService gets details of a specific service.
func GetService(creds *Credentials, serviceID string) (map[string]interface{}, error) {
	return makeRequest("GET", fmt.Sprintf("/services/%s", serviceID), creds, nil)
}

// UpdateService updates a service's configuration.
func UpdateService(creds *Credentials, serviceID string, opts *ServiceUpdateOptions) (map[string]interface{}, error) {
	data := make(map[string]interface{})
	if opts != nil {
		if opts.VCPU > 0 {
			data["vcpu"] = opts.VCPU
		}
	}
	return makeRequest("PATCH", fmt.Sprintf("/services/%s", serviceID), creds, data)
}

// DeleteService destroys a service.
// This operation may require sudo OTP confirmation (428 response handling)
func DeleteService(creds *Credentials, serviceID string) (map[string]interface{}, error) {
	return makeDestructiveRequest("DELETE", fmt.Sprintf("/services/%s", serviceID), creds, nil)
}

// FreezeService freezes a service (pauses execution, preserves state).
func FreezeService(creds *Credentials, serviceID string) (map[string]interface{}, error) {
	return makeRequest("POST", fmt.Sprintf("/services/%s/freeze", serviceID), creds, map[string]interface{}{})
}

// UnfreezeService unfreezes a previously frozen service.
func UnfreezeService(creds *Credentials, serviceID string) (map[string]interface{}, error) {
	return makeRequest("POST", fmt.Sprintf("/services/%s/unfreeze", serviceID), creds, map[string]interface{}{})
}

// LockService locks a service to prevent modifications or deletion.
func LockService(creds *Credentials, serviceID string) (map[string]interface{}, error) {
	return makeRequest("POST", fmt.Sprintf("/services/%s/lock", serviceID), creds, map[string]interface{}{})
}

// UnlockService unlocks a previously locked service.
// This operation may require sudo OTP confirmation (428 response handling)
func UnlockService(creds *Credentials, serviceID string) (map[string]interface{}, error) {
	return makeDestructiveRequest("POST", fmt.Sprintf("/services/%s/unlock", serviceID), creds, map[string]interface{}{})
}

// SetUnfreezeOnDemand enables or disables automatic unfreezing on HTTP request.
func SetUnfreezeOnDemand(creds *Credentials, serviceID string, enabled bool) (map[string]interface{}, error) {
	return makeRequest("PATCH", fmt.Sprintf("/services/%s", serviceID), creds, map[string]interface{}{
		"unfreeze_on_demand": enabled,
	})
}

// GetServiceLogs retrieves logs from a service.
//
// Args:
//
//	creds: API credentials
//	serviceID: Service ID
//	all: If true, returns all logs; if false, returns only recent logs
func GetServiceLogs(creds *Credentials, serviceID string, all bool) (map[string]interface{}, error) {
	path := fmt.Sprintf("/services/%s/logs", serviceID)
	if all {
		path = fmt.Sprintf("/services/%s/logs?all=true", serviceID)
	}
	return makeRequest("GET", path, creds, nil)
}

// GetServiceEnv retrieves the environment variable names for a service.
// Note: Values are not returned for security; use ExportServiceEnv for full export.
func GetServiceEnv(creds *Credentials, serviceID string) (map[string]interface{}, error) {
	return makeRequest("GET", fmt.Sprintf("/services/%s/env", serviceID), creds, nil)
}

// SetServiceEnv sets environment variables for a service.
//
// Args:
//
//	creds: API credentials
//	serviceID: Service ID
//	env: Map of environment variable names to values
func SetServiceEnv(creds *Credentials, serviceID string, env map[string]string) (map[string]interface{}, error) {
	return makeRequest("POST", fmt.Sprintf("/services/%s/env", serviceID), creds, env)
}

// DeleteServiceEnv deletes environment variables from a service.
//
// Args:
//
//	creds: API credentials
//	serviceID: Service ID
//	keys: List of environment variable names to delete (nil deletes all)
func DeleteServiceEnv(creds *Credentials, serviceID string, keys []string) (map[string]interface{}, error) {
	var data interface{}
	if keys != nil {
		data = map[string]interface{}{"keys": keys}
	}
	return makeRequest("DELETE", fmt.Sprintf("/services/%s/env", serviceID), creds, data)
}

// ExportServiceEnv exports all environment variables for a service.
// Returns the full .env format content with values.
func ExportServiceEnv(creds *Credentials, serviceID string) (map[string]interface{}, error) {
	return makeRequest("POST", fmt.Sprintf("/services/%s/env/export", serviceID), creds, map[string]interface{}{})
}

// RedeployService redeploys a service with optional new bootstrap script.
//
// Args:
//
//	creds: API credentials
//	serviceID: Service ID
//	bootstrap: New bootstrap script (empty string to keep existing)
func RedeployService(creds *Credentials, serviceID string, bootstrap string) (map[string]interface{}, error) {
	data := make(map[string]interface{})
	if bootstrap != "" {
		data["bootstrap"] = bootstrap
	}
	return makeRequest("POST", fmt.Sprintf("/services/%s/redeploy", serviceID), creds, data)
}

// ExecuteInService executes a command in a running service.
func ExecuteInService(creds *Credentials, serviceID, command string) (map[string]interface{}, error) {
	data := map[string]interface{}{
		"command": command,
	}
	return makeRequest("POST", fmt.Sprintf("/services/%s/execute", serviceID), creds, data)
}

// ResizeService changes the vCPU count for a running service.
//
// Args:
//
//	creds: API credentials
//	serviceID: Service ID
//	vcpu: New vCPU count (1-8)
func ResizeService(creds *Credentials, serviceID string, vcpu int) (map[string]interface{}, error) {
	data := map[string]interface{}{
		"vcpu": vcpu,
	}
	return makeRequest("POST", fmt.Sprintf("/services/%s/resize", serviceID), creds, data)
}

// ============================================================================
// Additional Snapshot Operations
// ============================================================================

// LockSnapshot locks a snapshot to prevent deletion.
func LockSnapshot(creds *Credentials, snapshotID string) (map[string]interface{}, error) {
	return makeRequest("POST", fmt.Sprintf("/snapshots/%s/lock", snapshotID), creds, map[string]interface{}{})
}

// UnlockSnapshot unlocks a previously locked snapshot.
// This operation may require sudo OTP confirmation (428 response handling)
func UnlockSnapshot(creds *Credentials, snapshotID string) (map[string]interface{}, error) {
	return makeDestructiveRequest("POST", fmt.Sprintf("/snapshots/%s/unlock", snapshotID), creds, map[string]interface{}{})
}

// CloneSnapshotOptions contains optional parameters for snapshot cloning.
type CloneSnapshotOptions struct {
	Name   string   // Name for the cloned resource
	Shell  string   // Shell to use (for session clones)
	Ports  []int    // Ports to expose (for service clones)
}

// CloneSnapshot clones a snapshot into a new session or service.
//
// Args:
//
//	creds: API credentials
//	snapshotID: Snapshot ID to clone
//	cloneType: "session" or "service"
//	opts: Optional clone configuration (can be nil)
func CloneSnapshot(creds *Credentials, snapshotID, cloneType string, opts *CloneSnapshotOptions) (map[string]interface{}, error) {
	data := map[string]interface{}{
		"type": cloneType,
	}

	if opts != nil {
		if opts.Name != "" {
			data["name"] = opts.Name
		}
		if opts.Shell != "" {
			data["shell"] = opts.Shell
		}
		if opts.Ports != nil {
			data["ports"] = opts.Ports
		}
	}

	return makeRequest("POST", fmt.Sprintf("/snapshots/%s/clone", snapshotID), creds, data)
}

// ============================================================================
// Key Validation
// ============================================================================

// ValidateKeys validates the API credentials with the server.
// Returns account information if valid, error if invalid.
func ValidateKeys(creds *Credentials) (map[string]interface{}, error) {
	return makeRequest("POST", "/keys/validate", creds, map[string]interface{}{})
}

// ============================================================================
// Image Generation
// ============================================================================

// ImageOptions contains options for image generation
type ImageOptions struct {
	Model   string
	Size    string // default "1024x1024"
	Quality string // "standard" or "hd"
	N       int    // number of images
}

// ImageResult contains the generated images
type ImageResult struct {
	Images    []string `json:"images"`
	CreatedAt string   `json:"created_at"`
}

// Image generates images from a text prompt using AI.
func Image(creds *Credentials, prompt string, opts *ImageOptions) (*ImageResult, error) {
	if opts == nil {
		opts = &ImageOptions{}
	}
	if opts.Size == "" {
		opts.Size = "1024x1024"
	}
	if opts.Quality == "" {
		opts.Quality = "standard"
	}
	if opts.N == 0 {
		opts.N = 1
	}

	payload := map[string]interface{}{
		"prompt":  prompt,
		"size":    opts.Size,
		"quality": opts.Quality,
		"n":       opts.N,
	}
	if opts.Model != "" {
		payload["model"] = opts.Model
	}

	response, err := makeRequest("POST", "/image", creds, payload)
	if err != nil {
		return nil, err
	}

	// Extract images array from response
	var images []string
	if imagesInterface, ok := response["images"].([]interface{}); ok {
		for _, img := range imagesInterface {
			if s, ok := img.(string); ok {
				images = append(images, s)
			}
		}
	}

	createdAt := ""
	if ca, ok := response["created_at"].(string); ok {
		createdAt = ca
	}

	return &ImageResult{
		Images:    images,
		CreatedAt: createdAt,
	}, nil
}

// ============================================================================
// Snapshot Info
// ============================================================================

// GetSnapshot gets details of a specific snapshot.
func GetSnapshot(creds *Credentials, snapshotID string) (map[string]interface{}, error) {
	return makeRequest("GET", fmt.Sprintf("/snapshots/%s", snapshotID), creds, nil)
}

// ============================================================================
// LXD Container Images API
// ============================================================================

// ImagePublishOptions contains options for publishing an LXD container image.
type ImagePublishOptions struct {
	Name        string // Optional name for the image
	Description string // Optional description for the image
}

// ImagePublish publishes an LXD container image from a session or service.
//
// Args:
//
//	creds: API credentials
//	sourceType: Source type ("session" or "service")
//	sourceID: ID of the session or service to publish
//	opts: Optional image configuration (can be nil)
//
// Returns:
//
//	Image info including image_id
func ImagePublish(creds *Credentials, sourceType, sourceID string, opts *ImagePublishOptions) (map[string]interface{}, error) {
	data := map[string]interface{}{
		"source_type": sourceType,
		"source_id":   sourceID,
	}

	if opts != nil {
		if opts.Name != "" {
			data["name"] = opts.Name
		}
		if opts.Description != "" {
			data["description"] = opts.Description
		}
	}

	return makeRequest("POST", "/images", creds, data)
}

// ListImages lists LXD container images.
//
// Args:
//
//	creds: API credentials
//	filterType: Optional filter type ("mine", "shared", "public", or empty for all accessible)
//
// Returns:
//
//	List of images
func ListImages(creds *Credentials, filterType string) ([]map[string]interface{}, error) {
	path := "/images"
	if filterType != "" {
		path = fmt.Sprintf("/images/%s", filterType)
	}

	response, err := makeRequest("GET", path, creds, nil)
	if err != nil {
		return nil, err
	}

	if images, ok := response["images"].([]interface{}); ok {
		result := make([]map[string]interface{}, len(images))
		for i, image := range images {
			if m, ok := image.(map[string]interface{}); ok {
				result[i] = m
			}
		}
		return result, nil
	}

	return []map[string]interface{}{}, nil
}

// GetImage gets details of a specific LXD container image.
func GetImage(creds *Credentials, imageID string) (map[string]interface{}, error) {
	return makeRequest("GET", fmt.Sprintf("/images/%s", imageID), creds, nil)
}

// DeleteImage deletes an LXD container image.
//
// Note: Locked images cannot be deleted. Use UnlockImage first if needed.
// This operation may require sudo OTP confirmation (428 response handling)
func DeleteImage(creds *Credentials, imageID string) (map[string]interface{}, error) {
	return makeDestructiveRequest("DELETE", fmt.Sprintf("/images/%s", imageID), creds, nil)
}

// LockImage locks an LXD container image to prevent deletion.
func LockImage(creds *Credentials, imageID string) (map[string]interface{}, error) {
	return makeRequest("POST", fmt.Sprintf("/images/%s/lock", imageID), creds, map[string]interface{}{})
}

// UnlockImage unlocks a previously locked LXD container image.
// This operation may require sudo OTP confirmation (428 response handling)
func UnlockImage(creds *Credentials, imageID string) (map[string]interface{}, error) {
	return makeDestructiveRequest("POST", fmt.Sprintf("/images/%s/unlock", imageID), creds, map[string]interface{}{})
}

// SetImageVisibility sets the visibility of an LXD container image.
//
// Args:
//
//	creds: API credentials
//	imageID: Image ID
//	visibility: Visibility level ("private", "shared", or "public")
func SetImageVisibility(creds *Credentials, imageID, visibility string) (map[string]interface{}, error) {
	data := map[string]interface{}{
		"visibility": visibility,
	}
	return makeRequest("POST", fmt.Sprintf("/images/%s/visibility", imageID), creds, data)
}

// GrantImageAccess grants access to an LXD container image for another API key.
//
// Args:
//
//	creds: API credentials
//	imageID: Image ID
//	trustedKey: The public API key to grant access to
func GrantImageAccess(creds *Credentials, imageID, trustedKey string) (map[string]interface{}, error) {
	data := map[string]interface{}{
		"trusted_key": trustedKey,
	}
	return makeRequest("POST", fmt.Sprintf("/images/%s/grant", imageID), creds, data)
}

// RevokeImageAccess revokes access to an LXD container image from another API key.
//
// Args:
//
//	creds: API credentials
//	imageID: Image ID
//	trustedKey: The public API key to revoke access from
func RevokeImageAccess(creds *Credentials, imageID, trustedKey string) (map[string]interface{}, error) {
	data := map[string]interface{}{
		"trusted_key": trustedKey,
	}
	return makeRequest("POST", fmt.Sprintf("/images/%s/revoke", imageID), creds, data)
}

// ListImageTrusted lists the API keys that have been granted access to an image.
func ListImageTrusted(creds *Credentials, imageID string) ([]map[string]interface{}, error) {
	response, err := makeRequest("GET", fmt.Sprintf("/images/%s/trusted", imageID), creds, nil)
	if err != nil {
		return nil, err
	}

	if trusted, ok := response["trusted"].([]interface{}); ok {
		result := make([]map[string]interface{}, len(trusted))
		for i, t := range trusted {
			if m, ok := t.(map[string]interface{}); ok {
				result[i] = m
			}
		}
		return result, nil
	}

	return []map[string]interface{}{}, nil
}

// TransferImage transfers ownership of an LXD container image to another API key.
//
// Args:
//
//	creds: API credentials
//	imageID: Image ID
//	toAPIKey: The public API key to transfer ownership to
func TransferImage(creds *Credentials, imageID, toAPIKey string) (map[string]interface{}, error) {
	data := map[string]interface{}{
		"to_api_key": toAPIKey,
	}
	return makeRequest("POST", fmt.Sprintf("/images/%s/transfer", imageID), creds, data)
}

// SpawnFromImageOptions contains options for spawning a service from an image.
type SpawnFromImageOptions struct {
	Name        string // Service name
	Ports       []int  // Ports to expose
	Bootstrap   string // Bootstrap script
	NetworkMode string // "zerotrust" or "semitrusted"
}

// SpawnFromImage spawns a new service from an LXD container image.
//
// Args:
//
//	creds: API credentials
//	imageID: Image ID to spawn from
//	opts: Spawn configuration options
//
// Returns:
//
//	Service info including service_id
func SpawnFromImage(creds *Credentials, imageID string, opts *SpawnFromImageOptions) (map[string]interface{}, error) {
	data := make(map[string]interface{})

	if opts != nil {
		if opts.Name != "" {
			data["name"] = opts.Name
		}
		if opts.Ports != nil {
			data["ports"] = opts.Ports
		}
		if opts.Bootstrap != "" {
			data["bootstrap"] = opts.Bootstrap
		}
		if opts.NetworkMode != "" {
			data["network_mode"] = opts.NetworkMode
		}
	}

	return makeRequest("POST", fmt.Sprintf("/images/%s/spawn", imageID), creds, data)
}

// CloneImageOptions contains options for cloning an LXD container image.
type CloneImageOptions struct {
	Name        string // Name for the cloned image
	Description string // Description for the cloned image
}

// CloneImage creates a copy of an LXD container image.
//
// Args:
//
//	creds: API credentials
//	imageID: Image ID to clone
//	opts: Clone configuration options (can be nil)
//
// Returns:
//
//	New image info including image_id
func CloneImage(creds *Credentials, imageID string, opts *CloneImageOptions) (map[string]interface{}, error) {
	data := make(map[string]interface{})

	if opts != nil {
		if opts.Name != "" {
			data["name"] = opts.Name
		}
		if opts.Description != "" {
			data["description"] = opts.Description
		}
	}

	return makeRequest("POST", fmt.Sprintf("/images/%s/clone", imageID), creds, data)
}

// ============================================================================
// PaaS Logs API
// ============================================================================

// LogsFetchOptions contains options for fetching logs.
type LogsFetchOptions struct {
	Lines int    // Number of lines (1-10000)
	Since string // Time window ("1m", "5m", "1h", "1d")
	Grep  string // Optional filter pattern
}

// LogsFetch fetches batch logs from the portal.
//
// Args:
//
//	creds: API credentials
//	source: Log source ("all", "api", "portal", "pool/cammy", "pool/ai")
//	opts: Fetch options (can be nil for defaults)
//
// Returns:
//
//	JSON response with log entries
func LogsFetch(creds *Credentials, source string, opts *LogsFetchOptions) (map[string]interface{}, error) {
	path := "/paas/logs"
	params := []string{}

	if source != "" {
		params = append(params, fmt.Sprintf("source=%s", source))
	}

	if opts != nil {
		if opts.Lines > 0 {
			params = append(params, fmt.Sprintf("lines=%d", opts.Lines))
		}
		if opts.Since != "" {
			params = append(params, fmt.Sprintf("since=%s", opts.Since))
		}
		if opts.Grep != "" {
			params = append(params, fmt.Sprintf("grep=%s", opts.Grep))
		}
	}

	if len(params) > 0 {
		path = path + "?" + strings.Join(params, "&")
	}

	return makeRequest("GET", path, creds, nil)
}

// LogCallback is called for each log line received during streaming.
type LogCallback func(source, line string)

// LogsStream streams logs via Server-Sent Events.
// This function blocks until the stream is closed or an error occurs.
//
// Args:
//
//	creds: API credentials
//	source: Log source ("all", "api", "portal", "pool/cammy", "pool/ai")
//	grep: Optional filter pattern (empty string for no filter)
//	callback: Function called for each log line
//
// Returns:
//
//	nil on clean shutdown, error on failure
func LogsStream(creds *Credentials, source, grep string, callback LogCallback) error {
	path := "/paas/logs/stream"
	params := []string{}

	if source != "" {
		params = append(params, fmt.Sprintf("source=%s", source))
	}
	if grep != "" {
		params = append(params, fmt.Sprintf("grep=%s", grep))
	}

	if len(params) > 0 {
		path = path + "?" + strings.Join(params, "&")
	}

	url := APIBase + path
	timestamp := time.Now().Unix()
	message := fmt.Sprintf("%d:GET:%s:", timestamp, path)
	mac := hmac.New(sha256.New, []byte(creds.SecretKey))
	mac.Write([]byte(message))
	signature := hex.EncodeToString(mac.Sum(nil))

	req, err := http.NewRequest("GET", url, nil)
	if err != nil {
		return err
	}

	req.Header.Set("Authorization", "Bearer "+creds.PublicKey)
	req.Header.Set("X-Timestamp", fmt.Sprintf("%d", timestamp))
	req.Header.Set("X-Signature", signature)
	req.Header.Set("Accept", "text/event-stream")

	client := &http.Client{Timeout: 0} // No timeout for streaming
	resp, err := client.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	if resp.StatusCode != 200 {
		body, _ := io.ReadAll(resp.Body)
		return fmt.Errorf("stream error (HTTP %d): %s", resp.StatusCode, string(body))
	}

	reader := bufio.NewReader(resp.Body)
	currentSource := source

	for {
		line, err := reader.ReadString('\n')
		if err != nil {
			if err == io.EOF {
				return nil // Clean shutdown
			}
			return err
		}

		line = strings.TrimSpace(line)
		if line == "" {
			continue
		}

		// Parse SSE format
		if strings.HasPrefix(line, "event:") {
			// New source from event type
			currentSource = strings.TrimSpace(strings.TrimPrefix(line, "event:"))
		} else if strings.HasPrefix(line, "data:") {
			data := strings.TrimSpace(strings.TrimPrefix(line, "data:"))
			if callback != nil && data != "" {
				callback(currentSource, data)
			}
		}
	}
}

// ============================================================================
// Utility Functions
// ============================================================================

// SDKVersion is the version of this SDK.
const SDKVersion = "4.3.4"

// HmacSign computes an HMAC-SHA256 signature for the given message using the secret key.
// Returns the signature as a lowercase hex string.
func HmacSign(secretKey, message string) string {
	mac := hmac.New(sha256.New, []byte(secretKey))
	mac.Write([]byte(message))
	return hex.EncodeToString(mac.Sum(nil))
}

// HealthCheck checks if the API is reachable and responding.
// Returns true if healthy, false otherwise.
func HealthCheck() bool {
	client := &http.Client{Timeout: 10 * time.Second}
	resp, err := client.Get(APIBase + "/health")
	if err != nil {
		return false
	}
	defer resp.Body.Close()
	return resp.StatusCode == 200
}

// Version returns the SDK version string.
func Version() string {
	return SDKVersion
}

// lastError holds the most recent error message for thread-safe access.
var lastError string

// SetLastError sets the last error message (internal use).
func SetLastError(msg string) {
	lastError = msg
}

// LastError returns the most recent error message from the SDK.
func LastError() string {
	return lastError
}

// ============================================================================
// CLI Implementation
// ============================================================================

// Exit codes
const (
	ExitSuccess        = 0
	ExitGeneralError   = 1
	ExitInvalidArgs    = 2
	ExitAuthError      = 3
	ExitAPIError       = 4
	ExitTimeout        = 5
)

// CLIOptions holds parsed CLI arguments
type CLIOptions struct {
	// Global options
	Shell      string
	Env        []string
	Files      []string
	FilePaths  []string
	Artifacts  bool
	OutputDir  string
	PublicKey  string
	SecretKey  string
	Network    string
	VCPU       int
	Yes        bool
	Help       bool

	// Command
	Command    string
	SubCommand string
	Args       []string
}

// printUsage prints the main help message
func printUsage() {
	fmt.Fprintf(os.Stderr, `unsandbox - Execute code securely in sandboxed containers

Usage:
  un [options] <source_file>        Execute code file
  un [options] -s LANG 'code'       Execute inline code
  un session [options]              Interactive session
  un service [options]              Manage services
  un snapshot [options]             Manage snapshots
  un key                            Check API key
  un languages [--json]             List available languages

Global Options:
  -s, --shell LANG       Language for inline code
  -e, --env KEY=VAL      Set environment variable (can repeat)
  -f, --file FILE        Add input file to /tmp/
  -F, --file-path FILE   Add input file with path preserved
  -a, --artifacts        Return compiled artifacts
  -o, --output DIR       Output directory for artifacts
  -p, --public-key KEY   API public key
  -k, --secret-key KEY   API secret key
  -n, --network MODE     Network: zerotrust (default) or semitrusted
  -v, --vcpu N           vCPU count (1-8)
  -y, --yes              Skip confirmation prompts
  -h, --help             Show help

Examples:
  un script.py                      Execute Python script
  un -s bash 'echo hello'           Inline bash command
  un -e DEBUG=1 script.py           With environment variable
  un -n semitrusted crawler.py      With network access
  un session --tmux                 Persistent interactive session
  un service --list                 List all services
  un languages                      List available languages
  un languages --json               List languages as JSON
`)
}

// printSessionUsage prints session subcommand help
func printSessionUsage() {
	fmt.Fprintf(os.Stderr, `un session - Interactive session management

Usage:
  un session [options]              Start new interactive session
  un session --list                 List active sessions
  un session --attach ID            Reconnect to session
  un session --kill ID              Terminate session

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
  -n, --network MODE     Network: zerotrust or semitrusted
  -v, --vcpu N           vCPU count (1-8)

Examples:
  un session                        Interactive bash
  un session --shell python3        Python REPL
  un session --tmux                 Persistent session
  un session --list                 List sessions
  un session --attach abc123        Reconnect to session
`)
}

// printServiceUsage prints service subcommand help
func printServiceUsage() {
	fmt.Fprintf(os.Stderr, `un service - Service management

Usage:
  un service --list                 List all services
  un service --name NAME --ports PORTS --bootstrap CMD
  un service --info ID              Get service details
  un service --logs ID              Get bootstrap logs
  un service env [status|set|export|delete] 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
  --snapshot ID          Create snapshot

Service Environment Vault:
  un service env status ID          Show vault status
  un service env set ID             Set from --env-file or stdin
  un service env export ID          Export to stdout
  un service env delete ID          Delete vault

Examples:
  un service --list
  un service --name web --ports 80 --bootstrap "python -m http.server 80"
  un service --info abc123
  un service --execute abc123 'ls -la'
  un service env set abc123 --env-file .env
`)
}

// printSnapshotUsage prints snapshot subcommand help
func printSnapshotUsage() {
	fmt.Fprintf(os.Stderr, `un snapshot - Snapshot management

Usage:
  un snapshot --list                List all snapshots
  un snapshot --info ID             Get details
  un snapshot --delete ID           Delete snapshot
  un snapshot --clone ID            Clone to new session/service

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 service
  --shell SHELL          Shell for cloned session
  --ports PORTS          Ports for cloned service

Examples:
  un snapshot --list
  un snapshot --info abc123
  un snapshot --delete abc123
  un snapshot --clone abc123 --type service --name myapp --ports 80
`)
}

// printImageUsage prints image subcommand help
func printImageUsage() {
	fmt.Fprintf(os.Stderr, `un image - Image management

Usage:
  un image --list                 List all images
  un image --info ID              Get details
  un image --delete ID            Delete image
  un image --publish ID           Publish image from service/snapshot

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:
  un image --list
  un image --info abc123
  un image --publish svc123 --source-type service --name myimage
  un image --spawn img123 --name myservice --ports 80,443
  un image --visibility img123 public
`)
}

// cliError prints error to stderr and returns exit code
func cliError(msg string, code int) int {
	fmt.Fprintf(os.Stderr, "Error: %s\n", msg)
	return code
}

// parsePorts parses comma-separated port numbers
func parsePorts(s string) ([]int, error) {
	if s == "" {
		return nil, nil
	}
	parts := strings.Split(s, ",")
	ports := make([]int, 0, len(parts))
	for _, p := range parts {
		p = strings.TrimSpace(p)
		if p == "" {
			continue
		}
		port, err := strconv.Atoi(p)
		if err != nil {
			return nil, fmt.Errorf("invalid port: %s", p)
		}
		ports = append(ports, port)
	}
	return ports, nil
}

// formatList formats a list of items for display
func formatList(items []map[string]interface{}, fields []string) {
	if len(items) == 0 {
		fmt.Println("No items found.")
		return
	}

	// Print header
	header := strings.Join(fields, "\t")
	fmt.Println(header)

	// Print items
	for _, item := range items {
		values := make([]string, len(fields))
		for i, field := range fields {
			if val, ok := item[field]; ok {
				values[i] = fmt.Sprintf("%v", val)
			} else {
				values[i] = "-"
			}
		}
		fmt.Println(strings.Join(values, "\t"))
	}
}

// readFileContents reads file contents for bootstrapping
func readFileContents(path string) (string, error) {
	data, err := os.ReadFile(path)
	if err != nil {
		return "", err
	}
	return string(data), nil
}

// readEnvFile reads environment variables from a .env file
func readEnvFile(path string) (map[string]string, error) {
	data, err := os.ReadFile(path)
	if err != nil {
		return nil, err
	}

	env := make(map[string]string)
	lines := strings.Split(string(data), "\n")
	for _, line := range lines {
		line = strings.TrimSpace(line)
		if line == "" || strings.HasPrefix(line, "#") {
			continue
		}
		parts := strings.SplitN(line, "=", 2)
		if len(parts) == 2 {
			key := strings.TrimSpace(parts[0])
			value := strings.TrimSpace(parts[1])
			// Remove surrounding quotes if present
			if len(value) >= 2 && (value[0] == '"' && value[len(value)-1] == '"' ||
				value[0] == '\'' && value[len(value)-1] == '\'') {
				value = value[1 : len(value)-1]
			}
			env[key] = value
		}
	}
	return env, nil
}

// runExecute handles the execute command (default)
func runExecute(creds *Credentials, opts *CLIOptions) int {
	var language, code string

	if opts.Shell != "" {
		// Inline code mode: -s LANG 'code'
		language = opts.Shell
		if len(opts.Args) == 0 {
			return cliError("no code provided for inline execution", ExitInvalidArgs)
		}
		code = opts.Args[0]
	} else {
		// File mode
		if len(opts.Args) == 0 {
			return cliError("no source file provided", ExitInvalidArgs)
		}
		filename := opts.Args[0]
		language = DetectLanguage(filename)
		if language == "" {
			return cliError(fmt.Sprintf("cannot detect language for: %s", filename), ExitInvalidArgs)
		}
		fileContent, err := readFileContents(filename)
		if err != nil {
			return cliError(fmt.Sprintf("cannot read file: %s", err), ExitGeneralError)
		}
		code = fileContent
	}

	// Build request data
	data := map[string]interface{}{
		"language": language,
		"code":     code,
	}

	if opts.Network != "" {
		data["network_mode"] = opts.Network
	}
	if opts.VCPU > 0 {
		data["vcpu"] = opts.VCPU
	}
	if len(opts.Env) > 0 {
		envMap := make(map[string]string)
		for _, e := range opts.Env {
			parts := strings.SplitN(e, "=", 2)
			if len(parts) == 2 {
				envMap[parts[0]] = parts[1]
			}
		}
		data["env"] = envMap
	}

	// Execute
	result, err := makeRequest("POST", "/execute", creds, data)
	if err != nil {
		return cliError(err.Error(), ExitAPIError)
	}

	// Poll if job is pending/running
	if jobID, ok := result["job_id"].(string); ok {
		if status, ok := result["status"].(string); ok && (status == "pending" || status == "running") {
			result, err = WaitForJob(creds, jobID)
			if err != nil {
				return cliError(err.Error(), ExitAPIError)
			}
		}
	}

	// Print output
	if stdout, ok := result["stdout"].(string); ok && stdout != "" {
		fmt.Print(stdout)
	}
	if stderr, ok := result["stderr"].(string); ok && stderr != "" {
		fmt.Fprint(os.Stderr, stderr)
	}

	// Print summary
	fmt.Println("---")
	if exitCode, ok := result["exit_code"].(float64); ok {
		fmt.Printf("Exit code: %d\n", int(exitCode))
	}
	if execTime, ok := result["execution_time_ms"].(float64); ok {
		fmt.Printf("Execution time: %dms\n", int(execTime))
	}

	if exitCode, ok := result["exit_code"].(float64); ok && exitCode != 0 {
		return int(exitCode)
	}
	return ExitSuccess
}

// runSession handles the session command
func runSession(creds *Credentials, args []string, opts *CLIOptions) int {
	fs := &sessionFlags{}
	parseSessionFlags(args, fs)

	if fs.help {
		printSessionUsage()
		return ExitSuccess
	}

	// List sessions
	if fs.list {
		sessions, err := ListSessions(creds)
		if err != nil {
			return cliError(err.Error(), ExitAPIError)
		}
		formatList(sessions, []string{"session_id", "status", "shell", "created_at"})
		return ExitSuccess
	}

	// Attach to session
	if fs.attach != "" {
		session, err := GetSession(creds, fs.attach)
		if err != nil {
			return cliError(err.Error(), ExitAPIError)
		}
		// Print connection info
		fmt.Printf("Session: %s\n", fs.attach)
		if wsURL, ok := session["websocket_url"].(string); ok {
			fmt.Printf("Connect via: %s\n", wsURL)
		}
		return ExitSuccess
	}

	// Kill session
	if fs.kill != "" {
		_, err := DeleteSession(creds, fs.kill)
		if err != nil {
			return cliError(err.Error(), ExitAPIError)
		}
		fmt.Printf("Session %s terminated\n", fs.kill)
		return ExitSuccess
	}

	// Freeze session
	if fs.freeze != "" {
		_, err := FreezeSession(creds, fs.freeze)
		if err != nil {
			return cliError(err.Error(), ExitAPIError)
		}
		fmt.Printf("Session %s frozen\n", fs.freeze)
		return ExitSuccess
	}

	// Unfreeze session
	if fs.unfreeze != "" {
		_, err := UnfreezeSession(creds, fs.unfreeze)
		if err != nil {
			return cliError(err.Error(), ExitAPIError)
		}
		fmt.Printf("Session %s unfrozen\n", fs.unfreeze)
		return ExitSuccess
	}

	// Boost session
	if fs.boost != "" {
		vcpu := opts.VCPU
		if vcpu == 0 {
			vcpu = 2
		}
		_, err := BoostSession(creds, fs.boost, vcpu)
		if err != nil {
			return cliError(err.Error(), ExitAPIError)
		}
		fmt.Printf("Session %s boosted to %d vCPUs\n", fs.boost, vcpu)
		return ExitSuccess
	}

	// Unboost session
	if fs.unboost != "" {
		_, err := UnboostSession(creds, fs.unboost)
		if err != nil {
			return cliError(err.Error(), ExitAPIError)
		}
		fmt.Printf("Session %s unboost\n", fs.unboost)
		return ExitSuccess
	}

	// Snapshot session
	if fs.snapshot != "" {
		snapshotID, err := SessionSnapshot(creds, fs.snapshot, fs.snapshotName, fs.hot)
		if err != nil {
			return cliError(err.Error(), ExitAPIError)
		}
		fmt.Printf("Snapshot created: %s\n", snapshotID)
		return ExitSuccess
	}

	// Create new session
	sessionOpts := &SessionOptions{}
	if fs.shell != "" {
		sessionOpts.Shell = fs.shell
	}
	if opts.Network != "" {
		sessionOpts.NetworkMode = opts.Network
	}
	if opts.VCPU > 0 {
		sessionOpts.VCPU = opts.VCPU
	}
	if fs.tmux {
		sessionOpts.Multiplexer = "tmux"
	} else if fs.screen {
		sessionOpts.Multiplexer = "screen"
	}

	session, err := CreateSession(creds, sessionOpts)
	if err != nil {
		return cliError(err.Error(), ExitAPIError)
	}

	fmt.Printf("Session created: %v\n", session["session_id"])
	if wsURL, ok := session["websocket_url"].(string); ok {
		fmt.Printf("Connect via: %s\n", wsURL)
	}
	return ExitSuccess
}

type sessionFlags struct {
	shell        string
	list         bool
	attach       string
	kill         string
	freeze       string
	unfreeze     string
	boost        string
	unboost      string
	tmux         bool
	screen       bool
	snapshot     string
	snapshotName string
	hot          bool
	audit        bool
	help         bool
}

func parseSessionFlags(args []string, fs *sessionFlags) {
	for i := 0; i < len(args); i++ {
		arg := args[i]
		switch arg {
		case "--shell":
			if i+1 < len(args) {
				fs.shell = args[i+1]
				i++
			}
		case "-l", "--list":
			fs.list = true
		case "--attach":
			if i+1 < len(args) {
				fs.attach = args[i+1]
				i++
			}
		case "--kill":
			if i+1 < len(args) {
				fs.kill = args[i+1]
				i++
			}
		case "--freeze":
			if i+1 < len(args) {
				fs.freeze = args[i+1]
				i++
			}
		case "--unfreeze":
			if i+1 < len(args) {
				fs.unfreeze = args[i+1]
				i++
			}
		case "--boost":
			if i+1 < len(args) {
				fs.boost = args[i+1]
				i++
			}
		case "--unboost":
			if i+1 < len(args) {
				fs.unboost = args[i+1]
				i++
			}
		case "--tmux":
			fs.tmux = true
		case "--screen":
			fs.screen = true
		case "--snapshot":
			if i+1 < len(args) {
				fs.snapshot = args[i+1]
				i++
			}
		case "--snapshot-name":
			if i+1 < len(args) {
				fs.snapshotName = args[i+1]
				i++
			}
		case "--hot":
			fs.hot = true
		case "--audit":
			fs.audit = true
		case "-h", "--help":
			fs.help = true
		}
	}
}

// runService handles the service command
func runService(creds *Credentials, args []string, opts *CLIOptions) int {
	fs := &serviceFlags{}
	parseServiceFlags(args, fs)

	if fs.help {
		printServiceUsage()
		return ExitSuccess
	}

	// Handle env subcommand
	if fs.envCmd != "" {
		return runServiceEnv(creds, fs, opts)
	}

	// List services
	if fs.list {
		services, err := ListServices(creds)
		if err != nil {
			return cliError(err.Error(), ExitAPIError)
		}
		formatList(services, []string{"service_id", "name", "status", "created_at"})
		return ExitSuccess
	}

	// Get service info
	if fs.info != "" {
		service, err := GetService(creds, fs.info)
		if err != nil {
			return cliError(err.Error(), ExitAPIError)
		}
		jsonOut, _ := json.MarshalIndent(service, "", "  ")
		fmt.Println(string(jsonOut))
		return ExitSuccess
	}

	// Get service logs
	if fs.logs != "" {
		logs, err := GetServiceLogs(creds, fs.logs, true)
		if err != nil {
			return cliError(err.Error(), ExitAPIError)
		}
		if content, ok := logs["logs"].(string); ok {
			fmt.Print(content)
		}
		return ExitSuccess
	}

	// Get service tail logs
	if fs.tail != "" {
		logs, err := GetServiceLogs(creds, fs.tail, false)
		if err != nil {
			return cliError(err.Error(), ExitAPIError)
		}
		if content, ok := logs["logs"].(string); ok {
			fmt.Print(content)
		}
		return ExitSuccess
	}

	// Freeze service
	if fs.freeze != "" {
		_, err := FreezeService(creds, fs.freeze)
		if err != nil {
			return cliError(err.Error(), ExitAPIError)
		}
		fmt.Printf("Service %s frozen\n", fs.freeze)
		return ExitSuccess
	}

	// Unfreeze service
	if fs.unfreeze != "" {
		_, err := UnfreezeService(creds, fs.unfreeze)
		if err != nil {
			return cliError(err.Error(), ExitAPIError)
		}
		fmt.Printf("Service %s unfrozen\n", fs.unfreeze)
		return ExitSuccess
	}

	// Destroy service
	if fs.destroy != "" {
		_, err := DeleteService(creds, fs.destroy)
		if err != nil {
			return cliError(err.Error(), ExitAPIError)
		}
		fmt.Printf("Service %s destroyed\n", fs.destroy)
		return ExitSuccess
	}

	// Lock service
	if fs.lock != "" {
		_, err := LockService(creds, fs.lock)
		if err != nil {
			return cliError(err.Error(), ExitAPIError)
		}
		fmt.Printf("Service %s locked\n", fs.lock)
		return ExitSuccess
	}

	// Unlock service
	if fs.unlock != "" {
		_, err := UnlockService(creds, fs.unlock)
		if err != nil {
			return cliError(err.Error(), ExitAPIError)
		}
		fmt.Printf("Service %s unlocked\n", fs.unlock)
		return ExitSuccess
	}

	// Resize service
	if fs.resize != "" {
		if opts.VCPU == 0 {
			return cliError("--vcpu required for resize", ExitInvalidArgs)
		}
		_, err := UpdateService(creds, fs.resize, &ServiceUpdateOptions{VCPU: opts.VCPU})
		if err != nil {
			return cliError(err.Error(), ExitAPIError)
		}
		fmt.Printf("Service %s resized to %d vCPUs\n", fs.resize, opts.VCPU)
		return ExitSuccess
	}

	// Redeploy service
	if fs.redeploy != "" {
		_, err := RedeployService(creds, fs.redeploy, fs.bootstrap)
		if err != nil {
			return cliError(err.Error(), ExitAPIError)
		}
		fmt.Printf("Service %s redeployed\n", fs.redeploy)
		return ExitSuccess
	}

	// Execute in service
	if fs.execute != "" && fs.executeCmd != "" {
		result, err := ExecuteInService(creds, fs.execute, fs.executeCmd)
		if err != nil {
			return cliError(err.Error(), ExitAPIError)
		}
		if stdout, ok := result["stdout"].(string); ok {
			fmt.Print(stdout)
		}
		if stderr, ok := result["stderr"].(string); ok {
			fmt.Fprint(os.Stderr, stderr)
		}
		return ExitSuccess
	}

	// Snapshot service
	if fs.snapshot != "" {
		snapshotID, err := ServiceSnapshot(creds, fs.snapshot, fs.snapshotName, fs.hot)
		if err != nil {
			return cliError(err.Error(), ExitAPIError)
		}
		fmt.Printf("Snapshot created: %s\n", snapshotID)
		return ExitSuccess
	}

	// Create new service
	if fs.name != "" {
		if fs.ports == "" {
			return cliError("--ports required for new service", ExitInvalidArgs)
		}
		ports, err := parsePorts(fs.ports)
		if err != nil {
			return cliError(err.Error(), ExitInvalidArgs)
		}

		bootstrap := fs.bootstrap
		if fs.bootstrapFile != "" {
			content, err := readFileContents(fs.bootstrapFile)
			if err != nil {
				return cliError(fmt.Sprintf("cannot read bootstrap file: %s", err), ExitGeneralError)
			}
			bootstrap = content
		}

		serviceOpts := &ServiceOptions{}
		if opts.Network != "" {
			serviceOpts.NetworkMode = opts.Network
		}
		if opts.VCPU > 0 {
			serviceOpts.VCPU = opts.VCPU
		}

		service, err := CreateService(creds, fs.name, ports, bootstrap, serviceOpts)
		if err != nil {
			return cliError(err.Error(), ExitAPIError)
		}

		fmt.Printf("Service created: %v\n", service["service_id"])
		if url, ok := service["url"].(string); ok {
			fmt.Printf("URL: %s\n", url)
		}
		return ExitSuccess
	}

	printServiceUsage()
	return ExitInvalidArgs
}

type serviceFlags struct {
	name          string
	ports         string
	domains       string
	serviceType   string
	bootstrap     string
	bootstrapFile string
	envFile       string
	list          bool
	info          string
	logs          string
	tail          string
	freeze        string
	unfreeze      string
	destroy       string
	lock          string
	unlock        string
	resize        string
	redeploy      string
	execute       string
	executeCmd    string
	snapshot      string
	snapshotName  string
	hot           bool
	envCmd        string
	envID         string
	help          bool
}

func parseServiceFlags(args []string, fs *serviceFlags) {
	for i := 0; i < len(args); i++ {
		arg := args[i]
		switch arg {
		case "--name":
			if i+1 < len(args) {
				fs.name = args[i+1]
				i++
			}
		case "--ports":
			if i+1 < len(args) {
				fs.ports = args[i+1]
				i++
			}
		case "--domains":
			if i+1 < len(args) {
				fs.domains = args[i+1]
				i++
			}
		case "--type":
			if i+1 < len(args) {
				fs.serviceType = args[i+1]
				i++
			}
		case "--bootstrap":
			if i+1 < len(args) {
				fs.bootstrap = args[i+1]
				i++
			}
		case "--bootstrap-file":
			if i+1 < len(args) {
				fs.bootstrapFile = args[i+1]
				i++
			}
		case "--env-file":
			if i+1 < len(args) {
				fs.envFile = args[i+1]
				i++
			}
		case "-l", "--list":
			fs.list = true
		case "--info":
			if i+1 < len(args) {
				fs.info = args[i+1]
				i++
			}
		case "--logs":
			if i+1 < len(args) {
				fs.logs = args[i+1]
				i++
			}
		case "--tail":
			if i+1 < len(args) {
				fs.tail = args[i+1]
				i++
			}
		case "--freeze":
			if i+1 < len(args) {
				fs.freeze = args[i+1]
				i++
			}
		case "--unfreeze":
			if i+1 < len(args) {
				fs.unfreeze = args[i+1]
				i++
			}
		case "--destroy":
			if i+1 < len(args) {
				fs.destroy = args[i+1]
				i++
			}
		case "--lock":
			if i+1 < len(args) {
				fs.lock = args[i+1]
				i++
			}
		case "--unlock":
			if i+1 < len(args) {
				fs.unlock = args[i+1]
				i++
			}
		case "--resize":
			if i+1 < len(args) {
				fs.resize = args[i+1]
				i++
			}
		case "--redeploy":
			if i+1 < len(args) {
				fs.redeploy = args[i+1]
				i++
			}
		case "--execute":
			if i+1 < len(args) {
				fs.execute = args[i+1]
				i++
				// Next arg is the command
				if i+1 < len(args) && !strings.HasPrefix(args[i+1], "-") {
					fs.executeCmd = args[i+1]
					i++
				}
			}
		case "--snapshot":
			if i+1 < len(args) {
				fs.snapshot = args[i+1]
				i++
			}
		case "--snapshot-name":
			if i+1 < len(args) {
				fs.snapshotName = args[i+1]
				i++
			}
		case "--hot":
			fs.hot = true
		case "env":
			// Handle env subcommand
			if i+1 < len(args) {
				fs.envCmd = args[i+1]
				i++
				if i+1 < len(args) {
					fs.envID = args[i+1]
					i++
				}
			}
		case "-h", "--help":
			fs.help = true
		}
	}
}

// runServiceEnv handles service env subcommands
func runServiceEnv(creds *Credentials, fs *serviceFlags, opts *CLIOptions) int {
	if fs.envID == "" {
		return cliError("service ID required for env command", ExitInvalidArgs)
	}

	switch fs.envCmd {
	case "status":
		env, err := GetServiceEnv(creds, fs.envID)
		if err != nil {
			return cliError(err.Error(), ExitAPIError)
		}
		jsonOut, _ := json.MarshalIndent(env, "", "  ")
		fmt.Println(string(jsonOut))

	case "set":
		var env map[string]string
		var err error
		if fs.envFile != "" {
			env, err = readEnvFile(fs.envFile)
			if err != nil {
				return cliError(fmt.Sprintf("cannot read env file: %s", err), ExitGeneralError)
			}
		} else {
			// Read from stdin
			data, err := io.ReadAll(os.Stdin)
			if err != nil {
				return cliError(fmt.Sprintf("cannot read stdin: %s", err), ExitGeneralError)
			}
			env = make(map[string]string)
			lines := strings.Split(string(data), "\n")
			for _, line := range lines {
				line = strings.TrimSpace(line)
				if line == "" || strings.HasPrefix(line, "#") {
					continue
				}
				parts := strings.SplitN(line, "=", 2)
				if len(parts) == 2 {
					env[parts[0]] = parts[1]
				}
			}
		}
		_, err = SetServiceEnv(creds, fs.envID, env)
		if err != nil {
			return cliError(err.Error(), ExitAPIError)
		}
		fmt.Println("Environment variables set")

	case "export":
		env, err := ExportServiceEnv(creds, fs.envID)
		if err != nil {
			return cliError(err.Error(), ExitAPIError)
		}
		if content, ok := env["content"].(string); ok {
			fmt.Print(content)
		}

	case "delete":
		_, err := DeleteServiceEnv(creds, fs.envID, nil)
		if err != nil {
			return cliError(err.Error(), ExitAPIError)
		}
		fmt.Println("Environment vault deleted")

	default:
		return cliError(fmt.Sprintf("unknown env command: %s", fs.envCmd), ExitInvalidArgs)
	}

	return ExitSuccess
}

// runSnapshot handles the snapshot command
func runSnapshot(creds *Credentials, args []string, opts *CLIOptions) int {
	fs := &snapshotFlags{}
	parseSnapshotFlags(args, fs)

	if fs.help {
		printSnapshotUsage()
		return ExitSuccess
	}

	// List snapshots
	if fs.list {
		snapshots, err := ListSnapshots(creds)
		if err != nil {
			return cliError(err.Error(), ExitAPIError)
		}
		formatList(snapshots, []string{"snapshot_id", "name", "type", "created_at"})
		return ExitSuccess
	}

	// Get snapshot info
	if fs.info != "" {
		snapshot, err := GetSnapshot(creds, fs.info)
		if err != nil {
			return cliError(err.Error(), ExitAPIError)
		}
		jsonOut, _ := json.MarshalIndent(snapshot, "", "  ")
		fmt.Println(string(jsonOut))
		return ExitSuccess
	}

	// Delete snapshot
	if fs.deleteID != "" {
		_, err := DeleteSnapshot(creds, fs.deleteID)
		if err != nil {
			return cliError(err.Error(), ExitAPIError)
		}
		fmt.Printf("Snapshot %s deleted\n", fs.deleteID)
		return ExitSuccess
	}

	// Lock snapshot
	if fs.lock != "" {
		_, err := LockSnapshot(creds, fs.lock)
		if err != nil {
			return cliError(err.Error(), ExitAPIError)
		}
		fmt.Printf("Snapshot %s locked\n", fs.lock)
		return ExitSuccess
	}

	// Unlock snapshot
	if fs.unlock != "" {
		_, err := UnlockSnapshot(creds, fs.unlock)
		if err != nil {
			return cliError(err.Error(), ExitAPIError)
		}
		fmt.Printf("Snapshot %s unlocked\n", fs.unlock)
		return ExitSuccess
	}

	// Clone snapshot
	if fs.clone != "" {
		cloneOpts := &CloneSnapshotOptions{}
		if fs.name != "" {
			cloneOpts.Name = fs.name
		}
		if fs.shell != "" {
			cloneOpts.Shell = fs.shell
		}
		if fs.ports != "" {
			ports, err := parsePorts(fs.ports)
			if err != nil {
				return cliError(err.Error(), ExitInvalidArgs)
			}
			cloneOpts.Ports = ports
		}

		cloneType := fs.cloneType
		if cloneType == "" {
			cloneType = "session"
		}

		result, err := CloneSnapshot(creds, fs.clone, cloneType, cloneOpts)
		if err != nil {
			return cliError(err.Error(), ExitAPIError)
		}
		jsonOut, _ := json.MarshalIndent(result, "", "  ")
		fmt.Println(string(jsonOut))
		return ExitSuccess
	}

	printSnapshotUsage()
	return ExitInvalidArgs
}

type snapshotFlags struct {
	list      bool
	info      string
	deleteID  string
	lock      string
	unlock    string
	clone     string
	cloneType string
	name      string
	shell     string
	ports     string
	help      bool
}

func parseSnapshotFlags(args []string, fs *snapshotFlags) {
	for i := 0; i < len(args); i++ {
		arg := args[i]
		switch arg {
		case "-l", "--list":
			fs.list = true
		case "--info":
			if i+1 < len(args) {
				fs.info = args[i+1]
				i++
			}
		case "--delete":
			if i+1 < len(args) {
				fs.deleteID = args[i+1]
				i++
			}
		case "--lock":
			if i+1 < len(args) {
				fs.lock = args[i+1]
				i++
			}
		case "--unlock":
			if i+1 < len(args) {
				fs.unlock = args[i+1]
				i++
			}
		case "--clone":
			if i+1 < len(args) {
				fs.clone = args[i+1]
				i++
			}
		case "--type":
			if i+1 < len(args) {
				fs.cloneType = args[i+1]
				i++
			}
		case "--name":
			if i+1 < len(args) {
				fs.name = args[i+1]
				i++
			}
		case "--shell":
			if i+1 < len(args) {
				fs.shell = args[i+1]
				i++
			}
		case "--ports":
			if i+1 < len(args) {
				fs.ports = args[i+1]
				i++
			}
		case "-h", "--help":
			fs.help = true
		}
	}
}

type imageFlags struct {
	list       bool
	info       string
	deleteID   string
	lock       string
	unlock     string
	publish    string
	sourceType string
	visibility string
	visMode    string
	spawn      string
	clone      string
	name       string
	ports      string
	help       bool
}

func parseImageFlags(args []string, fs *imageFlags) {
	for i := 0; i < len(args); i++ {
		arg := args[i]
		switch arg {
		case "-l", "--list":
			fs.list = true
		case "--info":
			if i+1 < len(args) {
				fs.info = args[i+1]
				i++
			}
		case "--delete":
			if i+1 < len(args) {
				fs.deleteID = args[i+1]
				i++
			}
		case "--lock":
			if i+1 < len(args) {
				fs.lock = args[i+1]
				i++
			}
		case "--unlock":
			if i+1 < len(args) {
				fs.unlock = args[i+1]
				i++
			}
		case "--publish":
			if i+1 < len(args) {
				fs.publish = args[i+1]
				i++
			}
		case "--source-type":
			if i+1 < len(args) {
				fs.sourceType = args[i+1]
				i++
			}
		case "--visibility":
			if i+1 < len(args) {
				fs.visibility = args[i+1]
				i++
			}
			if i+1 < len(args) && !strings.HasPrefix(args[i+1], "-") {
				fs.visMode = args[i+1]
				i++
			}
		case "--spawn":
			if i+1 < len(args) {
				fs.spawn = args[i+1]
				i++
			}
		case "--clone":
			if i+1 < len(args) {
				fs.clone = args[i+1]
				i++
			}
		case "--name":
			if i+1 < len(args) {
				fs.name = args[i+1]
				i++
			}
		case "--ports":
			if i+1 < len(args) {
				fs.ports = args[i+1]
				i++
			}
		case "-h", "--help":
			fs.help = true
		}
	}
}

func runImage(creds *Credentials, args []string, opts *CLIOptions) int {
	fs := &imageFlags{}
	parseImageFlags(args, fs)

	if fs.help {
		printImageUsage()
		return ExitSuccess
	}

	// List images
	if fs.list {
		images, err := ListImages(creds, "")
		if err != nil {
			return cliError(err.Error(), ExitAPIError)
		}
		formatList(images, []string{"image_id", "name", "visibility", "source_type", "created_at"})
		return ExitSuccess
	}

	// Get image info
	if fs.info != "" {
		image, err := GetImage(creds, fs.info)
		if err != nil {
			return cliError(err.Error(), ExitAPIError)
		}
		jsonOut, _ := json.MarshalIndent(image, "", "  ")
		fmt.Println(string(jsonOut))
		return ExitSuccess
	}

	// Delete image
	if fs.deleteID != "" {
		_, err := DeleteImage(creds, fs.deleteID)
		if err != nil {
			return cliError(err.Error(), ExitAPIError)
		}
		fmt.Printf("Image %s deleted\n", fs.deleteID)
		return ExitSuccess
	}

	// Lock image
	if fs.lock != "" {
		_, err := LockImage(creds, fs.lock)
		if err != nil {
			return cliError(err.Error(), ExitAPIError)
		}
		fmt.Printf("Image %s locked\n", fs.lock)
		return ExitSuccess
	}

	// Unlock image
	if fs.unlock != "" {
		_, err := UnlockImage(creds, fs.unlock)
		if err != nil {
			return cliError(err.Error(), ExitAPIError)
		}
		fmt.Printf("Image %s unlocked\n", fs.unlock)
		return ExitSuccess
	}

	// Publish image
	if fs.publish != "" {
		if fs.sourceType == "" {
			return cliError("--source-type required for --publish", ExitInvalidArgs)
		}
		pubOpts := &ImagePublishOptions{}
		if fs.name != "" {
			pubOpts.Name = fs.name
		}
		result, err := ImagePublish(creds, fs.sourceType, fs.publish, pubOpts)
		if err != nil {
			return cliError(err.Error(), ExitAPIError)
		}
		imageID := ""
		if id, ok := result["image_id"].(string); ok {
			imageID = id
		} else if id, ok := result["id"].(string); ok {
			imageID = id
		}
		fmt.Printf("Image published: %s\n", imageID)
		return ExitSuccess
	}

	// Set visibility
	if fs.visibility != "" && fs.visMode != "" {
		if fs.visMode != "private" && fs.visMode != "unlisted" && fs.visMode != "public" {
			return cliError("visibility must be private, unlisted, or public", ExitInvalidArgs)
		}
		_, err := SetImageVisibility(creds, fs.visibility, fs.visMode)
		if err != nil {
			return cliError(err.Error(), ExitAPIError)
		}
		fmt.Printf("Image %s visibility set to %s\n", fs.visibility, fs.visMode)
		return ExitSuccess
	}

	// Spawn from image
	if fs.spawn != "" {
		if fs.name == "" {
			return cliError("--name required for --spawn", ExitInvalidArgs)
		}
		spawnOpts := &SpawnFromImageOptions{
			Name: fs.name,
		}
		if fs.ports != "" {
			ports, err := parsePorts(fs.ports)
			if err != nil {
				return cliError(err.Error(), ExitInvalidArgs)
			}
			spawnOpts.Ports = ports
		}
		result, err := SpawnFromImage(creds, fs.spawn, spawnOpts)
		if err != nil {
			return cliError(err.Error(), ExitAPIError)
		}
		serviceID := ""
		if id, ok := result["service_id"].(string); ok {
			serviceID = id
		} else if id, ok := result["id"].(string); ok {
			serviceID = id
		}
		fmt.Printf("Service spawned: %s\n", serviceID)
		return ExitSuccess
	}

	// Clone image
	if fs.clone != "" {
		cloneOpts := &CloneImageOptions{}
		if fs.name != "" {
			cloneOpts.Name = fs.name
		}
		result, err := CloneImage(creds, fs.clone, cloneOpts)
		if err != nil {
			return cliError(err.Error(), ExitAPIError)
		}
		imageID := ""
		if id, ok := result["image_id"].(string); ok {
			imageID = id
		} else if id, ok := result["id"].(string); ok {
			imageID = id
		}
		fmt.Printf("Image cloned: %s\n", imageID)
		return ExitSuccess
	}

	printImageUsage()
	return ExitInvalidArgs
}

// runKey handles the key command
func runKey(creds *Credentials) int {
	result, err := ValidateKeys(creds)
	if err != nil {
		return cliError(err.Error(), ExitAuthError)
	}

	fmt.Println("API key is valid")
	if account, ok := result["account"].(map[string]interface{}); ok {
		jsonOut, _ := json.MarshalIndent(account, "", "  ")
		fmt.Println(string(jsonOut))
	}
	return ExitSuccess
}

// runLanguages handles the languages command
func runLanguages(creds *Credentials, args []string) int {
	// Check for --json flag
	jsonOutput := false
	for _, arg := range args {
		if arg == "--json" {
			jsonOutput = true
		}
	}

	languages, err := GetLanguages(creds)
	if err != nil {
		return cliError(err.Error(), ExitAPIError)
	}

	if jsonOutput {
		// Output as JSON array
		jsonOut, _ := json.Marshal(languages)
		fmt.Println(string(jsonOut))
	} else {
		// Output one language per line (pipe-friendly)
		for _, lang := range languages {
			fmt.Println(lang)
		}
	}
	return ExitSuccess
}

// parseGlobalFlags parses global CLI options
func parseGlobalFlags(args []string) (*CLIOptions, []string) {
	opts := &CLIOptions{}
	remaining := []string{}

	for i := 0; i < len(args); i++ {
		arg := args[i]
		switch {
		case arg == "-s" || arg == "--shell":
			if i+1 < len(args) {
				opts.Shell = args[i+1]
				i++
			}
		case arg == "-e" || arg == "--env":
			if i+1 < len(args) {
				opts.Env = append(opts.Env, args[i+1])
				i++
			}
		case arg == "-f" || arg == "--file":
			if i+1 < len(args) {
				opts.Files = append(opts.Files, args[i+1])
				i++
			}
		case arg == "-F" || arg == "--file-path":
			if i+1 < len(args) {
				opts.FilePaths = append(opts.FilePaths, args[i+1])
				i++
			}
		case arg == "-a" || arg == "--artifacts":
			opts.Artifacts = true
		case arg == "-o" || arg == "--output":
			if i+1 < len(args) {
				opts.OutputDir = args[i+1]
				i++
			}
		case arg == "-p" || arg == "--public-key":
			if i+1 < len(args) {
				opts.PublicKey = args[i+1]
				i++
			}
		case arg == "-k" || arg == "--secret-key":
			if i+1 < len(args) {
				opts.SecretKey = args[i+1]
				i++
			}
		case arg == "-n" || arg == "--network":
			if i+1 < len(args) {
				opts.Network = args[i+1]
				i++
			}
		case arg == "-v" || arg == "--vcpu":
			if i+1 < len(args) {
				if v, err := strconv.Atoi(args[i+1]); err == nil {
					opts.VCPU = v
				}
				i++
			}
		case arg == "-y" || arg == "--yes":
			opts.Yes = true
		case arg == "-h" || arg == "--help":
			opts.Help = true
		default:
			remaining = append(remaining, arg)
		}
	}

	return opts, remaining
}

// CliMain is the main entry point for the CLI
func CliMain() {
	if len(os.Args) < 2 {
		printUsage()
		os.Exit(ExitInvalidArgs)
	}

	opts, remaining := parseGlobalFlags(os.Args[1:])

	if opts.Help && len(remaining) == 0 {
		printUsage()
		os.Exit(ExitSuccess)
	}

	// Determine command
	command := ""
	cmdArgs := remaining
	if len(remaining) > 0 {
		switch remaining[0] {
		case "session", "service", "snapshot", "key", "languages":
			command = remaining[0]
			cmdArgs = remaining[1:]
		default:
			command = "execute"
			opts.Args = remaining
		}
	}

	// Resolve credentials
	creds, err := ResolveCredentials(opts.PublicKey, opts.SecretKey)
	if err != nil {
		fmt.Fprintf(os.Stderr, "Error: %s\n", err)
		os.Exit(ExitAuthError)
	}

	// Dispatch command
	var exitCode int
	switch command {
	case "execute", "":
		if opts.Help {
			printUsage()
			exitCode = ExitSuccess
		} else {
			exitCode = runExecute(creds, opts)
		}
	case "session":
		exitCode = runSession(creds, cmdArgs, opts)
	case "service":
		exitCode = runService(creds, cmdArgs, opts)
	case "snapshot":
		exitCode = runSnapshot(creds, cmdArgs, opts)
	case "image":
		exitCode = runImage(creds, cmdArgs, opts)
	case "key":
		exitCode = runKey(creds)
	case "languages":
		exitCode = runLanguages(creds, cmdArgs)
	default:
		printUsage()
		exitCode = ExitInvalidArgs
	}

	os.Exit(exitCode)
}

Documentation clarifications

Dependencies

C Binary (un1) — requires libcurl and libwebsockets:

sudo apt install build-essential libcurl4-openssl-dev libwebsockets-dev
wget unsandbox.com/downloads/un.c && gcc -O2 -o un un.c -lcurl -lwebsockets

SDK Implementations — most use stdlib only (Ruby, JS, Go, etc). Some require minimal deps:

pip install requests  # Python

Execute Code

Run a Script

./un hello.py
./un app.js
./un main.rs

With Environment Variables

./un -e DEBUG=1 -e NAME=World script.py

With Input Files (teleport files into sandbox)

./un -f data.csv -f config.json process.py

Get Compiled Binary (teleport artifacts out)

./un -a -o ./bin main.c

Interactive Sessions

Start a Shell Session

# Default bash shell
./un session

# Choose your shell
./un session --shell zsh
./un session --shell fish

# Jump into a REPL
./un session --shell python3
./un session --shell node
./un session --shell julia

Session with Network Access

./un session -n semitrusted

Session Auditing (full terminal recording)

# Record everything (including vim, interactive programs)
./un session --audit -o ./logs

# Replay session later
zcat session.log*.gz | less -R

Collect Artifacts from Session

# Files in /tmp/artifacts/ are collected on exit
./un session -a -o ./outputs

Session Persistence (tmux/screen)

# Default: session terminates on disconnect (clean exit)
./un session

# With tmux: session persists, can reconnect later
./un session --tmux
# Press Ctrl+b then d to detach

# With screen: alternative multiplexer
./un session --screen
# Press Ctrl+a then d to detach

List Active Sessions

./un session --list

# Output:
# Active sessions: 2
#
# SESSION ID                               CONTAINER            SHELL      TTL      STATUS
# abc123...                                unsb-vm-12345        python3    45m30s   active
# def456...                                unsb-vm-67890        bash       1h2m     active

Reconnect to Existing Session

# Reconnect by container name (requires --tmux or --screen)
./un session --attach unsb-vm-12345

# Use exit to terminate session, or detach to keep it running

Terminate a Session

./un session --kill unsb-vm-12345

Available Shells & REPLs

Shells: bash, dash, sh, zsh, fish, ksh, tcsh, csh, elvish, xonsh, ash

REPLs:  python3, bpython, ipython    # Python
        node                          # JavaScript
        ruby, irb                     # Ruby
        lua                           # Lua
        php                           # PHP
        perl                          # Perl
        guile, scheme                 # Scheme
        ghci                          # Haskell
        erl, iex                      # Erlang/Elixir
        sbcl, clisp                   # Common Lisp
        r                             # R
        julia                         # Julia
        clojure                       # Clojure

API Key Management

Check Key Status

# Check if your API key is valid
./un key

# Output:
# Valid: key expires in 30 days

Extend Expired Key

# Open the portal to extend an expired key
./un key --extend

# This opens the unsandbox.com portal where you can
# add more credits to extend your key's expiration

Authentication

Credentials are loaded in priority order (highest first):

# 1. CLI flags (highest priority)
./un -p unsb-pk-xxxx -k unsb-sk-xxxxx script.py

# 2. Environment variables
export UNSANDBOX_PUBLIC_KEY=unsb-pk-xxxx-xxxx-xxxx-xxxx
export UNSANDBOX_SECRET_KEY=unsb-sk-xxxxx-xxxxx-xxxxx-xxxxx
./un script.py

# 3. Config file (lowest priority)
# ~/.unsandbox/accounts.csv format: public_key,secret_key
mkdir -p ~/.unsandbox
echo "unsb-pk-xxxx-xxxx-xxxx-xxxx,unsb-sk-xxxxx-xxxxx-xxxxx-xxxxx" > ~/.unsandbox/accounts.csv
./un script.py

Requests are signed with HMAC-SHA256. The bearer token contains only the public key; the secret key computes the signature (never transmitted).

Resource Scaling

Set vCPU Count

# Default: 1 vCPU, 2GB RAM
./un script.py

# Scale up: 4 vCPUs, 8GB RAM
./un -v 4 script.py

# Maximum: 8 vCPUs, 16GB RAM
./un --vcpu 8 heavy_compute.py

Live Session Boosting

# Boost a running session to 2 vCPU, 4GB RAM
./un session --boost sandbox-abc

# Boost to specific vCPU count (4 vCPU, 8GB RAM)
./un session --boost sandbox-abc --boost-vcpu 4

# Return to base resources (1 vCPU, 2GB RAM)
./un session --unboost sandbox-abc

Session Freeze/Unfreeze

Freeze and Unfreeze Sessions

# Freeze a session (stop billing, preserve state)
./un session --freeze sandbox-abc

# Unfreeze a frozen session
./un session --unfreeze sandbox-abc

# Note: Requires --tmux or --screen for persistence

Persistent Services

Create a Service

# Web server with ports
./un service --name web --ports 80,443 --bootstrap "python -m http.server 80"

# With custom domains
./un service --name blog --ports 8000 --domains blog.example.com

# Game server with SRV records
./un service --name mc --type minecraft --bootstrap ./setup.sh

# Deploy app tarball with bootstrap script
./un service --name app --ports 8000 -f app.tar.gz --bootstrap-file ./setup.sh
# setup.sh: cd /tmp && tar xzf app.tar.gz && ./app/start.sh

Manage Services

# List all services
./un service --list

# Get service details
./un service --info abc123

# View bootstrap logs
./un service --logs abc123
./un service --tail abc123  # last 9000 lines

# Execute command in running service
./un service --execute abc123 'journalctl -u myapp -n 50'

# Dump bootstrap script (for migrations)
./un service --dump-bootstrap abc123
./un service --dump-bootstrap abc123 backup.sh

# Freeze/unfreeze service
./un service --freeze abc123
./un service --unfreeze abc123

# Service settings (auto-wake, freeze page display)
./un service --auto-unfreeze abc123      # enable auto-wake on HTTP
./un service --no-auto-unfreeze abc123   # disable auto-wake
./un service --show-freeze-page abc123   # show HTML payment page (default)
./un service --no-show-freeze-page abc123  # return JSON error instead

# Redeploy with new bootstrap
./un service --redeploy abc123 --bootstrap ./new-setup.sh

# Destroy service
./un service --destroy abc123

Snapshots

List Snapshots

./un snapshot --list

# Output:
# Snapshots: 3
#
# SNAPSHOT ID                              NAME             SOURCE     SIZE     CREATED
# unsb-snapshot-a1b2-c3d4-e5f6-g7h8        before-upgrade   session    512 MB   2h ago
# unsb-snapshot-i9j0-k1l2-m3n4-o5p6        stable-v1.0      service    1.2 GB   1d ago

Create Session Snapshot

# Snapshot with name
./un session --snapshot unsb-vm-12345 --name "before upgrade"

# Quick snapshot (auto-generated name)
./un session --snapshot unsb-vm-12345

Create Service Snapshot

# Standard snapshot (pauses container briefly)
./un service --snapshot unsb-service-abc123 --name "stable v1.0"

# Hot snapshot (no pause, may be inconsistent)
./un service --snapshot unsb-service-abc123 --hot

Restore from Snapshot

# Restore session from snapshot
./un session --restore unsb-snapshot-a1b2-c3d4-e5f6-g7h8

# Restore service from snapshot
./un service --restore unsb-snapshot-i9j0-k1l2-m3n4-o5p6

Delete Snapshot

./un snapshot --delete unsb-snapshot-a1b2-c3d4-e5f6-g7h8

Images

Images are independent, transferable container images that survive container deletion. Unlike snapshots (which live with their container), images can be shared with other users, transferred between API keys, or made public in the marketplace.

List Images

# List all images (owned + shared + public)
./un image --list

# List only your images
./un image --list owned

# List images shared with you
./un image --list shared

# List public marketplace images
./un image --list public

# Get image details
./un image --info unsb-image-xxxx-xxxx-xxxx-xxxx

Publish Images

# Publish from a stopped or frozen service
./un image --publish-service unsb-service-abc123 \
   --name "My App v1.0" --description "Production snapshot"

# Publish from a snapshot
./un image --publish-snapshot unsb-snapshot-xxxx-xxxx-xxxx-xxxx \
   --name "Stable Release"

# Note: Cannot publish from running containers - stop or freeze first

Create Services from Images

# Spawn a new service from an image
./un image --spawn unsb-image-xxxx-xxxx-xxxx-xxxx \
   --name new-service --ports 80,443

# Clone an image (creates a copy you own)
./un image --clone unsb-image-xxxx-xxxx-xxxx-xxxx

Image Protection

# Lock image to prevent accidental deletion
./un image --lock unsb-image-xxxx-xxxx-xxxx-xxxx

# Unlock image to allow deletion
./un image --unlock unsb-image-xxxx-xxxx-xxxx-xxxx

# Delete image (must be unlocked)
./un image --delete unsb-image-xxxx-xxxx-xxxx-xxxx

Visibility & Sharing

# Set visibility level
./un image --visibility unsb-image-xxxx-xxxx-xxxx-xxxx private   # owner only (default)
./un image --visibility unsb-image-xxxx-xxxx-xxxx-xxxx unlisted  # can be shared
./un image --visibility unsb-image-xxxx-xxxx-xxxx-xxxx public    # marketplace

# Share with specific user
./un image --grant unsb-image-xxxx-xxxx-xxxx-xxxx \
   --key unsb-pk-friend-friend-friend-friend

# Revoke access
./un image --revoke unsb-image-xxxx-xxxx-xxxx-xxxx \
   --key unsb-pk-friend-friend-friend-friend

# List who has access
./un image --trusted unsb-image-xxxx-xxxx-xxxx-xxxx

Transfer Ownership

# Transfer image to another API key
./un image --transfer unsb-image-xxxx-xxxx-xxxx-xxxx \
   --to unsb-pk-newowner-newowner-newowner-newowner

Usage Reference

Usage: ./un [options] <source_file>
       ./un session [options]
       ./un service [options]
       ./un snapshot [options]
       ./un image [options]
       ./un key

Commands:
  (default)        Execute source file in sandbox
  session          Open interactive shell/REPL session
  service          Manage persistent services
  snapshot         Manage container snapshots
  image            Manage container images (publish, share, transfer)
  key              Check API key validity and expiration

Options:
  -e KEY=VALUE     Set environment variable (can use multiple times)
  -f FILE          Add input file (can use multiple times)
  -a               Return and save artifacts from /tmp/artifacts/
  -o DIR           Output directory for artifacts (default: current dir)
  -p KEY           Public key (or set UNSANDBOX_PUBLIC_KEY env var)
  -k KEY           Secret key (or set UNSANDBOX_SECRET_KEY env var)
  -n MODE          Network mode: zerotrust (default) or semitrusted
  -v N, --vcpu N   vCPU count 1-8, each vCPU gets 2GB RAM (default: 1)
  -y               Skip confirmation for large uploads (>1GB)
  -h               Show this help

Authentication (priority order):
  1. -p and -k flags (public and secret key)
  2. UNSANDBOX_PUBLIC_KEY + UNSANDBOX_SECRET_KEY env vars
  3. ~/.unsandbox/accounts.csv (format: public_key,secret_key per line)

Session options:
  -s, --shell SHELL  Shell/REPL to use (default: bash)
  -l, --list         List active sessions
  --attach ID        Reconnect to existing session (ID or container name)
  --kill ID          Terminate a session (ID or container name)
  --freeze ID        Freeze a session (requires --tmux/--screen)
  --unfreeze ID      Unfreeze a frozen session
  --boost ID         Boost session resources (2 vCPU, 4GB RAM)
  --boost-vcpu N     Specify vCPU count for boost (1-8)
  --unboost ID       Return to base resources
  --audit            Record full session for auditing
  --tmux             Enable session persistence with tmux (allows reconnect)
  --screen           Enable session persistence with screen (allows reconnect)

Service options:
  --name NAME        Service name (creates new service)
  --ports PORTS      Comma-separated ports (e.g., 80,443)
  --domains DOMAINS  Custom domains (e.g., example.com,www.example.com)
  --type TYPE        Service type: minecraft, mumble, teamspeak, source, tcp, udp
  --bootstrap CMD    Bootstrap command/file/URL to run on startup
  -f FILE            Upload file to /tmp/ (can use multiple times)
  -l, --list         List all services
  --info ID          Get service details
  --tail ID          Get last 9000 lines of bootstrap logs
  --logs ID          Get all bootstrap logs
  --freeze ID        Freeze a service
  --unfreeze ID      Unfreeze a service
  --auto-unfreeze ID       Enable auto-wake on HTTP request
  --no-auto-unfreeze ID    Disable auto-wake on HTTP request
  --show-freeze-page ID    Show HTML payment page when frozen (default)
  --no-show-freeze-page ID Return JSON error when frozen
  --destroy ID       Destroy a service
  --redeploy ID      Re-run bootstrap script (requires --bootstrap)
  --execute ID CMD   Run a command in a running service
  --dump-bootstrap ID [FILE]  Dump bootstrap script (for migrations)
  --snapshot ID    Create snapshot of session or service
  --snapshot-name  User-friendly name for snapshot
  --hot            Create snapshot without pausing (may be inconsistent)
  --restore ID     Restore session/service from snapshot ID

Snapshot options:
  -l, --list       List all snapshots
  --info ID        Get snapshot details
  --delete ID      Delete a snapshot permanently

Image options:
  -l, --list [owned|shared|public]  List images (all, owned, shared, or public)
  --info ID        Get image details
  --publish-service ID   Publish image from stopped/frozen service
  --publish-snapshot ID  Publish image from snapshot
  --name NAME      Name for published image
  --description DESC  Description for published image
  --delete ID      Delete image (must be unlocked)
  --clone ID       Clone image (creates copy you own)
  --spawn ID       Create service from image (requires --name)
  --lock ID        Lock image to prevent deletion
  --unlock ID      Unlock image to allow deletion
  --visibility ID LEVEL  Set visibility (private|unlisted|public)
  --grant ID --key KEY   Grant access to another API key
  --revoke ID --key KEY  Revoke access from API key
  --transfer ID --to KEY Transfer ownership to API key
  --trusted ID     List API keys with access

Key options:
  (no options)       Check API key validity
  --extend           Open portal to extend an expired key

Examples:
  ./un script.py                       # execute Python script
  ./un -e DEBUG=1 script.py            # with environment variable
  ./un -f data.csv process.py          # with input file
  ./un -a -o ./bin main.c              # save compiled artifacts
  ./un -v 4 heavy.py                   # with 4 vCPUs, 8GB RAM
  ./un session                         # interactive bash session
  ./un session --tmux                  # bash with reconnect support
  ./un session --list                  # list active sessions
  ./un session --attach unsb-vm-12345  # reconnect to session
  ./un session --kill unsb-vm-12345    # terminate a session
  ./un session --freeze unsb-vm-12345  # freeze session
  ./un session --unfreeze unsb-vm-12345  # unfreeze session
  ./un session --boost unsb-vm-12345   # boost resources
  ./un session --unboost unsb-vm-12345 # return to base
  ./un session --shell python3         # Python REPL
  ./un session --shell node            # Node.js REPL
  ./un session -n semitrusted          # session with network access
  ./un session --audit -o ./logs       # record session for auditing
  ./un service --name web --ports 80   # create web service
  ./un service --list                  # list all services
  ./un service --logs abc123           # view bootstrap logs
  ./un key                             # check API key
  ./un key --extend                    # extend expired key
  ./un snapshot --list                 # list all snapshots
  ./un session --snapshot unsb-vm-123  # snapshot a session
  ./un service --snapshot abc123       # snapshot a service
  ./un session --restore unsb-snapshot-xxxx  # restore from snapshot
  ./un image --list                  # list all images
  ./un image --list owned            # list your images
  ./un image --publish-service abc   # publish image from service
  ./un image --spawn img123 --name x # create service from image
  ./un image --grant img --key pk    # share image with user

CLI Inception

The UN CLI has been implemented in 42 programming languages, demonstrating that the unsandbox API can be accessed from virtually any environment.

View All 42 Implementations →

License

PUBLIC DOMAIN - NO LICENSE, NO WARRANTY

This is free public domain software for the public good of a permacomputer hosted
at permacomputer.com - an always-on computer by the people, for the people. One
that is durable, easy to repair, and distributed like tap water for machine
learning intelligence.

The permacomputer is community-owned infrastructure optimized around four values:

  TRUTH    - First principles, math & science, open source code freely distributed
  FREEDOM  - Voluntary partnerships, freedom from tyranny & corporate control
  HARMONY  - Minimal waste, self-renewing systems with diverse thriving connections
  LOVE     - Be yourself without hurting others, cooperation through natural law

This software contributes to that vision by enabling code execution across all 42
programming languages through a unified interface, accessible to everyone. Code is
seeds to sprout on any abandoned technology.

Learn more: https://www.permacomputer.com

Anyone is free to copy, modify, publish, use, compile, sell, or distribute this
software, either in source code form or as a compiled binary, for any purpose,
commercial or non-commercial, and by any means.

NO WARRANTY. THE SOFTWARE IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND.

That said, our permacomputer's digital membrane stratum continuously runs unit,
integration, and functional tests on all its own software - with our permacomputer
monitoring itself, repairing itself, with minimal human guidance in the loop.
Our agents do their best.

Copyright 2025 TimeHexOn & foxhop & russell@unturf
https://www.timehexon.com
https://www.foxhop.net
https://www.unturf.com/software

Export Vault

Enter a password to encrypt your exported vault. You'll need this password to import the vault on another device.

Import Vault

Select an exported vault file and enter the export password to decrypt it.