#!/usr/bin/env lua -- 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.lua - Unsandbox CLI Client (Lua Implementation) -- -- Full-featured CLI matching un.c capabilities: -- - Execute code with env vars, input files, artifacts -- - Interactive sessions with shell/REPL support -- - Persistent services with domains and ports -- -- Usage: -- un.lua [options] -- un.lua session [options] -- un.lua service [options] -- -- Requires: UNSANDBOX_API_KEY environment variable -- Note: Uses curl for HTTP requests (requires curl to be installed) local json = require("cjson") local API_BASE = "https://api.unsandbox.com" local PORTAL_BASE = "https://unsandbox.com" local BLUE = "\27[34m" local RED = "\27[31m" local GREEN = "\27[32m" local YELLOW = "\27[33m" local RESET = "\27[0m" local EXT_MAP = { [".py"] = "python", [".js"] = "javascript", [".ts"] = "typescript", [".rb"] = "ruby", [".php"] = "php", [".pl"] = "perl", [".lua"] = "lua", [".sh"] = "bash", [".go"] = "go", [".rs"] = "rust", [".c"] = "c", [".cpp"] = "cpp", [".cc"] = "cpp", [".cxx"] = "cpp", [".java"] = "java", [".kt"] = "kotlin", [".cs"] = "csharp", [".fs"] = "fsharp", [".hs"] = "haskell", [".ml"] = "ocaml", [".clj"] = "clojure", [".scm"] = "scheme", [".lisp"] = "commonlisp", [".erl"] = "erlang", [".ex"] = "elixir", [".exs"] = "elixir", [".jl"] = "julia", [".r"] = "r", [".R"] = "r", [".cr"] = "crystal", [".d"] = "d", [".nim"] = "nim", [".zig"] = "zig", [".v"] = "v", [".dart"] = "dart", [".groovy"] = "groovy", [".scala"] = "scala", [".f90"] = "fortran", [".f95"] = "fortran", [".cob"] = "cobol", [".pro"] = "prolog", [".forth"] = "forth", [".4th"] = "forth", [".tcl"] = "tcl", [".raku"] = "raku", [".m"] = "objc" } local function get_api_key(args_key) local key = args_key or os.getenv("UNSANDBOX_API_KEY") if not key then io.stderr:write(RED .. "Error: UNSANDBOX_API_KEY not set" .. RESET .. "\n") os.exit(1) end return key end local function detect_language(filename) local ext = filename:match("%.([^.]+)$") if ext then local lang = EXT_MAP["." .. ext:lower()] if lang then return lang end end local file = io.open(filename, "r") if file then local first_line = file:read("*line") file:close() if first_line and first_line:match("^#!") then if first_line:match("python") then return "python" end if first_line:match("node") then return "javascript" end if first_line:match("ruby") then return "ruby" end if first_line:match("perl") then return "perl" end if first_line:match("bash") or first_line:match("/sh") then return "bash" end if first_line:match("lua") then return "lua" end if first_line:match("php") then return "php" end end end io.stderr:write(RED .. "Error: Cannot detect language for " .. filename .. RESET .. "\n") os.exit(1) end local function shell_escape(str) return "'" .. str:gsub("'", "'\\''") .. "'" end local function api_request(endpoint, method, data, api_key) method = method or "GET" local url = API_BASE .. endpoint local tmpfile = os.tmpname() local cmd = "curl -s -X " .. method .. " " .. shell_escape(url) .. " -H 'Authorization: Bearer " .. api_key .. "'" .. " -H 'Content-Type: application/json'" if data then local payload = json.encode(data) local data_file = os.tmpname() local f = io.open(data_file, "w") f:write(payload) f:close() cmd = cmd .. " -d @" .. shell_escape(data_file) end cmd = cmd .. " -w '\\n%{http_code}' -o " .. shell_escape(tmpfile) local handle = io.popen(cmd) local http_code = handle:read("*a"):match("(%d+)$") handle:close() local file = io.open(tmpfile, "r") local response = file:read("*all") file:close() os.remove(tmpfile) if data then os.remove(data_file) end if not http_code or tonumber(http_code) < 200 or tonumber(http_code) >= 300 then io.stderr:write(RED .. "Error: HTTP " .. (http_code or "000") .. " - " .. response .. RESET .. "\n") os.exit(1) end return json.decode(response) end local function read_file(filename) local file, err = io.open(filename, "rb") if not file then io.stderr:write(RED .. "Error: File not found: " .. filename .. RESET .. "\n") os.exit(1) end local content = file:read("*all") file:close() return content end local function base64_encode(data) local b64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" return ((data:gsub('.', function(x) local r,b='',x:byte() for i=8,1,-1 do r=r..(b%2^i-b%2^(i-1)>0 and '1' or '0') end return r; end)..'0000'):gsub('%d%d%d?%d?%d?%d?', function(x) if (#x < 6) then return '' end local c=0 for i=1,6 do c=c+(x:sub(i,i)=='1' and 2^(6-i) or 0) end return b64:sub(c+1,c+1) end)..({ '', '==', '=' })[#data%3+1]) end local function base64_decode(data) local b64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" data = string.gsub(data, '[^'..b64..'=]', '') return (data:gsub('.', function(x) if (x == '=') then return '' end local r,f='',(b64:find(x)-1) for i=6,1,-1 do r=r..(f%2^i-f%2^(i-1)>0 and '1' or '0') end return r; end):gsub('%d%d%d?%d?%d?%d?%d?%d?', function(x) if (#x ~= 8) then return '' end local c=0 for i=1,8 do c=c+(x:sub(i,i)=='1' and 2^(8-i) or 0) end return string.char(c) end)) end local function cmd_execute(options) local api_key = get_api_key(options.api_key) local code = read_file(options.source_file) local language = detect_language(options.source_file) local payload = { language = language, code = code } if options.env and #options.env > 0 then local env_vars = {} for _, e in ipairs(options.env) do local k, v = e:match("^([^=]+)=(.*)$") if k and v then env_vars[k] = v end end if next(env_vars) then payload.env = env_vars end end if options.files and #options.files > 0 then local input_files = {} for _, filepath in ipairs(options.files) do local content = read_file(filepath) table.insert(input_files, { filename = filepath:match("([^/]+)$"), content_base64 = base64_encode(content) }) end payload.input_files = input_files end if options.artifacts then payload.return_artifacts = true end if options.network then payload.network = options.network end if options.vcpu then payload.vcpu = options.vcpu end local result = api_request("/execute", "POST", payload, api_key) if result.stdout then io.write(BLUE .. result.stdout .. RESET) end if result.stderr then io.stderr:write(RED .. result.stderr .. RESET) end if options.artifacts and result.artifacts then local out_dir = options.output_dir or "." os.execute("mkdir -p " .. shell_escape(out_dir)) for _, artifact in ipairs(result.artifacts) do local filename = artifact.filename or "artifact" local content = base64_decode(artifact.content_base64) local filepath = out_dir .. "/" .. filename local file = io.open(filepath, "wb") file:write(content) file:close() os.execute("chmod 755 " .. shell_escape(filepath)) io.stderr:write(GREEN .. "Saved: " .. filepath .. RESET .. "\n") end end os.exit(result.exit_code or 0) end local function cmd_session(options) local api_key = get_api_key(options.api_key) if options.list then local result = api_request("/sessions", "GET", nil, api_key) local sessions = result.sessions or {} if #sessions == 0 then print("No active sessions") else print(string.format("%-40s %-10s %-10s %s", "ID", "Shell", "Status", "Created")) for _, s in ipairs(sessions) do print(string.format("%-40s %-10s %-10s %s", s.id or "N/A", s.shell or "N/A", s.status or "N/A", s.created_at or "N/A")) end end return end if options.kill then api_request("/sessions/" .. options.kill, "DELETE", nil, api_key) print(GREEN .. "Session terminated: " .. options.kill .. RESET) return end if options.attach then print(YELLOW .. "Attaching to session " .. options.attach .. "..." .. RESET) print(YELLOW .. "(Interactive sessions require WebSocket - use un2 for full support)" .. RESET) return end local payload = { shell = options.shell or "bash" } if options.network then payload.network = options.network end if options.vcpu then payload.vcpu = options.vcpu end if options.tmux then payload.persistence = "tmux" end if options.screen then payload.persistence = "screen" end if options.audit then payload.audit = true end print(YELLOW .. "Creating session..." .. RESET) local result = api_request("/sessions", "POST", payload, api_key) print(GREEN .. "Session created: " .. (result.id or "N/A") .. RESET) print(YELLOW .. "(Interactive sessions require WebSocket - use un2 for full support)" .. RESET) end local function cmd_key(options) local api_key = get_api_key(options.api_key) if options.extend then -- Get public_key from validation response local url = PORTAL_BASE .. "/keys/validate" local tmpfile = os.tmpname() local cmd = "curl -s -X POST " .. shell_escape(url) .. " -H 'Authorization: Bearer " .. api_key .. "'" .. " -H 'Content-Type: application/json'" .. " -w '\\n%{http_code}' -o " .. shell_escape(tmpfile) local handle = io.popen(cmd) local http_code = handle:read("*a"):match("(%d+)$") handle:close() local file = io.open(tmpfile, "r") local response = file:read("*all") file:close() os.remove(tmpfile) if not http_code or tonumber(http_code) < 200 or tonumber(http_code) >= 300 then io.stderr:write(RED .. "Error: HTTP " .. (http_code or "000") .. " - " .. response .. RESET .. "\n") os.exit(1) end local result = json.decode(response) local public_key = result.public_key if not public_key then io.stderr:write(RED .. "Error: Could not retrieve public key" .. RESET .. "\n") os.exit(1) end -- Open browser with extend URL local extend_url = PORTAL_BASE .. "/keys/extend?pk=" .. public_key print(GREEN .. "Opening browser to extend key..." .. RESET) print(extend_url) os.execute("xdg-open " .. shell_escape(extend_url) .. " 2>/dev/null || open " .. shell_escape(extend_url) .. " 2>/dev/null") return end -- Validate key (default action) local url = PORTAL_BASE .. "/keys/validate" local tmpfile = os.tmpname() local cmd = "curl -s -X POST " .. shell_escape(url) .. " -H 'Authorization: Bearer " .. api_key .. "'" .. " -H 'Content-Type: application/json'" .. " -w '\\n%{http_code}' -o " .. shell_escape(tmpfile) local handle = io.popen(cmd) local http_code = handle:read("*a"):match("(%d+)$") handle:close() local file = io.open(tmpfile, "r") local response = file:read("*all") file:close() os.remove(tmpfile) if not http_code or tonumber(http_code) < 200 or tonumber(http_code) >= 300 then io.stderr:write(RED .. "Error: Invalid API key" .. RESET .. "\n") os.exit(1) end local result = json.decode(response) if result.status == "valid" then print(GREEN .. "Valid" .. RESET) if result.public_key then print("Public Key: " .. result.public_key) end if result.tier then print("Tier: " .. result.tier) end if result.expires_at then print("Expires: " .. result.expires_at) end elseif result.status == "expired" then print(RED .. "Expired" .. RESET) if result.public_key then print("Public Key: " .. result.public_key) end if result.tier then print("Tier: " .. result.tier) end if result.expired_at then print("Expired: " .. result.expired_at) end print(YELLOW .. "To renew: Visit https://unsandbox.com/keys/extend" .. RESET) else print(RED .. "Invalid" .. RESET) if result.message then print("Message: " .. result.message) end end end local function cmd_service(options) local api_key = get_api_key(options.api_key) if options.list then local result = api_request("/services", "GET", nil, api_key) local services = result.services or {} if #services == 0 then print("No services") else print(string.format("%-20s %-15s %-10s %-15s %s", "ID", "Name", "Status", "Ports", "Domains")) for _, s in ipairs(services) do local ports = table.concat(s.ports or {}, ",") local domains = table.concat(s.domains or {}, ",") print(string.format("%-20s %-15s %-10s %-15s %s", s.id or "N/A", s.name or "N/A", s.status or "N/A", ports, domains)) end end return end if options.info then local result = api_request("/services/" .. options.info, "GET", nil, api_key) print(json.encode(result)) return end if options.logs then local result = api_request("/services/" .. options.logs .. "/logs", "GET", nil, api_key) print(result.logs or "") return end if options.tail then local result = api_request("/services/" .. options.tail .. "/logs?lines=9000", "GET", nil, api_key) print(result.logs or "") return end if options.sleep then api_request("/services/" .. options.sleep .. "/sleep", "POST", nil, api_key) print(GREEN .. "Service sleeping: " .. options.sleep .. RESET) return end if options.wake then api_request("/services/" .. options.wake .. "/wake", "POST", nil, api_key) print(GREEN .. "Service waking: " .. options.wake .. RESET) return end if options.destroy then api_request("/services/" .. options.destroy, "DELETE", nil, api_key) print(GREEN .. "Service destroyed: " .. options.destroy .. RESET) return end if options.execute then local payload = { command = options.command } local result = api_request("/services/" .. options.execute .. "/execute", "POST", payload, api_key) if result.stdout then io.write(BLUE .. result.stdout .. RESET) end if result.stderr then io.stderr:write(RED .. result.stderr .. RESET) end return end if options.name then local payload = { name = options.name } if options.ports then local ports = {} for p in options.ports:gmatch("[^,]+") do table.insert(ports, tonumber(p)) end payload.ports = ports end if options.domains then local domains = {} for d in options.domains:gmatch("[^,]+") do table.insert(domains, d) end payload.domains = domains end if options.type then payload.service_type = options.type end if options.bootstrap then local file = io.open(options.bootstrap, "r") if file then payload.bootstrap = file:read("*all") file:close() else payload.bootstrap = options.bootstrap end end if options.network then payload.network = options.network end if options.vcpu then payload.vcpu = options.vcpu end local result = api_request("/services", "POST", payload, api_key) print(GREEN .. "Service created: " .. (result.id or "N/A") .. RESET) print("Name: " .. (result.name or "N/A")) if result.url then print("URL: " .. result.url) end return end io.stderr:write(RED .. "Error: Specify --name to create a service, or use --list, --info, etc." .. RESET .. "\n") os.exit(1) end local function main() local options = { command = nil, source_file = nil, env = {}, files = {}, artifacts = false, output_dir = nil, network = nil, vcpu = nil, api_key = nil, shell = nil, list = false, attach = nil, kill = nil, audit = false, tmux = false, screen = false, name = nil, ports = nil, domains = nil, type = nil, bootstrap = nil, info = nil, logs = nil, tail = nil, sleep = nil, wake = nil, destroy = nil, execute = nil, command = nil, extend = false } local i = 1 while i <= #arg do local a = arg[i] if a == "session" or a == "service" or a == "key" then options.command = a elseif a == "-e" then i = i + 1 table.insert(options.env, arg[i]) elseif a == "-f" then i = i + 1 table.insert(options.files, arg[i]) elseif a == "-a" then options.artifacts = true elseif a == "-o" then i = i + 1 options.output_dir = arg[i] elseif a == "-n" then i = i + 1 options.network = arg[i] elseif a == "-v" then i = i + 1 options.vcpu = tonumber(arg[i]) elseif a == "-k" then i = i + 1 options.api_key = arg[i] elseif a == "-s" or a == "--shell" then i = i + 1 options.shell = arg[i] elseif a == "-l" or a == "--list" then options.list = true elseif a == "--attach" then i = i + 1 options.attach = arg[i] elseif a == "--kill" then i = i + 1 options.kill = arg[i] elseif a == "--audit" then options.audit = true elseif a == "--tmux" then options.tmux = true elseif a == "--screen" then options.screen = true elseif a == "--name" then i = i + 1 options.name = arg[i] elseif a == "--ports" then i = i + 1 options.ports = arg[i] elseif a == "--domains" then i = i + 1 options.domains = arg[i] elseif a == "--type" then i = i + 1 options.type = arg[i] elseif a == "--bootstrap" then i = i + 1 options.bootstrap = arg[i] elseif a == "--info" then i = i + 1 options.info = arg[i] elseif a == "--logs" then i = i + 1 options.logs = arg[i] elseif a == "--tail" then i = i + 1 options.tail = arg[i] elseif a == "--sleep" then i = i + 1 options.sleep = arg[i] elseif a == "--wake" then i = i + 1 options.wake = arg[i] elseif a == "--destroy" then i = i + 1 options.destroy = arg[i] elseif a == "--execute" then i = i + 1 options.execute = arg[i] elseif a == "--command" then i = i + 1 options.command = arg[i] elseif a == "--extend" then options.extend = true elseif not a:match("^%-") then options.source_file = a end i = i + 1 end if options.command == "session" then cmd_session(options) elseif options.command == "service" then cmd_service(options) elseif options.command == "key" then cmd_key(options) elseif options.source_file then cmd_execute(options) else print([[ Unsandbox CLI - Execute code in secure sandboxes Usage: ]] .. arg[0] .. [[ [options] ]] .. arg[0] .. [[ session [options] ]] .. arg[0] .. [[ service [options] ]] .. arg[0] .. [[ key [options] Execute options: -e KEY=VALUE Environment variable (multiple allowed) -f FILE Input file (multiple allowed) -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: -s, --shell NAME Shell/REPL (default: bash) -l, --list List sessions --attach ID Attach to session --kill ID Terminate session --audit Record session --tmux Enable tmux persistence --screen Enable screen persistence Service options: --name NAME Service name --ports PORTS Comma-separated ports --domains DOMAINS Custom domains --type TYPE Service type (minecraft|mumble|teamspeak|source|tcp|udp) --bootstrap CMD Bootstrap command/file -l, --list List services --info ID Get service details --logs ID Get all logs --tail ID Get last 9000 lines --sleep ID Freeze service --wake ID Unfreeze service --destroy ID Destroy service --execute ID Execute command in service --command CMD Command to execute (with --execute) Key options: --extend Open browser to extend/renew key ]]) os.exit(1) end end main()