- Update all un.* implementations to use HMAC-SHA256 signing - Headers: Authorization (Bearer public_key), X-Timestamp, X-Signature - Signature: HMAC-SHA256(secret_key, "timestamp:METHOD:path:body") - Fix test suite issues (bash arithmetic, ES module compat, TCL shebang) - Add CLAUDE.md with inception testing documentation - Update README.md with HMAC auth and dependency table - All 38 implementations pass inception test via un2
658 lines
23 KiB
Perl
658 lines
23 KiB
Perl
# 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 perl
|
|
# un.pl - Unsandbox CLI Client (Perl 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.pl [options] <source_file>
|
|
# un.pl session [options]
|
|
# un.pl service [options]
|
|
#
|
|
# Requires: UNSANDBOX_API_KEY environment variable
|
|
|
|
use strict;
|
|
use warnings;
|
|
use File::Basename;
|
|
use JSON::PP;
|
|
use LWP::UserAgent;
|
|
use HTTP::Request;
|
|
use MIME::Base64;
|
|
use File::Path qw(make_path);
|
|
use Digest::SHA qw(hmac_sha256_hex);
|
|
|
|
my $API_BASE = 'https://api.unsandbox.com';
|
|
my $PORTAL_BASE = 'https://unsandbox.com';
|
|
my $BLUE = "\033[34m";
|
|
my $RED = "\033[31m";
|
|
my $GREEN = "\033[32m";
|
|
my $YELLOW = "\033[33m";
|
|
my $RESET = "\033[0m";
|
|
|
|
my %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'
|
|
);
|
|
|
|
sub get_api_key {
|
|
my ($args_key) = @_;
|
|
my $public_key = $ENV{'UNSANDBOX_PUBLIC_KEY'} || '';
|
|
my $secret_key = $ENV{'UNSANDBOX_SECRET_KEY'} || '';
|
|
|
|
# Fallback to old UNSANDBOX_API_KEY for backwards compat
|
|
if (!$public_key && $ENV{'UNSANDBOX_API_KEY'}) {
|
|
$public_key = $ENV{'UNSANDBOX_API_KEY'};
|
|
$secret_key = '';
|
|
}
|
|
|
|
unless ($public_key) {
|
|
print STDERR "${RED}Error: UNSANDBOX_PUBLIC_KEY or UNSANDBOX_API_KEY not set${RESET}\n";
|
|
exit 1;
|
|
}
|
|
return ($public_key, $secret_key);
|
|
}
|
|
|
|
sub detect_language {
|
|
my ($filename) = @_;
|
|
my ($name, $dir, $ext) = fileparse($filename, qr/\.[^.]*/);
|
|
my $lang = $EXT_MAP{lc($ext)};
|
|
unless ($lang) {
|
|
if (open my $fh, '<', $filename) {
|
|
my $first_line = <$fh>;
|
|
close $fh;
|
|
if ($first_line && $first_line =~ /^#!/) {
|
|
return 'python' if $first_line =~ /python/;
|
|
return 'javascript' if $first_line =~ /node/;
|
|
return 'ruby' if $first_line =~ /ruby/;
|
|
return 'perl' if $first_line =~ /perl/;
|
|
return 'bash' if $first_line =~ /bash|\/sh/;
|
|
return 'lua' if $first_line =~ /lua/;
|
|
return 'php' if $first_line =~ /php/;
|
|
}
|
|
}
|
|
print STDERR "${RED}Error: Cannot detect language for $filename${RESET}\n";
|
|
exit 1;
|
|
}
|
|
return $lang;
|
|
}
|
|
|
|
sub api_request {
|
|
my ($endpoint, $method, $data, $public_key, $secret_key) = @_;
|
|
$method //= 'GET';
|
|
|
|
my $url = "$API_BASE$endpoint";
|
|
my $ua = LWP::UserAgent->new(timeout => 300);
|
|
my $request = HTTP::Request->new($method => $url);
|
|
$request->header('Authorization' => "Bearer $public_key");
|
|
$request->header('Content-Type' => 'application/json');
|
|
|
|
my $body = '';
|
|
if ($data) {
|
|
$body = encode_json($data);
|
|
$request->content($body);
|
|
}
|
|
|
|
# Add HMAC signature if secret_key is present
|
|
if ($secret_key) {
|
|
my $timestamp = time();
|
|
my $sig_input = "${timestamp}:${method}:${endpoint}:${body}";
|
|
my $signature = hmac_sha256_hex($sig_input, $secret_key);
|
|
$request->header('X-Timestamp' => $timestamp);
|
|
$request->header('X-Signature' => $signature);
|
|
}
|
|
|
|
my $response = $ua->request($request);
|
|
|
|
unless ($response->is_success) {
|
|
print STDERR "${RED}Error: HTTP ", $response->code, " - ", $response->content, "${RESET}\n";
|
|
exit 1;
|
|
}
|
|
|
|
return decode_json($response->content);
|
|
}
|
|
|
|
sub cmd_execute {
|
|
my ($options) = @_;
|
|
my ($public_key, $secret_key) = get_api_key($options->{api_key});
|
|
|
|
unless (-e $options->{source_file}) {
|
|
print STDERR "${RED}Error: File not found: $options->{source_file}${RESET}\n";
|
|
exit 1;
|
|
}
|
|
|
|
open my $fh, '<', $options->{source_file} or die "Cannot read file: $!";
|
|
local $/;
|
|
my $code = <$fh>;
|
|
close $fh;
|
|
|
|
my $language = detect_language($options->{source_file});
|
|
my $payload = { language => $language, code => $code };
|
|
|
|
if ($options->{env} && @{$options->{env}}) {
|
|
my %env_vars;
|
|
foreach my $e (@{$options->{env}}) {
|
|
if ($e =~ /^([^=]+)=(.*)$/) {
|
|
$env_vars{$1} = $2;
|
|
}
|
|
}
|
|
$payload->{env} = \%env_vars if %env_vars;
|
|
}
|
|
|
|
if ($options->{files} && @{$options->{files}}) {
|
|
my @input_files;
|
|
foreach my $filepath (@{$options->{files}}) {
|
|
unless (-e $filepath) {
|
|
print STDERR "${RED}Error: Input file not found: $filepath${RESET}\n";
|
|
exit 1;
|
|
}
|
|
open my $f, '<:raw', $filepath or die "Cannot read file: $!";
|
|
local $/;
|
|
my $content = <$f>;
|
|
close $f;
|
|
push @input_files, {
|
|
filename => basename($filepath),
|
|
content_base64 => encode_base64($content, '')
|
|
};
|
|
}
|
|
$payload->{input_files} = \@input_files;
|
|
}
|
|
|
|
$payload->{return_artifacts} = JSON::PP::true if $options->{artifacts};
|
|
$payload->{network} = $options->{network} if $options->{network};
|
|
$payload->{vcpu} = $options->{vcpu} if $options->{vcpu};
|
|
|
|
my $result = api_request('/execute', 'POST', $payload, $public_key, $secret_key);
|
|
|
|
print "${BLUE}$result->{stdout}${RESET}" if $result->{stdout};
|
|
print STDERR "${RED}$result->{stderr}${RESET}" if $result->{stderr};
|
|
|
|
if ($options->{artifacts} && $result->{artifacts}) {
|
|
my $out_dir = $options->{output_dir} || '.';
|
|
make_path($out_dir) unless -d $out_dir;
|
|
foreach my $artifact (@{$result->{artifacts}}) {
|
|
my $filename = $artifact->{filename} || 'artifact';
|
|
my $content = decode_base64($artifact->{content_base64});
|
|
my $filepath = "$out_dir/$filename";
|
|
open my $f, '>:raw', $filepath or die "Cannot write file: $!";
|
|
print $f $content;
|
|
close $f;
|
|
chmod 0755, $filepath;
|
|
print STDERR "${GREEN}Saved: $filepath${RESET}\n";
|
|
}
|
|
}
|
|
|
|
exit($result->{exit_code} // 0);
|
|
}
|
|
|
|
sub cmd_session {
|
|
my ($options) = @_;
|
|
my ($public_key, $secret_key) = get_api_key($options->{api_key});
|
|
|
|
if ($options->{list}) {
|
|
my $result = api_request('/sessions', 'GET', undef, $public_key, $secret_key);
|
|
my $sessions = $result->{sessions} || [];
|
|
if (@$sessions == 0) {
|
|
print "No active sessions\n";
|
|
} else {
|
|
printf "%-40s %-10s %-10s %s\n", 'ID', 'Shell', 'Status', 'Created';
|
|
foreach my $s (@$sessions) {
|
|
printf "%-40s %-10s %-10s %s\n",
|
|
$s->{id} // 'N/A', $s->{shell} // 'N/A',
|
|
$s->{status} // 'N/A', $s->{created_at} // 'N/A';
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
if ($options->{kill}) {
|
|
api_request("/sessions/$options->{kill}", 'DELETE', undef, $public_key, $secret_key);
|
|
print "${GREEN}Session terminated: $options->{kill}${RESET}\n";
|
|
return;
|
|
}
|
|
|
|
if ($options->{attach}) {
|
|
print "${YELLOW}Attaching to session $options->{attach}...${RESET}\n";
|
|
print "${YELLOW}(Interactive sessions require WebSocket - use un2 for full support)${RESET}\n";
|
|
return;
|
|
}
|
|
|
|
my $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} = JSON::PP::true if $options->{audit};
|
|
|
|
print "${YELLOW}Creating session...${RESET}\n";
|
|
my $result = api_request('/sessions', 'POST', $payload, $public_key, $secret_key);
|
|
print "${GREEN}Session created: ", ($result->{id} // 'N/A'), "${RESET}\n";
|
|
print "${YELLOW}(Interactive sessions require WebSocket - use un2 for full support)${RESET}\n";
|
|
}
|
|
|
|
sub cmd_service {
|
|
my ($options) = @_;
|
|
my ($public_key, $secret_key) = get_api_key($options->{api_key});
|
|
|
|
if ($options->{list}) {
|
|
my $result = api_request('/services', 'GET', undef, $public_key, $secret_key);
|
|
my $services = $result->{services} || [];
|
|
if (@$services == 0) {
|
|
print "No services\n";
|
|
} else {
|
|
printf "%-20s %-15s %-10s %-15s %s\n", 'ID', 'Name', 'Status', 'Ports', 'Domains';
|
|
foreach my $s (@$services) {
|
|
my $ports = join(',', @{$s->{ports} || []});
|
|
my $domains = join(',', @{$s->{domains} || []});
|
|
printf "%-20s %-15s %-10s %-15s %s\n",
|
|
$s->{id} // 'N/A', $s->{name} // 'N/A',
|
|
$s->{status} // 'N/A', $ports, $domains;
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
if ($options->{info}) {
|
|
my $result = api_request("/services/$options->{info}", 'GET', undef, $public_key, $secret_key);
|
|
print encode_json($result);
|
|
print "\n";
|
|
return;
|
|
}
|
|
|
|
if ($options->{logs}) {
|
|
my $result = api_request("/services/$options->{logs}/logs", 'GET', undef, $public_key, $secret_key);
|
|
print $result->{logs} // '';
|
|
return;
|
|
}
|
|
|
|
if ($options->{tail}) {
|
|
my $result = api_request("/services/$options->{tail}/logs?lines=9000", 'GET', undef, $public_key, $secret_key);
|
|
print $result->{logs} // '';
|
|
return;
|
|
}
|
|
|
|
if ($options->{sleep}) {
|
|
api_request("/services/$options->{sleep}/sleep", 'POST', undef, $public_key, $secret_key);
|
|
print "${GREEN}Service sleeping: $options->{sleep}${RESET}\n";
|
|
return;
|
|
}
|
|
|
|
if ($options->{wake}) {
|
|
api_request("/services/$options->{wake}/wake", 'POST', undef, $public_key, $secret_key);
|
|
print "${GREEN}Service waking: $options->{wake}${RESET}\n";
|
|
return;
|
|
}
|
|
|
|
if ($options->{destroy}) {
|
|
api_request("/services/$options->{destroy}", 'DELETE', undef, $public_key, $secret_key);
|
|
print "${GREEN}Service destroyed: $options->{destroy}${RESET}\n";
|
|
return;
|
|
}
|
|
|
|
if ($options->{execute}) {
|
|
my $payload = { command => $options->{command} };
|
|
my $result = api_request("/services/$options->{execute}/execute", 'POST', $payload, $public_key, $secret_key);
|
|
print "${BLUE}$result->{stdout}${RESET}" if $result->{stdout};
|
|
print STDERR "${RED}$result->{stderr}${RESET}" if $result->{stderr};
|
|
return;
|
|
}
|
|
|
|
if ($options->{dump_bootstrap}) {
|
|
print STDERR "Fetching bootstrap script from $options->{dump_bootstrap}...\n";
|
|
my $payload = { command => 'cat /tmp/bootstrap.sh' };
|
|
my $result = api_request("/services/$options->{dump_bootstrap}/execute", 'POST', $payload, $public_key, $secret_key);
|
|
|
|
if ($result->{stdout}) {
|
|
my $bootstrap = $result->{stdout};
|
|
if ($options->{dump_file}) {
|
|
# Write to file
|
|
open my $fh, '>', $options->{dump_file} or do {
|
|
print STDERR "${RED}Error: Could not write to $options->{dump_file}: $!${RESET}\n";
|
|
exit 1;
|
|
};
|
|
print $fh $bootstrap;
|
|
close $fh;
|
|
chmod 0755, $options->{dump_file};
|
|
print "Bootstrap saved to $options->{dump_file}\n";
|
|
} else {
|
|
# Print to stdout
|
|
print $bootstrap;
|
|
}
|
|
} else {
|
|
print STDERR "${RED}Error: Failed to fetch bootstrap (service not running or no bootstrap file)${RESET}\n";
|
|
exit 1;
|
|
}
|
|
return;
|
|
}
|
|
|
|
if ($options->{name}) {
|
|
my $payload = { name => $options->{name} };
|
|
if ($options->{ports}) {
|
|
my @ports = map { int($_) } split(',', $options->{ports});
|
|
$payload->{ports} = \@ports;
|
|
}
|
|
if ($options->{domains}) {
|
|
my @domains = split(',', $options->{domains});
|
|
$payload->{domains} = \@domains;
|
|
}
|
|
if ($options->{type}) {
|
|
$payload->{service_type} = $options->{type};
|
|
}
|
|
if ($options->{bootstrap}) {
|
|
if (-e $options->{bootstrap}) {
|
|
open my $fh, '<', $options->{bootstrap} or die "Cannot read file: $!";
|
|
local $/;
|
|
$payload->{bootstrap} = <$fh>;
|
|
close $fh;
|
|
} else {
|
|
$payload->{bootstrap} = $options->{bootstrap};
|
|
}
|
|
}
|
|
$payload->{network} = $options->{network} if $options->{network};
|
|
$payload->{vcpu} = $options->{vcpu} if $options->{vcpu};
|
|
|
|
my $result = api_request('/services', 'POST', $payload, $public_key, $secret_key);
|
|
print "${GREEN}Service created: ", ($result->{id} // 'N/A'), "${RESET}\n";
|
|
print "Name: ", ($result->{name} // 'N/A'), "\n";
|
|
print "URL: $result->{url}\n" if $result->{url};
|
|
return;
|
|
}
|
|
|
|
print STDERR "${RED}Error: Specify --name to create a service, or use --list, --info, etc.${RESET}\n";
|
|
exit 1;
|
|
}
|
|
|
|
sub open_browser {
|
|
my ($url) = @_;
|
|
|
|
# Try different browser open commands based on platform
|
|
if ($^O eq 'darwin') {
|
|
system('open', $url);
|
|
} elsif ($^O eq 'MSWin32') {
|
|
system('start', $url);
|
|
} else {
|
|
# Linux/Unix
|
|
system('xdg-open', $url, '>/dev/null', '2>&1', '&');
|
|
}
|
|
}
|
|
|
|
sub validate_key {
|
|
my ($public_key, $secret_key, $should_extend) = @_;
|
|
|
|
# Call /keys/validate endpoint
|
|
my $url = "$PORTAL_BASE/keys/validate";
|
|
my $ua = LWP::UserAgent->new(timeout => 30);
|
|
my $request = HTTP::Request->new('POST' => $url);
|
|
$request->header('Authorization' => "Bearer $public_key");
|
|
$request->header('Content-Type' => 'application/json');
|
|
|
|
# Add HMAC signature if secret_key is present
|
|
if ($secret_key) {
|
|
my $timestamp = time();
|
|
my $sig_input = "${timestamp}:POST:/keys/validate:";
|
|
my $signature = hmac_sha256_hex($sig_input, $secret_key);
|
|
$request->header('X-Timestamp' => $timestamp);
|
|
$request->header('X-Signature' => $signature);
|
|
}
|
|
|
|
my $response = $ua->request($request);
|
|
my $result = decode_json($response->content);
|
|
|
|
# Handle --extend flag first
|
|
if ($should_extend) {
|
|
my $public_key = $result->{public_key};
|
|
if ($public_key) {
|
|
my $extend_url = "$PORTAL_BASE/keys/extend?pk=$public_key";
|
|
print "${BLUE}Opening browser to extend key...${RESET}\n";
|
|
open_browser($extend_url);
|
|
return;
|
|
} else {
|
|
print STDERR "${RED}Error: Could not retrieve public key${RESET}\n";
|
|
exit 1;
|
|
}
|
|
}
|
|
|
|
# Check if key is expired
|
|
if ($result->{expired}) {
|
|
print "${RED}Expired${RESET}\n";
|
|
print "Public Key: ", ($result->{public_key} // 'N/A'), "\n";
|
|
print "Tier: ", ($result->{tier} // 'N/A'), "\n";
|
|
print "Expired: ", ($result->{expires_at} // 'N/A'), "\n";
|
|
print "${YELLOW}To renew: Visit https://unsandbox.com/keys/extend${RESET}\n";
|
|
exit 1;
|
|
}
|
|
|
|
# Valid key
|
|
print "${GREEN}Valid${RESET}\n";
|
|
print "Public Key: ", ($result->{public_key} // 'N/A'), "\n";
|
|
print "Tier: ", ($result->{tier} // 'N/A'), "\n";
|
|
print "Status: ", ($result->{status} // 'N/A'), "\n";
|
|
print "Expires: ", ($result->{expires_at} // 'N/A'), "\n";
|
|
print "Time Remaining: ", ($result->{time_remaining} // 'N/A'), "\n";
|
|
print "Rate Limit: ", ($result->{rate_limit} // 'N/A'), "\n";
|
|
print "Burst: ", ($result->{burst} // 'N/A'), "\n";
|
|
print "Concurrency: ", ($result->{concurrency} // 'N/A'), "\n";
|
|
}
|
|
|
|
sub cmd_key {
|
|
my ($options) = @_;
|
|
my ($public_key, $secret_key) = get_api_key($options->{api_key});
|
|
validate_key($public_key, $secret_key, $options->{extend});
|
|
}
|
|
|
|
sub main {
|
|
my %options = (
|
|
command => undef,
|
|
source_file => undef,
|
|
env => [],
|
|
files => [],
|
|
artifacts => 0,
|
|
output_dir => undef,
|
|
network => undef,
|
|
vcpu => undef,
|
|
api_key => undef,
|
|
shell => undef,
|
|
list => 0,
|
|
attach => undef,
|
|
kill => undef,
|
|
audit => 0,
|
|
tmux => 0,
|
|
screen => 0,
|
|
name => undef,
|
|
ports => undef,
|
|
domains => undef,
|
|
type => undef,
|
|
bootstrap => undef,
|
|
info => undef,
|
|
logs => undef,
|
|
tail => undef,
|
|
sleep => undef,
|
|
wake => undef,
|
|
destroy => undef,
|
|
execute => undef,
|
|
command => undef,
|
|
dump_bootstrap => undef,
|
|
dump_file => undef,
|
|
extend => 0
|
|
);
|
|
|
|
for (my $i = 0; $i < @ARGV; $i++) {
|
|
my $arg = $ARGV[$i];
|
|
|
|
if ($arg eq 'session' || $arg eq 'service' || $arg eq 'key') {
|
|
$options{command} = $arg;
|
|
} elsif ($arg eq '-e') {
|
|
push @{$options{env}}, $ARGV[++$i];
|
|
} elsif ($arg eq '-f') {
|
|
push @{$options{files}}, $ARGV[++$i];
|
|
} elsif ($arg eq '-a') {
|
|
$options{artifacts} = 1;
|
|
} elsif ($arg eq '-o') {
|
|
$options{output_dir} = $ARGV[++$i];
|
|
} elsif ($arg eq '-n') {
|
|
$options{network} = $ARGV[++$i];
|
|
} elsif ($arg eq '-v') {
|
|
$options{vcpu} = int($ARGV[++$i]);
|
|
} elsif ($arg eq '-k') {
|
|
$options{api_key} = $ARGV[++$i];
|
|
} elsif ($arg eq '-s' || $arg eq '--shell') {
|
|
$options{shell} = $ARGV[++$i];
|
|
} elsif ($arg eq '-l' || $arg eq '--list') {
|
|
$options{list} = 1;
|
|
} elsif ($arg eq '--attach') {
|
|
$options{attach} = $ARGV[++$i];
|
|
} elsif ($arg eq '--kill') {
|
|
$options{kill} = $ARGV[++$i];
|
|
} elsif ($arg eq '--audit') {
|
|
$options{audit} = 1;
|
|
} elsif ($arg eq '--tmux') {
|
|
$options{tmux} = 1;
|
|
} elsif ($arg eq '--screen') {
|
|
$options{screen} = 1;
|
|
} elsif ($arg eq '--name') {
|
|
$options{name} = $ARGV[++$i];
|
|
} elsif ($arg eq '--ports') {
|
|
$options{ports} = $ARGV[++$i];
|
|
} elsif ($arg eq '--domains') {
|
|
$options{domains} = $ARGV[++$i];
|
|
} elsif ($arg eq '--type') {
|
|
$options{type} = $ARGV[++$i];
|
|
} elsif ($arg eq '--bootstrap') {
|
|
$options{bootstrap} = $ARGV[++$i];
|
|
} elsif ($arg eq '--info') {
|
|
$options{info} = $ARGV[++$i];
|
|
} elsif ($arg eq '--logs') {
|
|
$options{logs} = $ARGV[++$i];
|
|
} elsif ($arg eq '--tail') {
|
|
$options{tail} = $ARGV[++$i];
|
|
} elsif ($arg eq '--freeze') {
|
|
$options{sleep} = $ARGV[++$i];
|
|
} elsif ($arg eq '--unfreeze') {
|
|
$options{wake} = $ARGV[++$i];
|
|
} elsif ($arg eq '--destroy') {
|
|
$options{destroy} = $ARGV[++$i];
|
|
} elsif ($arg eq '--execute') {
|
|
$options{execute} = $ARGV[++$i];
|
|
} elsif ($arg eq '--command') {
|
|
$options{command} = $ARGV[++$i];
|
|
} elsif ($arg eq '--dump-bootstrap') {
|
|
$options{dump_bootstrap} = $ARGV[++$i];
|
|
} elsif ($arg eq '--dump-file') {
|
|
$options{dump_file} = $ARGV[++$i];
|
|
} elsif ($arg eq '--extend') {
|
|
$options{extend} = 1;
|
|
} elsif ($arg !~ /^-/) {
|
|
$options{source_file} = $arg;
|
|
}
|
|
}
|
|
|
|
if ($options{command} && $options{command} eq 'session') {
|
|
cmd_session(\%options);
|
|
} elsif ($options{command} && $options{command} eq 'service') {
|
|
cmd_service(\%options);
|
|
} elsif ($options{command} && $options{command} eq 'key') {
|
|
cmd_key(\%options);
|
|
} elsif ($options{source_file}) {
|
|
cmd_execute(\%options);
|
|
} else {
|
|
print <<'HELP';
|
|
Unsandbox CLI - Execute code in secure sandboxes
|
|
|
|
Usage:
|
|
$0 [options] <source_file>
|
|
$0 session [options]
|
|
$0 service [options]
|
|
$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
|
|
--freeze ID Freeze service
|
|
--unfreeze ID Unfreeze service
|
|
--destroy ID Destroy service
|
|
--execute ID Execute command in service
|
|
--command CMD Command to execute (with --execute)
|
|
--dump-bootstrap ID Dump bootstrap script
|
|
--dump-file FILE File to save bootstrap (with --dump-bootstrap)
|
|
|
|
Key options:
|
|
--extend Open browser to extend/renew key
|
|
HELP
|
|
exit 1;
|
|
}
|
|
}
|
|
|
|
main();
|