CLI
Cliente de línea de comandos rápido para ejecución de código y sesiones interactivas. Más de 42 lenguajes, más de 30 shells/REPL.
Documentación oficial OpenAPI Swagger ↗Inicio Rápido — F#
# Download + setup
curl -O https://git.unturf.com/engineering/unturf/un-inception/-/raw/main/clients/fsharp/sync/src/un.fs
export UNSANDBOX_PUBLIC_KEY="unsb-pk-xxxx-xxxx-xxxx-xxxx"
export UNSANDBOX_SECRET_KEY="unsb-sk-xxxxx-xxxxx-xxxxx-xxxxx"
# Run code
./un script.fs
Descargar
Guía de instalación →Características:
- 42+ languages - Python, JS, Go, Rust, C++, Java...
- Sessions - 30+ shells/REPLs, tmux persistence
- Files - Upload files, collect artifacts
- Services - Persistent containers with domains
- Snapshots - Point-in-time backups
- Images - Publish, share, transfer
Inicio Rápido de Integración ⚡
Agrega superpoderes unsandbox a tu app F# existente:
curl -O https://git.unturf.com/engineering/unturf/un-inception/-/raw/main/clients/fsharp/sync/src/un.fs
# Option A: Environment variables
export UNSANDBOX_PUBLIC_KEY="unsb-pk-xxxx-xxxx-xxxx-xxxx"
export UNSANDBOX_SECRET_KEY="unsb-sk-xxxxx-xxxxx-xxxxx-xxxxx"
# Option B: Config file (persistent)
mkdir -p ~/.unsandbox
echo "unsb-pk-xxxx-xxxx-xxxx-xxxx,unsb-sk-xxxxx-xxxxx-xxxxx-xxxxx" > ~/.unsandbox/accounts.csv
// In your F# app:
open Un
let result = executeCode "fsharp" "printfn \"Hello from F# running on unsandbox!\""
printfn "%s" result.["stdout"] // Hello from F# running on unsandbox!
dotnet fsi main.fsx
57713228bca0fc56a97a6cb63237b14a
SHA256: b6bec76df057ff84c4503f41c3242ddd3a3b894f4c5b13154090709af68c48f2
// PUBLIC DOMAIN - NO LICENSE, NO WARRANTY
//
// This is free public domain software for the public good of a permacomputer hosted
// at permacomputer.com - an always-on computer by the people, for the people. One
// which is durable, easy to repair, and distributed like tap water for machine
// learning intelligence.
//
// The permacomputer is community-owned infrastructure optimized around four values:
//
// TRUTH - First principles, math & science, open source code freely distributed
// FREEDOM - Voluntary partnerships, freedom from tyranny & corporate control
// HARMONY - Minimal waste, self-renewing systems with diverse thriving connections
// LOVE - Be yourself without hurting others, cooperation through natural law
//
// This software contributes to that vision by enabling code execution across 42+
// programming languages through a unified interface, accessible to all. Code is
// seeds to sprout on any abandoned technology.
//
// Learn more: https://www.permacomputer.com
//
// Anyone is free to copy, modify, publish, use, compile, sell, or distribute this
// software, either in source code form or as a compiled binary, for any purpose,
// commercial or non-commercial, and by any means.
//
// NO WARRANTY. THE SOFTWARE IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND.
//
// That said, our permacomputer's digital membrane stratum continuously runs unit,
// integration, and functional tests on all of it's own software - with our
// permacomputer monitoring itself, repairing itself, with minimal human in the
// loop guidance. Our agents do their best.
//
// Copyright 2025 TimeHexOn & foxhop & russell@unturf
// https://www.timehexon.com
// https://www.foxhop.net
// https://www.unturf.com/software
// un.fs - Unsandbox CLI Client (F# Implementation)
// Compile: fsharpc un.fs
// Run: mono un.exe [options] <source_file>
// Requires: UNSANDBOX_API_KEY environment variable
open System
open System.IO
open System.Net
open System.Text
open System.Security.Cryptography
let apiBase = "https://api.unsandbox.com"
let portalBase = "https://unsandbox.com"
let languagesCacheTtl = 3600 // 1 hour in seconds
let blue = "\x1B[34m"
let red = "\x1B[31m"
let green = "\x1B[32m"
let yellow = "\x1B[33m"
let reset = "\x1B[0m"
let extMap =
Map.ofList [
(".py", "python"); (".js", "javascript"); (".ts", "typescript")
(".rb", "ruby"); (".php", "php"); (".pl", "perl"); (".lua", "lua")
(".sh", "bash"); (".go", "go"); (".rs", "rust"); (".c", "c")
(".cpp", "cpp"); (".cc", "cpp"); (".cxx", "cpp")
(".java", "java"); (".kt", "kotlin"); (".cs", "csharp"); (".fs", "fsharp")
(".hs", "haskell"); (".ml", "ocaml"); (".clj", "clojure"); (".scm", "scheme")
(".lisp", "commonlisp"); (".erl", "erlang"); (".ex", "elixir"); (".exs", "elixir")
(".jl", "julia"); (".r", "r"); (".R", "r"); (".cr", "crystal")
(".d", "d"); (".nim", "nim"); (".zig", "zig"); (".v", "v")
(".dart", "dart"); (".groovy", "groovy"); (".scala", "scala")
(".f90", "fortran"); (".f95", "fortran"); (".cob", "cobol")
(".pro", "prolog"); (".forth", "forth"); (".4th", "forth")
(".tcl", "tcl"); (".raku", "raku"); (".m", "objc")
]
type Args = {
mutable Command: string option
mutable SourceFile: string option
mutable ApiKey: string option
mutable AccountIndex: int option
mutable Network: string option
mutable Vcpu: int
Env: ResizeArray<string>
Files: ResizeArray<string>
mutable Artifacts: bool
mutable OutputDir: string option
mutable SessionList: bool
mutable SessionShell: string option
mutable SessionKill: string option
mutable SessionSnapshot: string option
mutable SessionRestore: string option
mutable SessionFrom: string option
mutable SessionSnapshotName: string option
mutable SessionHot: bool
mutable ServiceList: bool
mutable LanguagesJson: bool
mutable ServiceName: string option
mutable ServicePorts: string option
mutable ServiceType: string option
mutable ServiceBootstrap: string option
mutable ServiceBootstrapFile: string option
mutable ServiceInfo: string option
mutable ServiceLogs: string option
mutable ServiceTail: string option
mutable ServiceSleep: string option
mutable ServiceWake: string option
mutable ServiceDestroy: string option
mutable ServiceExecute: string option
mutable ServiceCommand: string option
mutable ServiceDumpBootstrap: string option
mutable ServiceDumpFile: string option
mutable ServiceResize: string option
mutable ServiceSetUnfreezeOnDemand: string option
mutable ServiceUnfreezeOnDemandValue: string option
mutable ServiceSnapshot: string option
mutable ServiceRestore: string option
mutable ServiceFrom: string option
mutable ServiceSnapshotName: string option
mutable ServiceHot: bool
mutable SnapshotList: bool
mutable SnapshotInfo: string option
mutable SnapshotDelete: string option
mutable SnapshotClone: string option
mutable SnapshotType: string option
mutable SnapshotName: string option
mutable SnapshotShell: string option
mutable SnapshotPorts: string option
mutable EnvFile: string option
mutable EnvAction: string option
mutable EnvTarget: string option
mutable KeyExtend: bool
// Image command options
mutable ImageList: bool
mutable ImageInfo: string option
mutable ImageDelete: string option
mutable ImageLock: string option
mutable ImageUnlock: string option
mutable ImagePublish: string option
mutable ImageSourceType: string option
mutable ImageVisibility: string option
mutable ImageVisibilityMode: string option
mutable ImageSpawn: string option
mutable ImageClone: string option
mutable ImageName: string option
mutable ImagePorts: string option
}
let loadCredentialsFromCsv (csvPath: string) (accountIndex: int) =
if File.Exists(csvPath) then
try
let lines = File.ReadAllLines(csvPath)
let accounts =
lines
|> Array.map (fun l -> l.Trim())
|> Array.filter (fun l -> l.Length > 0 && not (l.StartsWith("#")))
|> Array.choose (fun line ->
let parts = line.Split(',')
if parts.Length >= 2 then
let pk = parts.[0].Trim()
let sk = parts.[1].Trim()
if pk.Length > 8 && sk.Length > 8 then Some (pk, sk)
else None
else None)
if accountIndex < accounts.Length then Some accounts.[accountIndex]
else None
with _ -> None
else None
let getApiKeys (argsKey: string option) (accountIndex: int option) =
let home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile)
let homeCsv = Path.Combine(home, ".unsandbox", "accounts.csv")
// Priority 1: --account N -> accounts.csv row N (bypasses env vars)
match accountIndex with
| Some idx ->
let creds =
match loadCredentialsFromCsv homeCsv idx with
| Some c -> Some c
| None -> loadCredentialsFromCsv "accounts.csv" idx
match creds with
| Some (pk, sk) -> (pk, sk)
| None ->
eprintfn "%sError: No credentials found for account index %d in accounts.csv%s" red idx reset
exit 1
| None ->
let publicKey = Environment.GetEnvironmentVariable("UNSANDBOX_PUBLIC_KEY")
let secretKey = Environment.GetEnvironmentVariable("UNSANDBOX_SECRET_KEY")
// Priority 2: environment variables
if not (String.IsNullOrEmpty(publicKey)) && not (String.IsNullOrEmpty(secretKey)) then
(publicKey, secretKey)
else
// Fall back to legacy UNSANDBOX_API_KEY
let legacyKey = match argsKey with | Some k -> k | None -> Environment.GetEnvironmentVariable("UNSANDBOX_API_KEY")
if not (String.IsNullOrEmpty(legacyKey)) then
(legacyKey, null)
else
// Priority 3: ~/.unsandbox/accounts.csv (or UNSANDBOX_ACCOUNT index)
let defaultIndex =
let envIdx = Environment.GetEnvironmentVariable("UNSANDBOX_ACCOUNT")
if String.IsNullOrEmpty(envIdx) then 0
else match System.Int32.TryParse(envIdx) with | (true, n) -> n | _ -> 0
let creds =
match loadCredentialsFromCsv homeCsv defaultIndex with
| Some c -> Some c
| None -> loadCredentialsFromCsv "accounts.csv" defaultIndex
match creds with
| Some (pk, sk) -> (pk, sk)
| None ->
eprintfn "%sError: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set%s" red reset
exit 1
let detectLanguage (filename: string) =
let dotIndex = filename.LastIndexOf('.')
if dotIndex = -1 then
failwith "Cannot detect language: no file extension"
let ext = filename.Substring(dotIndex).ToLower()
match Map.tryFind ext extMap with
| Some lang -> lang
| None -> failwithf "Unsupported file extension: %s" ext
let jsonEscape (s: string) =
let sb = StringBuilder("\"")
for c in s do
match c with
| '"' -> sb.Append("\\\"") |> ignore
| '\\' -> sb.Append("\\\\") |> ignore
| '\n' -> sb.Append("\\n") |> ignore
| '\r' -> sb.Append("\\r") |> ignore
| '\t' -> sb.Append("\\t") |> ignore
| _ -> sb.Append(c) |> ignore
sb.Append("\"") |> ignore
sb.ToString()
let rec toJson (obj: obj) =
match obj with
| null -> "null"
| :? string as s -> jsonEscape s
| :? int as i -> i.ToString()
| :? float as f -> f.ToString()
| :? bool as b -> b.ToString().ToLower()
| :? Map<string, obj> as m ->
let entries = m |> Map.toSeq |> Seq.map (fun (k, v) -> sprintf "\"%s\":%s" k (toJson v)) |> String.concat ","
sprintf "{%s}" entries
| :? ResizeArray<obj> as lst ->
let items = lst |> Seq.map toJson |> String.concat ","
sprintf "[%s]" items
| :? (string * obj) list as lst ->
let entries = lst |> List.map (fun (k, v) -> sprintf "\"%s\":%s" k (toJson v)) |> String.concat ","
sprintf "{%s}" entries
| _ -> jsonEscape (obj.ToString())
let extractJsonValue (json: string) (key: string) =
let pattern = sprintf "\"%s\":" key
let startIndex = json.IndexOf(pattern)
if startIndex = -1 then None
else
let mutable idx = startIndex + pattern.Length
while idx < json.Length && Char.IsWhiteSpace(json.[idx]) do
idx <- idx + 1
if json.[idx] = '"' then
idx <- idx + 1
let sb = StringBuilder()
let mutable escaped = false
let mutable found = false
let mutable i = idx
while i < json.Length && not found do
let c = json.[i]
if escaped then
match c with
| 'n' -> sb.Append('\n') |> ignore
| 'r' -> sb.Append('\r') |> ignore
| 't' -> sb.Append('\t') |> ignore
| '"' -> sb.Append('"') |> ignore
| '\\' -> sb.Append('\\') |> ignore
| _ -> sb.Append(c) |> ignore
escaped <- false
else if c = '\\' then
escaped <- true
else if c = '"' then
found <- true
else
sb.Append(c) |> ignore
i <- i + 1
Some (sb.ToString())
else
let sb = StringBuilder()
let mutable i = idx
while i < json.Length && (Char.IsDigit(json.[i]) || json.[i] = '-') do
sb.Append(json.[i]) |> ignore
i <- i + 1
Some (sb.ToString())
let parseJson (json: string) =
let trimmed = json.Trim()
if not (trimmed.StartsWith("{")) then Map.empty
else
let result = ResizeArray<string * obj>()
let mutable i = 1
while i < trimmed.Length do
while i < trimmed.Length && Char.IsWhiteSpace(trimmed.[i]) do i <- i + 1
if trimmed.[i] = '}' then i <- trimmed.Length
elif trimmed.[i] = '"' then
let keyStart = i + 1
i <- i + 1
while i < trimmed.Length && trimmed.[i] <> '"' do
if trimmed.[i] = '\\' then i <- i + 1
i <- i + 1
let key = trimmed.Substring(keyStart, i - keyStart).Replace("\\\"", "\"").Replace("\\\\", "\\")
i <- i + 1
while i < trimmed.Length && (Char.IsWhiteSpace(trimmed.[i]) || trimmed.[i] = ':') do i <- i + 1
let value = extractJsonValue trimmed key
match value with
| Some v -> result.Add((key, box v))
| None -> ()
while i < trimmed.Length && (Char.IsWhiteSpace(trimmed.[i]) || trimmed.[i] = ',' || trimmed.[i] = '"' || Char.IsLetterOrDigit(trimmed.[i]) || trimmed.[i] = '\\') do i <- i + 1
else
i <- i + 1
result |> Seq.map (fun (k, v) -> k, v) |> Map.ofSeq
// Custom exception for HTTP errors with status code
exception HttpException of int * string
let apiRequestWithHeaders (endpoint: string) (method: string) (data: (string * obj) list option) (publicKey: string) (secretKey: string) (sudoOtp: string option) (sudoChallengeId: string option) =
ServicePointManager.SecurityProtocol <- SecurityProtocolType.Tls12 ||| SecurityProtocolType.Tls11 ||| SecurityProtocolType.Tls
let request = WebRequest.Create(apiBase + endpoint) :?> HttpWebRequest
request.Method <- method
request.ContentType <- "application/json"
request.Timeout <- 300000
let body = match data with | Some d -> toJson (box d) | None -> ""
// Add HMAC authentication headers if secretKey is provided
if not (String.IsNullOrEmpty(secretKey)) then
let timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds()
let message = sprintf "%d:%s:%s:%s" timestamp method endpoint body
use hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secretKey))
let hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(message))
let signature = BitConverter.ToString(hash).Replace("-", "").ToLower()
request.Headers.Add("Authorization", sprintf "Bearer %s" publicKey)
request.Headers.Add("X-Timestamp", timestamp.ToString())
request.Headers.Add("X-Signature", signature)
else
// Legacy API key authentication
request.Headers.Add("Authorization", sprintf "Bearer %s" publicKey)
// Add sudo OTP headers if provided
match sudoOtp with
| Some otp -> request.Headers.Add("X-Sudo-OTP", otp)
| None -> ()
match sudoChallengeId with
| Some cid -> request.Headers.Add("X-Sudo-Challenge", cid)
| None -> ()
match data with
| Some d ->
let bytes = Encoding.UTF8.GetBytes(body)
request.ContentLength <- int64 bytes.Length
use stream = request.GetRequestStream()
stream.Write(bytes, 0, bytes.Length)
| None -> ()
try
use response = request.GetResponse() :?> HttpWebResponse
if response.StatusCode <> HttpStatusCode.OK then
failwithf "HTTP %A" response.StatusCode
use reader = new StreamReader(response.GetResponseStream())
let responseText = reader.ReadToEnd()
parseJson responseText
with
| :? WebException as ex ->
let errorMsg =
if ex.Response <> null then
use reader = new StreamReader(ex.Response.GetResponseStream())
reader.ReadToEnd()
else
ex.Message
let statusCode =
if ex.Response <> null then
let httpResponse = ex.Response :?> HttpWebResponse
int httpResponse.StatusCode
else
0
// Check for clock drift error
if errorMsg.Contains("timestamp") && (errorMsg.Contains("401") || errorMsg.Contains("expired") || errorMsg.Contains("invalid")) then
eprintfn "%sError: Request timestamp expired (must be within 5 minutes of server time)%s" red reset
eprintfn "%sYour computer's clock may have drifted.%s" yellow reset
eprintfn "Check your system time and sync with NTP if needed:"
eprintfn " Linux: sudo ntpdate -s time.nist.gov"
eprintfn " macOS: sudo sntp -sS time.apple.com"
eprintfn " Windows: w32tm /resync%s" reset
exit 1
raise (HttpException(statusCode, errorMsg))
let apiRequest (endpoint: string) (method: string) (data: (string * obj) list option) (publicKey: string) (secretKey: string) =
apiRequestWithHeaders endpoint method data publicKey secretKey None None
// Handle 428 sudo OTP challenge - prompts user for OTP and retries the request
let handleSudoChallenge (responseBody: string) (endpoint: string) (method: string) (data: (string * obj) list option) (publicKey: string) (secretKey: string) =
let challengeId = extractJsonValue responseBody "challenge_id"
eprintfn "%sConfirmation required. Check your email for a one-time code.%s" yellow reset
eprintf "Enter OTP: "
let otp = Console.ReadLine()
if String.IsNullOrEmpty(otp) then
failwith "Operation cancelled"
let otp = otp.Trim()
// Retry the request with sudo headers
apiRequestWithHeaders endpoint method data publicKey secretKey (Some otp) challengeId
// Wrapper for destructive operations that may require 428 sudo OTP
let apiRequestWithSudo (endpoint: string) (method: string) (data: (string * obj) list option) (publicKey: string) (secretKey: string) =
try
apiRequest endpoint method data publicKey secretKey
with
| HttpException(428, responseBody) ->
handleSudoChallenge responseBody endpoint method data publicKey secretKey
| HttpException(_, errorMsg) ->
failwithf "HTTP error - %s" errorMsg
let apiRequestPatch (endpoint: string) (data: (string * obj) list) (publicKey: string) (secretKey: string) =
ServicePointManager.SecurityProtocol <- SecurityProtocolType.Tls12 ||| SecurityProtocolType.Tls11 ||| SecurityProtocolType.Tls
let request = WebRequest.Create(apiBase + endpoint) :?> HttpWebRequest
request.Method <- "PATCH"
request.ContentType <- "application/json"
request.Timeout <- 300000
let body = toJson (box data)
// Add HMAC authentication headers if secretKey is provided
if not (String.IsNullOrEmpty(secretKey)) then
let timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds()
let message = sprintf "%d:%s:%s:%s" timestamp "PATCH" endpoint body
use hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secretKey))
let hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(message))
let signature = BitConverter.ToString(hash).Replace("-", "").ToLower()
request.Headers.Add("Authorization", sprintf "Bearer %s" publicKey)
request.Headers.Add("X-Timestamp", timestamp.ToString())
request.Headers.Add("X-Signature", signature)
else
request.Headers.Add("Authorization", sprintf "Bearer %s" publicKey)
let bytes = Encoding.UTF8.GetBytes(body)
request.ContentLength <- int64 bytes.Length
use stream = request.GetRequestStream()
stream.Write(bytes, 0, bytes.Length)
try
use response = request.GetResponse() :?> HttpWebResponse
if response.StatusCode <> HttpStatusCode.OK then
failwithf "HTTP %A" response.StatusCode
use reader = new StreamReader(response.GetResponseStream())
let responseText = reader.ReadToEnd()
parseJson responseText
with
| :? WebException as ex ->
let errorMsg =
if ex.Response <> null then
use reader = new StreamReader(ex.Response.GetResponseStream())
reader.ReadToEnd()
else
ex.Message
// Check for clock drift error
if errorMsg.Contains("timestamp") && (errorMsg.Contains("401") || errorMsg.Contains("expired") || errorMsg.Contains("invalid")) then
eprintfn "%sError: Request timestamp expired (must be within 5 minutes of server time)%s" red reset
eprintfn "%sYour computer's clock may have drifted.%s" yellow reset
eprintfn "Check your system time and sync with NTP if needed:"
eprintfn " Linux: sudo ntpdate -s time.nist.gov"
eprintfn " macOS: sudo sntp -sS time.apple.com"
eprintfn " Windows: w32tm /resync%s" reset
exit 1
failwithf "HTTP error - %s" errorMsg
let apiRequestText (endpoint: string) (method: string) (body: string) (publicKey: string) (secretKey: string) =
ServicePointManager.SecurityProtocol <- SecurityProtocolType.Tls12 ||| SecurityProtocolType.Tls11 ||| SecurityProtocolType.Tls
let request = WebRequest.Create(apiBase + endpoint) :?> HttpWebRequest
request.Method <- method
request.ContentType <- "text/plain"
request.Timeout <- 300000
let bodyContent = if body = null then "" else body
// Add HMAC authentication headers if secretKey is provided
if not (String.IsNullOrEmpty(secretKey)) then
let timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds()
let message = sprintf "%d:%s:%s:%s" timestamp method endpoint bodyContent
use hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secretKey))
let hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(message))
let signature = BitConverter.ToString(hash).Replace("-", "").ToLower()
request.Headers.Add("Authorization", sprintf "Bearer %s" publicKey)
request.Headers.Add("X-Timestamp", timestamp.ToString())
request.Headers.Add("X-Signature", signature)
else
request.Headers.Add("Authorization", sprintf "Bearer %s" publicKey)
if not (String.IsNullOrEmpty(bodyContent)) then
let bytes = Encoding.UTF8.GetBytes(bodyContent)
request.ContentLength <- int64 bytes.Length
use stream = request.GetRequestStream()
stream.Write(bytes, 0, bytes.Length)
try
use response = request.GetResponse() :?> HttpWebResponse
use reader = new StreamReader(response.GetResponseStream())
reader.ReadToEnd()
with
| :? WebException as ex ->
let errorMsg =
if ex.Response <> null then
use reader = new StreamReader(ex.Response.GetResponseStream())
reader.ReadToEnd()
else
ex.Message
failwithf "HTTP error - %s" errorMsg
let getLanguagesCachePath () =
let home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile)
Path.Combine(home, ".unsandbox", "languages.json")
let loadLanguagesCache () =
let cachePath = getLanguagesCachePath ()
if File.Exists(cachePath) then
try
let content = File.ReadAllText(cachePath)
let timestamp = extractJsonValue content "timestamp"
match timestamp with
| Some ts ->
let cacheTime = int64 ts
let now = DateTimeOffset.UtcNow.ToUnixTimeSeconds()
if now - cacheTime < int64 languagesCacheTtl then
Some content
else
None
| None -> None
with _ -> None
else
None
let saveLanguagesCache (languages: string) =
let cachePath = getLanguagesCachePath ()
let cacheDir = Path.GetDirectoryName(cachePath)
if not (Directory.Exists(cacheDir)) then
Directory.CreateDirectory(cacheDir) |> ignore
let timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds()
let cacheContent = sprintf "{\"languages\":%s,\"timestamp\":%d}" languages timestamp
File.WriteAllText(cachePath, cacheContent)
let readEnvFile (path: string) =
if not (File.Exists(path)) then
failwithf "Env file not found: %s" path
File.ReadAllText(path)
let buildEnvContent (envs: ResizeArray<string>) (envFile: string option) =
let lines = ResizeArray<string>()
// Add from -e flags
for env in envs do
lines.Add(env)
// Add from --env-file
match envFile with
| Some path ->
let content = readEnvFile path
for line in content.Split('\n') do
let trimmed = line.Trim()
if not (String.IsNullOrEmpty(trimmed)) && not (trimmed.StartsWith("#")) then
lines.Add(trimmed)
| None -> ()
String.Join("\n", lines)
let serviceEnvStatus (serviceId: string) (publicKey: string) (secretKey: string) =
apiRequest (sprintf "/services/%s/env" serviceId) "GET" None publicKey secretKey
let serviceEnvSet (serviceId: string) (envContent: string) (publicKey: string) (secretKey: string) =
let maxEnvContentSize = 65536
if envContent.Length > maxEnvContentSize then
eprintfn "%sError: Env content exceeds maximum size of 64KB%s" red reset
false
else
try
apiRequestText (sprintf "/services/%s/env" serviceId) "PUT" envContent publicKey secretKey |> ignore
true
with _ ->
false
let serviceEnvExport (serviceId: string) (publicKey: string) (secretKey: string) =
apiRequest (sprintf "/services/%s/env/export" serviceId) "POST" None publicKey secretKey
let serviceEnvDelete (serviceId: string) (publicKey: string) (secretKey: string) =
try
apiRequest (sprintf "/services/%s/env" serviceId) "DELETE" None publicKey secretKey |> ignore
true
with _ ->
false
let cmdServiceEnv (args: Args) (publicKey: string) (secretKey: string) =
match args.EnvAction with
| Some "status" ->
match args.EnvTarget with
| Some target ->
let result = serviceEnvStatus target publicKey secretKey
match result.TryFind "has_vault" with
| Some hasVault when hasVault.ToString() = "True" ->
printfn "%sVault: configured%s" green reset
match result.TryFind "env_count" with
| Some count -> printfn "Variables: %s" (count.ToString())
| None -> ()
match result.TryFind "updated_at" with
| Some updated -> printfn "Updated: %s" (updated.ToString())
| None -> ()
| _ ->
printfn "%sVault: not configured%s" yellow reset
| None ->
eprintfn "%sError: service env status requires service ID%s" red reset
exit 1
| Some "set" ->
match args.EnvTarget with
| Some target ->
if args.Env.Count = 0 && args.EnvFile.IsNone then
eprintfn "%sError: service env set requires -e or --env-file%s" red reset
exit 1
let envContent = buildEnvContent args.Env args.EnvFile
if serviceEnvSet target envContent publicKey secretKey then
printfn "%sVault updated for service %s%s" green target reset
else
eprintfn "%sError: Failed to update vault%s" red reset
exit 1
| None ->
eprintfn "%sError: service env set requires service ID%s" red reset
exit 1
| Some "export" ->
match args.EnvTarget with
| Some target ->
let result = serviceEnvExport target publicKey secretKey
match result.TryFind "content" with
| Some content -> printf "%s" (content.ToString())
| None -> ()
| None ->
eprintfn "%sError: service env export requires service ID%s" red reset
exit 1
| Some "delete" ->
match args.EnvTarget with
| Some target ->
if serviceEnvDelete target publicKey secretKey then
printfn "%sVault deleted for service %s%s" green target reset
else
eprintfn "%sError: Failed to delete vault%s" red reset
exit 1
| None ->
eprintfn "%sError: service env delete requires service ID%s" red reset
exit 1
| Some action ->
eprintfn "%sError: Unknown env action: %s%s" red action reset
eprintfn "Usage: un.fs service env <status|set|export|delete> <service_id>"
exit 1
| None ->
eprintfn "%sError: env action required%s" red reset
exit 1
let cmdExecute (args: Args) =
let (publicKey, secretKey) = getApiKeys args.ApiKey args.AccountIndex
let code = File.ReadAllText(args.SourceFile.Value)
let language = detectLanguage args.SourceFile.Value
let mutable payload = [("language", box language); ("code", box code)]
if args.Env.Count > 0 then
let envVars = args.Env |> Seq.choose (fun e ->
let parts = e.Split([|'='|], 2)
if parts.Length = 2 then Some (parts.[0], box parts.[1]) else None
) |> Seq.toList
if not (List.isEmpty envVars) then
payload <- payload @ [("env", box envVars)]
if args.Files.Count > 0 then
let inputFiles = args.Files |> Seq.map (fun filepath ->
let content = File.ReadAllBytes(filepath)
[("filename", box (Path.GetFileName(filepath))); ("content_base64", box (Convert.ToBase64String(content)))]
) |> Seq.toList
payload <- payload @ [("input_files", box inputFiles)]
if args.Artifacts then
payload <- payload @ [("return_artifacts", box true)]
if args.Network.IsSome then
payload <- payload @ [("network", box args.Network.Value)]
if args.Vcpu > 0 then
payload <- payload @ [("vcpu", box args.Vcpu)]
let result = apiRequest "/execute" "POST" (Some payload) publicKey secretKey
match result.TryFind "stdout" with
| Some stdout when not (String.IsNullOrEmpty(stdout.ToString())) ->
printf "%s%s%s" blue (stdout.ToString()) reset
| _ -> ()
match result.TryFind "stderr" with
| Some stderr when not (String.IsNullOrEmpty(stderr.ToString())) ->
eprintf "%s%s%s" red (stderr.ToString()) reset
| _ -> ()
if args.Artifacts && result.ContainsKey("artifacts") then
let outDir = match args.OutputDir with | Some d -> d | None -> "."
Directory.CreateDirectory(outDir) |> ignore
eprintfn "%sSaved artifacts to %s%s" green outDir reset
let exitCode = match result.TryFind "exit_code" with | Some ec -> int (ec.ToString()) | None -> 0
exit exitCode
let cmdSession (args: Args) =
let (publicKey, secretKey) = getApiKeys args.ApiKey args.AccountIndex
if args.SessionSnapshot.IsSome then
let mutable payload = []
if args.SessionSnapshotName.IsSome then
payload <- payload @ [("name", box args.SessionSnapshotName.Value)]
if args.SessionHot then
payload <- payload @ [("hot", box true)]
let result = apiRequest (sprintf "/sessions/%s/snapshot" args.SessionSnapshot.Value) "POST" (Some payload) publicKey secretKey
printfn "%sSnapshot created%s" green reset
printfn "%s" (toJson (box result))
elif args.SessionRestore.IsSome then
// --restore takes snapshot ID directly, calls /snapshots/:id/restore
let result = apiRequest (sprintf "/snapshots/%s/restore" args.SessionRestore.Value) "POST" None publicKey secretKey
printfn "%sSession restored from snapshot%s" green reset
printfn "%s" (toJson (box result))
elif args.SessionList then
let result = apiRequest "/sessions" "GET" None publicKey secretKey
printfn "%-40s %-10s %-10s %s" "ID" "Shell" "Status" "Created"
printfn "No sessions (list parsing not implemented)"
elif args.SessionKill.IsSome then
let result = apiRequest (sprintf "/sessions/%s" args.SessionKill.Value) "DELETE" None publicKey secretKey
printfn "%sSession terminated: %s%s" green args.SessionKill.Value reset
else
let mutable payload = [("shell", box (match args.SessionShell with | Some s -> s | None -> "bash"))]
if args.Network.IsSome then
payload <- payload @ [("network", box args.Network.Value)]
if args.Vcpu > 0 then
payload <- payload @ [("vcpu", box args.Vcpu)]
if args.Files.Count > 0 then
let inputFiles = args.Files |> Seq.map (fun filepath ->
let content = File.ReadAllBytes(filepath)
[("filename", box (Path.GetFileName(filepath))); ("content_base64", box (Convert.ToBase64String(content)))]
) |> Seq.toList
payload <- payload @ [("input_files", box inputFiles)]
printfn "%sCreating session...%s" yellow reset
let result = apiRequest "/sessions" "POST" (Some payload) publicKey secretKey
match result.TryFind "id" with
| Some id -> printfn "%sSession created: %s%s" green (id.ToString()) reset
| None -> printfn "%sSession created%s" green reset
printfn "%s(Interactive sessions require WebSocket - use un2 for full support)%s" yellow reset
let openBrowser (url: string) =
try
let os = Environment.OSVersion.Platform
let cmd =
if os = PlatformID.Unix || os = PlatformID.MacOSX then
if System.IO.File.Exists("/usr/bin/xdg-open") then
System.Diagnostics.Process.Start("xdg-open", url)
else
System.Diagnostics.Process.Start("open", url)
else
System.Diagnostics.Process.Start("cmd", sprintf "/c start %s" url)
cmd.WaitForExit()
with ex ->
eprintfn "%sError opening browser: %s%s" red ex.Message reset
let cmdKey (args: Args) =
let (publicKey, secretKey) = getApiKeys args.ApiKey args.AccountIndex
ServicePointManager.SecurityProtocol <- SecurityProtocolType.Tls12 ||| SecurityProtocolType.Tls11 ||| SecurityProtocolType.Tls
let request = WebRequest.Create(portalBase + "/keys/validate") :?> HttpWebRequest
request.Method <- "POST"
request.ContentType <- "application/json"
request.Headers.Add("Authorization", sprintf "Bearer %s" publicKey)
request.Timeout <- 30000
try
use response = request.GetResponse() :?> HttpWebResponse
use reader = new StreamReader(response.GetResponseStream())
let responseText = reader.ReadToEnd()
let result = parseJson responseText
let resultPublicKey = match result.TryFind "public_key" with | Some v -> v.ToString() | None -> "N/A"
let tier = match result.TryFind "tier" with | Some v -> v.ToString() | None -> "N/A"
let status = match result.TryFind "status" with | Some v -> v.ToString() | None -> "N/A"
let expiresAt = match result.TryFind "expires_at" with | Some v -> v.ToString() | None -> "N/A"
let timeRemaining = match result.TryFind "time_remaining" with | Some v -> v.ToString() | None -> "N/A"
let rateLimit = match result.TryFind "rate_limit" with | Some v -> v.ToString() | None -> "N/A"
let burst = match result.TryFind "burst" with | Some v -> v.ToString() | None -> "N/A"
let concurrency = match result.TryFind "concurrency" with | Some v -> v.ToString() | None -> "N/A"
let expired = match result.TryFind "expired" with | Some v -> v.ToString() = "True" | None -> false
if args.KeyExtend && resultPublicKey <> "N/A" then
let extendUrl = sprintf "%s/keys/extend?pk=%s" portalBase resultPublicKey
printfn "%sOpening browser to extend key...%s" blue reset
openBrowser extendUrl
elif expired then
printfn "%sExpired%s" red reset
printfn "Public Key: %s" resultPublicKey
printfn "Tier: %s" tier
printfn "Expired: %s" expiresAt
printfn "%sTo renew: Visit https://unsandbox.com/keys/extend%s" yellow reset
exit 1
else
printfn "%sValid%s" green reset
printfn "Public Key: %s" resultPublicKey
printfn "Tier: %s" tier
printfn "Status: %s" status
printfn "Expires: %s" expiresAt
printfn "Time Remaining: %s" timeRemaining
printfn "Rate Limit: %s" rateLimit
printfn "Burst: %s" burst
printfn "Concurrency: %s" concurrency
with
| :? WebException as ex ->
printfn "%sInvalid%s" red reset
let errorMsg =
if ex.Response <> null then
use reader = new StreamReader(ex.Response.GetResponseStream())
let body = reader.ReadToEnd()
try
let errorResult = parseJson body
match errorResult.TryFind "error" with
| Some err -> err.ToString()
| None -> body
with _ -> body
else
ex.Message
// Check for clock drift error
if errorMsg.Contains("timestamp") && (errorMsg.Contains("401") || errorMsg.Contains("expired") || errorMsg.Contains("invalid")) then
eprintfn "%sError: Request timestamp expired (must be within 5 minutes of server time)%s" red reset
eprintfn "%sYour computer's clock may have drifted.%s" yellow reset
eprintfn "Check your system time and sync with NTP if needed:"
eprintfn " Linux: sudo ntpdate -s time.nist.gov"
eprintfn " macOS: sudo sntp -sS time.apple.com"
eprintfn " Windows: w32tm /resync%s" reset
exit 1
printfn "Reason: %s" errorMsg
exit 1
let cmdLanguages (args: Args) =
let (publicKey, secretKey) = getApiKeys args.ApiKey args.AccountIndex
// Try to load from cache first
let cachedResponse = loadLanguagesCache ()
let response =
match cachedResponse with
| Some cached -> cached
| None ->
// Fetch from API and cache the result
let result = apiRequest "/languages" "GET" None publicKey secretKey
// Extract the languages array as a string for caching
match result.TryFind "languages" with
| Some langs ->
let langStr = langs.ToString()
let languagesJson =
if langStr.StartsWith("[") && langStr.EndsWith("]") then
langStr
else
sprintf "[\"%s\"]" langStr
saveLanguagesCache languagesJson
| None -> ()
// Return the result as a formatted string
sprintf "{\"languages\":%s}" (match result.TryFind "languages" with | Some l -> l.ToString() | None -> "[]")
// Parse the response (either from cache or fresh)
let langStr =
match extractJsonValue response "languages" with
| Some l -> l
| None ->
// Try to find the array directly
let start = response.IndexOf("[")
let endIdx = response.LastIndexOf("]")
if start >= 0 && endIdx > start then
response.Substring(start, endIdx - start + 1)
else
"[]"
let languages =
if langStr.StartsWith("[") && langStr.EndsWith("]") then
langStr.Substring(1, langStr.Length - 2).Split(',')
|> Array.map (fun s -> s.Trim().Trim('"'))
|> Array.filter (fun s -> not (String.IsNullOrEmpty(s)))
else
[| langStr |]
if args.LanguagesJson then
// Output as JSON array
let jsonArray = sprintf "[%s]" (languages |> Array.map (sprintf "\"%s\"") |> String.concat ",")
printfn "%s" jsonArray
else
// Output one language per line
for lang in languages do
printfn "%s" lang
let cmdImage (args: Args) =
let (publicKey, secretKey) = getApiKeys args.ApiKey args.AccountIndex
if args.ImageList then
let result = apiRequest "/images" "GET" None publicKey secretKey
printfn "%s" (toJson (box result))
elif args.ImageInfo.IsSome then
let result = apiRequest (sprintf "/images/%s" args.ImageInfo.Value) "GET" None publicKey secretKey
printfn "%s" (toJson (box result))
elif args.ImageDelete.IsSome then
let result = apiRequestWithSudo (sprintf "/images/%s" args.ImageDelete.Value) "DELETE" None publicKey secretKey
printfn "%sImage deleted: %s%s" green args.ImageDelete.Value reset
elif args.ImageLock.IsSome then
let result = apiRequest (sprintf "/images/%s/lock" args.ImageLock.Value) "POST" None publicKey secretKey
printfn "%sImage locked: %s%s" green args.ImageLock.Value reset
elif args.ImageUnlock.IsSome then
let result = apiRequestWithSudo (sprintf "/images/%s/unlock" args.ImageUnlock.Value) "POST" None publicKey secretKey
printfn "%sImage unlocked: %s%s" green args.ImageUnlock.Value reset
elif args.ImagePublish.IsSome then
if args.ImageSourceType.IsNone then
eprintfn "%sError: --source-type required (service or snapshot)%s" red reset
exit 1
let mutable payload = [("source_type", box args.ImageSourceType.Value); ("source_id", box args.ImagePublish.Value)]
if args.ImageName.IsSome then
payload <- payload @ [("name", box args.ImageName.Value)]
let result = apiRequest "/images/publish" "POST" (Some payload) publicKey secretKey
printfn "%sImage published%s" green reset
printfn "%s" (toJson (box result))
elif args.ImageVisibility.IsSome then
if args.ImageVisibilityMode.IsNone then
eprintfn "%sError: --visibility requires MODE (private, unlisted, or public)%s" red reset
exit 1
let payload = [("visibility", box args.ImageVisibilityMode.Value)]
let result = apiRequest (sprintf "/images/%s/visibility" args.ImageVisibility.Value) "POST" (Some payload) publicKey secretKey
printfn "%sImage visibility set to %s%s" green args.ImageVisibilityMode.Value reset
elif args.ImageSpawn.IsSome then
let mutable payload = []
if args.ImageName.IsSome then
payload <- payload @ [("name", box args.ImageName.Value)]
if args.ImagePorts.IsSome then
let ports = args.ImagePorts.Value.Split(',') |> Array.map (fun p -> box (int (p.Trim())))
payload <- payload @ [("ports", box ports)]
let result = apiRequest (sprintf "/images/%s/spawn" args.ImageSpawn.Value) "POST" (Some payload) publicKey secretKey
printfn "%sService spawned from image%s" green reset
printfn "%s" (toJson (box result))
elif args.ImageClone.IsSome then
let mutable payload = []
if args.ImageName.IsSome then
payload <- payload @ [("name", box args.ImageName.Value)]
let result = apiRequest (sprintf "/images/%s/clone" args.ImageClone.Value) "POST" (Some payload) publicKey secretKey
printfn "%sImage cloned%s" green reset
printfn "%s" (toJson (box result))
else
eprintfn "%sError: Use --list, --info ID, --delete ID, --lock ID, --unlock ID, --publish ID, --visibility ID MODE, --spawn ID, or --clone ID%s" red reset
exit 1
let cmdSnapshot (args: Args) =
let (publicKey, secretKey) = getApiKeys args.ApiKey args.AccountIndex
if args.SnapshotList then
let result = apiRequest "/snapshots" "GET" None publicKey secretKey
printfn "%s" (toJson (box result))
elif args.SnapshotInfo.IsSome then
let result = apiRequest (sprintf "/snapshots/%s" args.SnapshotInfo.Value) "GET" None publicKey secretKey
printfn "%s" (toJson (box result))
elif args.SnapshotDelete.IsSome then
let result = apiRequestWithSudo (sprintf "/snapshots/%s" args.SnapshotDelete.Value) "DELETE" None publicKey secretKey
printfn "%sSnapshot deleted: %s%s" green args.SnapshotDelete.Value reset
elif args.SnapshotClone.IsSome then
if args.SnapshotType.IsNone then
eprintfn "%sError: --type required (session or service)%s" red reset
exit 1
let mutable payload = [("type", box args.SnapshotType.Value)]
if args.SnapshotName.IsSome then
payload <- payload @ [("name", box args.SnapshotName.Value)]
if args.SnapshotShell.IsSome then
payload <- payload @ [("shell", box args.SnapshotShell.Value)]
if args.SnapshotPorts.IsSome then
let ports = args.SnapshotPorts.Value.Split(',') |> Array.map (fun p -> box (int (p.Trim())))
payload <- payload @ [("ports", box ports)]
let result = apiRequest (sprintf "/snapshots/%s/clone" args.SnapshotClone.Value) "POST" (Some payload) publicKey secretKey
printfn "%sCreated from snapshot%s" green reset
printfn "%s" (toJson (box result))
else
eprintfn "%sError: Use --list, --info ID, --delete ID, or --clone ID --type TYPE%s" red reset
exit 1
let cmdService (args: Args) =
let (publicKey, secretKey) = getApiKeys args.ApiKey args.AccountIndex
// Handle env subcommand
if args.EnvAction.IsSome then
cmdServiceEnv args publicKey secretKey
elif args.ServiceSnapshot.IsSome then
let mutable payload = []
if args.ServiceSnapshotName.IsSome then
payload <- payload @ [("name", box args.ServiceSnapshotName.Value)]
if args.ServiceHot then
payload <- payload @ [("hot", box true)]
let result = apiRequest (sprintf "/services/%s/snapshot" args.ServiceSnapshot.Value) "POST" (Some payload) publicKey secretKey
printfn "%sSnapshot created%s" green reset
printfn "%s" (toJson (box result))
elif args.ServiceRestore.IsSome then
// --restore takes snapshot ID directly, calls /snapshots/:id/restore
let result = apiRequest (sprintf "/snapshots/%s/restore" args.ServiceRestore.Value) "POST" None publicKey secretKey
printfn "%sService restored from snapshot%s" green reset
printfn "%s" (toJson (box result))
elif args.ServiceList then
let result = apiRequest "/services" "GET" None publicKey secretKey
printfn "%-20s %-15s %-10s %-15s %s" "ID" "Name" "Status" "Ports" "Domains"
printfn "No services (list parsing not implemented)"
elif args.ServiceInfo.IsSome then
let result = apiRequest (sprintf "/services/%s" args.ServiceInfo.Value) "GET" None publicKey secretKey
printfn "%s" (toJson (box result))
elif args.ServiceLogs.IsSome then
let result = apiRequest (sprintf "/services/%s/logs" args.ServiceLogs.Value) "GET" None publicKey secretKey
match result.TryFind "logs" with
| Some logs -> printfn "%s" (logs.ToString())
| None -> ()
elif args.ServiceTail.IsSome then
let result = apiRequest (sprintf "/services/%s/logs?lines=9000" args.ServiceTail.Value) "GET" None publicKey secretKey
match result.TryFind "logs" with
| Some logs -> printfn "%s" (logs.ToString())
| None -> ()
elif args.ServiceSleep.IsSome then
let result = apiRequest (sprintf "/services/%s/freeze" args.ServiceSleep.Value) "POST" None publicKey secretKey
printfn "%sService frozen: %s%s" green args.ServiceSleep.Value reset
elif args.ServiceWake.IsSome then
let result = apiRequest (sprintf "/services/%s/unfreeze" args.ServiceWake.Value) "POST" None publicKey secretKey
printfn "%sService unfreezing: %s%s" green args.ServiceWake.Value reset
elif args.ServiceDestroy.IsSome then
let result = apiRequestWithSudo (sprintf "/services/%s" args.ServiceDestroy.Value) "DELETE" None publicKey secretKey
printfn "%sService destroyed: %s%s" green args.ServiceDestroy.Value reset
elif args.ServiceResize.IsSome then
if args.Vcpu <= 0 then
eprintfn "%sError: --resize requires --vcpu N (1-8)%s" red reset
exit 1
let payload = [("vcpu", box args.Vcpu)]
let result = apiRequestPatch (sprintf "/services/%s" args.ServiceResize.Value) payload publicKey secretKey
let ram = args.Vcpu * 2
printfn "%sService resized to %d vCPU, %d GB RAM%s" green args.Vcpu ram reset
elif args.ServiceSetUnfreezeOnDemand.IsSome then
let enabledStr = args.ServiceUnfreezeOnDemandValue.Value.ToLower()
let enabled = enabledStr = "true" || enabledStr = "1" || enabledStr = "yes" || enabledStr = "on"
let payload = [("unfreeze_on_demand", box enabled)]
let result = apiRequestPatch (sprintf "/services/%s" args.ServiceSetUnfreezeOnDemand.Value) payload publicKey secretKey
printfn "%sService unfreeze_on_demand set to %b: %s%s" green enabled args.ServiceSetUnfreezeOnDemand.Value reset
elif args.ServiceExecute.IsSome then
let payload = [("command", box args.ServiceCommand.Value)]
let result = apiRequest (sprintf "/services/%s/execute" args.ServiceExecute.Value) "POST" (Some payload) publicKey secretKey
match result.TryFind "stdout" with
| Some stdout when not (String.IsNullOrEmpty(stdout.ToString())) ->
printf "%s%s%s" blue (stdout.ToString()) reset
| _ -> ()
match result.TryFind "stderr" with
| Some stderr when not (String.IsNullOrEmpty(stderr.ToString())) ->
eprintf "%s%s%s" red (stderr.ToString()) reset
| _ -> ()
elif args.ServiceDumpBootstrap.IsSome then
eprintfn "Fetching bootstrap script from %s..." args.ServiceDumpBootstrap.Value
let payload = [("command", box "cat /tmp/bootstrap.sh")]
let result = apiRequest (sprintf "/services/%s/execute" args.ServiceDumpBootstrap.Value) "POST" (Some payload) publicKey secretKey
match result.TryFind "stdout" with
| Some bootstrap when not (String.IsNullOrEmpty(bootstrap.ToString())) ->
let bootstrapText = bootstrap.ToString()
if args.ServiceDumpFile.IsSome then
try
File.WriteAllText(args.ServiceDumpFile.Value, bootstrapText)
printfn "Bootstrap saved to %s" args.ServiceDumpFile.Value
with ex ->
eprintfn "%sError: Could not write to %s: %s%s" red args.ServiceDumpFile.Value ex.Message reset
exit 1
else
printf "%s" bootstrapText
| _ ->
eprintfn "%sError: Failed to fetch bootstrap (service not running or no bootstrap file)%s" red reset
exit 1
elif args.ServiceName.IsSome then
let mutable payload = [("name", box args.ServiceName.Value)]
if args.ServicePorts.IsSome then
let ports = args.ServicePorts.Value.Split(',') |> Array.map (fun p -> box (int (p.Trim())))
payload <- payload @ [("ports", box ports)]
if args.ServiceType.IsSome then
payload <- payload @ [("service_type", box args.ServiceType.Value)]
if args.ServiceBootstrap.IsSome then
payload <- payload @ [("bootstrap", box args.ServiceBootstrap.Value)]
if args.ServiceBootstrapFile.IsSome then
if File.Exists(args.ServiceBootstrapFile.Value) then
let content = File.ReadAllText(args.ServiceBootstrapFile.Value)
payload <- payload @ [("bootstrap_content", box content)]
else
eprintfn "%sError: Bootstrap file not found: %s%s" red args.ServiceBootstrapFile.Value reset
exit 1
if args.Files.Count > 0 then
let inputFiles = args.Files |> Seq.map (fun filepath ->
let content = File.ReadAllBytes(filepath)
[("filename", box (Path.GetFileName(filepath))); ("content_base64", box (Convert.ToBase64String(content)))]
) |> Seq.toList
payload <- payload @ [("input_files", box inputFiles)]
if args.Network.IsSome then
payload <- payload @ [("network", box args.Network.Value)]
if args.Vcpu > 0 then
payload <- payload @ [("vcpu", box args.Vcpu)]
let result = apiRequest "/services" "POST" (Some payload) publicKey secretKey
let serviceId = match result.TryFind "id" with | Some id -> Some (id.ToString()) | None -> None
match serviceId with
| Some id -> printfn "%sService created: %s%s" green id reset
| None -> printfn "%sService created%s" green reset
match result.TryFind "name" with
| Some name -> printfn "Name: %s" (name.ToString())
| None -> ()
match result.TryFind "url" with
| Some url -> printfn "URL: %s" (url.ToString())
| None -> ()
// Auto-set vault if env vars were provided
match serviceId with
| Some id when args.Env.Count > 0 || args.EnvFile.IsSome ->
let envContent = buildEnvContent args.Env args.EnvFile
if not (String.IsNullOrEmpty(envContent)) then
if serviceEnvSet id envContent publicKey secretKey then
printfn "%sVault configured with environment variables%s" green reset
else
eprintfn "%sWarning: Failed to set vault%s" yellow reset
| _ -> ()
else
eprintfn "%sError: Specify --name to create a service, or use --list, --info, etc.%s" red reset
exit 1
let parseArgs (argv: string[]) =
let args = {
Command = None
SourceFile = None
ApiKey = None
AccountIndex = None
Network = None
Vcpu = 0
Env = ResizeArray<string>()
Files = ResizeArray<string>()
Artifacts = false
OutputDir = None
SessionList = false
SessionShell = None
SessionKill = None
SessionSnapshot = None
SessionRestore = None
SessionFrom = None
SessionSnapshotName = None
SessionHot = false
ServiceList = false
LanguagesJson = false
ServiceName = None
ServicePorts = None
ServiceType = None
ServiceBootstrap = None
ServiceBootstrapFile = None
ServiceInfo = None
ServiceLogs = None
ServiceTail = None
ServiceSleep = None
ServiceWake = None
ServiceDestroy = None
ServiceExecute = None
ServiceCommand = None
ServiceDumpBootstrap = None
ServiceDumpFile = None
ServiceResize = None
ServiceSetUnfreezeOnDemand = None
ServiceUnfreezeOnDemandValue = None
ServiceSnapshot = None
ServiceRestore = None
ServiceFrom = None
ServiceSnapshotName = None
ServiceHot = false
SnapshotList = false
SnapshotInfo = None
SnapshotDelete = None
SnapshotClone = None
SnapshotType = None
SnapshotName = None
SnapshotShell = None
SnapshotPorts = None
EnvFile = None
EnvAction = None
EnvTarget = None
KeyExtend = false
ImageList = false
ImageInfo = None
ImageDelete = None
ImageLock = None
ImageUnlock = None
ImagePublish = None
ImageSourceType = None
ImageVisibility = None
ImageVisibilityMode = None
ImageSpawn = None
ImageClone = None
ImageName = None
ImagePorts = None
}
let mutable i = 0
while i < argv.Length do
match argv.[i] with
| "session" -> args.Command <- Some "session"
| "service" -> args.Command <- Some "service"
| "snapshot" -> args.Command <- Some "snapshot"
| "image" -> args.Command <- Some "image"
| "key" -> args.Command <- Some "key"
| "languages" -> args.Command <- Some "languages"
| "--json" when args.Command = Some "languages" -> args.LanguagesJson <- true
| "env" when args.Command = Some "service" ->
// Parse: service env <action> <target>
if i + 1 < argv.Length && not (argv.[i + 1].StartsWith("-")) then
i <- i + 1
args.EnvAction <- Some argv.[i]
if i + 1 < argv.Length && not (argv.[i + 1].StartsWith("-")) then
i <- i + 1
args.EnvTarget <- Some argv.[i]
| "-k" | "--api-key" -> i <- i + 1; args.ApiKey <- Some argv.[i]
| "--account" ->
i <- i + 1
match System.Int32.TryParse(argv.[i]) with
| (true, n) -> args.AccountIndex <- Some n
| _ ->
eprintfn "Error: --account requires an integer argument"
Environment.Exit(1)
| "-n" | "--network" -> i <- i + 1; args.Network <- Some argv.[i]
| "-v" | "--vcpu" -> i <- i + 1; args.Vcpu <- int argv.[i]
| "-e" | "--env" -> i <- i + 1; args.Env.Add(argv.[i])
| "--env-file" -> i <- i + 1; args.EnvFile <- Some argv.[i]
| "-f" | "--files" -> i <- i + 1; args.Files.Add(argv.[i])
| "-a" | "--artifacts" -> args.Artifacts <- true
| "-o" | "--output-dir" -> i <- i + 1; args.OutputDir <- Some argv.[i]
| "-l" | "--list" ->
match args.Command with
| Some "session" -> args.SessionList <- true
| Some "service" -> args.ServiceList <- true
| Some "image" -> args.ImageList <- true
| Some "snapshot" -> args.SnapshotList <- true
| _ -> ()
| "-s" | "--shell" ->
i <- i + 1
match args.Command with
| Some "snapshot" -> args.SnapshotShell <- Some argv.[i]
| _ -> args.SessionShell <- Some argv.[i]
| "--kill" -> i <- i + 1; args.SessionKill <- Some argv.[i]
| "--snapshot" ->
i <- i + 1
match args.Command with
| Some "session" -> args.SessionSnapshot <- Some argv.[i]
| Some "service" -> args.ServiceSnapshot <- Some argv.[i]
| _ -> ()
| "--restore" ->
i <- i + 1
match args.Command with
| Some "session" -> args.SessionRestore <- Some argv.[i]
| Some "service" -> args.ServiceRestore <- Some argv.[i]
| _ -> ()
| "--from" ->
i <- i + 1
match args.Command with
| Some "session" -> args.SessionFrom <- Some argv.[i]
| Some "service" -> args.ServiceFrom <- Some argv.[i]
| _ -> ()
| "--snapshot-name" ->
i <- i + 1
match args.Command with
| Some "session" -> args.SessionSnapshotName <- Some argv.[i]
| Some "service" -> args.ServiceSnapshotName <- Some argv.[i]
| _ -> ()
| "--hot" ->
match args.Command with
| Some "session" -> args.SessionHot <- true
| Some "service" -> args.ServiceHot <- true
| _ -> ()
| "--info" ->
i <- i + 1
match args.Command with
| Some "snapshot" -> args.SnapshotInfo <- Some argv.[i]
| _ -> args.ServiceInfo <- Some argv.[i]
| "--delete" ->
i <- i + 1
match args.Command with
| Some "snapshot" -> args.SnapshotDelete <- Some argv.[i]
| _ -> ()
| "--clone" -> i <- i + 1; args.SnapshotClone <- Some argv.[i]
| "--type" ->
i <- i + 1
match args.Command with
| Some "snapshot" -> args.SnapshotType <- Some argv.[i]
| _ -> args.ServiceType <- Some argv.[i]
| "--name" ->
i <- i + 1
match args.Command with
| Some "snapshot" -> args.SnapshotName <- Some argv.[i]
| Some "image" -> args.ImageName <- Some argv.[i]
| _ -> args.ServiceName <- Some argv.[i]
| "--ports" ->
i <- i + 1
match args.Command with
| Some "snapshot" -> args.SnapshotPorts <- Some argv.[i]
| Some "image" -> args.ImagePorts <- Some argv.[i]
| _ -> args.ServicePorts <- Some argv.[i]
| "--bootstrap" -> i <- i + 1; args.ServiceBootstrap <- Some argv.[i]
| "--bootstrap-file" -> i <- i + 1; args.ServiceBootstrapFile <- Some argv.[i]
| "--logs" -> i <- i + 1; args.ServiceLogs <- Some argv.[i]
| "--tail" -> i <- i + 1; args.ServiceTail <- Some argv.[i]
| "--freeze" -> i <- i + 1; args.ServiceSleep <- Some argv.[i]
| "--unfreeze" -> i <- i + 1; args.ServiceWake <- Some argv.[i]
| "--destroy" -> i <- i + 1; args.ServiceDestroy <- Some argv.[i]
| "--resize" -> i <- i + 1; args.ServiceResize <- Some argv.[i]
| "--set-unfreeze-on-demand" ->
i <- i + 1
args.ServiceSetUnfreezeOnDemand <- Some argv.[i]
if i + 1 < argv.Length && not (argv.[i + 1].StartsWith("-")) then
i <- i + 1
args.ServiceUnfreezeOnDemandValue <- Some argv.[i]
| "--execute" -> i <- i + 1; args.ServiceExecute <- Some argv.[i]
| "--command" -> i <- i + 1; args.ServiceCommand <- Some argv.[i]
| "--dump-bootstrap" -> i <- i + 1; args.ServiceDumpBootstrap <- Some argv.[i]
| "--dump-file" -> i <- i + 1; args.ServiceDumpFile <- Some argv.[i]
| "--extend" -> args.KeyExtend <- true
| "--info" ->
i <- i + 1
match args.Command with
| Some "image" -> args.ImageInfo <- Some argv.[i]
| _ -> args.ServiceInfo <- Some argv.[i]
| "--delete" ->
i <- i + 1
match args.Command with
| Some "image" -> args.ImageDelete <- Some argv.[i]
| Some "snapshot" -> args.SnapshotDelete <- Some argv.[i]
| _ -> ()
| "--lock" ->
i <- i + 1
if args.Command = Some "image" then
args.ImageLock <- Some argv.[i]
| "--unlock" ->
i <- i + 1
if args.Command = Some "image" then
args.ImageUnlock <- Some argv.[i]
| "--publish" ->
i <- i + 1
if args.Command = Some "image" then
args.ImagePublish <- Some argv.[i]
| "--source-type" ->
i <- i + 1
args.ImageSourceType <- Some argv.[i]
| "--visibility" ->
i <- i + 1
if args.Command = Some "image" then
args.ImageVisibility <- Some argv.[i]
if i + 1 < argv.Length && not (argv.[i + 1].StartsWith("-")) then
i <- i + 1
args.ImageVisibilityMode <- Some argv.[i]
| "--spawn" ->
i <- i + 1
if args.Command = Some "image" then
args.ImageSpawn <- Some argv.[i]
| "--clone" ->
i <- i + 1
match args.Command with
| Some "image" -> args.ImageClone <- Some argv.[i]
| Some "snapshot" -> args.SnapshotClone <- Some argv.[i]
| _ -> ()
| arg when not (arg.StartsWith("-")) -> args.SourceFile <- Some arg
| arg ->
if arg.StartsWith("-") && args.Command = Some "session" then
eprintfn "Unknown option: %s" arg
eprintfn "Usage: un.fs session [options]"
Environment.Exit(1)
i <- i + 1
args
let printHelp () =
printfn "Usage: un [options] <source_file>"
printfn " un session [options]"
printfn " un service [options]"
printfn " un service env <action> <service_id> [options]"
printfn " un image [options]"
printfn " un key [options]"
printfn " un languages [--json]"
printfn ""
printfn "Execute options:"
printfn " -e KEY=VALUE Set environment variable"
printfn " -f FILE Add input file"
printfn " -a Return artifacts"
printfn " -o DIR Output directory for artifacts"
printfn " -n MODE Network mode (zerotrust/semitrusted)"
printfn " -v N vCPU count (1-8)"
printfn " -k KEY API key"
printfn " --account N Use accounts.csv row N (bypasses env vars)"
printfn ""
printfn "Session options:"
printfn " --list List active sessions"
printfn " --shell NAME Shell/REPL to use"
printfn " --kill ID Terminate session"
printfn ""
printfn "Service options:"
printfn " --list List services"
printfn " --name NAME Service name"
printfn " --ports PORTS Comma-separated ports"
printfn " --type TYPE Service type (minecraft/mumble/teamspeak/source/tcp/udp)"
printfn " --bootstrap CMD Bootstrap command"
printfn " --info ID Get service details"
printfn " --logs ID Get all logs"
printfn " --tail ID Get last 9000 lines"
printfn " --freeze ID Freeze service"
printfn " --unfreeze ID Unfreeze service"
printfn " --destroy ID Destroy service"
printfn " --resize ID Resize service (requires --vcpu N)"
printfn " --set-unfreeze-on-demand ID true|false"
printfn " Set unfreeze_on_demand for service"
printfn " --execute ID Execute command in service"
printfn " --command CMD Command to execute (with --execute)"
printfn " --dump-bootstrap ID Dump bootstrap script"
printfn " --dump-file FILE File to save bootstrap (with --dump-bootstrap)"
printfn " -e KEY=VALUE Set vault env var (with --name or env set)"
printfn " --env-file FILE Load vault vars from file"
printfn ""
printfn "Service env commands:"
printfn " env status ID Check vault status"
printfn " env set ID Set vault (use -e or --env-file)"
printfn " env export ID Export vault contents"
printfn " env delete ID Delete vault"
printfn ""
printfn "Image options:"
printfn " -l, --list List all images"
printfn " --info ID Get image details"
printfn " --delete ID Delete an image"
printfn " --lock ID Lock image to prevent deletion"
printfn " --unlock ID Unlock image"
printfn " --publish ID Publish image (requires --source-type)"
printfn " --source-type TYPE Source type: service or snapshot"
printfn " --visibility ID MODE Set visibility: private, unlisted, or public"
printfn " --spawn ID Spawn new service from image"
printfn " --clone ID Clone an image"
printfn " --name NAME Name for spawned service or cloned image"
printfn " --ports PORTS Ports for spawned service"
printfn ""
printfn "Key options:"
printfn " --extend Open browser to extend key"
printfn " -k KEY API key to validate"
printfn ""
printfn "Languages options:"
printfn " --json Output as JSON array"
[<EntryPoint>]
let main argv =
try
let args = parseArgs argv
match args.Command with
| Some "session" -> cmdSession args; 0
| Some "service" -> cmdService args; 0
| Some "snapshot" -> cmdSnapshot args; 0
| Some "image" -> cmdImage args; 0
| Some "key" -> cmdKey args; 0
| Some "languages" -> cmdLanguages args; 0
| _ ->
match args.SourceFile with
| Some _ -> cmdExecute args; 0
| None -> printHelp(); 1
with ex ->
eprintfn "%sError: %s%s" red ex.Message reset
1
Aclaraciones de documentación
Dependencias
C Binary (un1) — requiere libcurl y 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
Implementaciones de SDK — la mayoría usa solo stdlib (Ruby, JS, Go, etc). Algunos requieren dependencias mínimas:
pip install requests # Python
Ejecutar Código
Ejecutar un script
./un hello.py
./un app.js
./un main.rs
Con variables de entorno
./un -e DEBUG=1 -e NAME=World script.py
Con Archivos de Entrada (teleportar archivos a sandbox)
./un -f data.csv -f config.json process.py
Obtener binario compilado
./un -a -o ./bin main.c
Sesiones Interactivas
Iniciar Sesión de Shell
# Default bash shell
./un session
# Choose your shell
./un session --shell zsh
./un session --shell fish
# Jump into a REPL
./un session --shell python3
./un session --shell node
./un session --shell julia
Sesión con Acceso a Red
./un session -n semitrusted
Auditoría de Sesión (grabación completa de terminal)
# Record everything (including vim, interactive programs)
./un session --audit -o ./logs
# Replay session later
zcat session.log*.gz | less -R
Recopilar Artefactos de la Sesión
# Files in /tmp/artifacts/ are collected on exit
./un session -a -o ./outputs
Persistencia de Sesión (tmux/screen)
# Default: session terminates on disconnect (clean exit)
./un session
# With tmux: session persists, can reconnect later
./un session --tmux
# Press Ctrl+b then d to detach
# With screen: alternative multiplexer
./un session --screen
# Press Ctrl+a then d to detach
Listar Trabajos Activos
./un session --list
# Output:
# Active sessions: 2
#
# SESSION ID CONTAINER SHELL TTL STATUS
# abc123... unsb-vm-12345 python3 45m30s active
# def456... unsb-vm-67890 bash 1h2m active
Reconectar a Sesión Existente
# Reconnect by container name (requires --tmux or --screen)
./un session --attach unsb-vm-12345
# Use exit to terminate session, or detach to keep it running
Terminar una Sesión
./un session --kill unsb-vm-12345
Shells y REPLs Disponibles
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
Gestión de Claves API
Verificar Estado del Pago
# Check if your API key is valid
./un key
# Output:
# Valid: key expires in 30 days
Extender Clave Expirada
# Open the portal to extend an expired key
./un key --extend
# This opens the unsandbox.com portal where you can
# add more credits to extend your key's expiration
Autenticación
Las credenciales se cargan en orden de prioridad (mayor primero):
# 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
Las solicitudes se firman con HMAC-SHA256. El token bearer contiene solo la clave pública; la clave secreta calcula la firma (nunca se transmite).
Escalado de Recursos
Establecer Conteo de vCPU
# Default: 1 vCPU, 2GB RAM
./un script.py
# Scale up: 4 vCPUs, 8GB RAM
./un -v 4 script.py
# Maximum: 8 vCPUs, 16GB RAM
./un --vcpu 8 heavy_compute.py
Impulso de Sesión en Vivo
# Boost a running session to 2 vCPU, 4GB RAM
./un session --boost sandbox-abc
# Boost to specific vCPU count (4 vCPU, 8GB RAM)
./un session --boost sandbox-abc --boost-vcpu 4
# Return to base resources (1 vCPU, 2GB RAM)
./un session --unboost sandbox-abc
Congelar/Descongelar Sesión
Congelar y Descongelar Sesiones
# 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
Servicios Persistentes
Crear un Servicio
# 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
Administrar Servicios
# 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
Instantáneas
Listar Instantáneas
./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
Crear Instantánea de Sesión
# Snapshot with name
./un session --snapshot unsb-vm-12345 --name "before upgrade"
# Quick snapshot (auto-generated name)
./un session --snapshot unsb-vm-12345
Crear Instantánea de Servicio
# Standard snapshot (pauses container briefly)
./un service --snapshot unsb-service-abc123 --name "stable v1.0"
# Hot snapshot (no pause, may be inconsistent)
./un service --snapshot unsb-service-abc123 --hot
Restaurar desde Instantánea
# 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
Eliminar Instantánea
./un snapshot --delete unsb-snapshot-a1b2-c3d4-e5f6-g7h8
Imágenes
Las imágenes son imágenes de contenedores independientes y transferibles que sobreviven a la eliminación del contenedor. A diferencia de las instantáneas (que permanecen con su contenedor), las imágenes pueden compartirse con otros usuarios, transferirse entre claves API o hacerse públicas en el marketplace.
Listar imágenes
# List all images (owned + shared + public)
./un image --list
# List only your images
./un image --list owned
# List images shared with you
./un image --list shared
# List public marketplace images
./un image --list public
# Get image details
./un image --info unsb-image-xxxx-xxxx-xxxx-xxxx
Publicar imágenes
# 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
Crear servicios desde imágenes
# 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
Protección de imágenes
# 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
Visibilidad y compartir
# Set visibility level
./un image --visibility unsb-image-xxxx-xxxx-xxxx-xxxx private # owner only (default)
./un image --visibility unsb-image-xxxx-xxxx-xxxx-xxxx unlisted # can be shared
./un image --visibility unsb-image-xxxx-xxxx-xxxx-xxxx public # marketplace
# Share with specific user
./un image --grant unsb-image-xxxx-xxxx-xxxx-xxxx \
--key unsb-pk-friend-friend-friend-friend
# Revoke access
./un image --revoke unsb-image-xxxx-xxxx-xxxx-xxxx \
--key unsb-pk-friend-friend-friend-friend
# List who has access
./un image --trusted unsb-image-xxxx-xxxx-xxxx-xxxx
Transferir propiedad
# Transfer image to another API key
./un image --transfer unsb-image-xxxx-xxxx-xxxx-xxxx \
--to unsb-pk-newowner-newowner-newowner-newowner
Referencia de Uso
Usage: ./un [options] <source_file>
./un session [options]
./un service [options]
./un snapshot [options]
./un image [options]
./un key
Commands:
(default) Execute source file in sandbox
session Open interactive shell/REPL session
service Manage persistent services
snapshot Manage container snapshots
image Manage container images (publish, share, transfer)
key Check API key validity and expiration
Options:
-e KEY=VALUE Set environment variable (can use multiple times)
-f FILE Add input file (can use multiple times)
-a Return and save artifacts from /tmp/artifacts/
-o DIR Output directory for artifacts (default: current dir)
-p KEY Public key (or set UNSANDBOX_PUBLIC_KEY env var)
-k KEY Secret key (or set UNSANDBOX_SECRET_KEY env var)
-n MODE Network mode: zerotrust (default) or semitrusted
-v N, --vcpu N vCPU count 1-8, each vCPU gets 2GB RAM (default: 1)
-y Skip confirmation for large uploads (>1GB)
-h Show this help
Authentication (priority order):
1. -p and -k flags (public and secret key)
2. UNSANDBOX_PUBLIC_KEY + UNSANDBOX_SECRET_KEY env vars
3. ~/.unsandbox/accounts.csv (format: public_key,secret_key per line)
Session options:
-s, --shell SHELL Shell/REPL to use (default: bash)
-l, --list List active sessions
--attach ID Reconnect to existing session (ID or container name)
--kill ID Terminate a session (ID or container name)
--freeze ID Freeze a session (requires --tmux/--screen)
--unfreeze ID Unfreeze a frozen session
--boost ID Boost session resources (2 vCPU, 4GB RAM)
--boost-vcpu N Specify vCPU count for boost (1-8)
--unboost ID Return to base resources
--audit Record full session for auditing
--tmux Enable session persistence with tmux (allows reconnect)
--screen Enable session persistence with screen (allows reconnect)
Service options:
--name NAME Service name (creates new service)
--ports PORTS Comma-separated ports (e.g., 80,443)
--domains DOMAINS Custom domains (e.g., example.com,www.example.com)
--type TYPE Service type: minecraft, mumble, teamspeak, source, tcp, udp
--bootstrap CMD Bootstrap command/file/URL to run on startup
-f FILE Upload file to /tmp/ (can use multiple times)
-l, --list List all services
--info ID Get service details
--tail ID Get last 9000 lines of bootstrap logs
--logs ID Get all bootstrap logs
--freeze ID Freeze a service
--unfreeze ID Unfreeze a service
--auto-unfreeze ID Enable auto-wake on HTTP request
--no-auto-unfreeze ID Disable auto-wake on HTTP request
--show-freeze-page ID Show HTML payment page when frozen (default)
--no-show-freeze-page ID Return JSON error when frozen
--destroy ID Destroy a service
--redeploy ID Re-run bootstrap script (requires --bootstrap)
--execute ID CMD Run a command in a running service
--dump-bootstrap ID [FILE] Dump bootstrap script (for migrations)
--snapshot ID Create snapshot of session or service
--snapshot-name User-friendly name for snapshot
--hot Create snapshot without pausing (may be inconsistent)
--restore ID Restore session/service from snapshot ID
Snapshot options:
-l, --list List all snapshots
--info ID Get snapshot details
--delete ID Delete a snapshot permanently
Image options:
-l, --list [owned|shared|public] List images (all, owned, shared, or public)
--info ID Get image details
--publish-service ID Publish image from stopped/frozen service
--publish-snapshot ID Publish image from snapshot
--name NAME Name for published image
--description DESC Description for published image
--delete ID Delete image (must be unlocked)
--clone ID Clone image (creates copy you own)
--spawn ID Create service from image (requires --name)
--lock ID Lock image to prevent deletion
--unlock ID Unlock image to allow deletion
--visibility ID LEVEL Set visibility (private|unlisted|public)
--grant ID --key KEY Grant access to another API key
--revoke ID --key KEY Revoke access from API key
--transfer ID --to KEY Transfer ownership to API key
--trusted ID List API keys with access
Key options:
(no options) Check API key validity
--extend Open portal to extend an expired key
Examples:
./un script.py # execute Python script
./un -e DEBUG=1 script.py # with environment variable
./un -f data.csv process.py # with input file
./un -a -o ./bin main.c # save compiled artifacts
./un -v 4 heavy.py # with 4 vCPUs, 8GB RAM
./un session # interactive bash session
./un session --tmux # bash with reconnect support
./un session --list # list active sessions
./un session --attach unsb-vm-12345 # reconnect to session
./un session --kill unsb-vm-12345 # terminate a session
./un session --freeze unsb-vm-12345 # freeze session
./un session --unfreeze unsb-vm-12345 # unfreeze session
./un session --boost unsb-vm-12345 # boost resources
./un session --unboost unsb-vm-12345 # return to base
./un session --shell python3 # Python REPL
./un session --shell node # Node.js REPL
./un session -n semitrusted # session with network access
./un session --audit -o ./logs # record session for auditing
./un service --name web --ports 80 # create web service
./un service --list # list all services
./un service --logs abc123 # view bootstrap logs
./un key # check API key
./un key --extend # extend expired key
./un snapshot --list # list all snapshots
./un session --snapshot unsb-vm-123 # snapshot a session
./un service --snapshot abc123 # snapshot a service
./un session --restore unsb-snapshot-xxxx # restore from snapshot
./un image --list # list all images
./un image --list owned # list your images
./un image --publish-service abc # publish image from service
./un image --spawn img123 --name x # create service from image
./un image --grant img --key pk # share image with user
CLI Inception
El CLI UN ha sido implementado en 42 lenguajes de programación, demostrando que la API de unsandbox puede ser accedida desde virtualmente cualquier entorno.
Licencia
DOMINIO PÚBLICO - SIN LICENCIA, SIN GARANTÍA
Este es software gratuito de dominio público para el bien público de una permacomputadora
alojada en permacomputer.com - una computadora siempre activa del pueblo, para el pueblo. Una
que es duradera, fácil de reparar y distribuida como agua del grifo para la inteligencia de
aprendizaje automático.
La permacomputadora es infraestructura de propiedad comunitaria optimizada en torno a cuatro valores:
VERDAD - Primeros principios, matemáticas & ciencia, código abierto distribuido libremente
LIBERTAD - Asociaciones voluntarias, libertad de la tiranía y el control corporativo
ARMONÍA - Mínimo desperdicio, sistemas auto-renovables con diversas conexiones prósperas
AMOR - Sé tú mismo sin dañar a otros, cooperación a través de la ley natural
Este software contribuye a esa visión al permitir la ejecución de código a través de más de 42
lenguajes de programación mediante una interfaz unificada, accesible para todos. El código es
semillas para brotar en cualquier tecnología abandonada.
Aprende más: https://www.permacomputer.com
Cualquiera es libre de copiar, modificar, publicar, usar, compilar, vender o distribuir este
software, ya sea en forma de código fuente o como binario compilado, para cualquier propósito,
comercial o no comercial, y por cualquier medio.
SIN GARANTÍA. EL SOFTWARE SE PROPORCIONA "TAL CUAL" SIN GARANTÍA DE NINGÚN TIPO.
Dicho esto, el estrato de membrana digital de nuestra permacomputadora ejecuta continuamente
pruebas unitarias, de integración y funcionales en todo su propio software - con nuestra
permacomputadora monitoreándose a sí misma, reparándose a sí misma, con mínima guía humana
en el ciclo. Nuestros agentes hacen su mejor esfuerzo.
Copyright 2025 TimeHexOn & foxhop & russell@unturf
https://www.timehexon.com
https://www.foxhop.net
https://www.unturf.com/software