un-inception/un.rb
Russell Ballestrini 5380af54cb Add key subcommand to all 31 implementations
- Validate API keys via portal endpoint
- Show key status, tier, expiration with color-coded output
- Add --extend flag to open browser for key renewal
- Update .gitignore for compiled binaries
- Update README with key command documentation
2025-12-27 11:50:34 -05:00

609 lines
18 KiB
Ruby

# 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 ruby
# un.rb - Unsandbox CLI Client (Ruby 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.rb [options] <source_file>
# un.rb session [options]
# un.rb service [options]
#
# Requires: UNSANDBOX_API_KEY environment variable
require 'json'
require 'net/http'
require 'uri'
require 'base64'
require 'fileutils'
require 'optparse'
API_BASE = 'https://api.unsandbox.com'
PORTAL_BASE = 'https://unsandbox.com'
BLUE = "\e[34m"
RED = "\e[31m"
GREEN = "\e[32m"
YELLOW = "\e[33m"
RESET = "\e[0m"
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'
}.freeze
def get_api_key(args_key = nil)
key = args_key || ENV['UNSANDBOX_API_KEY']
unless key
warn "#{RED}Error: UNSANDBOX_API_KEY not set#{RESET}"
exit 1
end
key
end
def detect_language(filename)
ext = File.extname(filename).downcase
lang = EXT_MAP[ext]
unless lang
begin
first_line = File.open(filename, &:readline)
if first_line.start_with?('#!')
return 'python' if first_line.include?('python')
return 'javascript' if first_line.include?('node')
return 'ruby' if first_line.include?('ruby')
return 'perl' if first_line.include?('perl')
return 'bash' if first_line.include?('bash') || first_line.include?('/sh')
return 'lua' if first_line.include?('lua')
return 'php' if first_line.include?('php')
end
rescue
end
warn "#{RED}Error: Cannot detect language for #{filename}#{RESET}"
exit 1
end
lang
end
def api_request(endpoint, method: 'GET', data: nil, api_key:)
uri = URI("#{API_BASE}#{endpoint}")
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
http.read_timeout = 300
request = case method
when 'GET' then Net::HTTP::Get.new(uri)
when 'POST' then Net::HTTP::Post.new(uri)
when 'DELETE' then Net::HTTP::Delete.new(uri)
else raise "Unknown method: #{method}"
end
request['Authorization'] = "Bearer #{api_key}"
request['Content-Type'] = 'application/json'
request.body = JSON.generate(data) if data
response = http.request(request)
unless response.is_a?(Net::HTTPSuccess)
warn "#{RED}Error: HTTP #{response.code} - #{response.body}#{RESET}"
exit 1
end
JSON.parse(response.body)
rescue => e
warn "#{RED}Error: #{e.message}#{RESET}"
exit 1
end
def cmd_execute(options)
api_key = get_api_key(options[:api_key])
unless File.exist?(options[:source_file])
warn "#{RED}Error: File not found: #{options[:source_file]}#{RESET}"
exit 1
end
code = File.read(options[:source_file])
language = detect_language(options[:source_file])
payload = { language: language, code: code }
if options[:env] && !options[:env].empty?
env_vars = {}
options[:env].each do |e|
k, v = e.split('=', 2)
env_vars[k] = v if k && v
end
payload[:env] = env_vars unless env_vars.empty?
end
if options[:files] && !options[:files].empty?
input_files = options[:files].map do |filepath|
unless File.exist?(filepath)
warn "#{RED}Error: Input file not found: #{filepath}#{RESET}"
exit 1
end
{
filename: File.basename(filepath),
content_base64: Base64.strict_encode64(File.read(filepath, mode: 'rb'))
}
end
payload[:input_files] = input_files
end
payload[:return_artifacts] = true if options[:artifacts]
payload[:network] = options[:network] if options[:network]
payload[:vcpu] = options[:vcpu] if options[:vcpu]
result = api_request('/execute', method: 'POST', data: payload, api_key: api_key)
print "#{BLUE}#{result['stdout']}#{RESET}" if result['stdout']
$stderr.print "#{RED}#{result['stderr']}#{RESET}" if result['stderr']
if options[:artifacts] && result['artifacts']
out_dir = options[:output_dir] || '.'
FileUtils.mkdir_p(out_dir) unless Dir.exist?(out_dir)
result['artifacts'].each do |artifact|
filename = artifact['filename'] || 'artifact'
content = Base64.strict_decode64(artifact['content_base64'])
filepath = File.join(out_dir, filename)
File.write(filepath, content, mode: 'wb')
File.chmod(0755, filepath)
warn "#{GREEN}Saved: #{filepath}#{RESET}"
end
end
exit(result['exit_code'] || 0)
end
def cmd_session(options)
api_key = get_api_key(options[:api_key])
if options[:list]
result = api_request('/sessions', api_key: api_key)
sessions = result['sessions'] || []
if sessions.empty?
puts 'No active sessions'
else
puts format('%-40s %-10s %-10s %s', 'ID', 'Shell', 'Status', 'Created')
sessions.each do |s|
puts format('%-40s %-10s %-10s %s',
s['id'] || 'N/A', s['shell'] || 'N/A',
s['status'] || 'N/A', s['created_at'] || 'N/A')
end
end
return
end
if options[:kill]
api_request("/sessions/#{options[:kill]}", method: 'DELETE', api_key: api_key)
puts "#{GREEN}Session terminated: #{options[:kill]}#{RESET}"
return
end
if options[:attach]
puts "#{YELLOW}Attaching to session #{options[:attach]}...#{RESET}"
puts "#{YELLOW}(Interactive sessions require WebSocket - use un2 for full support)#{RESET}"
return
end
payload = { shell: options[:shell] || 'bash' }
payload[:network] = options[:network] if options[:network]
payload[:vcpu] = options[:vcpu] if options[:vcpu]
payload[:persistence] = 'tmux' if options[:tmux]
payload[:persistence] = 'screen' if options[:screen]
payload[:audit] = true if options[:audit]
puts "#{YELLOW}Creating session...#{RESET}"
result = api_request('/sessions', method: 'POST', data: payload, api_key: api_key)
puts "#{GREEN}Session created: #{result['id'] || 'N/A'}#{RESET}"
puts "#{YELLOW}(Interactive sessions require WebSocket - use un2 for full support)#{RESET}"
end
def validate_key(api_key)
uri = URI("#{PORTAL_BASE}/keys/validate")
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
http.read_timeout = 30
request = Net::HTTP::Post.new(uri)
request['Authorization'] = "Bearer #{api_key}"
request['Content-Type'] = 'application/json'
response = http.request(request)
begin
result = JSON.parse(response.body)
rescue JSON::ParserError => e
warn "#{RED}Error: Failed to parse response: #{e.message}#{RESET}"
exit 1
end
if response.is_a?(Net::HTTPSuccess) && result['valid']
puts "#{GREEN}Valid#{RESET}"
puts "Public Key: #{result['public_key']}"
puts "Tier: #{result['tier']}"
puts "Status: #{result['status']}"
puts "Expires: #{result['expires_at']}"
puts "Time Remaining: #{result['time_remaining']}"
puts "Rate Limit: #{result['rate_limit']}"
puts "Burst: #{result['burst']}"
puts "Concurrency: #{result['concurrency']}"
result
elsif result['expired']
puts "#{RED}Expired#{RESET}"
puts "Public Key: #{result['public_key']}"
puts "Tier: #{result['tier']}"
puts "Expired: #{result['expires_at']}"
puts "#{YELLOW}To renew: Visit https://unsandbox.com/keys/extend#{RESET}"
result
else
puts "#{RED}Invalid#{RESET}"
puts "Error: #{result['error'] || result['reason'] || 'Unknown error'}"
exit 1
end
rescue => e
warn "#{RED}Error: #{e.message}#{RESET}"
exit 1
end
def open_browser(url)
case RbConfig::CONFIG['host_os']
when /mswin|mingw|cygwin/
system("start #{url}")
when /darwin/
system("open #{url}")
when /linux|bsd/
system("xdg-open #{url}")
else
puts "#{YELLOW}Please open this URL in your browser:#{RESET}"
puts url
end
end
def cmd_key(options)
api_key = get_api_key(options[:api_key])
if options[:extend]
result = validate_key(api_key)
public_key = result['public_key']
if public_key
url = "#{PORTAL_BASE}/keys/extend?pk=#{public_key}"
puts "#{GREEN}Opening browser to extend key...#{RESET}"
open_browser(url)
else
warn "#{RED}Error: Could not retrieve public key#{RESET}"
exit 1
end
else
validate_key(api_key)
end
end
def cmd_service(options)
api_key = get_api_key(options[:api_key])
if options[:list]
result = api_request('/services', api_key: api_key)
services = result['services'] || []
if services.empty?
puts 'No services'
else
puts format('%-20s %-15s %-10s %-15s %s', 'ID', 'Name', 'Status', 'Ports', 'Domains')
services.each do |s|
ports = (s['ports'] || []).join(',')
domains = (s['domains'] || []).join(',')
puts format('%-20s %-15s %-10s %-15s %s',
s['id'] || 'N/A', s['name'] || 'N/A',
s['status'] || 'N/A', ports, domains)
end
end
return
end
if options[:info]
result = api_request("/services/#{options[:info]}", api_key: api_key)
puts JSON.pretty_generate(result)
return
end
if options[:logs]
result = api_request("/services/#{options[:logs]}/logs", api_key: api_key)
puts result['logs'] || ''
return
end
if options[:tail]
result = api_request("/services/#{options[:tail]}/logs?lines=9000", api_key: api_key)
puts result['logs'] || ''
return
end
if options[:sleep]
api_request("/services/#{options[:sleep]}/sleep", method: 'POST', api_key: api_key)
puts "#{GREEN}Service sleeping: #{options[:sleep]}#{RESET}"
return
end
if options[:wake]
api_request("/services/#{options[:wake]}/wake", method: 'POST', api_key: api_key)
puts "#{GREEN}Service waking: #{options[:wake]}#{RESET}"
return
end
if options[:destroy]
api_request("/services/#{options[:destroy]}", method: 'DELETE', api_key: api_key)
puts "#{GREEN}Service destroyed: #{options[:destroy]}#{RESET}"
return
end
if options[:execute]
payload = { command: options[:command] }
result = api_request("/services/#{options[:execute]}/execute", method: 'POST', data: payload, api_key: api_key)
print "#{BLUE}#{result['stdout']}#{RESET}" if result['stdout']
$stderr.print "#{RED}#{result['stderr']}#{RESET}" if result['stderr']
return
end
if options[:name]
payload = { name: options[:name] }
payload[:ports] = options[:ports].split(',').map(&:to_i) if options[:ports]
payload[:domains] = options[:domains].split(',') if options[:domains]
payload[:service_type] = options[:type] if options[:type]
if options[:bootstrap]
payload[:bootstrap] = if File.exist?(options[:bootstrap])
File.read(options[:bootstrap])
else
options[:bootstrap]
end
end
payload[:network] = options[:network] if options[:network]
payload[:vcpu] = options[:vcpu] if options[:vcpu]
result = api_request('/services', method: 'POST', data: payload, api_key: api_key)
puts "#{GREEN}Service created: #{result['id'] || 'N/A'}#{RESET}"
puts "Name: #{result['name'] || 'N/A'}"
puts "URL: #{result['url']}" if result['url']
return
end
warn "#{RED}Error: Specify --name to create a service, or use --list, --info, etc.#{RESET}"
exit 1
end
def main
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,
extend: false
}
# Manual argument parsing
i = 0
while i < ARGV.length
arg = ARGV[i]
case arg
when 'session', 'service', 'key'
options[:command] = arg
when '-e'
i += 1
options[:env] << ARGV[i]
when '-f'
i += 1
options[:files] << ARGV[i]
when '-a'
options[:artifacts] = true
when '-o'
i += 1
options[:output_dir] = ARGV[i]
when '-n'
i += 1
options[:network] = ARGV[i]
when '-v'
i += 1
options[:vcpu] = ARGV[i].to_i
when '-k'
i += 1
options[:api_key] = ARGV[i]
when '-s', '--shell'
i += 1
options[:shell] = ARGV[i]
when '-l', '--list'
options[:list] = true
when '--attach'
i += 1
options[:attach] = ARGV[i]
when '--kill'
i += 1
options[:kill] = ARGV[i]
when '--audit'
options[:audit] = true
when '--tmux'
options[:tmux] = true
when '--screen'
options[:screen] = true
when '--name'
i += 1
options[:name] = ARGV[i]
when '--ports'
i += 1
options[:ports] = ARGV[i]
when '--domains'
i += 1
options[:domains] = ARGV[i]
when '--type'
i += 1
options[:type] = ARGV[i]
when '--bootstrap'
i += 1
options[:bootstrap] = ARGV[i]
when '--info'
i += 1
options[:info] = ARGV[i]
when '--logs'
i += 1
options[:logs] = ARGV[i]
when '--tail'
i += 1
options[:tail] = ARGV[i]
when '--sleep'
i += 1
options[:sleep] = ARGV[i]
when '--wake'
i += 1
options[:wake] = ARGV[i]
when '--destroy'
i += 1
options[:destroy] = ARGV[i]
when '--execute'
i += 1
options[:execute] = ARGV[i]
when '--command'
i += 1
options[:command] = ARGV[i]
when '--extend'
options[:extend] = true
else
options[:source_file] = arg unless arg.start_with?('-')
end
i += 1
end
case options[:command]
when 'session'
cmd_session(options)
when 'service'
cmd_service(options)
when 'key'
cmd_key(options)
else
if options[:source_file]
cmd_execute(options)
else
puts <<~HELP
Unsandbox CLI - Execute code in secure sandboxes
Usage:
#{$PROGRAM_NAME} [options] <source_file>
#{$PROGRAM_NAME} session [options]
#{$PROGRAM_NAME} service [options]
#{$PROGRAM_NAME} 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:
-k KEY API key (or use UNSANDBOX_API_KEY env var)
--extend Validate key and open browser to extend
HELP
exit 1
end
end
end
main if __FILE__ == $PROGRAM_NAME