CLI
Cliente de linha de comando rápido para execução de código e sessões interativas. Mais de 42 linguagens, mais de 30 shells/REPLs.
Documentação Oficial OpenAPI Swagger ↗Início Rápido — Kotlin
# Download + setup
curl -O https://git.unturf.com/engineering/unturf/un-inception/-/raw/main/clients/kotlin/sync/src/un.kt
export UNSANDBOX_PUBLIC_KEY="unsb-pk-xxxx-xxxx-xxxx-xxxx"
export UNSANDBOX_SECRET_KEY="unsb-sk-xxxxx-xxxxx-xxxxx-xxxxx"
# Run code
./un script.kt
Baixar
Guia de Instalação →Características:
- 42+ languages - Python, JS, Go, Rust, C++, Java...
- Sessions - 30+ shells/REPLs, tmux persistence
- Files - Upload files, collect artifacts
- Services - Persistent containers with domains
- Snapshots - Point-in-time backups
- Images - Publish, share, transfer
Início Rápido de Integração ⚡
Adicione superpoderes unsandbox ao seu app Kotlin existente:
curl -O https://git.unturf.com/engineering/unturf/un-inception/-/raw/main/clients/kotlin/sync/src/un.kt
# 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 Kotlin app:
import un.executeCode
fun main() {
val result = executeCode("kotlin", "println(\"Hello from Kotlin running on unsandbox!\")")
println(result["stdout"]) // Hello from Kotlin running on unsandbox!
}
kotlinc MyApp.kt Un.kt -include-runtime -d myapp.jar && java -jar myapp.jar
7d977c7b5fba8e5e4892a86dc1177a9e
SHA256: c4fbc4312dcbfcd3f9c8135128624620dc456682e9056e2332d8e90fdb852b0a
// 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.kt - Unsandbox CLI Client (Kotlin Implementation)
// Compile: kotlinc un.kt -include-runtime -d un.jar
// Run: java -jar un.jar [options] <source_file>
// Requires: UNSANDBOX_API_KEY environment variable
import java.io.File
import java.net.HttpURLConnection
import java.net.URL
import java.util.Base64
import kotlin.system.exitProcess
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec
val API_BASE = "https://api.unsandbox.com"
val PORTAL_BASE = "https://unsandbox.com"
val LANGUAGES_CACHE_TTL = 3600L // 1 hour in seconds
val BLUE = "\u001B[34m"
val RED = "\u001B[31m"
val GREEN = "\u001B[32m"
val YELLOW = "\u001B[33m"
val RESET = "\u001B[0m"
val EXT_MAP = mapOf(
".py" to "python", ".js" to "javascript", ".ts" to "typescript",
".rb" to "ruby", ".php" to "php", ".pl" to "perl", ".lua" to "lua",
".sh" to "bash", ".go" to "go", ".rs" to "rust", ".c" to "c",
".cpp" to "cpp", ".cc" to "cpp", ".cxx" to "cpp",
".java" to "java", ".kt" to "kotlin", ".cs" to "csharp", ".fs" to "fsharp",
".hs" to "haskell", ".ml" to "ocaml", ".clj" to "clojure", ".scm" to "scheme",
".lisp" to "commonlisp", ".erl" to "erlang", ".ex" to "elixir", ".exs" to "elixir",
".jl" to "julia", ".r" to "r", ".R" to "r", ".cr" to "crystal",
".d" to "d", ".nim" to "nim", ".zig" to "zig", ".v" to "v",
".dart" to "dart", ".groovy" to "groovy", ".scala" to "scala",
".f90" to "fortran", ".f95" to "fortran", ".cob" to "cobol",
".pro" to "prolog", ".forth" to "forth", ".4th" to "forth",
".tcl" to "tcl", ".raku" to "raku", ".m" to "objc"
)
data class Args(
var command: String? = null,
var sourceFile: String? = null,
var apiKey: String? = null,
var network: String? = null,
var vcpu: Int = 0,
val env: MutableList<String> = mutableListOf(),
val files: MutableList<String> = mutableListOf(),
var artifacts: Boolean = false,
var outputDir: String? = null,
var sessionList: Boolean = false,
var sessionShell: String? = null,
var sessionKill: String? = null,
var serviceList: Boolean = false,
var serviceName: String? = null,
var servicePorts: String? = null,
var serviceType: String? = null,
var serviceBootstrap: String? = null,
var serviceBootstrapFile: String? = null,
var serviceInfo: String? = null,
var serviceLogs: String? = null,
var serviceTail: String? = null,
var serviceSleep: String? = null,
var serviceWake: String? = null,
var serviceDestroy: String? = null,
var serviceExecute: String? = null,
var serviceCommand: String? = null,
var serviceDumpBootstrap: String? = null,
var serviceDumpFile: String? = null,
var serviceResize: String? = null,
var serviceUnfreezeOnDemand: String? = null,
var serviceUnfreezeOnDemandValue: Boolean? = null,
var serviceShowFreezePage: String? = null,
var serviceshowFreezePageValue: Boolean? = null,
var keyExtend: Boolean = false,
var envFile: String? = null,
var envAction: String? = null,
var envTarget: String? = null,
var jsonOutput: Boolean = false,
var imageList: Boolean = false,
var imageInfo: String? = null,
var imageDelete: String? = null,
var imageLock: String? = null,
var imageUnlock: String? = null,
var imagePublish: String? = null,
var imageSourceType: String? = null,
var imageVisibility: String? = null,
var imageVisibilityMode: String? = null,
var imageSpawn: String? = null,
var imageClone: String? = null,
var imageName: String? = null,
var imagePorts: String? = null,
var accountIndex: Int = -1
)
fun main(args: Array<String>) {
try {
val parsedArgs = parseArgs(args)
when (parsedArgs.command) {
"session" -> cmdSession(parsedArgs)
"service" -> cmdService(parsedArgs)
"languages" -> cmdLanguages(parsedArgs)
"key" -> cmdKey(parsedArgs)
"image" -> cmdImage(parsedArgs)
else -> if (parsedArgs.sourceFile != null) {
cmdExecute(parsedArgs)
} else {
printHelp()
exitProcess(1)
}
}
} catch (e: Exception) {
System.err.println("${RED}Error: ${e.message}${RESET}")
exitProcess(1)
}
}
fun cmdExecute(args: Args) {
val (publicKey, secretKey) = getApiKeys(args.apiKey, args.accountIndex)
val code = File(args.sourceFile!!).readText()
val language = detectLanguage(args.sourceFile!!)
val payload = mutableMapOf<String, Any>(
"language" to language,
"code" to code
)
if (args.env.isNotEmpty()) {
val envVars = mutableMapOf<String, String>()
for (e in args.env) {
val parts = e.split("=", limit = 2)
if (parts.size == 2) {
envVars[parts[0]] = parts[1]
}
}
if (envVars.isNotEmpty()) {
payload["env"] = envVars
}
}
if (args.files.isNotEmpty()) {
val inputFiles = mutableListOf<Map<String, String>>()
for (filepath in args.files) {
val content = File(filepath).readBytes()
inputFiles.add(mapOf(
"filename" to File(filepath).name,
"content_base64" to Base64.getEncoder().encodeToString(content)
))
}
payload["input_files"] = inputFiles
}
if (args.artifacts) {
payload["return_artifacts"] = true
}
if (args.network != null) {
payload["network"] = args.network!!
}
if (args.vcpu > 0) {
payload["vcpu"] = args.vcpu
}
val result = apiRequest("/execute", "POST", payload, publicKey, secretKey)
val stdout = result["stdout"] as? String
val stderr = result["stderr"] as? String
if (!stdout.isNullOrEmpty()) {
print("$BLUE$stdout$RESET")
}
if (!stderr.isNullOrEmpty()) {
System.err.print("$RED$stderr$RESET")
}
if (args.artifacts && result.containsKey("artifacts")) {
@Suppress("UNCHECKED_CAST")
val artifacts = result["artifacts"] as? List<Map<String, String>>
val outDir = args.outputDir ?: "."
File(outDir).mkdirs()
artifacts?.forEach { artifact ->
val filename = artifact["filename"] ?: "artifact"
val content = Base64.getDecoder().decode(artifact["content_base64"])
val file = File(outDir, filename)
file.writeBytes(content)
file.setExecutable(true)
System.err.println("${GREEN}Saved: ${file.path}${RESET}")
}
}
val exitCode = (result["exit_code"] as? Number)?.toInt() ?: 0
exitProcess(exitCode)
}
fun cmdSession(args: Args) {
val (publicKey, secretKey) = getApiKeys(args.apiKey, args.accountIndex)
if (args.sessionList) {
val result = apiRequest("/sessions", "GET", null, publicKey, secretKey)
@Suppress("UNCHECKED_CAST")
val sessions = result["sessions"] as? List<Map<String, Any>>
if (sessions.isNullOrEmpty()) {
println("No active sessions")
} else {
println("%-40s %-10s %-10s %s".format("ID", "Shell", "Status", "Created"))
for (s in sessions) {
println("%-40s %-10s %-10s %s".format(
s["id"] ?: "N/A",
s["shell"] ?: "N/A",
s["status"] ?: "N/A",
s["created_at"] ?: "N/A"
))
}
}
return
}
if (args.sessionKill != null) {
apiRequest("/sessions/${args.sessionKill}", "DELETE", null, publicKey, secretKey)
println("${GREEN}Session terminated: ${args.sessionKill}${RESET}")
return
}
val payload = mutableMapOf<String, Any>(
"shell" to (args.sessionShell ?: "bash")
)
if (args.network != null) {
payload["network"] = args.network!!
}
if (args.vcpu > 0) {
payload["vcpu"] = args.vcpu
}
// Add input files
if (args.files.isNotEmpty()) {
val inputFiles = args.files.map { filepath ->
val file = java.io.File(filepath)
if (!file.exists()) {
System.err.println("${RED}Error: Input file not found: $filepath${RESET}")
exitProcess(1)
}
mapOf(
"filename" to file.name,
"content_base64" to java.util.Base64.getEncoder().encodeToString(file.readBytes())
)
}
payload["input_files"] = inputFiles
}
println("${YELLOW}Creating session...${RESET}")
val result = apiRequest("/sessions", "POST", payload, publicKey, secretKey)
println("${GREEN}Session created: ${result["id"] ?: "N/A"}${RESET}")
println("${YELLOW}(Interactive sessions require WebSocket - use un2 for full support)${RESET}")
}
fun cmdService(args: Args) {
val (publicKey, secretKey) = getApiKeys(args.apiKey, args.accountIndex)
// Handle env subcommand
if (args.envAction != null) {
cmdServiceEnv(args)
return
}
if (args.serviceList) {
val result = apiRequest("/services", "GET", null, publicKey, secretKey)
@Suppress("UNCHECKED_CAST")
val services = result["services"] as? List<Map<String, Any>>
if (services.isNullOrEmpty()) {
println("No services")
} else {
println("%-20s %-15s %-10s %-15s %s".format("ID", "Name", "Status", "Ports", "Domains"))
for (s in services) {
@Suppress("UNCHECKED_CAST")
val ports = (s["ports"] as? List<Int>)?.joinToString(",") ?: ""
@Suppress("UNCHECKED_CAST")
val domains = (s["domains"] as? List<String>)?.joinToString(",") ?: ""
println("%-20s %-15s %-10s %-15s %s".format(
s["id"] ?: "N/A",
s["name"] ?: "N/A",
s["status"] ?: "N/A",
ports, domains
))
}
}
return
}
if (args.serviceInfo != null) {
val result = apiRequest("/services/${args.serviceInfo}", "GET", null, publicKey, secretKey)
println(toJson(result))
return
}
if (args.serviceLogs != null) {
val result = apiRequest("/services/${args.serviceLogs}/logs", "GET", null, publicKey, secretKey)
println(result["logs"] ?: "")
return
}
if (args.serviceTail != null) {
val result = apiRequest("/services/${args.serviceTail}/logs?lines=9000", "GET", null, publicKey, secretKey)
println(result["logs"] ?: "")
return
}
if (args.serviceSleep != null) {
apiRequest("/services/${args.serviceSleep}/freeze", "POST", null, publicKey, secretKey)
println("${GREEN}Service frozen: ${args.serviceSleep}${RESET}")
return
}
if (args.serviceWake != null) {
apiRequest("/services/${args.serviceWake}/unfreeze", "POST", null, publicKey, secretKey)
println("${GREEN}Service unfreezing: ${args.serviceWake}${RESET}")
return
}
if (args.serviceDestroy != null) {
apiRequestDestructive("/services/${args.serviceDestroy}", "DELETE", null, publicKey, secretKey)
println("${GREEN}Service destroyed: ${args.serviceDestroy}${RESET}")
return
}
if (args.serviceResize != null) {
if (args.vcpu <= 0) {
System.err.println("${RED}Error: --resize requires --vcpu N (1-8)${RESET}")
exitProcess(1)
}
val payload = mapOf("vcpu" to args.vcpu)
apiRequestPatch("/services/${args.serviceResize}", payload, publicKey, secretKey)
val ram = args.vcpu * 2
println("${GREEN}Service resized to ${args.vcpu} vCPU, ${ram} GB RAM${RESET}")
return
}
if (args.serviceUnfreezeOnDemand != null) {
if (args.serviceUnfreezeOnDemandValue == null) {
System.err.println("${RED}Error: --unfreeze-on-demand requires true or false${RESET}")
exitProcess(1)
}
val payload = mapOf("unfreeze_on_demand" to args.serviceUnfreezeOnDemandValue!!)
apiRequestPatch("/services/${args.serviceUnfreezeOnDemand}", payload, publicKey, secretKey)
println("${GREEN}Service unfreeze_on_demand set to ${args.serviceUnfreezeOnDemandValue}: ${args.serviceUnfreezeOnDemand}${RESET}")
return
}
if (args.serviceShowFreezePage != null) {
if (args.serviceshowFreezePageValue == null) {
System.err.println("${RED}Error: --show-freeze-page requires true or false${RESET}")
exitProcess(1)
}
val payload = mapOf("show_freeze_page" to args.serviceshowFreezePageValue!!)
apiRequestPatch("/services/${args.serviceShowFreezePage}", payload, publicKey, secretKey)
println("${GREEN}Service show_freeze_page set to ${args.serviceshowFreezePageValue}: ${args.serviceShowFreezePage}${RESET}")
return
}
if (args.serviceExecute != null) {
val payload = mutableMapOf<String, Any>("command" to args.serviceCommand!!)
val result = apiRequest("/services/${args.serviceExecute}/execute", "POST", payload, publicKey, secretKey)
if (result.containsKey("stdout")) {
val stdout = result["stdout"] as? String
if (stdout != null && stdout.isNotEmpty()) {
print("$BLUE$stdout$RESET")
}
}
if (result.containsKey("stderr")) {
val stderr = result["stderr"] as? String
if (stderr != null && stderr.isNotEmpty()) {
System.err.print("$RED$stderr$RESET")
}
}
return
}
if (args.serviceDumpBootstrap != null) {
System.err.println("Fetching bootstrap script from ${args.serviceDumpBootstrap}...")
val payload = mutableMapOf<String, Any>("command" to "cat /tmp/bootstrap.sh")
val result = apiRequest("/services/${args.serviceDumpBootstrap}/execute", "POST", payload, publicKey, secretKey)
val bootstrap = result["stdout"] as? String
if (bootstrap != null && bootstrap.isNotEmpty()) {
if (args.serviceDumpFile != null) {
try {
val file = java.io.File(args.serviceDumpFile!!)
file.writeText(bootstrap)
file.setExecutable(true)
println("Bootstrap saved to ${args.serviceDumpFile}")
} catch (e: Exception) {
System.err.println("${RED}Error: Could not write to ${args.serviceDumpFile}: ${e.message}${RESET}")
exitProcess(1)
}
} else {
print(bootstrap)
}
} else {
System.err.println("${RED}Error: Failed to fetch bootstrap (service not running or no bootstrap file)${RESET}")
exitProcess(1)
}
return
}
if (args.serviceName != null) {
val payload = mutableMapOf<String, Any>("name" to args.serviceName!!)
if (args.servicePorts != null) {
payload["ports"] = args.servicePorts!!.split(",").map { it.trim().toInt() }
}
if (args.serviceType != null) {
payload["service_type"] = args.serviceType!!
}
if (args.serviceBootstrap != null) {
payload["bootstrap"] = args.serviceBootstrap!!
}
if (args.serviceBootstrapFile != null) {
val file = File(args.serviceBootstrapFile!!)
if (file.exists()) {
payload["bootstrap_content"] = file.readText()
} else {
System.err.println("${RED}Error: Bootstrap file not found: ${args.serviceBootstrapFile}${RESET}")
exitProcess(1)
}
}
// Add input files
if (args.files.isNotEmpty()) {
val inputFiles = args.files.map { filepath ->
val file = java.io.File(filepath)
if (!file.exists()) {
System.err.println("${RED}Error: Input file not found: $filepath${RESET}")
exitProcess(1)
}
mapOf(
"filename" to file.name,
"content_base64" to java.util.Base64.getEncoder().encodeToString(file.readBytes())
)
}
payload["input_files"] = inputFiles
}
if (args.network != null) {
payload["network"] = args.network!!
}
if (args.vcpu > 0) {
payload["vcpu"] = args.vcpu
}
val result = apiRequest("/services", "POST", payload, publicKey, secretKey)
val serviceId = result["id"] as? String
println("${GREEN}Service created: ${serviceId ?: "N/A"}${RESET}")
println("Name: ${result["name"] ?: "N/A"}")
if (result.containsKey("url")) {
println("URL: ${result["url"]}")
}
// Auto-set vault if env vars were provided
if (serviceId != null && (args.env.isNotEmpty() || args.envFile != null)) {
val envContent = buildEnvContent(args.env, args.envFile)
if (envContent.isNotEmpty()) {
if (serviceEnvSet(serviceId, envContent, publicKey, secretKey)) {
println("${GREEN}Vault configured with environment variables${RESET}")
} else {
println("${YELLOW}Warning: Failed to set vault${RESET}")
}
}
}
return
}
System.err.println("${RED}Error: Specify --name to create a service, or use --list, --info, etc.${RESET}")
exitProcess(1)
}
fun getLanguagesCachePath(): String {
val home = System.getenv("HOME") ?: System.getProperty("user.home") ?: "."
return "$home/.unsandbox/languages.json"
}
fun loadLanguagesCache(): List<String>? {
try {
val cacheFile = File(getLanguagesCachePath())
if (!cacheFile.exists()) return null
// Check if cache is fresh (< 1 hour old)
val ageSeconds = (System.currentTimeMillis() - cacheFile.lastModified()) / 1000
if (ageSeconds >= LANGUAGES_CACHE_TTL) return null
val content = cacheFile.readText()
val parsed = parseJson(content)
@Suppress("UNCHECKED_CAST")
return parsed["languages"] as? List<String>
} catch (e: Exception) {
return null
}
}
fun saveLanguagesCache(languages: List<String>) {
try {
val cachePath = getLanguagesCachePath()
val cacheDir = File(cachePath).parentFile
if (!cacheDir.exists()) {
cacheDir.mkdirs()
}
val timestamp = System.currentTimeMillis() / 1000
val data = mapOf("languages" to languages, "timestamp" to timestamp)
File(cachePath).writeText(toJson(data))
} catch (e: Exception) {
// Cache failures are non-fatal
}
}
fun cmdLanguages(args: Args) {
val (publicKey, secretKey) = getApiKeys(args.apiKey, args.accountIndex)
// Try cache first
var languages = loadLanguagesCache()
if (languages == null) {
val result = apiRequest("/languages", "GET", null, publicKey, secretKey)
@Suppress("UNCHECKED_CAST")
languages = result["languages"] as? List<String> ?: emptyList()
// Save to cache
saveLanguagesCache(languages)
}
if (args.jsonOutput) {
println(toJson(languages))
} else {
for (lang in languages) {
println(lang)
}
}
}
fun cmdImage(args: Args) {
val (publicKey, secretKey) = getApiKeys(args.apiKey, args.accountIndex)
if (args.imageList) {
val result = apiRequest("/images", "GET", null, publicKey, secretKey)
println(toJson(result))
return
}
if (args.imageInfo != null) {
val result = apiRequest("/images/${args.imageInfo}", "GET", null, publicKey, secretKey)
println(toJson(result))
return
}
if (args.imageDelete != null) {
apiRequestDestructive("/images/${args.imageDelete}", "DELETE", null, publicKey, secretKey)
println("${GREEN}Image deleted: ${args.imageDelete}${RESET}")
return
}
if (args.imageLock != null) {
apiRequest("/images/${args.imageLock}/lock", "POST", null, publicKey, secretKey)
println("${GREEN}Image locked: ${args.imageLock}${RESET}")
return
}
if (args.imageUnlock != null) {
apiRequestDestructive("/images/${args.imageUnlock}/unlock", "POST", null, publicKey, secretKey)
println("${GREEN}Image unlocked: ${args.imageUnlock}${RESET}")
return
}
if (args.imagePublish != null) {
if (args.imageSourceType == null) {
System.err.println("${RED}Error: --source-type required (service or snapshot)${RESET}")
exitProcess(1)
}
val payload = mutableMapOf<String, Any>(
"source_type" to args.imageSourceType!!,
"source_id" to args.imagePublish!!
)
if (args.imageName != null) {
payload["name"] = args.imageName!!
}
val result = apiRequest("/images/publish", "POST", payload, publicKey, secretKey)
println("${GREEN}Image published${RESET}")
println(toJson(result))
return
}
if (args.imageVisibility != null) {
if (args.imageVisibilityMode == null) {
System.err.println("${RED}Error: --visibility requires MODE (private, unlisted, or public)${RESET}")
exitProcess(1)
}
val payload = mapOf("visibility" to args.imageVisibilityMode!!)
apiRequest("/images/${args.imageVisibility}/visibility", "POST", payload, publicKey, secretKey)
println("${GREEN}Image visibility set to ${args.imageVisibilityMode}: ${args.imageVisibility}${RESET}")
return
}
if (args.imageSpawn != null) {
val payload = mutableMapOf<String, Any>()
if (args.imageName != null) {
payload["name"] = args.imageName!!
}
if (args.imagePorts != null) {
payload["ports"] = args.imagePorts!!.split(",").map { it.trim().toInt() }
}
val result = apiRequest("/images/${args.imageSpawn}/spawn", "POST", payload, publicKey, secretKey)
println("${GREEN}Service spawned from image${RESET}")
println(toJson(result))
return
}
if (args.imageClone != null) {
val payload = mutableMapOf<String, Any>()
if (args.imageName != null) {
payload["name"] = args.imageName!!
}
val result = apiRequest("/images/${args.imageClone}/clone", "POST", payload, publicKey, secretKey)
println("${GREEN}Image cloned${RESET}")
println(toJson(result))
return
}
System.err.println("${RED}Error: Use --list, --info ID, --delete ID, --lock ID, --unlock ID, --publish ID, --visibility ID MODE, --spawn ID, or --clone ID${RESET}")
exitProcess(1)
}
fun cmdKey(args: Args) {
val (publicKey, secretKey) = getApiKeys(args.apiKey, args.accountIndex)
val result = validateKey(publicKey, secretKey)
val valid = result["valid"] as? Boolean ?: false
val expired = result["expired"] as? Boolean ?: false
val pubKey = result["public_key"] as? String ?: ""
val tier = result["tier"] as? String ?: ""
val expiresAt = result["expires_at"] as? String ?: ""
if (args.keyExtend) {
if (pubKey.isEmpty()) {
System.err.println("${RED}Error: Could not retrieve public key${RESET}")
exitProcess(1)
}
val extendUrl = "$PORTAL_BASE/keys/extend?pk=$pubKey"
println("${YELLOW}Opening browser to extend key...${RESET}")
println(extendUrl)
// Try to open browser using common commands
val osName = System.getProperty("os.name").lowercase()
val openCmd = when {
osName.contains("mac") || osName.contains("darwin") -> "open"
osName.contains("win") -> "start"
else -> "xdg-open"
}
try {
Runtime.getRuntime().exec(arrayOf(openCmd, extendUrl))
} catch (e: Exception) {
println("${YELLOW}Could not open browser automatically. Please visit the URL above.${RESET}")
}
return
}
if (expired) {
println("${RED}Status: Expired${RESET}")
println("Public Key: $pubKey")
println("Tier: $tier")
println("Expired: $expiresAt")
println("${YELLOW}To renew: Visit $PORTAL_BASE/keys/extend${RESET}")
} else if (valid) {
println("${GREEN}Status: Valid${RESET}")
println("Public Key: $pubKey")
println("Tier: $tier")
println("Expires: $expiresAt")
} else {
println("${RED}Status: Invalid${RESET}")
exitProcess(1)
}
}
fun validateKey(publicKey: String?, secretKey: String): Map<String, Any> {
val timestamp = System.currentTimeMillis() / 1000
val method = "POST"
val path = "/keys/validate"
val body = ""
val signatureData = "$timestamp:$method:$path:$body"
val signature = hmacSha256(secretKey, signatureData)
val url = URL("$PORTAL_BASE$path")
val connection = url.openConnection() as HttpURLConnection
connection.requestMethod = method
connection.setRequestProperty("Authorization", "Bearer ${publicKey ?: secretKey}")
connection.setRequestProperty("X-Timestamp", timestamp.toString())
connection.setRequestProperty("X-Signature", signature)
connection.setRequestProperty("Content-Type", "application/json")
connection.connectTimeout = 30000
connection.readTimeout = 30000
if (connection.responseCode !in 200..299) {
val error = connection.errorStream?.bufferedReader()?.readText() ?: ""
throw RuntimeException("HTTP ${connection.responseCode} - $error")
}
val response = connection.inputStream.bufferedReader().readText()
return parseJson(response)
}
// Load a row from an accounts.csv file (format: public_key,secret_key per line).
// Lines starting with '#' and blank lines are skipped. Returns the Nth data row, or null.
fun loadAccountsCSV(path: String, index: Int): Pair<String, String>? {
val file = java.io.File(path)
if (!file.exists()) return null
var row = 0
for (line in file.readLines()) {
val stripped = line.trim()
if (stripped.isEmpty() || stripped.startsWith("#")) continue
if (row == index) {
val comma = stripped.indexOf(',')
if (comma < 0) return null
return Pair(stripped.substring(0, comma), stripped.substring(comma + 1))
}
row++
}
return null
}
fun getApiKeys(argsKey: String?, accountIndex: Int = -1): Pair<String?, String> {
var publicKey: String? = null
var secretKey: String? = null
if (accountIndex >= 0) {
// --account N: load from ~/.unsandbox/accounts.csv, bypassing env vars
val home = System.getenv("HOME") ?: System.getProperty("user.home") ?: "."
var creds = loadAccountsCSV("$home/.unsandbox/accounts.csv", accountIndex)
if (creds == null) {
creds = loadAccountsCSV("accounts.csv", accountIndex)
}
if (creds != null) {
publicKey = if (argsKey != null) argsKey else creds.first
secretKey = creds.second
}
} else if (argsKey != null) {
secretKey = argsKey
publicKey = System.getenv("UNSANDBOX_PUBLIC_KEY")
} else {
publicKey = System.getenv("UNSANDBOX_PUBLIC_KEY")
secretKey = System.getenv("UNSANDBOX_SECRET_KEY")
if (publicKey == null || secretKey == null) {
val apiKey = System.getenv("UNSANDBOX_API_KEY")
if (apiKey != null && apiKey.isNotEmpty()) {
secretKey = apiKey
}
}
// Try UNSANDBOX_ACCOUNT env var to pick a row
val envAcct = System.getenv("UNSANDBOX_ACCOUNT")
val envAccount = envAcct?.toIntOrNull() ?: -1
if (publicKey.isNullOrEmpty()) {
val home = System.getenv("HOME") ?: System.getProperty("user.home") ?: "."
var creds = loadAccountsCSV("$home/.unsandbox/accounts.csv", if (envAccount >= 0) envAccount else 0)
if (creds == null) {
creds = loadAccountsCSV("accounts.csv", if (envAccount >= 0) envAccount else 0)
}
if (creds != null) {
publicKey = creds.first
secretKey = creds.second
}
}
}
if (secretKey.isNullOrEmpty()) {
System.err.println("${RED}Error: UNSANDBOX_SECRET_KEY or UNSANDBOX_API_KEY not set${RESET}")
exitProcess(1)
}
return Pair(publicKey, secretKey)
}
fun hmacSha256(secretKey: String, data: String): String {
val mac = Mac.getInstance("HmacSHA256")
val keySpec = SecretKeySpec(secretKey.toByteArray(Charsets.UTF_8), "HmacSHA256")
mac.init(keySpec)
val hash = mac.doFinal(data.toByteArray(Charsets.UTF_8))
return hash.joinToString("") { "%02x".format(it) }
}
fun detectLanguage(filename: String): String {
val ext = filename.substringAfterLast('.', "")
if (ext.isEmpty()) {
throw RuntimeException("Cannot detect language: no file extension")
}
return EXT_MAP[".$ext"] ?: throw RuntimeException("Unsupported file extension: .$ext")
}
// Exception for 428 Sudo Challenge
class SudoChallengeException(val challengeId: String?, val responseBody: String) : RuntimeException("Sudo challenge required")
fun apiRequest(endpoint: String, method: String, data: Map<String, Any>?, publicKey: String?, secretKey: String): Map<String, Any> {
val timestamp = System.currentTimeMillis() / 1000
val body = if (data != null) toJson(data) else ""
val signatureData = "$timestamp:$method:$endpoint:$body"
val signature = hmacSha256(secretKey, signatureData)
val url = URL(API_BASE + endpoint)
val connection = url.openConnection() as HttpURLConnection
connection.requestMethod = method
connection.setRequestProperty("Authorization", "Bearer ${publicKey ?: secretKey}")
connection.setRequestProperty("X-Timestamp", timestamp.toString())
connection.setRequestProperty("X-Signature", signature)
connection.setRequestProperty("Content-Type", "application/json")
connection.connectTimeout = 30000
connection.readTimeout = 300000
if (data != null) {
connection.doOutput = true
connection.outputStream.use { it.write(body.toByteArray()) }
}
if (connection.responseCode !in 200..299) {
val error = connection.errorStream?.bufferedReader()?.readText() ?: ""
if (connection.responseCode == 428) {
// Extract challenge_id from response
var challengeId: String? = null
try {
val errorJson = parseJson(error)
challengeId = errorJson["challenge_id"]?.toString()
} catch (e: Exception) {
// Ignore parse errors
}
throw SudoChallengeException(challengeId, error)
}
if (connection.responseCode == 401 && error.lowercase().contains("timestamp")) {
System.err.println("${RED}Error: Request timestamp expired (must be within 5 minutes of server time)${RESET}")
System.err.println("${YELLOW}Your computer's clock may have drifted.${RESET}")
System.err.println("Check your system time and sync with NTP if needed:")
System.err.println(" Linux: sudo ntpdate -s time.nist.gov")
System.err.println(" macOS: sudo sntp -sS time.apple.com")
System.err.println(" Windows: w32tm /resync")
exitProcess(1)
}
throw RuntimeException("HTTP ${connection.responseCode} - $error")
}
val response = connection.inputStream.bufferedReader().readText()
return parseJson(response)
}
fun apiRequestWithSudo(endpoint: String, method: String, data: Map<String, Any>?, publicKey: String?, secretKey: String, otp: String, challengeId: String?): Map<String, Any> {
val timestamp = System.currentTimeMillis() / 1000
val body = if (data != null) toJson(data) else ""
val signatureData = "$timestamp:$method:$endpoint:$body"
val signature = hmacSha256(secretKey, signatureData)
val url = URL(API_BASE + endpoint)
val connection = url.openConnection() as HttpURLConnection
connection.requestMethod = method
connection.setRequestProperty("Authorization", "Bearer ${publicKey ?: secretKey}")
connection.setRequestProperty("X-Timestamp", timestamp.toString())
connection.setRequestProperty("X-Signature", signature)
connection.setRequestProperty("Content-Type", "application/json")
connection.setRequestProperty("X-Sudo-OTP", otp)
if (challengeId != null) {
connection.setRequestProperty("X-Sudo-Challenge", challengeId)
}
connection.connectTimeout = 30000
connection.readTimeout = 300000
if (data != null) {
connection.doOutput = true
connection.outputStream.use { it.write(body.toByteArray()) }
}
if (connection.responseCode !in 200..299) {
val error = connection.errorStream?.bufferedReader()?.readText() ?: ""
throw RuntimeException("HTTP ${connection.responseCode} - $error")
}
val response = connection.inputStream.bufferedReader().readText()
return parseJson(response)
}
fun handleSudoChallenge(challengeId: String?, method: String, endpoint: String, data: Map<String, Any>?, publicKey: String?, secretKey: String): Map<String, Any> {
System.err.println("${YELLOW}Confirmation required. Check your email for a one-time code.${RESET}")
System.err.print("Enter OTP: ")
System.err.flush()
val reader = java.io.BufferedReader(java.io.InputStreamReader(System.`in`))
val otp = reader.readLine()?.trim()
if (otp.isNullOrEmpty()) {
throw RuntimeException("Operation cancelled - no OTP provided")
}
return apiRequestWithSudo(endpoint, method, data, publicKey, secretKey, otp, challengeId)
}
fun apiRequestDestructive(endpoint: String, method: String, data: Map<String, Any>?, publicKey: String?, secretKey: String): Map<String, Any> {
return try {
apiRequest(endpoint, method, data, publicKey, secretKey)
} catch (e: SudoChallengeException) {
handleSudoChallenge(e.challengeId, method, endpoint, data, publicKey, secretKey)
}
}
fun apiRequestPatch(endpoint: String, data: Map<String, Any>, publicKey: String?, secretKey: String): Map<String, Any> {
val timestamp = System.currentTimeMillis() / 1000
val body = toJson(data)
val signatureData = "$timestamp:PATCH:$endpoint:$body"
val signature = hmacSha256(secretKey, signatureData)
val url = URL(API_BASE + endpoint)
val connection = url.openConnection() as HttpURLConnection
connection.requestMethod = "PATCH"
connection.setRequestProperty("Authorization", "Bearer ${publicKey ?: secretKey}")
connection.setRequestProperty("X-Timestamp", timestamp.toString())
connection.setRequestProperty("X-Signature", signature)
connection.setRequestProperty("Content-Type", "application/json")
connection.connectTimeout = 30000
connection.readTimeout = 300000
connection.doOutput = true
connection.outputStream.use { it.write(body.toByteArray()) }
if (connection.responseCode !in 200..299) {
val error = connection.errorStream?.bufferedReader()?.readText() ?: ""
if (connection.responseCode == 401 && error.lowercase().contains("timestamp")) {
System.err.println("${RED}Error: Request timestamp expired (must be within 5 minutes of server time)${RESET}")
System.err.println("${YELLOW}Your computer's clock may have drifted.${RESET}")
System.err.println("Check your system time and sync with NTP if needed:")
System.err.println(" Linux: sudo ntpdate -s time.nist.gov")
System.err.println(" macOS: sudo sntp -sS time.apple.com")
System.err.println(" Windows: w32tm /resync")
exitProcess(1)
}
throw RuntimeException("HTTP ${connection.responseCode} - $error")
}
val response = connection.inputStream.bufferedReader().readText()
return parseJson(response)
}
fun apiRequestText(endpoint: String, method: String, body: String, publicKey: String?, secretKey: String): Pair<Boolean, String> {
val timestamp = System.currentTimeMillis() / 1000
val signatureData = "$timestamp:$method:$endpoint:$body"
val signature = hmacSha256(secretKey, signatureData)
val url = URL(API_BASE + endpoint)
val connection = url.openConnection() as HttpURLConnection
connection.requestMethod = method
connection.setRequestProperty("Authorization", "Bearer ${publicKey ?: secretKey}")
connection.setRequestProperty("X-Timestamp", timestamp.toString())
connection.setRequestProperty("X-Signature", signature)
connection.setRequestProperty("Content-Type", "text/plain")
connection.connectTimeout = 30000
connection.readTimeout = 300000
connection.doOutput = true
connection.outputStream.use { it.write(body.toByteArray()) }
return if (connection.responseCode in 200..299) {
Pair(true, connection.inputStream.bufferedReader().readText())
} else {
Pair(false, connection.errorStream?.bufferedReader()?.readText() ?: "")
}
}
const val MAX_ENV_CONTENT_SIZE = 65536
fun readEnvFile(path: String): String {
val file = File(path)
if (!file.exists()) {
System.err.println("${RED}Error: Env file not found: $path${RESET}")
exitProcess(1)
}
return file.readText()
}
fun buildEnvContent(envs: List<String>, envFile: String?): String {
val lines = mutableListOf<String>()
lines.addAll(envs)
if (envFile != null) {
val content = readEnvFile(envFile)
for (line in content.lines()) {
val trimmed = line.trim()
if (trimmed.isNotEmpty() && !trimmed.startsWith("#")) {
lines.add(trimmed)
}
}
}
return lines.joinToString("\n")
}
fun serviceEnvStatus(serviceId: String, publicKey: String?, secretKey: String): Map<String, Any> {
return apiRequest("/services/$serviceId/env", "GET", null, publicKey, secretKey)
}
fun serviceEnvSet(serviceId: String, envContent: String, publicKey: String?, secretKey: String): Boolean {
if (envContent.length > MAX_ENV_CONTENT_SIZE) {
System.err.println("${RED}Error: Env content exceeds maximum size of 64KB${RESET}")
return false
}
val (success, _) = apiRequestText("/services/$serviceId/env", "PUT", envContent, publicKey, secretKey)
return success
}
fun serviceEnvExport(serviceId: String, publicKey: String?, secretKey: String): Map<String, Any> {
return apiRequest("/services/$serviceId/env/export", "POST", emptyMap(), publicKey, secretKey)
}
fun serviceEnvDelete(serviceId: String, publicKey: String?, secretKey: String): Boolean {
return try {
apiRequest("/services/$serviceId/env", "DELETE", null, publicKey, secretKey)
true
} catch (e: Exception) {
false
}
}
fun cmdServiceEnv(args: Args) {
val (publicKey, secretKey) = getApiKeys(args.apiKey, args.accountIndex)
val action = args.envAction
val target = args.envTarget
when (action) {
"status" -> {
if (target == null) {
System.err.println("${RED}Error: service env status requires service ID${RESET}")
exitProcess(1)
}
val result = serviceEnvStatus(target, publicKey, secretKey)
val hasVault = result["has_vault"] as? Boolean ?: false
if (hasVault) {
println("${GREEN}Vault: configured${RESET}")
val envCount = result["env_count"]
if (envCount != null) println("Variables: $envCount")
val updatedAt = result["updated_at"]
if (updatedAt != null) println("Updated: $updatedAt")
} else {
println("${YELLOW}Vault: not configured${RESET}")
}
}
"set" -> {
if (target == null) {
System.err.println("${RED}Error: service env set requires service ID${RESET}")
exitProcess(1)
}
if (args.env.isEmpty() && args.envFile == null) {
System.err.println("${RED}Error: service env set requires -e or --env-file${RESET}")
exitProcess(1)
}
val envContent = buildEnvContent(args.env, args.envFile)
if (serviceEnvSet(target, envContent, publicKey, secretKey)) {
println("${GREEN}Vault updated for service $target${RESET}")
} else {
System.err.println("${RED}Error: Failed to update vault${RESET}")
exitProcess(1)
}
}
"export" -> {
if (target == null) {
System.err.println("${RED}Error: service env export requires service ID${RESET}")
exitProcess(1)
}
val result = serviceEnvExport(target, publicKey, secretKey)
val content = result["content"] as? String
if (content != null) print(content)
}
"delete" -> {
if (target == null) {
System.err.println("${RED}Error: service env delete requires service ID${RESET}")
exitProcess(1)
}
if (serviceEnvDelete(target, publicKey, secretKey)) {
println("${GREEN}Vault deleted for service $target${RESET}")
} else {
System.err.println("${RED}Error: Failed to delete vault${RESET}")
exitProcess(1)
}
}
else -> {
System.err.println("${RED}Error: Unknown env action: $action${RESET}")
System.err.println("Usage: kotlin UnKt service env <status|set|export|delete> <service_id>")
exitProcess(1)
}
}
}
fun toJson(obj: Any?): String = when (obj) {
null -> "null"
is String -> "\"${obj.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n").replace("\r", "\\r").replace("\t", "\\t")}\""
is Number -> obj.toString()
is Boolean -> obj.toString()
is Map<*, *> -> {
val entries = obj.entries.joinToString(",") { (k, v) ->
"\"$k\":${toJson(v)}"
}
"{$entries}"
}
is List<*> -> {
val items = obj.joinToString(",") { toJson(it) }
"[$items]"
}
else -> toJson(obj.toString())
}
fun parseJson(json: String): Map<String, Any> {
val trimmed = json.trim()
if (!trimmed.startsWith("{")) return emptyMap()
val result = mutableMapOf<String, Any>()
var i = 1
while (i < trimmed.length) {
while (i < trimmed.length && trimmed[i].isWhitespace()) i++
if (trimmed[i] == '}') break
if (trimmed[i] == '"') {
val keyStart = ++i
while (i < trimmed.length && trimmed[i] != '"') {
if (trimmed[i] == '\\') i++
i++
}
val key = trimmed.substring(keyStart, i).replace("\\\"", "\"").replace("\\\\", "\\")
i++
while (i < trimmed.length && (trimmed[i].isWhitespace() || trimmed[i] == ':')) i++
val (value, endIdx) = parseJsonValue(trimmed, i)
result[key] = value
i = endIdx
while (i < trimmed.length && (trimmed[i].isWhitespace() || trimmed[i] == ',')) i++
} else {
i++
}
}
return result
}
fun parseJsonValue(json: String, start: Int): Pair<Any, Int> {
var i = start
while (i < json.length && json[i].isWhitespace()) i++
return when {
json[i] == '"' -> {
i++
val sb = StringBuilder()
var escaped = false
while (i < json.length) {
val c = json[i]
when {
escaped -> {
when (c) {
'n' -> sb.append('\n')
'r' -> sb.append('\r')
't' -> sb.append('\t')
'"' -> sb.append('"')
'\\' -> sb.append('\\')
else -> sb.append(c)
}
escaped = false
}
c == '\\' -> escaped = true
c == '"' -> return Pair(sb.toString(), i + 1)
else -> sb.append(c)
}
i++
}
Pair(sb.toString(), i)
}
json[i] == '{' -> {
var depth = 1
val objStart = i++
while (i < json.length && depth > 0) {
if (json[i] == '{') depth++
else if (json[i] == '}') depth--
i++
}
Pair(parseJson(json.substring(objStart, i)), i)
}
json[i] == '[' -> {
val list = mutableListOf<Any>()
i++
while (i < json.length) {
while (i < json.length && json[i].isWhitespace()) i++
if (json[i] == ']') {
i++
break
}
val (item, endIdx) = parseJsonValue(json, i)
list.add(item)
i = endIdx
while (i < json.length && (json[i].isWhitespace() || json[i] == ',')) i++
}
Pair(list, i)
}
json[i].isDigit() || json[i] == '-' -> {
val numStart = i
while (i < json.length && (json[i].isDigit() || json[i] == '.' || json[i] == '-')) i++
val num = json.substring(numStart, i)
Pair(if (num.contains(".")) num.toDouble() else num.toInt(), i)
}
json.startsWith("true", i) -> Pair(true, i + 4)
json.startsWith("false", i) -> Pair(false, i + 5)
json.startsWith("null", i) -> Pair("null", i + 4)
else -> Pair("null", i)
}
}
fun parseArgs(args: Array<String>): Args {
val result = Args()
var i = 0
while (i < args.size) {
when (args[i]) {
"session" -> result.command = "session"
"service" -> result.command = "service"
"languages" -> result.command = "languages"
"key" -> result.command = "key"
"image" -> result.command = "image"
"--json" -> result.jsonOutput = true
"-k", "--api-key" -> result.apiKey = args[++i]
"-n", "--network" -> result.network = args[++i]
"-v", "--vcpu" -> result.vcpu = args[++i].toInt()
"-e", "--env" -> result.env.add(args[++i])
"-f", "--files" -> result.files.add(args[++i])
"-a", "--artifacts" -> result.artifacts = true
"-o", "--output-dir" -> result.outputDir = args[++i]
"-l", "--list" -> {
when (result.command) {
"session" -> result.sessionList = true
"service" -> result.serviceList = true
"image" -> result.imageList = true
}
}
"-s", "--shell" -> result.sessionShell = args[++i]
"--kill" -> result.sessionKill = args[++i]
"--name" -> {
when (result.command) {
"image" -> result.imageName = args[++i]
else -> result.serviceName = args[++i]
}
}
"--ports" -> {
when (result.command) {
"image" -> result.imagePorts = args[++i]
else -> result.servicePorts = args[++i]
}
}
"--type" -> result.serviceType = args[++i]
"--bootstrap" -> result.serviceBootstrap = args[++i]
"--bootstrap-file" -> result.serviceBootstrapFile = args[++i]
"--logs" -> result.serviceLogs = args[++i]
"--tail" -> result.serviceTail = args[++i]
"--freeze" -> result.serviceSleep = args[++i]
"--unfreeze" -> result.serviceWake = args[++i]
"--destroy" -> result.serviceDestroy = args[++i]
"--resize" -> result.serviceResize = args[++i]
"--unfreeze-on-demand" -> {
result.serviceUnfreezeOnDemand = args[++i]
if (i + 1 < args.size && (args[i + 1] == "true" || args[i + 1] == "false")) {
result.serviceUnfreezeOnDemandValue = args[++i].toBoolean()
}
}
"--show-freeze-page" -> {
result.serviceShowFreezePage = args[++i]
if (i + 1 < args.size && (args[i + 1] == "true" || args[i + 1] == "false")) {
result.serviceshowFreezePageValue = args[++i].toBoolean()
}
}
"--execute" -> result.serviceExecute = args[++i]
"--command" -> result.serviceCommand = args[++i]
"--dump-bootstrap" -> result.serviceDumpBootstrap = args[++i]
"--dump-file" -> result.serviceDumpFile = args[++i]
"--extend" -> result.keyExtend = true
"--account" -> result.accountIndex = args[++i].toInt()
"--env-file" -> result.envFile = args[++i]
"--info" -> {
when (result.command) {
"service" -> result.serviceInfo = args[++i]
"image" -> result.imageInfo = args[++i]
}
}
"--delete" -> {
if (result.command == "image") result.imageDelete = args[++i]
}
"--lock" -> {
if (result.command == "image") result.imageLock = args[++i]
}
"--unlock" -> {
if (result.command == "image") result.imageUnlock = args[++i]
}
"--publish" -> {
if (result.command == "image") result.imagePublish = args[++i]
}
"--source-type" -> result.imageSourceType = args[++i]
"--visibility" -> {
if (result.command == "image") {
result.imageVisibility = args[++i]
if (i + 1 < args.size && !args[i + 1].startsWith("-")) {
result.imageVisibilityMode = args[++i]
}
}
}
"--spawn" -> {
if (result.command == "image") result.imageSpawn = args[++i]
}
"--clone" -> {
if (result.command == "image") result.imageClone = args[++i]
}
"env" -> {
if (result.command == "service" && i + 1 < args.size) {
result.envAction = args[++i]
if (i + 1 < args.size && !args[i + 1].startsWith("-")) {
result.envTarget = args[++i]
}
}
}
else -> {
if (args[i].startsWith("-")) {
System.err.println("${RED}Unknown option: ${args[i]}${RESET}")
kotlin.system.exitProcess(1)
} else {
result.sourceFile = args[i]
}
}
}
i++
}
return result
}
// ============================================================================
// Library Functions (for programmatic use)
// ============================================================================
/**
* SDK version string.
*/
fun version(): String = "4.2.0"
/**
* Check API health status.
*/
fun healthCheck(): Boolean {
return try {
val url = URL("$API_BASE/health")
val connection = url.openConnection() as HttpURLConnection
connection.requestMethod = "GET"
connection.connectTimeout = 5000
connection.readTimeout = 5000
connection.responseCode == 200
} catch (e: Exception) {
false
}
}
/**
* Generate HMAC-SHA256 signature.
*/
fun hmacSign(secretKey: String, message: String): String {
val mac = Mac.getInstance("HmacSHA256")
val keySpec = SecretKeySpec(secretKey.toByteArray(Charsets.UTF_8), "HmacSHA256")
mac.init(keySpec)
return mac.doFinal(message.toByteArray(Charsets.UTF_8)).joinToString("") { "%02x".format(it) }
}
/**
* Execute code synchronously.
*/
fun execute(language: String, code: String, publicKey: String? = null, secretKey: String? = null): Map<String, Any> {
val (pk, sk) = if (publicKey != null && secretKey != null) {
Pair(publicKey, secretKey)
} else {
getApiKeys(null)
}
val payload = mapOf("language" to language, "code" to code)
return apiRequest("/execute", "POST", payload, pk, sk)
}
/**
* Execute code asynchronously, returns job ID.
*/
fun executeAsync(language: String, code: String, publicKey: String? = null, secretKey: String? = null): String? {
val result = execute(language, code, publicKey, secretKey)
return result["job_id"]?.toString()
}
/**
* Get job status and result.
*/
fun getJob(jobId: String, publicKey: String? = null, secretKey: String? = null): Map<String, Any> {
val (pk, sk) = if (publicKey != null && secretKey != null) {
Pair(publicKey, secretKey)
} else {
getApiKeys(null)
}
return apiRequest("/jobs/$jobId", "GET", null, pk, sk)
}
/**
* Wait for job completion with polling.
*/
fun waitJob(jobId: String, publicKey: String? = null, secretKey: String? = null, timeoutMs: Long = 0): Map<String, Any> {
val (pk, sk) = if (publicKey != null && secretKey != null) {
Pair(publicKey, secretKey)
} else {
getApiKeys(null)
}
val pollDelays = intArrayOf(300, 450, 700, 900, 650, 1600, 2000)
val terminalStates = setOf("completed", "failed", "timeout", "cancelled")
val startTime = System.currentTimeMillis()
var pollCount = 0
while (true) {
val delayIdx = minOf(pollCount, pollDelays.size - 1)
Thread.sleep(pollDelays[delayIdx].toLong())
pollCount++
if (timeoutMs > 0 && System.currentTimeMillis() - startTime > timeoutMs) {
throw RuntimeException("Timeout waiting for job $jobId")
}
val result = getJob(jobId, pk, sk)
val status = result["status"]?.toString() ?: ""
if (status in terminalStates) {
return result
}
}
}
/**
* Cancel a running job.
*/
fun cancelJob(jobId: String, publicKey: String? = null, secretKey: String? = null): Map<String, Any> {
val (pk, sk) = if (publicKey != null && secretKey != null) {
Pair(publicKey, secretKey)
} else {
getApiKeys(null)
}
return apiRequest("/jobs/$jobId", "DELETE", null, pk, sk)
}
/**
* List all jobs.
*/
fun listJobs(publicKey: String? = null, secretKey: String? = null): List<Map<String, Any>> {
val (pk, sk) = if (publicKey != null && secretKey != null) {
Pair(publicKey, secretKey)
} else {
getApiKeys(null)
}
val result = apiRequest("/jobs", "GET", null, pk, sk)
@Suppress("UNCHECKED_CAST")
return result["jobs"] as? List<Map<String, Any>> ?: emptyList()
}
/**
* Get supported languages.
*/
fun getLanguages(publicKey: String? = null, secretKey: String? = null): List<String> {
val cached = loadLanguagesCache()
if (cached != null) return cached
val (pk, sk) = if (publicKey != null && secretKey != null) {
Pair(publicKey, secretKey)
} else {
getApiKeys(null)
}
val result = apiRequest("/languages", "GET", null, pk, sk)
@Suppress("UNCHECKED_CAST")
val languages = result["languages"] as? List<String> ?: emptyList()
saveLanguagesCache(languages)
return languages
}
// Session functions
fun sessionList(publicKey: String? = null, secretKey: String? = null): List<Map<String, Any>> {
val (pk, sk) = if (publicKey != null && secretKey != null) Pair(publicKey, secretKey) else getApiKeys(null)
val result = apiRequest("/sessions", "GET", null, pk, sk)
@Suppress("UNCHECKED_CAST")
return result["sessions"] as? List<Map<String, Any>> ?: emptyList()
}
fun sessionGet(sessionId: String, publicKey: String? = null, secretKey: String? = null): Map<String, Any> {
val (pk, sk) = if (publicKey != null && secretKey != null) Pair(publicKey, secretKey) else getApiKeys(null)
return apiRequest("/sessions/$sessionId", "GET", null, pk, sk)
}
fun sessionCreate(networkMode: String? = null, shell: String? = null, publicKey: String? = null, secretKey: String? = null): Map<String, Any> {
val (pk, sk) = if (publicKey != null && secretKey != null) Pair(publicKey, secretKey) else getApiKeys(null)
val payload = mutableMapOf<String, Any>("shell" to (shell ?: "bash"))
if (networkMode != null) payload["network_mode"] = networkMode
return apiRequest("/sessions", "POST", payload, pk, sk)
}
fun sessionDestroy(sessionId: String, publicKey: String? = null, secretKey: String? = null): Map<String, Any> {
val (pk, sk) = if (publicKey != null && secretKey != null) Pair(publicKey, secretKey) else getApiKeys(null)
return apiRequest("/sessions/$sessionId", "DELETE", null, pk, sk)
}
fun sessionFreeze(sessionId: String, publicKey: String? = null, secretKey: String? = null): Map<String, Any> {
val (pk, sk) = if (publicKey != null && secretKey != null) Pair(publicKey, secretKey) else getApiKeys(null)
return apiRequest("/sessions/$sessionId/freeze", "POST", null, pk, sk)
}
fun sessionUnfreeze(sessionId: String, publicKey: String? = null, secretKey: String? = null): Map<String, Any> {
val (pk, sk) = if (publicKey != null && secretKey != null) Pair(publicKey, secretKey) else getApiKeys(null)
return apiRequest("/sessions/$sessionId/unfreeze", "POST", null, pk, sk)
}
fun sessionBoost(sessionId: String, vcpu: Int = 2, publicKey: String? = null, secretKey: String? = null): Map<String, Any> {
val (pk, sk) = if (publicKey != null && secretKey != null) Pair(publicKey, secretKey) else getApiKeys(null)
return apiRequest("/sessions/$sessionId/boost", "POST", mapOf("vcpu" to vcpu), pk, sk)
}
fun sessionUnboost(sessionId: String, publicKey: String? = null, secretKey: String? = null): Map<String, Any> {
val (pk, sk) = if (publicKey != null && secretKey != null) Pair(publicKey, secretKey) else getApiKeys(null)
return apiRequest("/sessions/$sessionId/unboost", "POST", null, pk, sk)
}
fun sessionExecute(sessionId: String, command: String, publicKey: String? = null, secretKey: String? = null): Map<String, Any> {
val (pk, sk) = if (publicKey != null && secretKey != null) Pair(publicKey, secretKey) else getApiKeys(null)
return apiRequest("/sessions/$sessionId/shell", "POST", mapOf("command" to command), pk, sk)
}
// Service functions
fun serviceList(publicKey: String? = null, secretKey: String? = null): List<Map<String, Any>> {
val (pk, sk) = if (publicKey != null && secretKey != null) Pair(publicKey, secretKey) else getApiKeys(null)
val result = apiRequest("/services", "GET", null, pk, sk)
@Suppress("UNCHECKED_CAST")
return result["services"] as? List<Map<String, Any>> ?: emptyList()
}
fun serviceGet(serviceId: String, publicKey: String? = null, secretKey: String? = null): Map<String, Any> {
val (pk, sk) = if (publicKey != null && secretKey != null) Pair(publicKey, secretKey) else getApiKeys(null)
return apiRequest("/services/$serviceId", "GET", null, pk, sk)
}
fun serviceCreate(name: String, ports: String? = null, domains: String? = null, bootstrap: String? = null, networkMode: String? = null, publicKey: String? = null, secretKey: String? = null): String? {
val (pk, sk) = if (publicKey != null && secretKey != null) Pair(publicKey, secretKey) else getApiKeys(null)
val payload = mutableMapOf<String, Any>("name" to name)
if (ports != null) payload["ports"] = ports.split(",").map { it.trim().toInt() }
if (domains != null) payload["domains"] = domains
if (bootstrap != null) payload["bootstrap"] = bootstrap
if (networkMode != null) payload["network_mode"] = networkMode
val result = apiRequest("/services", "POST", payload, pk, sk)
return result["id"]?.toString()
}
fun serviceDestroy(serviceId: String, publicKey: String? = null, secretKey: String? = null): Map<String, Any> {
val (pk, sk) = if (publicKey != null && secretKey != null) Pair(publicKey, secretKey) else getApiKeys(null)
return apiRequestDestructive("/services/$serviceId", "DELETE", null, pk, sk)
}
fun serviceFreeze(serviceId: String, publicKey: String? = null, secretKey: String? = null): Map<String, Any> {
val (pk, sk) = if (publicKey != null && secretKey != null) Pair(publicKey, secretKey) else getApiKeys(null)
return apiRequest("/services/$serviceId/freeze", "POST", null, pk, sk)
}
fun serviceUnfreeze(serviceId: String, publicKey: String? = null, secretKey: String? = null): Map<String, Any> {
val (pk, sk) = if (publicKey != null && secretKey != null) Pair(publicKey, secretKey) else getApiKeys(null)
return apiRequest("/services/$serviceId/unfreeze", "POST", null, pk, sk)
}
fun serviceLock(serviceId: String, publicKey: String? = null, secretKey: String? = null): Map<String, Any> {
val (pk, sk) = if (publicKey != null && secretKey != null) Pair(publicKey, secretKey) else getApiKeys(null)
return apiRequest("/services/$serviceId/lock", "POST", null, pk, sk)
}
fun serviceUnlock(serviceId: String, publicKey: String? = null, secretKey: String? = null): Map<String, Any> {
val (pk, sk) = if (publicKey != null && secretKey != null) Pair(publicKey, secretKey) else getApiKeys(null)
return apiRequestDestructive("/services/$serviceId/unlock", "POST", null, pk, sk)
}
fun serviceSetUnfreezeOnDemand(serviceId: String, enabled: Boolean, publicKey: String? = null, secretKey: String? = null): Map<String, Any> {
val (pk, sk) = if (publicKey != null && secretKey != null) Pair(publicKey, secretKey) else getApiKeys(null)
return apiRequestPatch("/services/$serviceId", mapOf("unfreeze_on_demand" to enabled), pk, sk)
}
fun serviceRedeploy(serviceId: String, bootstrap: String? = null, publicKey: String? = null, secretKey: String? = null): Map<String, Any> {
val (pk, sk) = if (publicKey != null && secretKey != null) Pair(publicKey, secretKey) else getApiKeys(null)
val payload = if (bootstrap != null) mapOf("bootstrap" to bootstrap) else emptyMap()
return apiRequest("/services/$serviceId/redeploy", "POST", payload, pk, sk)
}
fun serviceLogs(serviceId: String, allLogs: Boolean = false, publicKey: String? = null, secretKey: String? = null): String? {
val (pk, sk) = if (publicKey != null && secretKey != null) Pair(publicKey, secretKey) else getApiKeys(null)
val path = if (allLogs) "/services/$serviceId/logs?all=true" else "/services/$serviceId/logs"
val result = apiRequest(path, "GET", null, pk, sk)
return result["logs"]?.toString()
}
fun serviceExecute(serviceId: String, command: String, publicKey: String? = null, secretKey: String? = null): Map<String, Any> {
val (pk, sk) = if (publicKey != null && secretKey != null) Pair(publicKey, secretKey) else getApiKeys(null)
return apiRequest("/services/$serviceId/execute", "POST", mapOf("command" to command), pk, sk)
}
fun serviceEnvGet(serviceId: String, publicKey: String? = null, secretKey: String? = null): Map<String, Any> {
val (pk, sk) = if (publicKey != null && secretKey != null) Pair(publicKey, secretKey) else getApiKeys(null)
return apiRequest("/services/$serviceId/env", "GET", null, pk, sk)
}
fun serviceEnvSet(serviceId: String, envContent: String, publicKey: String? = null, secretKey: String? = null): Boolean {
val (pk, sk) = if (publicKey != null && secretKey != null) Pair(publicKey, secretKey) else getApiKeys(null)
val (success, _) = apiRequestText("/services/$serviceId/env", "PUT", envContent, pk, sk)
return success
}
fun serviceEnvDelete(serviceId: String, publicKey: String? = null, secretKey: String? = null): Map<String, Any> {
val (pk, sk) = if (publicKey != null && secretKey != null) Pair(publicKey, secretKey) else getApiKeys(null)
return apiRequest("/services/$serviceId/env", "DELETE", null, pk, sk)
}
fun serviceEnvExport(serviceId: String, publicKey: String? = null, secretKey: String? = null): Map<String, Any> {
val (pk, sk) = if (publicKey != null && secretKey != null) Pair(publicKey, secretKey) else getApiKeys(null)
return apiRequest("/services/$serviceId/env/export", "POST", emptyMap<String, Any>(), pk, sk)
}
fun serviceResize(serviceId: String, vcpu: Int, publicKey: String? = null, secretKey: String? = null): Map<String, Any> {
val (pk, sk) = if (publicKey != null && secretKey != null) Pair(publicKey, secretKey) else getApiKeys(null)
return apiRequestPatch("/services/$serviceId", mapOf("vcpu" to vcpu), pk, sk)
}
// Snapshot functions
fun snapshotList(publicKey: String? = null, secretKey: String? = null): List<Map<String, Any>> {
val (pk, sk) = if (publicKey != null && secretKey != null) Pair(publicKey, secretKey) else getApiKeys(null)
val result = apiRequest("/snapshots", "GET", null, pk, sk)
@Suppress("UNCHECKED_CAST")
return result["snapshots"] as? List<Map<String, Any>> ?: emptyList()
}
fun snapshotGet(snapshotId: String, publicKey: String? = null, secretKey: String? = null): Map<String, Any> {
val (pk, sk) = if (publicKey != null && secretKey != null) Pair(publicKey, secretKey) else getApiKeys(null)
return apiRequest("/snapshots/$snapshotId", "GET", null, pk, sk)
}
fun snapshotSession(sessionId: String, name: String? = null, hot: Boolean = false, publicKey: String? = null, secretKey: String? = null): String? {
val (pk, sk) = if (publicKey != null && secretKey != null) Pair(publicKey, secretKey) else getApiKeys(null)
val payload = mutableMapOf<String, Any>("session_id" to sessionId, "hot" to hot)
if (name != null) payload["name"] = name
val result = apiRequest("/snapshots", "POST", payload, pk, sk)
return result["snapshot_id"]?.toString()
}
fun snapshotService(serviceId: String, name: String? = null, hot: Boolean = false, publicKey: String? = null, secretKey: String? = null): String? {
val (pk, sk) = if (publicKey != null && secretKey != null) Pair(publicKey, secretKey) else getApiKeys(null)
val payload = mutableMapOf<String, Any>("service_id" to serviceId, "hot" to hot)
if (name != null) payload["name"] = name
val result = apiRequest("/snapshots", "POST", payload, pk, sk)
return result["snapshot_id"]?.toString()
}
fun snapshotRestore(snapshotId: String, publicKey: String? = null, secretKey: String? = null): Map<String, Any> {
val (pk, sk) = if (publicKey != null && secretKey != null) Pair(publicKey, secretKey) else getApiKeys(null)
return apiRequest("/snapshots/$snapshotId/restore", "POST", emptyMap<String, Any>(), pk, sk)
}
fun snapshotDelete(snapshotId: String, publicKey: String? = null, secretKey: String? = null): Map<String, Any> {
val (pk, sk) = if (publicKey != null && secretKey != null) Pair(publicKey, secretKey) else getApiKeys(null)
return apiRequestDestructive("/snapshots/$snapshotId", "DELETE", null, pk, sk)
}
fun snapshotLock(snapshotId: String, publicKey: String? = null, secretKey: String? = null): Map<String, Any> {
val (pk, sk) = if (publicKey != null && secretKey != null) Pair(publicKey, secretKey) else getApiKeys(null)
return apiRequest("/snapshots/$snapshotId/lock", "POST", null, pk, sk)
}
fun snapshotUnlock(snapshotId: String, publicKey: String? = null, secretKey: String? = null): Map<String, Any> {
val (pk, sk) = if (publicKey != null && secretKey != null) Pair(publicKey, secretKey) else getApiKeys(null)
return apiRequestDestructive("/snapshots/$snapshotId/unlock", "POST", null, pk, sk)
}
fun snapshotClone(snapshotId: String, cloneType: String, name: String? = null, ports: String? = null, shell: String? = null, publicKey: String? = null, secretKey: String? = null): String? {
val (pk, sk) = if (publicKey != null && secretKey != null) Pair(publicKey, secretKey) else getApiKeys(null)
val payload = mutableMapOf<String, Any>("type" to cloneType)
if (name != null) payload["name"] = name
if (ports != null) payload["ports"] = ports.split(",").map { it.trim().toInt() }
if (shell != null) payload["shell"] = shell
val result = apiRequest("/snapshots/$snapshotId/clone", "POST", payload, pk, sk)
return (result["session_id"] ?: result["service_id"])?.toString()
}
// Image functions
fun imageList(filter: String? = null, publicKey: String? = null, secretKey: String? = null): List<Map<String, Any>> {
val (pk, sk) = if (publicKey != null && secretKey != null) Pair(publicKey, secretKey) else getApiKeys(null)
val path = if (filter != null) "/images/$filter" else "/images"
val result = apiRequest(path, "GET", null, pk, sk)
@Suppress("UNCHECKED_CAST")
return result["images"] as? List<Map<String, Any>> ?: emptyList()
}
fun imageGet(imageId: String, publicKey: String? = null, secretKey: String? = null): Map<String, Any> {
val (pk, sk) = if (publicKey != null && secretKey != null) Pair(publicKey, secretKey) else getApiKeys(null)
return apiRequest("/images/$imageId", "GET", null, pk, sk)
}
fun imagePublish(sourceType: String, sourceId: String, name: String? = null, description: String? = null, publicKey: String? = null, secretKey: String? = null): String? {
val (pk, sk) = if (publicKey != null && secretKey != null) Pair(publicKey, secretKey) else getApiKeys(null)
val payload = mutableMapOf<String, Any>("source_type" to sourceType, "source_id" to sourceId)
if (name != null) payload["name"] = name
if (description != null) payload["description"] = description
val result = apiRequest("/images", "POST", payload, pk, sk)
return result["image_id"]?.toString()
}
fun imageDelete(imageId: String, publicKey: String? = null, secretKey: String? = null): Map<String, Any> {
val (pk, sk) = if (publicKey != null && secretKey != null) Pair(publicKey, secretKey) else getApiKeys(null)
return apiRequestDestructive("/images/$imageId", "DELETE", null, pk, sk)
}
fun imageLock(imageId: String, publicKey: String? = null, secretKey: String? = null): Map<String, Any> {
val (pk, sk) = if (publicKey != null && secretKey != null) Pair(publicKey, secretKey) else getApiKeys(null)
return apiRequest("/images/$imageId/lock", "POST", null, pk, sk)
}
fun imageUnlock(imageId: String, publicKey: String? = null, secretKey: String? = null): Map<String, Any> {
val (pk, sk) = if (publicKey != null && secretKey != null) Pair(publicKey, secretKey) else getApiKeys(null)
return apiRequestDestructive("/images/$imageId/unlock", "POST", null, pk, sk)
}
fun imageSetVisibility(imageId: String, visibility: String, publicKey: String? = null, secretKey: String? = null): Map<String, Any> {
val (pk, sk) = if (publicKey != null && secretKey != null) Pair(publicKey, secretKey) else getApiKeys(null)
return apiRequest("/images/$imageId/visibility", "POST", mapOf("visibility" to visibility), pk, sk)
}
fun imageGrantAccess(imageId: String, trustedApiKey: String, publicKey: String? = null, secretKey: String? = null): Map<String, Any> {
val (pk, sk) = if (publicKey != null && secretKey != null) Pair(publicKey, secretKey) else getApiKeys(null)
return apiRequest("/images/$imageId/grant", "POST", mapOf("trusted_api_key" to trustedApiKey), pk, sk)
}
fun imageRevokeAccess(imageId: String, trustedApiKey: String, publicKey: String? = null, secretKey: String? = null): Map<String, Any> {
val (pk, sk) = if (publicKey != null && secretKey != null) Pair(publicKey, secretKey) else getApiKeys(null)
return apiRequest("/images/$imageId/revoke", "POST", mapOf("trusted_api_key" to trustedApiKey), pk, sk)
}
fun imageListTrusted(imageId: String, publicKey: String? = null, secretKey: String? = null): List<String> {
val (pk, sk) = if (publicKey != null && secretKey != null) Pair(publicKey, secretKey) else getApiKeys(null)
val result = apiRequest("/images/$imageId/trusted", "GET", null, pk, sk)
@Suppress("UNCHECKED_CAST")
return result["trusted"] as? List<String> ?: emptyList()
}
fun imageTransfer(imageId: String, toApiKey: String, publicKey: String? = null, secretKey: String? = null): Map<String, Any> {
val (pk, sk) = if (publicKey != null && secretKey != null) Pair(publicKey, secretKey) else getApiKeys(null)
return apiRequest("/images/$imageId/transfer", "POST", mapOf("to_api_key" to toApiKey), pk, sk)
}
fun imageSpawn(imageId: String, name: String? = null, ports: String? = null, bootstrap: String? = null, networkMode: String? = null, publicKey: String? = null, secretKey: String? = null): String? {
val (pk, sk) = if (publicKey != null && secretKey != null) Pair(publicKey, secretKey) else getApiKeys(null)
val payload = mutableMapOf<String, Any>()
if (name != null) payload["name"] = name
if (ports != null) payload["ports"] = ports.split(",").map { it.trim().toInt() }
if (bootstrap != null) payload["bootstrap"] = bootstrap
if (networkMode != null) payload["network_mode"] = networkMode
val result = apiRequest("/images/$imageId/spawn", "POST", payload, pk, sk)
return result["service_id"]?.toString()
}
fun imageClone(imageId: String, name: String? = null, description: String? = null, publicKey: String? = null, secretKey: String? = null): String? {
val (pk, sk) = if (publicKey != null && secretKey != null) Pair(publicKey, secretKey) else getApiKeys(null)
val payload = mutableMapOf<String, Any>()
if (name != null) payload["name"] = name
if (description != null) payload["description"] = description
val result = apiRequest("/images/$imageId/clone", "POST", payload, pk, sk)
return result["image_id"]?.toString()
}
// PaaS Logs functions
fun logsFetch(source: String = "all", lines: Int = 100, since: String? = null, grep: String? = null, publicKey: String? = null, secretKey: String? = null): Map<String, Any> {
val (pk, sk) = if (publicKey != null && secretKey != null) Pair(publicKey, secretKey) else getApiKeys(null)
val params = mutableListOf("source=$source", "lines=$lines")
if (since != null) params.add("since=$since")
if (grep != null) params.add("grep=${java.net.URLEncoder.encode(grep, "UTF-8")}")
return apiRequest("/paas/logs?${params.joinToString("&")}", "GET", null, pk, sk)
}
// Validate keys
fun validateKeys(publicKey: String? = null, secretKey: String? = null): Map<String, Any> {
val (pk, sk) = if (publicKey != null && secretKey != null) Pair(publicKey, secretKey) else getApiKeys(null)
return validateKey(pk, sk)
}
fun printHelp() {
println("""
Usage: kotlin UnKt [options] <source_file>
kotlin UnKt session [options]
kotlin UnKt service [options]
kotlin UnKt image [options]
kotlin UnKt languages [--json]
kotlin UnKt key [options]
Execute options:
-e KEY=VALUE Set environment variable
-f FILE Add input file
-a Return artifacts
-o DIR Output directory for artifacts
-n MODE Network mode (zerotrust/semitrusted)
-v N vCPU count (1-8)
-k KEY API key
Session options:
--list List active sessions
--shell NAME Shell/REPL to use
--kill ID Terminate session
Service options:
--list List services
--name NAME Service name
--ports PORTS Comma-separated ports
--type TYPE Service type (minecraft/mumble/teamspeak/source/tcp/udp)
--bootstrap CMD Bootstrap command
-e KEY=VALUE Environment variable for vault
--env-file FILE Load vault variables from file
--info ID Get service details
--logs ID Get all logs
--tail ID Get last 9000 lines
--freeze ID Freeze service
--unfreeze ID Unfreeze service
--destroy ID Destroy service
--resize ID Resize service (requires --vcpu N)
--unfreeze-on-demand ID true|false Enable/disable auto-unfreeze on HTTP request
--show-freeze-page ID true|false Enable/disable showing freeze page when frozen
--execute ID Execute command in service
--command CMD Command to execute (with --execute)
--dump-bootstrap ID Dump bootstrap script
--dump-file FILE File to save bootstrap (with --dump-bootstrap)
Service env commands:
env status ID Show vault status
env set ID Set vault (-e KEY=VALUE or --env-file FILE)
env export ID Export vault contents
env delete ID Delete vault
Key options:
--extend Open browser to extend key
Image options:
--list List all images
--info ID Get image details
--delete ID Delete an image
--lock ID Lock image to prevent deletion
--unlock ID Unlock image
--publish ID Publish image from service/snapshot (requires --source-type)
--source-type TYPE Source type: service or snapshot
--visibility ID MODE Set visibility: private, unlisted, or public
--spawn ID Spawn new service from image
--clone ID Clone an image
--name NAME Name for spawned service or cloned image
--ports PORTS Ports for spawned service
Languages options:
--json Output as JSON array
""".trimIndent())
}
Esclarecimentos de documentação
Dependências
C Binary (un1) — requer libcurl e libwebsockets:
sudo apt install build-essential libcurl4-openssl-dev libwebsockets-dev
wget unsandbox.com/downloads/un.c && gcc -O2 -o un un.c -lcurl -lwebsockets
Implementações SDK — a maioria usa apenas stdlib (Ruby, JS, Go, etc). Alguns requerem dependências mínimas:
pip install requests # Python
Executar Código
Executar um script
./un hello.py
./un app.js
./un main.rs
Com variáveis de ambiente
./un -e DEBUG=1 -e NAME=World script.py
Com arquivos de entrada (teletransportar arquivos para sandbox)
./un -f data.csv -f config.json process.py
Obter binário compilado
./un -a -o ./bin main.c
Sessões interativas
Iniciar uma sessão de shell
# Default bash shell
./un session
# Choose your shell
./un session --shell zsh
./un session --shell fish
# Jump into a REPL
./un session --shell python3
./un session --shell node
./un session --shell julia
Sessão com acesso à rede
./un session -n semitrusted
Auditoria de sessão (gravação completa do terminal)
# Record everything (including vim, interactive programs)
./un session --audit -o ./logs
# Replay session later
zcat session.log*.gz | less -R
Coletar artefatos da sessão
# Files in /tmp/artifacts/ are collected on exit
./un session -a -o ./outputs
Persistência de sessão (tmux/screen)
# Default: session terminates on disconnect (clean exit)
./un session
# With tmux: session persists, can reconnect later
./un session --tmux
# Press Ctrl+b then d to detach
# With screen: alternative multiplexer
./un session --screen
# Press Ctrl+a then d to detach
Listar Trabalhos Ativos
./un session --list
# Output:
# Active sessions: 2
#
# SESSION ID CONTAINER SHELL TTL STATUS
# abc123... unsb-vm-12345 python3 45m30s active
# def456... unsb-vm-67890 bash 1h2m active
Reconectar à sessão existente
# Reconnect by container name (requires --tmux or --screen)
./un session --attach unsb-vm-12345
# Use exit to terminate session, or detach to keep it running
Encerrar uma sessão
./un session --kill unsb-vm-12345
Shells e REPLs disponíveis
Shells: bash, dash, sh, zsh, fish, ksh, tcsh, csh, elvish, xonsh, ash
REPLs: python3, bpython, ipython # Python
node # JavaScript
ruby, irb # Ruby
lua # Lua
php # PHP
perl # Perl
guile, scheme # Scheme
ghci # Haskell
erl, iex # Erlang/Elixir
sbcl, clisp # Common Lisp
r # R
julia # Julia
clojure # Clojure
Gerenciamento de Chave API
Verificar Status do Pagamento
# Check if your API key is valid
./un key
# Output:
# Valid: key expires in 30 days
Estender Chave Expirada
# Open the portal to extend an expired key
./un key --extend
# This opens the unsandbox.com portal where you can
# add more credits to extend your key's expiration
Autenticação
As credenciais são carregadas em ordem de prioridade (maior primeiro):
# 1. CLI flags (highest priority)
./un -p unsb-pk-xxxx -k unsb-sk-xxxxx script.py
# 2. Environment variables
export UNSANDBOX_PUBLIC_KEY=unsb-pk-xxxx-xxxx-xxxx-xxxx
export UNSANDBOX_SECRET_KEY=unsb-sk-xxxxx-xxxxx-xxxxx-xxxxx
./un script.py
# 3. Config file (lowest priority)
# ~/.unsandbox/accounts.csv format: public_key,secret_key
mkdir -p ~/.unsandbox
echo "unsb-pk-xxxx-xxxx-xxxx-xxxx,unsb-sk-xxxxx-xxxxx-xxxxx-xxxxx" > ~/.unsandbox/accounts.csv
./un script.py
As requisições são assinadas com HMAC-SHA256. O token bearer contém apenas a chave pública; a chave secreta calcula a assinatura (nunca é transmitida).
Escalonamento de Recursos
Definir Quantidade de vCPU
# Default: 1 vCPU, 2GB RAM
./un script.py
# Scale up: 4 vCPUs, 8GB RAM
./un -v 4 script.py
# Maximum: 8 vCPUs, 16GB RAM
./un --vcpu 8 heavy_compute.py
Reforço de Sessão Ao Vivo
# Boost a running session to 2 vCPU, 4GB RAM
./un session --boost sandbox-abc
# Boost to specific vCPU count (4 vCPU, 8GB RAM)
./un session --boost sandbox-abc --boost-vcpu 4
# Return to base resources (1 vCPU, 2GB RAM)
./un session --unboost sandbox-abc
Congelar/Descongelar Sessão
Congelar e Descongelar Sessões
# Freeze a session (stop billing, preserve state)
./un session --freeze sandbox-abc
# Unfreeze a frozen session
./un session --unfreeze sandbox-abc
# Note: Requires --tmux or --screen for persistence
Serviços Persistentes
Criar um Serviço
# Web server with ports
./un service --name web --ports 80,443 --bootstrap "python -m http.server 80"
# With custom domains
./un service --name blog --ports 8000 --domains blog.example.com
# Game server with SRV records
./un service --name mc --type minecraft --bootstrap ./setup.sh
# Deploy app tarball with bootstrap script
./un service --name app --ports 8000 -f app.tar.gz --bootstrap-file ./setup.sh
# setup.sh: cd /tmp && tar xzf app.tar.gz && ./app/start.sh
Gerenciar Serviços
# List all services
./un service --list
# Get service details
./un service --info abc123
# View bootstrap logs
./un service --logs abc123
./un service --tail abc123 # last 9000 lines
# Execute command in running service
./un service --execute abc123 'journalctl -u myapp -n 50'
# Dump bootstrap script (for migrations)
./un service --dump-bootstrap abc123
./un service --dump-bootstrap abc123 backup.sh
# Freeze/unfreeze service
./un service --freeze abc123
./un service --unfreeze abc123
# Service settings (auto-wake, freeze page display)
./un service --auto-unfreeze abc123 # enable auto-wake on HTTP
./un service --no-auto-unfreeze abc123 # disable auto-wake
./un service --show-freeze-page abc123 # show HTML payment page (default)
./un service --no-show-freeze-page abc123 # return JSON error instead
# Redeploy with new bootstrap
./un service --redeploy abc123 --bootstrap ./new-setup.sh
# Destroy service
./un service --destroy abc123
Snapshots
Listar Snapshots
./un snapshot --list
# Output:
# Snapshots: 3
#
# SNAPSHOT ID NAME SOURCE SIZE CREATED
# unsb-snapshot-a1b2-c3d4-e5f6-g7h8 before-upgrade session 512 MB 2h ago
# unsb-snapshot-i9j0-k1l2-m3n4-o5p6 stable-v1.0 service 1.2 GB 1d ago
Criar Snapshot da Sessão
# Snapshot with name
./un session --snapshot unsb-vm-12345 --name "before upgrade"
# Quick snapshot (auto-generated name)
./un session --snapshot unsb-vm-12345
Criar Snapshot do Serviço
# Standard snapshot (pauses container briefly)
./un service --snapshot unsb-service-abc123 --name "stable v1.0"
# Hot snapshot (no pause, may be inconsistent)
./un service --snapshot unsb-service-abc123 --hot
Restaurar a partir do Snapshot
# Restore session from snapshot
./un session --restore unsb-snapshot-a1b2-c3d4-e5f6-g7h8
# Restore service from snapshot
./un service --restore unsb-snapshot-i9j0-k1l2-m3n4-o5p6
Excluir Snapshot
./un snapshot --delete unsb-snapshot-a1b2-c3d4-e5f6-g7h8
Imagens
Imagens são imagens de container independentes e transferíveis que sobrevivem à exclusão do container. Diferente dos snapshots (que permanecem com seu container), imagens podem ser compartilhadas com outros usuários, transferidas entre chaves de API ou tornadas públicas no marketplace.
Listar Imagens
# List all images (owned + shared + public)
./un image --list
# List only your images
./un image --list owned
# List images shared with you
./un image --list shared
# List public marketplace images
./un image --list public
# Get image details
./un image --info unsb-image-xxxx-xxxx-xxxx-xxxx
Publicar Imagens
# Publish from a stopped or frozen service
./un image --publish-service unsb-service-abc123 \
--name "My App v1.0" --description "Production snapshot"
# Publish from a snapshot
./un image --publish-snapshot unsb-snapshot-xxxx-xxxx-xxxx-xxxx \
--name "Stable Release"
# Note: Cannot publish from running containers - stop or freeze first
Criar Serviços a partir de Imagens
# Spawn a new service from an image
./un image --spawn unsb-image-xxxx-xxxx-xxxx-xxxx \
--name new-service --ports 80,443
# Clone an image (creates a copy you own)
./un image --clone unsb-image-xxxx-xxxx-xxxx-xxxx
Proteção de Imagem
# Lock image to prevent accidental deletion
./un image --lock unsb-image-xxxx-xxxx-xxxx-xxxx
# Unlock image to allow deletion
./un image --unlock unsb-image-xxxx-xxxx-xxxx-xxxx
# Delete image (must be unlocked)
./un image --delete unsb-image-xxxx-xxxx-xxxx-xxxx
Visibilidade e Compartilhamento
# Set visibility level
./un image --visibility unsb-image-xxxx-xxxx-xxxx-xxxx private # owner only (default)
./un image --visibility unsb-image-xxxx-xxxx-xxxx-xxxx unlisted # can be shared
./un image --visibility unsb-image-xxxx-xxxx-xxxx-xxxx public # marketplace
# Share with specific user
./un image --grant unsb-image-xxxx-xxxx-xxxx-xxxx \
--key unsb-pk-friend-friend-friend-friend
# Revoke access
./un image --revoke unsb-image-xxxx-xxxx-xxxx-xxxx \
--key unsb-pk-friend-friend-friend-friend
# List who has access
./un image --trusted unsb-image-xxxx-xxxx-xxxx-xxxx
Transferir Propriedade
# Transfer image to another API key
./un image --transfer unsb-image-xxxx-xxxx-xxxx-xxxx \
--to unsb-pk-newowner-newowner-newowner-newowner
Referência de uso
Usage: ./un [options] <source_file>
./un session [options]
./un service [options]
./un snapshot [options]
./un image [options]
./un key
Commands:
(default) Execute source file in sandbox
session Open interactive shell/REPL session
service Manage persistent services
snapshot Manage container snapshots
image Manage container images (publish, share, transfer)
key Check API key validity and expiration
Options:
-e KEY=VALUE Set environment variable (can use multiple times)
-f FILE Add input file (can use multiple times)
-a Return and save artifacts from /tmp/artifacts/
-o DIR Output directory for artifacts (default: current dir)
-p KEY Public key (or set UNSANDBOX_PUBLIC_KEY env var)
-k KEY Secret key (or set UNSANDBOX_SECRET_KEY env var)
-n MODE Network mode: zerotrust (default) or semitrusted
-v N, --vcpu N vCPU count 1-8, each vCPU gets 2GB RAM (default: 1)
-y Skip confirmation for large uploads (>1GB)
-h Show this help
Authentication (priority order):
1. -p and -k flags (public and secret key)
2. UNSANDBOX_PUBLIC_KEY + UNSANDBOX_SECRET_KEY env vars
3. ~/.unsandbox/accounts.csv (format: public_key,secret_key per line)
Session options:
-s, --shell SHELL Shell/REPL to use (default: bash)
-l, --list List active sessions
--attach ID Reconnect to existing session (ID or container name)
--kill ID Terminate a session (ID or container name)
--freeze ID Freeze a session (requires --tmux/--screen)
--unfreeze ID Unfreeze a frozen session
--boost ID Boost session resources (2 vCPU, 4GB RAM)
--boost-vcpu N Specify vCPU count for boost (1-8)
--unboost ID Return to base resources
--audit Record full session for auditing
--tmux Enable session persistence with tmux (allows reconnect)
--screen Enable session persistence with screen (allows reconnect)
Service options:
--name NAME Service name (creates new service)
--ports PORTS Comma-separated ports (e.g., 80,443)
--domains DOMAINS Custom domains (e.g., example.com,www.example.com)
--type TYPE Service type: minecraft, mumble, teamspeak, source, tcp, udp
--bootstrap CMD Bootstrap command/file/URL to run on startup
-f FILE Upload file to /tmp/ (can use multiple times)
-l, --list List all services
--info ID Get service details
--tail ID Get last 9000 lines of bootstrap logs
--logs ID Get all bootstrap logs
--freeze ID Freeze a service
--unfreeze ID Unfreeze a service
--auto-unfreeze ID Enable auto-wake on HTTP request
--no-auto-unfreeze ID Disable auto-wake on HTTP request
--show-freeze-page ID Show HTML payment page when frozen (default)
--no-show-freeze-page ID Return JSON error when frozen
--destroy ID Destroy a service
--redeploy ID Re-run bootstrap script (requires --bootstrap)
--execute ID CMD Run a command in a running service
--dump-bootstrap ID [FILE] Dump bootstrap script (for migrations)
--snapshot ID Create snapshot of session or service
--snapshot-name User-friendly name for snapshot
--hot Create snapshot without pausing (may be inconsistent)
--restore ID Restore session/service from snapshot ID
Snapshot options:
-l, --list List all snapshots
--info ID Get snapshot details
--delete ID Delete a snapshot permanently
Image options:
-l, --list [owned|shared|public] List images (all, owned, shared, or public)
--info ID Get image details
--publish-service ID Publish image from stopped/frozen service
--publish-snapshot ID Publish image from snapshot
--name NAME Name for published image
--description DESC Description for published image
--delete ID Delete image (must be unlocked)
--clone ID Clone image (creates copy you own)
--spawn ID Create service from image (requires --name)
--lock ID Lock image to prevent deletion
--unlock ID Unlock image to allow deletion
--visibility ID LEVEL Set visibility (private|unlisted|public)
--grant ID --key KEY Grant access to another API key
--revoke ID --key KEY Revoke access from API key
--transfer ID --to KEY Transfer ownership to API key
--trusted ID List API keys with access
Key options:
(no options) Check API key validity
--extend Open portal to extend an expired key
Examples:
./un script.py # execute Python script
./un -e DEBUG=1 script.py # with environment variable
./un -f data.csv process.py # with input file
./un -a -o ./bin main.c # save compiled artifacts
./un -v 4 heavy.py # with 4 vCPUs, 8GB RAM
./un session # interactive bash session
./un session --tmux # bash with reconnect support
./un session --list # list active sessions
./un session --attach unsb-vm-12345 # reconnect to session
./un session --kill unsb-vm-12345 # terminate a session
./un session --freeze unsb-vm-12345 # freeze session
./un session --unfreeze unsb-vm-12345 # unfreeze session
./un session --boost unsb-vm-12345 # boost resources
./un session --unboost unsb-vm-12345 # return to base
./un session --shell python3 # Python REPL
./un session --shell node # Node.js REPL
./un session -n semitrusted # session with network access
./un session --audit -o ./logs # record session for auditing
./un service --name web --ports 80 # create web service
./un service --list # list all services
./un service --logs abc123 # view bootstrap logs
./un key # check API key
./un key --extend # extend expired key
./un snapshot --list # list all snapshots
./un session --snapshot unsb-vm-123 # snapshot a session
./un service --snapshot abc123 # snapshot a service
./un session --restore unsb-snapshot-xxxx # restore from snapshot
./un image --list # list all images
./un image --list owned # list your images
./un image --publish-service abc # publish image from service
./un image --spawn img123 --name x # create service from image
./un image --grant img --key pk # share image with user
CLI Inception
O UN CLI foi implementado em 42 linguagens de programação, demonstrando que a API do unsandbox pode ser acessada de praticamente qualquer ambiente.
Ver Todas as 42 Implementações →
Licença
DOMÍNIO PÚBLICO - SEM LICENÇA, SEM GARANTIA
Este é software gratuito de domínio público para o bem público de um permacomputador hospedado
em permacomputer.com - um computador sempre ativo pelo povo, para o povo. Um que é
durável, fácil de reparar e distribuído como água da torneira para inteligência de
aprendizado de máquina.
O permacomputador é infraestrutura de propriedade comunitária otimizada em torno de quatro valores:
VERDADE - Primeiros princípios, matemática & ciência, código aberto distribuído livremente
LIBERDADE - Parcerias voluntárias, liberdade da tirania e controle corporativo
HARMONIA - Desperdício mínimo, sistemas auto-renováveis com diversas conexões prósperas
AMOR - Seja você mesmo sem ferir os outros, cooperação através da lei natural
Este software contribui para essa visão ao permitir a execução de código em mais de 42
linguagens de programação através de uma interface unificada, acessível a todos. Código são
sementes que brotam em qualquer tecnologia abandonada.
Saiba mais: https://www.permacomputer.com
Qualquer pessoa é livre para copiar, modificar, publicar, usar, compilar, vender ou distribuir
este software, seja em forma de código-fonte ou como binário compilado, para qualquer propósito,
comercial ou não comercial, e por qualquer meio.
SEM GARANTIA. O SOFTWARE É FORNECIDO "COMO ESTÁ" SEM GARANTIA DE QUALQUER TIPO.
Dito isso, a camada de membrana digital do nosso permacomputador executa continuamente testes
unitários, de integração e funcionais em todo o seu próprio software - com nosso permacomputador
monitorando a si mesmo, reparando a si mesmo, com orientação humana mínima no ciclo.
Nossos agentes fazem o seu melhor.
Copyright 2025 TimeHexOn & foxhop & russell@unturf
https://www.timehexon.com
https://www.foxhop.net
https://www.unturf.com/software