#!/usr/bin/env julia # 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 #!/usr/bin/env julia using HTTP using JSON using Base64 using ArgParse using Printf using SHA # Extension to language mapping const EXT_MAP = Dict( ".jl" => "julia", ".r" => "r", ".cr" => "crystal", ".f90" => "fortran", ".cob" => "cobol", ".pro" => "prolog", ".forth" => "forth", ".4th" => "forth", ".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", ".d" => "d", ".nim" => "nim", ".zig" => "zig", ".v" => "v", ".dart" => "dart", ".groovy" => "groovy", ".scala" => "scala", ".tcl" => "tcl", ".raku" => "raku", ".m" => "objc" ) # ANSI color codes const BLUE = "\033[34m" const RED = "\033[31m" const GREEN = "\033[32m" const YELLOW = "\033[33m" const RESET = "\033[0m" const API_BASE = "https://api.unsandbox.com" const PORTAL_BASE = "https://unsandbox.com" const LANGUAGES_CACHE_TTL = 3600 function detect_language(filename::String)::String ext = lowercase(match(r"\.[^.]+$", filename).match) return get(EXT_MAP, ext, "unknown") end function load_accounts_csv(path::String)::Vector{Tuple{String,String}} accounts = Tuple{String,String}[] isfile(path) || return accounts try for line in eachline(path) trimmed = strip(line) isempty(trimmed) && continue startswith(trimmed, "#") && continue parts = split(trimmed, ","; limit=2) length(parts) >= 2 || continue pk = strip(parts[1]) sk = strip(parts[2]) if startswith(pk, "unsb-pk-") && startswith(sk, "unsb-sk-") push!(accounts, (pk, sk)) end end catch end return accounts end function get_credentials(; account_index::Int=-1)::Tuple{String,String} # Priority 2: --account N => accounts.csv row N (bypasses env vars) if account_index >= 0 for path in [joinpath(homedir(), ".unsandbox", "accounts.csv"), "accounts.csv"] accts = load_accounts_csv(path) if account_index < length(accts) return accts[account_index + 1] end end println(stderr, "$(RED)Error: No account at index $account_index in accounts.csv$(RESET)") exit(1) end # Priority 3: Environment variables public_key = get(ENV, "UNSANDBOX_PUBLIC_KEY", "") secret_key = get(ENV, "UNSANDBOX_SECRET_KEY", "") if !isempty(public_key) && !isempty(secret_key) return (public_key, secret_key) end # Priority 4: ~/.unsandbox/accounts.csv row 0 (or UNSANDBOX_ACCOUNT env) default_idx = tryparse(Int, get(ENV, "UNSANDBOX_ACCOUNT", "0")) default_idx = something(default_idx, 0) for path in [joinpath(homedir(), ".unsandbox", "accounts.csv"), "accounts.csv"] accts = load_accounts_csv(path) if default_idx < length(accts) return accts[default_idx + 1] end end # Legacy fallback old_key = get(ENV, "UNSANDBOX_API_KEY", "") if !isempty(old_key) return (old_key, old_key) end println(stderr, "$(RED)Error: UNSANDBOX_PUBLIC_KEY/UNSANDBOX_SECRET_KEY or UNSANDBOX_API_KEY not set$(RESET)") exit(1) end function get_api_keys(args_key=nothing; account_index::Int=-1)::Tuple{String,String} # Priority 1: explicit -k flag if args_key !== nothing && !isempty(string(args_key)) public_key = string(args_key) secret_key = get(ENV, "UNSANDBOX_SECRET_KEY", "") if !isempty(secret_key) return (public_key, secret_key) end end return get_credentials(account_index=account_index) end function hmac_sha256_hex(key::String, message::String)::String h = hmac_sha256(Vector{UInt8}(key), Vector{UInt8}(message)) return bytes2hex(h) end function compute_signature(secret_key::String, timestamp::Int64, method::String, path::String, body::String)::String message = "$(timestamp):$(method):$(path):$(body)" return hmac_sha256_hex(secret_key, message) end function api_request(endpoint::String, public_key::String, secret_key::String; method="GET", data=nothing, sudo_otp=nothing, sudo_challenge=nothing) url = API_BASE * endpoint # Prepare body body = data !== nothing ? JSON.json(data) : "" # Generate timestamp and signature timestamp = Int64(floor(time())) signature = compute_signature(secret_key, timestamp, method, endpoint, body) headers = [ "Authorization" => "Bearer $public_key", "X-Timestamp" => string(timestamp), "X-Signature" => signature, "Content-Type" => "application/json" ] # Add sudo headers if provided if sudo_otp !== nothing push!(headers, "X-Sudo-OTP" => sudo_otp) end if sudo_challenge !== nothing push!(headers, "X-Sudo-Challenge" => sudo_challenge) end try if method == "GET" response = HTTP.get(url, headers, readtimeout=300) elseif method == "POST" response = HTTP.post(url, headers, body, readtimeout=300) elseif method == "DELETE" response = HTTP.delete(url, headers, readtimeout=300) else error("Unsupported method: $method") end return JSON.parse(String(response.body)) catch e if isa(e, HTTP.ExceptionRequest.StatusError) error_body = String(e.response.body) if e.status == 401 && occursin("timestamp", lowercase(error_body)) println(stderr, "$(RED)Error: Request timestamp expired (must be within 5 minutes of server time)$(RESET)") println(stderr, "$(YELLOW)Your computer's clock may have drifted.$(RESET)") println(stderr, "Check your system time and sync with NTP if needed:") println(stderr, " Linux: sudo ntpdate -s time.nist.gov") println(stderr, " macOS: sudo sntp -sS time.apple.com") println(stderr, " Windows: w32tm /resync") else println(stderr, "$(RED)Error: HTTP $(e.status) - $(error_body)$(RESET)") end else println(stderr, "$(RED)Error: Request failed: $e$(RESET)") end exit(1) end end # Handle 428 sudo OTP challenge - prompts user for OTP and retries the request function handle_sudo_challenge(endpoint::String, public_key::String, secret_key::String, method::String, data, response_body::String) # Extract challenge_id from response parsed = JSON.parse(response_body) challenge_id = get(parsed, "challenge_id", nothing) println(stderr, "$(YELLOW)Confirmation required. Check your email for a one-time code.$(RESET)") print(stderr, "Enter OTP: ") otp = readline() if isempty(strip(otp)) println(stderr, "$(RED)Error: Operation cancelled$(RESET)") exit(1) end # Retry the request with sudo headers return api_request(endpoint, public_key, secret_key, method=method, data=data, sudo_otp=strip(otp), sudo_challenge=challenge_id) end # API request that handles 428 sudo challenges for destructive operations function api_request_with_sudo(endpoint::String, public_key::String, secret_key::String; method="DELETE", data=nothing) url = API_BASE * endpoint # Prepare body body = data !== nothing ? JSON.json(data) : "" # Generate timestamp and signature timestamp = Int64(floor(time())) signature = compute_signature(secret_key, timestamp, method, endpoint, body) headers = [ "Authorization" => "Bearer $public_key", "X-Timestamp" => string(timestamp), "X-Signature" => signature, "Content-Type" => "application/json" ] try if method == "GET" response = HTTP.get(url, headers, readtimeout=300, status_exception=false) elseif method == "POST" response = HTTP.post(url, headers, body, readtimeout=300, status_exception=false) elseif method == "DELETE" response = HTTP.delete(url, headers, readtimeout=300, status_exception=false) else error("Unsupported method: $method") end response_body = String(response.body) # Handle 428 - sudo OTP required if response.status == 428 return handle_sudo_challenge(endpoint, public_key, secret_key, method, data, response_body) end # Handle other errors if response.status >= 400 if response.status == 401 && occursin("timestamp", lowercase(response_body)) println(stderr, "$(RED)Error: Request timestamp expired (must be within 5 minutes of server time)$(RESET)") println(stderr, "$(YELLOW)Your computer's clock may have drifted.$(RESET)") println(stderr, "Check your system time and sync with NTP if needed:") println(stderr, " Linux: sudo ntpdate -s time.nist.gov") println(stderr, " macOS: sudo sntp -sS time.apple.com") println(stderr, " Windows: w32tm /resync") else println(stderr, "$(RED)Error: HTTP $(response.status) - $(response_body)$(RESET)") end exit(1) end return JSON.parse(response_body) catch e println(stderr, "$(RED)Error: Request failed: $e$(RESET)") exit(1) end end function api_request_patch(endpoint::String, public_key::String, secret_key::String; data=nothing) url = API_BASE * endpoint # Prepare body body = data !== nothing ? JSON.json(data) : "" # Generate timestamp and signature timestamp = Int64(floor(time())) signature = compute_signature(secret_key, timestamp, "PATCH", endpoint, body) headers = [ "Authorization" => "Bearer $public_key", "X-Timestamp" => string(timestamp), "X-Signature" => signature, "Content-Type" => "application/json" ] try response = HTTP.request("PATCH", url, headers, body, readtimeout=300) return JSON.parse(String(response.body)) catch e if isa(e, HTTP.ExceptionRequest.StatusError) error_body = String(e.response.body) if e.status == 401 && occursin("timestamp", lowercase(error_body)) println(stderr, "$(RED)Error: Request timestamp expired (must be within 5 minutes of server time)$(RESET)") println(stderr, "$(YELLOW)Your computer's clock may have drifted.$(RESET)") println(stderr, "Check your system time and sync with NTP if needed:") println(stderr, " Linux: sudo ntpdate -s time.nist.gov") println(stderr, " macOS: sudo sntp -sS time.apple.com") println(stderr, " Windows: w32tm /resync") else println(stderr, "$(RED)Error: HTTP $(e.status) - $(error_body)$(RESET)") end else println(stderr, "$(RED)Error: Request failed: $e$(RESET)") end exit(1) end end function api_request_text(endpoint::String, public_key::String, secret_key::String, body::String)::Bool url = API_BASE * endpoint timestamp = Int64(floor(time())) signature = compute_signature(secret_key, timestamp, "PUT", endpoint, body) headers = [ "Authorization" => "Bearer $public_key", "X-Timestamp" => string(timestamp), "X-Signature" => signature, "Content-Type" => "text/plain" ] try response = HTTP.put(url, headers, body, readtimeout=300) return response.status >= 200 && response.status < 300 catch e return false end end const MAX_ENV_CONTENT_SIZE = 65536 function read_env_file(path::String)::String if !isfile(path) println(stderr, "$(RED)Error: Env file not found: $path$(RESET)") exit(1) end return read(path, String) end function build_env_content(envs::Vector{String}, env_file::Union{String,Nothing})::String lines = copy(envs) if env_file !== nothing content = read_env_file(env_file) for line in split(content, '\n') trimmed = strip(line) if !isempty(trimmed) && !startswith(trimmed, "#") push!(lines, trimmed) end end end return join(lines, "\n") end function service_env_status(service_id::String, public_key::String, secret_key::String) return api_request("/services/$service_id/env", public_key, secret_key) end function service_env_set(service_id::String, env_content::String, public_key::String, secret_key::String)::Bool if length(env_content) > MAX_ENV_CONTENT_SIZE println(stderr, "$(RED)Error: Env content exceeds maximum size of 64KB$(RESET)") return false end return api_request_text("/services/$service_id/env", public_key, secret_key, env_content) end function service_env_export(service_id::String, public_key::String, secret_key::String) return api_request("/services/$service_id/env/export", public_key, secret_key, method="POST", data=Dict()) end function service_env_delete(service_id::String, public_key::String, secret_key::String)::Bool try api_request("/services/$service_id/env", public_key, secret_key, method="DELETE") return true catch return false end end function cmd_service_env(args) (public_key, secret_key) = get_api_keys(args["api-key"]; account_index=something(get(args, "account", nothing), -1)) action = get(args, "env-action", nothing) target = get(args, "env-target", nothing) if action == "status" if target === nothing println(stderr, "$(RED)Error: service env status requires service ID$(RESET)") exit(1) end result = service_env_status(target, public_key, secret_key) has_vault = get(result, "has_vault", false) if has_vault println("$(GREEN)Vault: configured$(RESET)") env_count = get(result, "env_count", nothing) if env_count !== nothing println("Variables: $env_count") end updated_at = get(result, "updated_at", nothing) if updated_at !== nothing println("Updated: $updated_at") end else println("$(YELLOW)Vault: not configured$(RESET)") end elseif action == "set" if target === nothing println(stderr, "$(RED)Error: service env set requires service ID$(RESET)") exit(1) end envs = something(args["vault-env"], String[]) env_file = get(args, "env-file", nothing) if isempty(envs) && env_file === nothing println(stderr, "$(RED)Error: service env set requires -e or --env-file$(RESET)") exit(1) end env_content = build_env_content(envs, env_file) if service_env_set(target, env_content, public_key, secret_key) println("$(GREEN)Vault updated for service $target$(RESET)") else println(stderr, "$(RED)Error: Failed to update vault$(RESET)") exit(1) end elseif action == "export" if target === nothing println(stderr, "$(RED)Error: service env export requires service ID$(RESET)") exit(1) end result = service_env_export(target, public_key, secret_key) content = get(result, "content", nothing) if content !== nothing print(content) end elseif action == "delete" if target === nothing println(stderr, "$(RED)Error: service env delete requires service ID$(RESET)") exit(1) end if service_env_delete(target, public_key, secret_key) println("$(GREEN)Vault deleted for service $target$(RESET)") else println(stderr, "$(RED)Error: Failed to delete vault$(RESET)") exit(1) end else println(stderr, "$(RED)Error: Unknown env action: $action$(RESET)") println(stderr, "Usage: un.jl service env ") exit(1) end end function cmd_execute(args) (public_key, secret_key) = get_api_keys(args["api-key"]; account_index=something(get(args, "account", nothing), -1)) filename = args["source_file"] if !isfile(filename) println(stderr, "$(RED)Error: File not found: $filename$(RESET)") exit(1) end language = detect_language(filename) if language == "unknown" println(stderr, "$(RED)Error: Cannot detect language for $filename$(RESET)") exit(1) end code = read(filename, String) # Build request payload payload = Dict("language" => language, "code" => code) # Add environment variables if args["env"] !== nothing env_vars = Dict{String,String}() for e in args["env"] if occursin('=', e) k, v = split(e, '=', limit=2) env_vars[k] = v end end if !isempty(env_vars) payload["env"] = env_vars end end # Add input files if args["files"] !== nothing input_files = [] for filepath in args["files"] if !isfile(filepath) println(stderr, "$(RED)Error: Input file not found: $filepath$(RESET)") exit(1) end content = base64encode(read(filepath)) push!(input_files, Dict( "filename" => basename(filepath), "content_base64" => content )) end if !isempty(input_files) payload["input_files"] = input_files end end # Add options if args["artifacts"] payload["return_artifacts"] = true end if args["network"] !== nothing payload["network"] = args["network"] end # Execute result = api_request("/execute", public_key, secret_key, method="POST", data=payload) # Print output if haskey(result, "stdout") && !isempty(result["stdout"]) print(BLUE, result["stdout"], RESET) end if haskey(result, "stderr") && !isempty(result["stderr"]) print(RED, result["stderr"], RESET) end # Save artifacts if args["artifacts"] && haskey(result, "artifacts") out_dir = something(args["output-dir"], ".") mkpath(out_dir) for artifact in result["artifacts"] filename = get(artifact, "filename", "artifact") content = base64decode(artifact["content_base64"]) path = joinpath(out_dir, filename) write(path, content) chmod(path, 0o755) println(stderr, "$(GREEN)Saved: $path$(RESET)") end end exit_code = get(result, "exit_code", 0) exit(exit_code) end function cmd_session(args) (public_key, secret_key) = get_api_keys(args["api-key"]; account_index=something(get(args, "account", nothing), -1)) if args["list"] result = api_request("/sessions", public_key, secret_key) sessions = get(result, "sessions", []) if isempty(sessions) println("No active sessions") else @printf("%-40s %-10s %-10s %s\n", "ID", "Shell", "Status", "Created") for s in sessions @printf("%-40s %-10s %-10s %s\n", get(s, "id", "N/A"), get(s, "shell", "N/A"), get(s, "status", "N/A"), get(s, "created_at", "N/A")) end end return end if args["kill"] !== nothing api_request("/sessions/$(args["kill"])", public_key, secret_key, method="DELETE") println("$(GREEN)Session terminated: $(args["kill"])$(RESET)") return end # Create new session payload = Dict("shell" => "bash") if args["network"] !== nothing payload["network"] = args["network"] end # Add input files if args["files"] !== nothing input_files = [] for filepath in args["files"] if !isfile(filepath) println(stderr, "$(RED)Error: Input file not found: $filepath$(RESET)") exit(1) end content = base64encode(read(filepath)) push!(input_files, Dict( "filename" => basename(filepath), "content_base64" => content )) end if !isempty(input_files) payload["input_files"] = input_files end end println("$(YELLOW)Creating session...$(RESET)") result = api_request("/sessions", public_key, secret_key, method="POST", data=payload) println("$(GREEN)Session created: $(get(result, "id", "N/A"))$(RESET)") println("$(YELLOW)(Interactive sessions require WebSocket - use un2 for full support)$(RESET)") end function cmd_service(args) (public_key, secret_key) = get_api_keys(args["api-key"]; account_index=something(get(args, "account", nothing), -1)) # Handle env subcommand if get(args, "env-action", nothing) !== nothing cmd_service_env(args) return end if args["list"] result = api_request("/services", public_key, secret_key) services = get(result, "services", []) if isempty(services) println("No services") else @printf("%-20s %-15s %-10s %-15s %s\n", "ID", "Name", "Status", "Ports", "Domains") for s in services ports = join(get(s, "ports", []), ',') domains = join(get(s, "domains", []), ',') @printf("%-20s %-15s %-10s %-15s %s\n", get(s, "id", "N/A"), get(s, "name", "N/A"), get(s, "status", "N/A"), ports, domains) end end return end if args["info"] !== nothing result = api_request("/services/$(args["info"])", public_key, secret_key) println(JSON.json(result, 2)) return end if args["logs"] !== nothing result = api_request("/services/$(args["logs"])/logs", public_key, secret_key) println(get(result, "logs", "")) return end if args["sleep"] !== nothing api_request("/services/$(args["sleep"])/freeze", public_key, secret_key, method="POST") println("$(GREEN)Service frozen: $(args["sleep"])$(RESET)") return end if args["wake"] !== nothing api_request("/services/$(args["wake"])/unfreeze", public_key, secret_key, method="POST") println("$(GREEN)Service unfreezing: $(args["wake"])$(RESET)") return end if args["destroy"] !== nothing api_request_with_sudo("/services/$(args["destroy"])", public_key, secret_key, method="DELETE") println("$(GREEN)Service destroyed: $(args["destroy"])$(RESET)") return end if args["resize"] !== nothing vcpu = args["vcpu"] if vcpu === nothing || vcpu <= 0 println(stderr, "$(RED)Error: --resize requires --vcpu N (1-8)$(RESET)") exit(1) end api_request_patch("/services/$(args["resize"])", public_key, secret_key, data=Dict("vcpu" => vcpu)) ram = vcpu * 2 println("$(GREEN)Service resized to $(vcpu) vCPU, $(ram) GB RAM$(RESET)") return end if args["unfreeze-on-demand"] !== nothing enabled = args["unfreeze-on-demand-value"] if enabled === nothing println(stderr, "$(RED)Error: --unfreeze-on-demand requires true or false$(RESET)") exit(1) end api_request_patch("/services/$(args["unfreeze-on-demand"])", public_key, secret_key, data=Dict("unfreeze_on_demand" => enabled)) println("$(GREEN)Service unfreeze_on_demand set to $(enabled): $(args["unfreeze-on-demand"])$(RESET)") return end if args["dump-bootstrap"] !== nothing println(stderr, "Fetching bootstrap script from $(args["dump-bootstrap"])...") payload = Dict("command" => "cat /tmp/bootstrap.sh") result = api_request("/services/$(args["dump-bootstrap"])/execute", public_key, secret_key, method="POST", data=payload) if haskey(result, "stdout") && !isempty(result["stdout"]) bootstrap = result["stdout"] if args["dump-file"] !== nothing # Write to file try write(args["dump-file"], bootstrap) chmod(args["dump-file"], 0o755) println("Bootstrap saved to $(args["dump-file"])") catch e println(stderr, "$(RED)Error: Could not write to $(args["dump-file"]): $e$(RESET)") exit(1) end else # Print to stdout print(bootstrap) end else println(stderr, "$(RED)Error: Failed to fetch bootstrap (service not running or no bootstrap file)$(RESET)") exit(1) end return end # Create new service if args["name"] !== nothing payload = Dict("name" => args["name"]) if args["ports"] !== nothing ports = [parse(Int, strip(p)) for p in split(args["ports"], ',')] payload["ports"] = ports end if args["domains"] !== nothing domains = [strip(d) for d in split(args["domains"], ',')] payload["domains"] = domains end if args["type"] !== nothing payload["service_type"] = args["type"] end if args["bootstrap"] !== nothing payload["bootstrap"] = args["bootstrap"] end if args["bootstrap-file"] !== nothing bootstrap_file = args["bootstrap-file"] if isfile(bootstrap_file) payload["bootstrap_content"] = read(bootstrap_file, String) else println(stderr, "$(RED)Error: Bootstrap file not found: $bootstrap_file$(RESET)") exit(1) end end if args["network"] !== nothing payload["network"] = args["network"] end if args["vcpu"] !== nothing payload["vcpu"] = args["vcpu"] end # Add input files if args["files"] !== nothing input_files = [] for filepath in args["files"] if !isfile(filepath) println(stderr, "$(RED)Error: Input file not found: $filepath$(RESET)") exit(1) end content = base64encode(read(filepath)) push!(input_files, Dict( "filename" => basename(filepath), "content_base64" => content )) end if !isempty(input_files) payload["input_files"] = input_files end end result = api_request("/services", public_key, secret_key, method="POST", data=payload) service_id = get(result, "id", nothing) println("$(GREEN)Service created: $(something(service_id, "N/A"))$(RESET)") println("Name: $(get(result, "name", "N/A"))") if haskey(result, "url") println("URL: $(result["url"])") end # Auto-set vault if env vars were provided vault_envs = something(args["vault-env"], String[]) vault_env_file = get(args, "env-file", nothing) if service_id !== nothing && (!isempty(vault_envs) || vault_env_file !== nothing) env_content = build_env_content(vault_envs, vault_env_file) if !isempty(env_content) if service_env_set(service_id, env_content, public_key, secret_key) println("$(GREEN)Vault configured with environment variables$(RESET)") else println("$(YELLOW)Warning: Failed to set vault$(RESET)") end end end return end println(stderr, "$(RED)Error: Use --name to create, or --list, --info, --logs, --freeze, --unfreeze, --destroy$(RESET)") exit(1) end function validate_key(api_key::String) url = PORTAL_BASE * "/keys/validate" headers = [ "Authorization" => "Bearer $api_key", "Content-Type" => "application/json" ] try response = HTTP.post(url, headers, "{}", readtimeout=300) data = JSON.parse(String(response.body)) # Check if valid if get(data, "valid", false) # Print valid key info println("$(GREEN)Valid$(RESET)\n") println(@sprintf("%-20s %s", "Public Key:", get(data, "public_key", "N/A"))) println(@sprintf("%-20s %s", "Tier:", get(data, "tier", "N/A"))) println(@sprintf("%-20s %s", "Status:", get(data, "status", "N/A"))) println(@sprintf("%-20s %s", "Expires:", get(data, "valid_through_datetime", "N/A"))) println(@sprintf("%-20s %s", "Time Remaining:", get(data, "valid_for_human", "N/A"))) println(@sprintf("%-20s %s/min", "Rate Limit:", get(data, "rate_per_minute", "N/A"))) println(@sprintf("%-20s %s", "Burst:", get(data, "burst", "N/A"))) println(@sprintf("%-20s %s", "Concurrency:", get(data, "concurrency", "N/A"))) return 0 else # Handle invalid response reason = get(data, "reason", "unknown") if reason == "expired" println("$(RED)Expired$(RESET)\n") println(@sprintf("%-20s %s", "Public Key:", get(data, "public_key", "N/A"))) println(@sprintf("%-20s %s", "Tier:", get(data, "tier", "N/A"))) expired_at = get(data, "expired_at_datetime", "N/A") expired_ago = get(data, "expired_ago", "") if !isempty(expired_ago) println(@sprintf("%-20s %s (%s)", "Expired:", expired_at, expired_ago)) else println(@sprintf("%-20s %s", "Expired:", expired_at)) end renew_url = get(data, "renew_url", "https://unsandbox.com/pricing") println("\n$(YELLOW)To renew:$(RESET) Visit $renew_url") elseif reason == "invalid_key" println("$(RED)Invalid$(RESET): key not found") elseif reason == "suspended" println("$(RED)Suspended$(RESET): key has been suspended") else println("$(RED)Invalid$(RESET): $reason") end return 1 end catch e if isa(e, HTTP.ExceptionRequest.StatusError) # Parse error response from body try data = JSON.parse(String(e.response.body)) reason = get(data, "reason", "unknown") if reason == "expired" println("$(RED)Expired$(RESET)\n") println(@sprintf("%-20s %s", "Public Key:", get(data, "public_key", "N/A"))) println(@sprintf("%-20s %s", "Tier:", get(data, "tier", "N/A"))) expired_at = get(data, "expired_at_datetime", "N/A") expired_ago = get(data, "expired_ago", "") if !isempty(expired_ago) println(@sprintf("%-20s %s (%s)", "Expired:", expired_at, expired_ago)) else println(@sprintf("%-20s %s", "Expired:", expired_at)) end renew_url = get(data, "renew_url", "https://unsandbox.com/pricing") println("\n$(YELLOW)To renew:$(RESET) Visit $renew_url") elseif reason == "invalid_key" println("$(RED)Invalid$(RESET): key not found") elseif reason == "suspended" println("$(RED)Suspended$(RESET): key has been suspended") else println("$(RED)Invalid$(RESET): $reason") end catch println(stderr, "$(RED)Error: HTTP $(e.status)$(RESET)") end return 1 else println(stderr, "$(RED)Error: Request failed: $e$(RESET)") return 1 end end end function get_languages_cache_path()::String home = get(ENV, "HOME", ".") return joinpath(home, ".unsandbox", "languages.json") end function load_languages_cache()::Union{Vector{String}, Nothing} cache_path = get_languages_cache_path() if !isfile(cache_path) return nothing end try content = read(cache_path, String) data = JSON.parse(content) timestamp = get(data, "timestamp", 0) now = Int64(floor(time())) if now - timestamp < LANGUAGES_CACHE_TTL langs = get(data, "languages", []) return [string(l) for l in langs] else return nothing end catch return nothing end end function save_languages_cache(languages::Vector) cache_path = get_languages_cache_path() cache_dir = dirname(cache_path) # Ensure directory exists mkpath(cache_dir) timestamp = Int64(floor(time())) data = Dict("languages" => languages, "timestamp" => timestamp) try open(cache_path, "w") do f write(f, JSON.json(data)) end catch # Ignore write errors end end function cmd_languages(args) # Try to load from cache first langs = load_languages_cache() if langs === nothing # Cache miss or expired, fetch from API (public_key, secret_key) = get_api_keys(args["api-key"]; account_index=something(get(args, "account", nothing), -1)) result = api_request("/languages", public_key, secret_key) langs = get(result, "languages", []) save_languages_cache(langs) end if args["json"] println(JSON.json(langs)) else for lang in langs println(lang) end end end function cmd_key(args) (public_key, secret_key) = get_api_keys(args["api-key"]; account_index=something(get(args, "account", nothing), -1)) # For portal validation, we still use public_key as bearer token api_key = public_key # Handle --extend flag if args["extend"] # Validate key to get public key url = PORTAL_BASE * "/keys/validate" headers = [ "Authorization" => "Bearer $api_key", "Content-Type" => "application/json" ] try response = HTTP.post(url, headers, "{}", readtimeout=300) data = JSON.parse(String(response.body)) public_key = get(data, "public_key", nothing) if public_key === nothing println(stderr, "$(RED)Error: Invalid key or could not retrieve public key$(RESET)") exit(1) end # Build extend URL extend_url = "$(PORTAL_BASE)/keys/extend?pk=$(public_key)" println("Opening extension page in browser...") println("If browser doesn't open, visit: $extend_url") # Try to open browser if Sys.isapple() run(`open $extend_url`) elseif Sys.islinux() try run(`xdg-open $extend_url`) catch try run(`sensible-browser $extend_url`) catch # Already printed the URL end end elseif Sys.iswindows() run(`cmd /c start $extend_url`) end exit(0) catch e println(stderr, "$(RED)Error: Failed to validate key: $e$(RESET)") exit(1) end end # Default: validate and display key info exit(validate_key(api_key)) end function main() s = ArgParseSettings(description="Unsandbox CLI - Execute code in secure sandboxes") @add_arg_table! s begin "source_file" help = "Source file to execute" required = false "--api-key", "-k" help = "API key (or set UNSANDBOX_API_KEY)" "--account" help = "Account index in ~/.unsandbox/accounts.csv (bypasses env vars)" arg_type = Int "--network", "-n" help = "Network mode" arg_type = String range_tester = x -> x in ["zerotrust", "semitrusted"] "--env", "-e" help = "Set environment variable (KEY=VALUE)" action = :append_arg "--files", "-f" help = "Add input file" action = :append_arg "--artifacts", "-a" help = "Return artifacts" action = :store_true "--output-dir", "-o" help = "Output directory for artifacts" "session" help = "Manage interactive sessions" action = :command "service" help = "Manage persistent services" action = :command "languages" help = "List available programming languages" action = :command "key" help = "Check API key validity and expiration" action = :command "image" help = "Manage images" action = :command "snapshot" help = "Manage snapshots" action = :command end @add_arg_table! s["snapshot"] begin "--list", "-l" help = "List all snapshots" action = :store_true "--info" help = "Get snapshot details" "--delete" help = "Delete a snapshot" "--lock" help = "Lock snapshot to prevent deletion" "--unlock" help = "Unlock snapshot" "--restore" help = "Restore from snapshot" "--clone" help = "Clone snapshot to session/service (requires --type)" "--type" help = "Clone type: session or service" "--name" help = "Name for cloned resource" "--shell" help = "Shell for cloned session" "--ports" help = "Comma-separated ports for cloned service" "--api-key", "-k" help = "API key" "--account" help = "Account index in ~/.unsandbox/accounts.csv (bypasses env vars)" arg_type = Int end @add_arg_table! s["session"] begin "--list", "-l" help = "List active sessions" action = :store_true "--kill" help = "Terminate session" "--files", "-f" help = "Add input file" action = :append_arg "--network", "-n" help = "Network mode" arg_type = String range_tester = x -> x in ["zerotrust", "semitrusted"] "--api-key", "-k" help = "API key" "--account" help = "Account index in ~/.unsandbox/accounts.csv (bypasses env vars)" arg_type = Int end @add_arg_table! s["service"] begin "--name" help = "Service name" "--ports" help = "Comma-separated ports" "--domains" help = "Comma-separated custom domains" "--type" help = "Service type for SRV records (minecraft, mumble, teamspeak, source, tcp, udp)" "--bootstrap" help = "Bootstrap command or URI" "--bootstrap-file" help = "Upload local file as bootstrap script" "--files", "-f" help = "Add input file" action = :append_arg "--vault-env", "-e" help = "Environment variable for vault (KEY=VALUE)" action = :append_arg "--env-file" help = "Load vault variables from file" "--network", "-n" help = "Network mode" arg_type = String range_tester = x -> x in ["zerotrust", "semitrusted"] "--vcpu", "-v" help = "vCPU count (1-8)" arg_type = Int range_tester = x -> x >= 1 && x <= 8 "--list", "-l" help = "List services" action = :store_true "--info" help = "Get service details" "--logs" help = "Get all logs" "--freeze" help = "Freeze service" "--unfreeze" help = "Unfreeze service" "--destroy" help = "Destroy service" "--resize" help = "Resize service (requires --vcpu N)" "--unfreeze-on-demand" help = "Service ID to set unfreeze_on_demand for" "--unfreeze-on-demand-value" help = "Enable/disable unfreeze_on_demand (true or false)" arg_type = Bool "--dump-bootstrap" help = "Dump bootstrap script from service" "--dump-file" help = "File to save bootstrap (with --dump-bootstrap)" "--env-action" help = "Env action (status, set, export, delete)" "--env-target" help = "Service ID for env commands" "--api-key", "-k" help = "API key" "--account" help = "Account index in ~/.unsandbox/accounts.csv (bypasses env vars)" arg_type = Int "env" help = "Manage service environment vault" action = :command end @add_arg_table! s["service"]["env"] begin "action" help = "Env action: status, set, export, delete" required = true "service_id" help = "Service ID" required = false "-e" help = "Environment variable (KEY=VALUE)" action = :append_arg dest_name = "vault-env" "--env-file" help = "Load vault variables from file" "--api-key", "-k" help = "API key" "--account" help = "Account index in ~/.unsandbox/accounts.csv (bypasses env vars)" arg_type = Int end @add_arg_table! s["key"] begin "--extend" help = "Open browser to extend/renew key" action = :store_true "--api-key", "-k" help = "API key" "--account" help = "Account index in ~/.unsandbox/accounts.csv (bypasses env vars)" arg_type = Int end @add_arg_table! s["languages"] begin "--json" help = "Output as JSON array" action = :store_true "--api-key", "-k" help = "API key" "--account" help = "Account index in ~/.unsandbox/accounts.csv (bypasses env vars)" arg_type = Int end @add_arg_table! s["image"] begin "--list", "-l" help = "List all images" action = :store_true "--info" help = "Get image details" "--delete" help = "Delete an image" "--lock" help = "Lock image to prevent deletion" "--unlock" help = "Unlock image" "--publish" help = "Publish image from service/snapshot (requires --source-type)" "--source-type" help = "Source type: service or snapshot" "--visibility" help = "Image ID to set visibility for" "--visibility-mode" help = "Visibility mode: private, unlisted, or public" "--spawn" help = "Spawn new service from image" "--clone" help = "Clone an image" "--name" help = "Name for spawned service or cloned image" "--ports" help = "Comma-separated ports for spawned service" "--api-key", "-k" help = "API key" "--account" help = "Account index in ~/.unsandbox/accounts.csv (bypasses env vars)" arg_type = Int end args = parse_args(ARGS, s) if args["%COMMAND%"] == "session" cmd_session(args["session"]) elseif args["%COMMAND%"] == "service" service_args = args["service"] # Check if env subcommand was used if get(service_args, "%COMMAND%", nothing) == "env" env_args = service_args["env"] # Copy env args to service args service_args["env-action"] = get(env_args, "action", nothing) service_args["env-target"] = get(env_args, "service_id", nothing) service_args["vault-env"] = get(env_args, "vault-env", nothing) service_args["env-file"] = get(env_args, "env-file", nothing) service_args["api-key"] = get(env_args, "api-key", nothing) service_args["account"] = get(env_args, "account", nothing) end cmd_service(service_args) elseif args["%COMMAND%"] == "languages" cmd_languages(args["languages"]) elseif args["%COMMAND%"] == "key" cmd_key(args["key"]) elseif args["%COMMAND%"] == "image" cmd_image(args["image"]) elseif args["%COMMAND%"] == "snapshot" cmd_snapshot(args["snapshot"]) elseif args["source_file"] !== nothing cmd_execute(args) else println(stderr, "$(RED)Error: Provide source_file or use 'session'/'service'/'snapshot'/'languages'/'key'/'image' subcommand$(RESET)") exit(1) end end function cmd_image(args) (public_key, secret_key) = get_api_keys(args["api-key"]; account_index=something(get(args, "account", nothing), -1)) if args["list"] result = api_request("/images", public_key, secret_key) println(JSON.json(result, 2)) return end if args["info"] !== nothing result = api_request("/images/$(args["info"])", public_key, secret_key) println(JSON.json(result, 2)) return end if args["delete"] !== nothing api_request_with_sudo("/images/$(args["delete"])", public_key, secret_key, method="DELETE") println("$(GREEN)Image deleted: $(args["delete"])$(RESET)") return end if args["lock"] !== nothing api_request("/images/$(args["lock"])/lock", public_key, secret_key, method="POST") println("$(GREEN)Image locked: $(args["lock"])$(RESET)") return end if args["unlock"] !== nothing api_request_with_sudo("/images/$(args["unlock"])/unlock", public_key, secret_key, method="POST", data=Dict()) println("$(GREEN)Image unlocked: $(args["unlock"])$(RESET)") return end if args["publish"] !== nothing source_type = args["source-type"] if source_type === nothing println(stderr, "$(RED)Error: --source-type required (service or snapshot)$(RESET)") exit(1) end payload = Dict("source_type" => source_type, "source_id" => args["publish"]) if args["name"] !== nothing payload["name"] = args["name"] end result = api_request("/images/publish", public_key, secret_key, method="POST", data=payload) println("$(GREEN)Image published$(RESET)") println(JSON.json(result, 2)) return end if args["visibility"] !== nothing mode = args["visibility-mode"] if mode === nothing println(stderr, "$(RED)Error: --visibility requires MODE (private, unlisted, or public)$(RESET)") exit(1) end payload = Dict("visibility" => mode) api_request("/images/$(args["visibility"])/visibility", public_key, secret_key, method="POST", data=payload) println("$(GREEN)Image visibility set to $(mode): $(args["visibility"])$(RESET)") return end if args["spawn"] !== nothing payload = Dict() if args["name"] !== nothing payload["name"] = args["name"] end if args["ports"] !== nothing ports = [parse(Int, strip(p)) for p in split(args["ports"], ',')] payload["ports"] = ports end result = api_request("/images/$(args["spawn"])/spawn", public_key, secret_key, method="POST", data=payload) println("$(GREEN)Service spawned from image$(RESET)") println(JSON.json(result, 2)) return end if args["clone"] !== nothing payload = Dict() if args["name"] !== nothing payload["name"] = args["name"] end result = api_request("/images/$(args["clone"])/clone", public_key, secret_key, method="POST", data=payload) println("$(GREEN)Image cloned$(RESET)") println(JSON.json(result, 2)) return end println(stderr, "$(RED)Error: Use --list, --info ID, --delete ID, --lock ID, --unlock ID, --publish ID, --visibility ID, --spawn ID, or --clone ID$(RESET)") exit(1) end function cmd_snapshot(args) (public_key, secret_key) = get_api_keys(args["api-key"]; account_index=something(get(args, "account", nothing), -1)) if args["list"] result = api_request("/snapshots", public_key, secret_key) snapshots = get(result, "snapshots", []) if isempty(snapshots) println("No snapshots found") else @printf("%-40s %-20s %-12s %-30s %s\n", "ID", "Name", "Type", "Source ID", "Size") for s in snapshots @printf("%-40s %-20s %-12s %-30s %s\n", get(s, "id", "N/A"), get(s, "name", "-"), get(s, "source_type", "N/A"), get(s, "source_id", "N/A"), get(s, "size", "N/A")) end end return end if args["info"] !== nothing result = api_request("/snapshots/$(args["info"])", public_key, secret_key) println(JSON.json(result, 2)) return end if args["delete"] !== nothing api_request_with_sudo("/snapshots/$(args["delete"])", public_key, secret_key, method="DELETE") println("$(GREEN)Snapshot deleted: $(args["delete"])$(RESET)") return end if args["lock"] !== nothing api_request("/snapshots/$(args["lock"])/lock", public_key, secret_key, method="POST") println("$(GREEN)Snapshot locked: $(args["lock"])$(RESET)") return end if args["unlock"] !== nothing api_request_with_sudo("/snapshots/$(args["unlock"])/unlock", public_key, secret_key, method="POST", data=Dict()) println("$(GREEN)Snapshot unlocked: $(args["unlock"])$(RESET)") return end if args["restore"] !== nothing api_request("/snapshots/$(args["restore"])/restore", public_key, secret_key, method="POST", data=Dict()) println("$(GREEN)Snapshot restored: $(args["restore"])$(RESET)") return end if args["clone"] !== nothing clone_type = args["type"] if clone_type === nothing println(stderr, "$(RED)Error: --type required for --clone (session or service)$(RESET)") exit(1) end payload = Dict("type" => clone_type) if args["name"] !== nothing payload["name"] = args["name"] end if args["shell"] !== nothing payload["shell"] = args["shell"] end if args["ports"] !== nothing ports = [parse(Int, strip(p)) for p in split(args["ports"], ',')] payload["ports"] = ports end result = api_request("/snapshots/$(args["clone"])/clone", public_key, secret_key, method="POST", data=payload) println("$(GREEN)Cloned from snapshot$(RESET)") println(JSON.json(result, 2)) return end println(stderr, "$(RED)Error: Use --list, --info ID, --delete ID, --lock ID, --unlock ID, --restore ID, or --clone ID$(RESET)") exit(1) end # ============================================================================= # Library API Functions (for import/use as a module) # ============================================================================= const VERSION = "1.0.0" """ execute(language::String, code::String; kwargs...) -> Dict Execute code synchronously and return the result. # Arguments - `language`: Programming language (e.g., "python", "javascript") - `code`: Source code to execute # Keyword Arguments - `env::Dict{String,String}`: Environment variables - `network::String`: Network mode ("zerotrust" or "semitrusted") - `public_key::String`: API public key (optional) - `secret_key::String`: API secret key (optional) # Returns Dict with `stdout`, `stderr`, `exit_code`, `success` # Example ```julia result = execute("python", "print('Hello, World!')") println(result["stdout"]) ``` """ function execute(language::String, code::String; env::Union{Dict{String,String}, Nothing}=nothing, network::String="zerotrust", public_key::Union{String,Nothing}=nothing, secret_key::Union{String,Nothing}=nothing) (pk, sk) = if public_key !== nothing && secret_key !== nothing (public_key, secret_key) else get_api_keys(nothing) end payload = Dict("language" => language, "code" => code) if env !== nothing payload["env"] = env end if network != "zerotrust" payload["network"] = network end return api_request("/execute", pk, sk, method="POST", data=payload) end """ execute_async(language::String, code::String; kwargs...) -> String Execute code asynchronously and return job ID. # Returns Job ID string for polling with `wait_job` or `get_job`. """ function execute_async(language::String, code::String; env::Union{Dict{String,String}, Nothing}=nothing, network::String="zerotrust", public_key::Union{String,Nothing}=nothing, secret_key::Union{String,Nothing}=nothing) (pk, sk) = if public_key !== nothing && secret_key !== nothing (public_key, secret_key) else get_api_keys(nothing) end payload = Dict("language" => language, "code" => code, "async" => true) if env !== nothing payload["env"] = env end if network != "zerotrust" payload["network"] = network end result = api_request("/execute", pk, sk, method="POST", data=payload) return get(result, "job_id", "") end """ wait_job(job_id::String; kwargs...) -> Dict Wait for an async job to complete and return results. """ function wait_job(job_id::String; poll_interval::Int=1, max_wait::Int=300, public_key::Union{String,Nothing}=nothing, secret_key::Union{String,Nothing}=nothing) (pk, sk) = if public_key !== nothing && secret_key !== nothing (public_key, secret_key) else get_api_keys(nothing) end start_time = time() while true result = get_job(job_id, public_key=pk, secret_key=sk) status = get(result, "status", "") if status in ["completed", "failed", "timeout", "cancelled"] return result end if time() - start_time >= max_wait error("Job $job_id did not complete within $max_wait seconds") end sleep(poll_interval) end end """ get_job(job_id::String; kwargs...) -> Dict Get the status of an async job. """ function get_job(job_id::String; public_key::Union{String,Nothing}=nothing, secret_key::Union{String,Nothing}=nothing) (pk, sk) = if public_key !== nothing && secret_key !== nothing (public_key, secret_key) else get_api_keys(nothing) end return api_request("/jobs/$job_id", pk, sk) end """ cancel_job(job_id::String; kwargs...) -> Bool Cancel a running job. """ function cancel_job(job_id::String; public_key::Union{String,Nothing}=nothing, secret_key::Union{String,Nothing}=nothing) (pk, sk) = if public_key !== nothing && secret_key !== nothing (public_key, secret_key) else get_api_keys(nothing) end api_request("/jobs/$job_id", pk, sk, method="DELETE") return true end """ list_jobs(; kwargs...) -> Vector{Dict} List all jobs for the account. """ function list_jobs(; public_key::Union{String,Nothing}=nothing, secret_key::Union{String,Nothing}=nothing) (pk, sk) = if public_key !== nothing && secret_key !== nothing (public_key, secret_key) else get_api_keys(nothing) end result = api_request("/jobs", pk, sk) return get(result, "jobs", []) end """ get_languages(; kwargs...) -> Vector{String} Get list of supported programming languages. """ function get_languages(; public_key::Union{String,Nothing}=nothing, secret_key::Union{String,Nothing}=nothing) (pk, sk) = if public_key !== nothing && secret_key !== nothing (public_key, secret_key) else get_api_keys(nothing) end result = api_request("/languages", pk, sk) return get(result, "languages", []) end """ session_list(; kwargs...) -> Vector{Dict} List all active sessions. """ function session_list(; public_key::Union{String,Nothing}=nothing, secret_key::Union{String,Nothing}=nothing) (pk, sk) = if public_key !== nothing && secret_key !== nothing (public_key, secret_key) else get_api_keys(nothing) end result = api_request("/sessions", pk, sk) return get(result, "sessions", []) end """ session_get(session_id::String; kwargs...) -> Dict Get details of a session. """ function session_get(session_id::String; public_key::Union{String,Nothing}=nothing, secret_key::Union{String,Nothing}=nothing) (pk, sk) = if public_key !== nothing && secret_key !== nothing (public_key, secret_key) else get_api_keys(nothing) end return api_request("/sessions/$session_id", pk, sk) end """ session_create(; kwargs...) -> Dict Create a new interactive session. """ function session_create(; shell::String="bash", network::String="zerotrust", public_key::Union{String,Nothing}=nothing, secret_key::Union{String,Nothing}=nothing) (pk, sk) = if public_key !== nothing && secret_key !== nothing (public_key, secret_key) else get_api_keys(nothing) end payload = Dict("shell" => shell) if network != "zerotrust" payload["network"] = network end return api_request("/sessions", pk, sk, method="POST", data=payload) end """ session_destroy(session_id::String; kwargs...) -> Bool Destroy a session. """ function session_destroy(session_id::String; public_key::Union{String,Nothing}=nothing, secret_key::Union{String,Nothing}=nothing) (pk, sk) = if public_key !== nothing && secret_key !== nothing (public_key, secret_key) else get_api_keys(nothing) end api_request("/sessions/$session_id", pk, sk, method="DELETE") return true end """ session_freeze(session_id::String; kwargs...) -> Bool Freeze (pause) a session. """ function session_freeze(session_id::String; public_key::Union{String,Nothing}=nothing, secret_key::Union{String,Nothing}=nothing) (pk, sk) = if public_key !== nothing && secret_key !== nothing (public_key, secret_key) else get_api_keys(nothing) end api_request("/sessions/$session_id/freeze", pk, sk, method="POST") return true end """ session_unfreeze(session_id::String; kwargs...) -> Bool Unfreeze (resume) a session. """ function session_unfreeze(session_id::String; public_key::Union{String,Nothing}=nothing, secret_key::Union{String,Nothing}=nothing) (pk, sk) = if public_key !== nothing && secret_key !== nothing (public_key, secret_key) else get_api_keys(nothing) end api_request("/sessions/$session_id/unfreeze", pk, sk, method="POST") return true end """ session_boost(session_id::String, vcpu::Int; kwargs...) -> Bool Boost session resources. """ function session_boost(session_id::String, vcpu::Int; public_key::Union{String,Nothing}=nothing, secret_key::Union{String,Nothing}=nothing) (pk, sk) = if public_key !== nothing && secret_key !== nothing (public_key, secret_key) else get_api_keys(nothing) end api_request_patch("/sessions/$session_id", pk, sk, data=Dict("vcpu" => vcpu)) return true end """ session_unboost(session_id::String; kwargs...) -> Bool Remove session boost. """ function session_unboost(session_id::String; public_key::Union{String,Nothing}=nothing, secret_key::Union{String,Nothing}=nothing) (pk, sk) = if public_key !== nothing && secret_key !== nothing (public_key, secret_key) else get_api_keys(nothing) end api_request_patch("/sessions/$session_id", pk, sk, data=Dict("vcpu" => 1)) return true end """ session_execute(session_id::String, command::String; kwargs...) -> Dict Execute a command in a session. """ function session_execute(session_id::String, command::String; public_key::Union{String,Nothing}=nothing, secret_key::Union{String,Nothing}=nothing) (pk, sk) = if public_key !== nothing && secret_key !== nothing (public_key, secret_key) else get_api_keys(nothing) end return api_request("/sessions/$session_id/execute", pk, sk, method="POST", data=Dict("command" => command)) end """ service_list(; kwargs...) -> Vector{Dict} List all services. """ function service_list(; public_key::Union{String,Nothing}=nothing, secret_key::Union{String,Nothing}=nothing) (pk, sk) = if public_key !== nothing && secret_key !== nothing (public_key, secret_key) else get_api_keys(nothing) end result = api_request("/services", pk, sk) return get(result, "services", []) end """ service_get(service_id::String; kwargs...) -> Dict Get details of a service. """ function service_get(service_id::String; public_key::Union{String,Nothing}=nothing, secret_key::Union{String,Nothing}=nothing) (pk, sk) = if public_key !== nothing && secret_key !== nothing (public_key, secret_key) else get_api_keys(nothing) end return api_request("/services/$service_id", pk, sk) end """ service_create(name::String; kwargs...) -> Dict Create a new service. """ function service_create(name::String; ports::Union{Vector{Int}, Nothing}=nothing, domains::Union{Vector{String}, Nothing}=nothing, bootstrap::Union{String, Nothing}=nothing, network::String="semitrusted", public_key::Union{String,Nothing}=nothing, secret_key::Union{String,Nothing}=nothing) (pk, sk) = if public_key !== nothing && secret_key !== nothing (public_key, secret_key) else get_api_keys(nothing) end payload = Dict("name" => name) if ports !== nothing payload["ports"] = ports end if domains !== nothing payload["domains"] = domains end if bootstrap !== nothing payload["bootstrap"] = bootstrap end if network != "semitrusted" payload["network"] = network end return api_request("/services", pk, sk, method="POST", data=payload) end """ service_destroy(service_id::String; kwargs...) -> Bool Destroy a service. """ function service_destroy(service_id::String; public_key::Union{String,Nothing}=nothing, secret_key::Union{String,Nothing}=nothing) (pk, sk) = if public_key !== nothing && secret_key !== nothing (public_key, secret_key) else get_api_keys(nothing) end api_request_with_sudo("/services/$service_id", pk, sk, method="DELETE") return true end """ service_freeze(service_id::String; kwargs...) -> Bool Freeze (pause) a service. """ function service_freeze(service_id::String; public_key::Union{String,Nothing}=nothing, secret_key::Union{String,Nothing}=nothing) (pk, sk) = if public_key !== nothing && secret_key !== nothing (public_key, secret_key) else get_api_keys(nothing) end api_request("/services/$service_id/freeze", pk, sk, method="POST") return true end """ service_unfreeze(service_id::String; kwargs...) -> Bool Unfreeze (resume) a service. """ function service_unfreeze(service_id::String; public_key::Union{String,Nothing}=nothing, secret_key::Union{String,Nothing}=nothing) (pk, sk) = if public_key !== nothing && secret_key !== nothing (public_key, secret_key) else get_api_keys(nothing) end api_request("/services/$service_id/unfreeze", pk, sk, method="POST") return true end """ service_lock(service_id::String; kwargs...) -> Bool Lock a service to prevent deletion. """ function service_lock(service_id::String; public_key::Union{String,Nothing}=nothing, secret_key::Union{String,Nothing}=nothing) (pk, sk) = if public_key !== nothing && secret_key !== nothing (public_key, secret_key) else get_api_keys(nothing) end api_request("/services/$service_id/lock", pk, sk, method="POST") return true end """ service_unlock(service_id::String; kwargs...) -> Bool Unlock a service. """ function service_unlock(service_id::String; public_key::Union{String,Nothing}=nothing, secret_key::Union{String,Nothing}=nothing) (pk, sk) = if public_key !== nothing && secret_key !== nothing (public_key, secret_key) else get_api_keys(nothing) end api_request_with_sudo("/services/$service_id/unlock", pk, sk, method="POST", data=Dict()) return true end """ service_set_unfreeze_on_demand(service_id::String, enabled::Bool; kwargs...) -> Bool Set unfreeze-on-demand for a service. """ function service_set_unfreeze_on_demand(service_id::String, enabled::Bool; public_key::Union{String,Nothing}=nothing, secret_key::Union{String,Nothing}=nothing) (pk, sk) = if public_key !== nothing && secret_key !== nothing (public_key, secret_key) else get_api_keys(nothing) end api_request_patch("/services/$service_id", pk, sk, data=Dict("unfreeze_on_demand" => enabled)) return true end """ service_redeploy(service_id::String; kwargs...) -> Bool Redeploy a service (re-run bootstrap). """ function service_redeploy(service_id::String; bootstrap::Union{String, Nothing}=nothing, public_key::Union{String,Nothing}=nothing, secret_key::Union{String,Nothing}=nothing) (pk, sk) = if public_key !== nothing && secret_key !== nothing (public_key, secret_key) else get_api_keys(nothing) end payload = bootstrap !== nothing ? Dict("bootstrap" => bootstrap) : Dict() api_request("/services/$service_id/redeploy", pk, sk, method="POST", data=payload) return true end """ service_logs(service_id::String; kwargs...) -> String Get service logs. """ function service_logs(service_id::String; all_logs::Bool=false, public_key::Union{String,Nothing}=nothing, secret_key::Union{String,Nothing}=nothing) (pk, sk) = if public_key !== nothing && secret_key !== nothing (public_key, secret_key) else get_api_keys(nothing) end endpoint = all_logs ? "/services/$service_id/logs?all=true" : "/services/$service_id/logs" result = api_request(endpoint, pk, sk) return get(result, "logs", "") end """ service_execute(service_id::String, command::String; kwargs...) -> Dict Execute a command in a service. """ function service_execute(service_id::String, command::String; timeout_ms::Int=30000, public_key::Union{String,Nothing}=nothing, secret_key::Union{String,Nothing}=nothing) (pk, sk) = if public_key !== nothing && secret_key !== nothing (public_key, secret_key) else get_api_keys(nothing) end return api_request("/services/$service_id/execute", pk, sk, method="POST", data=Dict("command" => command, "timeout_ms" => timeout_ms)) end """ service_resize(service_id::String, vcpu::Int; kwargs...) -> Bool Resize a service. """ function service_resize(service_id::String, vcpu::Int; public_key::Union{String,Nothing}=nothing, secret_key::Union{String,Nothing}=nothing) (pk, sk) = if public_key !== nothing && secret_key !== nothing (public_key, secret_key) else get_api_keys(nothing) end api_request_patch("/services/$service_id", pk, sk, data=Dict("vcpu" => vcpu)) return true end """ service_env_get(service_id::String; kwargs...) -> String Get service environment variables. """ function service_env_get(service_id::String; public_key::Union{String,Nothing}=nothing, secret_key::Union{String,Nothing}=nothing) (pk, sk) = if public_key !== nothing && secret_key !== nothing (public_key, secret_key) else get_api_keys(nothing) end result = api_request("/services/$service_id/env", pk, sk) return get(result, "env", "") end """ service_env_set(service_id::String, env_content::String; kwargs...) -> Bool Set service environment variables. """ function service_env_set(service_id::String, env_content::String; public_key::Union{String,Nothing}=nothing, secret_key::Union{String,Nothing}=nothing) (pk, sk) = if public_key !== nothing && secret_key !== nothing (public_key, secret_key) else get_api_keys(nothing) end api_request("/services/$service_id/env", pk, sk, method="POST", data=Dict("env" => env_content)) return true end """ service_env_delete(service_id::String; kwargs...) -> Bool Delete service environment variables. """ function service_env_delete(service_id::String; public_key::Union{String,Nothing}=nothing, secret_key::Union{String,Nothing}=nothing) (pk, sk) = if public_key !== nothing && secret_key !== nothing (public_key, secret_key) else get_api_keys(nothing) end api_request("/services/$service_id/env", pk, sk, method="DELETE") return true end """ service_env_export(service_id::String; kwargs...) -> String Export service environment variables as shell format. """ function service_env_export(service_id::String; public_key::Union{String,Nothing}=nothing, secret_key::Union{String,Nothing}=nothing) (pk, sk) = if public_key !== nothing && secret_key !== nothing (public_key, secret_key) else get_api_keys(nothing) end result = api_request("/services/$service_id/env/export", pk, sk) return get(result, "export", "") end """ snapshot_list(; kwargs...) -> Vector{Dict} List all snapshots. """ function snapshot_list(; public_key::Union{String,Nothing}=nothing, secret_key::Union{String,Nothing}=nothing) (pk, sk) = if public_key !== nothing && secret_key !== nothing (public_key, secret_key) else get_api_keys(nothing) end result = api_request("/snapshots", pk, sk) return get(result, "snapshots", []) end """ snapshot_get(snapshot_id::String; kwargs...) -> Dict Get details of a snapshot. """ function snapshot_get(snapshot_id::String; public_key::Union{String,Nothing}=nothing, secret_key::Union{String,Nothing}=nothing) (pk, sk) = if public_key !== nothing && secret_key !== nothing (public_key, secret_key) else get_api_keys(nothing) end return api_request("/snapshots/$snapshot_id", pk, sk) end """ snapshot_session(session_id::String; kwargs...) -> String Create a snapshot from a session. Returns snapshot ID. """ function snapshot_session(session_id::String; name::Union{String, Nothing}=nothing, hot::Bool=false, public_key::Union{String,Nothing}=nothing, secret_key::Union{String,Nothing}=nothing) (pk, sk) = if public_key !== nothing && secret_key !== nothing (public_key, secret_key) else get_api_keys(nothing) end payload = Dict() if name !== nothing payload["name"] = name end if hot payload["hot"] = true end result = api_request("/sessions/$session_id/snapshot", pk, sk, method="POST", data=payload) return get(result, "id", "") end """ snapshot_service(service_id::String; kwargs...) -> String Create a snapshot from a service. Returns snapshot ID. """ function snapshot_service(service_id::String; name::Union{String, Nothing}=nothing, hot::Bool=false, public_key::Union{String,Nothing}=nothing, secret_key::Union{String,Nothing}=nothing) (pk, sk) = if public_key !== nothing && secret_key !== nothing (public_key, secret_key) else get_api_keys(nothing) end payload = Dict() if name !== nothing payload["name"] = name end if hot payload["hot"] = true end result = api_request("/services/$service_id/snapshot", pk, sk, method="POST", data=payload) return get(result, "id", "") end """ snapshot_restore(snapshot_id::String; kwargs...) -> Bool Restore from a snapshot. """ function snapshot_restore(snapshot_id::String; public_key::Union{String,Nothing}=nothing, secret_key::Union{String,Nothing}=nothing) (pk, sk) = if public_key !== nothing && secret_key !== nothing (public_key, secret_key) else get_api_keys(nothing) end api_request("/snapshots/$snapshot_id/restore", pk, sk, method="POST", data=Dict()) return true end """ snapshot_delete(snapshot_id::String; kwargs...) -> Bool Delete a snapshot. """ function snapshot_delete(snapshot_id::String; public_key::Union{String,Nothing}=nothing, secret_key::Union{String,Nothing}=nothing) (pk, sk) = if public_key !== nothing && secret_key !== nothing (public_key, secret_key) else get_api_keys(nothing) end api_request_with_sudo("/snapshots/$snapshot_id", pk, sk, method="DELETE") return true end """ snapshot_lock(snapshot_id::String; kwargs...) -> Bool Lock a snapshot to prevent deletion. """ function snapshot_lock(snapshot_id::String; public_key::Union{String,Nothing}=nothing, secret_key::Union{String,Nothing}=nothing) (pk, sk) = if public_key !== nothing && secret_key !== nothing (public_key, secret_key) else get_api_keys(nothing) end api_request("/snapshots/$snapshot_id/lock", pk, sk, method="POST") return true end """ snapshot_unlock(snapshot_id::String; kwargs...) -> Bool Unlock a snapshot. """ function snapshot_unlock(snapshot_id::String; public_key::Union{String,Nothing}=nothing, secret_key::Union{String,Nothing}=nothing) (pk, sk) = if public_key !== nothing && secret_key !== nothing (public_key, secret_key) else get_api_keys(nothing) end api_request_with_sudo("/snapshots/$snapshot_id/unlock", pk, sk, method="POST", data=Dict()) return true end """ snapshot_clone(snapshot_id::String, clone_type::String; kwargs...) -> String Clone a snapshot to a new session or service. Returns the new resource ID. """ function snapshot_clone(snapshot_id::String, clone_type::String; name::Union{String, Nothing}=nothing, ports::Union{Vector{Int}, Nothing}=nothing, shell::Union{String, Nothing}=nothing, public_key::Union{String,Nothing}=nothing, secret_key::Union{String,Nothing}=nothing) (pk, sk) = if public_key !== nothing && secret_key !== nothing (public_key, secret_key) else get_api_keys(nothing) end payload = Dict("type" => clone_type) if name !== nothing payload["name"] = name end if ports !== nothing payload["ports"] = ports end if shell !== nothing payload["shell"] = shell end result = api_request("/snapshots/$snapshot_id/clone", pk, sk, method="POST", data=payload) return get(result, "id", "") end """ image_list(; kwargs...) -> Vector{Dict} List all images. """ function image_list(; filter::Union{String, Nothing}=nothing, public_key::Union{String,Nothing}=nothing, secret_key::Union{String,Nothing}=nothing) (pk, sk) = if public_key !== nothing && secret_key !== nothing (public_key, secret_key) else get_api_keys(nothing) end endpoint = filter !== nothing ? "/images?filter=$filter" : "/images" result = api_request(endpoint, pk, sk) return get(result, "images", []) end """ image_get(image_id::String; kwargs...) -> Dict Get details of an image. """ function image_get(image_id::String; public_key::Union{String,Nothing}=nothing, secret_key::Union{String,Nothing}=nothing) (pk, sk) = if public_key !== nothing && secret_key !== nothing (public_key, secret_key) else get_api_keys(nothing) end return api_request("/images/$image_id", pk, sk) end """ image_publish(source_type::String, source_id::String; kwargs...) -> String Publish an image from a service or snapshot. Returns image ID. """ function image_publish(source_type::String, source_id::String; name::Union{String, Nothing}=nothing, description::Union{String, Nothing}=nothing, public_key::Union{String,Nothing}=nothing, secret_key::Union{String,Nothing}=nothing) (pk, sk) = if public_key !== nothing && secret_key !== nothing (public_key, secret_key) else get_api_keys(nothing) end payload = Dict("source_type" => source_type, "source_id" => source_id) if name !== nothing payload["name"] = name end if description !== nothing payload["description"] = description end result = api_request("/images/publish", pk, sk, method="POST", data=payload) return get(result, "id", "") end """ image_delete(image_id::String; kwargs...) -> Bool Delete an image. """ function image_delete(image_id::String; public_key::Union{String,Nothing}=nothing, secret_key::Union{String,Nothing}=nothing) (pk, sk) = if public_key !== nothing && secret_key !== nothing (public_key, secret_key) else get_api_keys(nothing) end api_request_with_sudo("/images/$image_id", pk, sk, method="DELETE") return true end """ image_lock(image_id::String; kwargs...) -> Bool Lock an image to prevent deletion. """ function image_lock(image_id::String; public_key::Union{String,Nothing}=nothing, secret_key::Union{String,Nothing}=nothing) (pk, sk) = if public_key !== nothing && secret_key !== nothing (public_key, secret_key) else get_api_keys(nothing) end api_request("/images/$image_id/lock", pk, sk, method="POST") return true end """ image_unlock(image_id::String; kwargs...) -> Bool Unlock an image. """ function image_unlock(image_id::String; public_key::Union{String,Nothing}=nothing, secret_key::Union{String,Nothing}=nothing) (pk, sk) = if public_key !== nothing && secret_key !== nothing (public_key, secret_key) else get_api_keys(nothing) end api_request_with_sudo("/images/$image_id/unlock", pk, sk, method="POST", data=Dict()) return true end """ image_set_visibility(image_id::String, visibility::String; kwargs...) -> Bool Set image visibility (private, unlisted, or public). """ function image_set_visibility(image_id::String, visibility::String; public_key::Union{String,Nothing}=nothing, secret_key::Union{String,Nothing}=nothing) (pk, sk) = if public_key !== nothing && secret_key !== nothing (public_key, secret_key) else get_api_keys(nothing) end api_request("/images/$image_id/visibility", pk, sk, method="POST", data=Dict("visibility" => visibility)) return true end """ image_grant_access(image_id::String, trusted_api_key::String; kwargs...) -> Bool Grant access to an image. """ function image_grant_access(image_id::String, trusted_api_key::String; public_key::Union{String,Nothing}=nothing, secret_key::Union{String,Nothing}=nothing) (pk, sk) = if public_key !== nothing && secret_key !== nothing (public_key, secret_key) else get_api_keys(nothing) end api_request("/images/$image_id/access", pk, sk, method="POST", data=Dict("trusted_api_key" => trusted_api_key)) return true end """ image_revoke_access(image_id::String, trusted_api_key::String; kwargs...) -> Bool Revoke access to an image. """ function image_revoke_access(image_id::String, trusted_api_key::String; public_key::Union{String,Nothing}=nothing, secret_key::Union{String,Nothing}=nothing) (pk, sk) = if public_key !== nothing && secret_key !== nothing (public_key, secret_key) else get_api_keys(nothing) end api_request("/images/$image_id/access/$trusted_api_key", pk, sk, method="DELETE") return true end """ image_list_trusted(image_id::String; kwargs...) -> Vector{String} List trusted API keys for an image. """ function image_list_trusted(image_id::String; public_key::Union{String,Nothing}=nothing, secret_key::Union{String,Nothing}=nothing) (pk, sk) = if public_key !== nothing && secret_key !== nothing (public_key, secret_key) else get_api_keys(nothing) end result = api_request("/images/$image_id/access", pk, sk) return get(result, "trusted_keys", []) end """ image_transfer(image_id::String, to_api_key::String; kwargs...) -> Bool Transfer ownership of an image. """ function image_transfer(image_id::String, to_api_key::String; public_key::Union{String,Nothing}=nothing, secret_key::Union{String,Nothing}=nothing) (pk, sk) = if public_key !== nothing && secret_key !== nothing (public_key, secret_key) else get_api_keys(nothing) end api_request("/images/$image_id/transfer", pk, sk, method="POST", data=Dict("to_api_key" => to_api_key)) return true end """ image_spawn(image_id::String; kwargs...) -> String Spawn a new service from an image. Returns service ID. """ function image_spawn(image_id::String; name::Union{String, Nothing}=nothing, ports::Union{Vector{Int}, Nothing}=nothing, bootstrap::Union{String, Nothing}=nothing, network::Union{String, Nothing}=nothing, public_key::Union{String,Nothing}=nothing, secret_key::Union{String,Nothing}=nothing) (pk, sk) = if public_key !== nothing && secret_key !== nothing (public_key, secret_key) else get_api_keys(nothing) end payload = Dict() if name !== nothing payload["name"] = name end if ports !== nothing payload["ports"] = ports end if bootstrap !== nothing payload["bootstrap"] = bootstrap end if network !== nothing payload["network"] = network end result = api_request("/images/$image_id/spawn", pk, sk, method="POST", data=payload) return get(result, "id", "") end """ image_clone(image_id::String; kwargs...) -> String Clone an image. Returns new image ID. """ function image_clone(image_id::String; name::Union{String, Nothing}=nothing, description::Union{String, Nothing}=nothing, public_key::Union{String,Nothing}=nothing, secret_key::Union{String,Nothing}=nothing) (pk, sk) = if public_key !== nothing && secret_key !== nothing (public_key, secret_key) else get_api_keys(nothing) end payload = Dict() if name !== nothing payload["name"] = name end if description !== nothing payload["description"] = description end result = api_request("/images/$image_id/clone", pk, sk, method="POST", data=payload) return get(result, "id", "") end """ logs_fetch(source::String; kwargs...) -> String Fetch PaaS logs. """ function logs_fetch(source::String; lines::Int=100, since::String="1h", grep::Union{String, Nothing}=nothing, public_key::Union{String,Nothing}=nothing, secret_key::Union{String,Nothing}=nothing) (pk, sk) = if public_key !== nothing && secret_key !== nothing (public_key, secret_key) else get_api_keys(nothing) end endpoint = "/logs?source=$source&lines=$lines&since=$since" if grep !== nothing endpoint *= "&grep=$grep" end result = api_request(endpoint, pk, sk) return get(result, "logs", "") end """ validate_keys(; kwargs...) -> Dict Validate API keys and return account info. """ function validate_keys(; public_key::Union{String,Nothing}=nothing, secret_key::Union{String,Nothing}=nothing) (pk, sk) = if public_key !== nothing && secret_key !== nothing (public_key, secret_key) else get_api_keys(nothing) end url = PORTAL_BASE * "/keys/validate" timestamp = Int64(floor(time())) body = "{}" signature = compute_signature(sk, timestamp, "POST", "/keys/validate", body) headers = [ "Authorization" => "Bearer $pk", "X-Timestamp" => string(timestamp), "X-Signature" => signature, "Content-Type" => "application/json" ] try response = HTTP.post(url, headers, body, readtimeout=30) return JSON.parse(String(response.body)) catch e return Dict("valid" => false, "error" => string(e)) end end """ health_check() -> Bool Check if the API is healthy. """ function health_check() try response = HTTP.get("$API_BASE/health", readtimeout=10) return response.status == 200 catch return false end end """ version() -> String Get SDK version. """ function version() return VERSION end # Thread-local error storage const _last_error = Ref{String}("") """ last_error() -> String Get the last error message. """ function last_error() return _last_error[] end """ set_last_error(msg::String) Set the last error message (internal use). """ function set_last_error(msg::String) _last_error[] = msg end """ hmac_sign(secret_key::String, message::String) -> String Compute HMAC-SHA256 signature. """ function hmac_sign(secret_key::String, message::String) return bytes2hex(SHA.hmac_sha256(Vector{UInt8}(secret_key), Vector{UInt8}(message))) end main()