CLI
Cliente de linha de comando rápido para execução de código e sessões interativas. Mais de 42 linguagens, mais de 30 shells/REPLs.
Documentação Oficial OpenAPI Swagger ↗Início Rápido — Perl
# Download + setup
curl -O https://git.unturf.com/engineering/unturf/un-inception/-/raw/main/clients/perl/sync/src/un.pl && chmod +x un.pl && ln -sf un.pl un
export UNSANDBOX_PUBLIC_KEY="unsb-pk-xxxx-xxxx-xxxx-xxxx"
export UNSANDBOX_SECRET_KEY="unsb-sk-xxxxx-xxxxx-xxxxx-xxxxx"
# Run code
./un script.pl
Baixar
Guia de Instalação →Características:
- 42+ languages - Python, JS, Go, Rust, C++, Java...
- Sessions - 30+ shells/REPLs, tmux persistence
- Files - Upload files, collect artifacts
- Services - Persistent containers with domains
- Snapshots - Point-in-time backups
- Images - Publish, share, transfer
Início Rápido de Integração ⚡
Adicione superpoderes unsandbox ao seu app Perl existente:
curl -O https://git.unturf.com/engineering/unturf/un-inception/-/raw/main/clients/perl/sync/src/un.pl
# Option A: Environment variables
export UNSANDBOX_PUBLIC_KEY="unsb-pk-xxxx-xxxx-xxxx-xxxx"
export UNSANDBOX_SECRET_KEY="unsb-sk-xxxxx-xxxxx-xxxxx-xxxxx"
# Option B: Config file (persistent)
mkdir -p ~/.unsandbox
echo "unsb-pk-xxxx-xxxx-xxxx-xxxx,unsb-sk-xxxxx-xxxxx-xxxxx-xxxxx" > ~/.unsandbox/accounts.csv
# In your Perl app:
use lib '.';
require "un.pl";
my $result = Un::execute("perl", "print \"Hello from Perl running on unsandbox!\\n\"");
print $result->{stdout}; # Hello from Perl running on unsandbox!
perl myapp.pl
c34dbcfa7b6c001db57bc4d83de485fc
SHA256: dcf5d934f37b2fb27035f14f837a3d957f12563b5644f3f8e3dfd75a5131e0e1
#!/usr/bin/env perl
# PUBLIC DOMAIN - NO LICENSE, NO WARRANTY
#
# unsandbox.com Perl SDK (Synchronous)
# Full API with execution, sessions, services, snapshots, and images.
#
# Library Usage:
# use Un;
# my $result = Un::execute("python", "print(42)");
# print $result->{stdout};
#
# CLI Usage:
# perl un.pl script.py
# perl un.pl -s python 'print(42)'
# perl un.pl session --list
# perl un.pl service --list
# perl un.pl snapshot --list
# perl un.pl image --list
#
# Authentication Priority (4-tier):
# 1. Function arguments (public_key, secret_key)
# 2. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY)
# 3. Config file (~/.unsandbox/accounts.csv, line 0 by default)
# 4. Local directory (./accounts.csv, line 0 by default)
#
# Copyright 2025 TimeHexOn & foxhop & russell@unturf
use strict;
use warnings;
use JSON::PP;
use LWP::UserAgent;
use HTTP::Request;
use Digest::SHA qw(hmac_sha256_hex);
use MIME::Base64;
use File::Basename;
use File::Path qw(make_path);
use Time::HiRes qw(time sleep);
package Un;
our $VERSION = "4.3.4";
our $API_BASE = 'https://api.unsandbox.com';
our $PORTAL_BASE = 'https://unsandbox.com';
# Thread-local error storage
our $LAST_ERROR = "";
our $ACCOUNT_INDEX = -1; # -1 means not set; set to N to use accounts.csv row N
# Colors
my $BLUE = "\033[34m";
my $RED = "\033[31m";
my $GREEN = "\033[32m";
my $YELLOW = "\033[33m";
my $RESET = "\033[0m";
# Extension to language mapping
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'
);
# ============================================================================
# Utility Functions
# ============================================================================
sub version { return $VERSION; }
sub last_error { return $LAST_ERROR; }
sub set_error {
my ($msg) = @_;
$LAST_ERROR = $msg;
}
sub detect_language {
my ($filename) = @_;
return undef unless $filename;
my ($name, $dir, $ext) = fileparse($filename, qr/\.[^.]*/);
return $EXT_MAP{lc($ext)};
}
sub hmac_sign {
my ($secret, $message) = @_;
return undef unless defined $secret && defined $message;
return hmac_sha256_hex($message, $secret);
}
# ============================================================================
# Credential Management
# ============================================================================
sub load_accounts_csv {
my ($path) = @_;
$path ||= "$ENV{HOME}/.unsandbox/accounts.csv";
return [] unless -e $path;
my @accounts;
open my $fh, '<', $path or return [];
while (my $line = <$fh>) {
chomp $line;
next if !$line || $line =~ /^\s*#/;
my ($pk, $sk) = split /,/, $line, 2;
push @accounts, [$pk, $sk] if $pk && $sk;
}
close $fh;
return \@accounts;
}
sub get_credentials {
my (%opts) = @_;
# Tier 1: Arguments
return ($opts{public_key}, $opts{secret_key}) if $opts{public_key} && $opts{secret_key};
# Tier 2: --account N flag → bypass env vars, load CSV row N directly
my $ai = exists $opts{account_index} ? $opts{account_index} : $Un::ACCOUNT_INDEX;
if (defined $ai && $ai >= 0) {
my $home_accounts = load_accounts_csv();
return @{$home_accounts->[$ai]} if @$home_accounts > $ai;
my $local_accounts = load_accounts_csv("./accounts.csv");
return @{$local_accounts->[$ai]} if @$local_accounts > $ai;
set_error("Account index $ai not found in accounts.csv");
return (undef, undef);
}
# Tier 3: Environment
if ($ENV{UNSANDBOX_PUBLIC_KEY} && $ENV{UNSANDBOX_SECRET_KEY}) {
return ($ENV{UNSANDBOX_PUBLIC_KEY}, $ENV{UNSANDBOX_SECRET_KEY});
}
# Legacy fallback
if ($ENV{UNSANDBOX_API_KEY}) {
return ($ENV{UNSANDBOX_API_KEY}, '');
}
# Tier 4: Home directory
my $home_accounts = load_accounts_csv();
return @{$home_accounts->[0]} if @$home_accounts;
# Tier 5: Local directory
my $local_accounts = load_accounts_csv("./accounts.csv");
return @{$local_accounts->[0]} if @$local_accounts;
set_error("No credentials found");
return (undef, undef);
}
# ============================================================================
# API Communication
# ============================================================================
sub api_request {
my ($method, $endpoint, $body, %opts) = @_;
my $extra_headers = delete $opts{extra_headers} || {};
my $content_type = delete $opts{content_type} || 'application/json';
my ($pk, $sk) = get_credentials(%opts);
unless ($pk) {
set_error("No credentials available");
return undef;
}
my $timestamp = int(time);
my $body_str = '';
if ($body) {
$body_str = ref($body) ? encode_json($body) : $body;
}
my $ua = LWP::UserAgent->new(timeout => 300);
my $url = "$API_BASE$endpoint";
my $req = HTTP::Request->new($method, $url);
$req->header('Authorization' => "Bearer $pk");
$req->header('Content-Type' => $content_type);
# HMAC signature
if ($sk) {
my $sig_input = "$timestamp:$method:$endpoint:$body_str";
my $signature = hmac_sign($sk, $sig_input);
$req->header('X-Timestamp' => $timestamp);
$req->header('X-Signature' => $signature);
}
# Add extra headers
for my $key (keys %$extra_headers) {
$req->header($key => $extra_headers->{$key});
}
$req->content($body_str) if $body_str;
my $res = $ua->request($req);
unless ($res->is_success) {
set_error("API error (" . $res->code . "): " . $res->content);
return undef;
}
return decode_json($res->content) if $res->content;
return { success => 1 };
}
sub api_request_with_sudo {
my ($method, $endpoint, $body, %opts) = @_;
my ($pk, $sk) = get_credentials(%opts);
my $timestamp = int(time);
my $body_str = '';
if ($body) {
$body_str = ref($body) ? encode_json($body) : $body;
}
my $ua = LWP::UserAgent->new(timeout => 300);
my $url = "$API_BASE$endpoint";
my $req = HTTP::Request->new($method, $url);
$req->header('Authorization' => "Bearer $pk");
$req->header('Content-Type' => 'application/json');
if ($sk) {
my $sig_input = "$timestamp:$method:$endpoint:$body_str";
my $signature = hmac_sign($sk, $sig_input);
$req->header('X-Timestamp' => $timestamp);
$req->header('X-Signature' => $signature);
}
$req->content($body_str) if $body_str;
my $res = $ua->request($req);
# Handle 428 - Sudo OTP required
if ($res->code == 428) {
my $response_data = eval { decode_json($res->content) } || {};
my $challenge_id = $response_data->{challenge_id} || '';
print STDERR "${YELLOW}Confirmation required. Check your email for a one-time code.${RESET}\n";
print STDERR "Enter OTP: ";
my $otp = <STDIN>;
return undef unless defined $otp;
chomp $otp;
$otp =~ s/\r//g;
if ($otp eq '') {
set_error("Operation cancelled");
return undef;
}
my $extra = { 'X-Sudo-OTP' => $otp };
$extra->{'X-Sudo-Challenge'} = $challenge_id if $challenge_id;
return api_request($method, $endpoint, $body, %opts, extra_headers => $extra);
}
unless ($res->is_success) {
set_error("API error (" . $res->code . "): " . $res->content);
return undef;
}
return decode_json($res->content) if $res->content;
return { success => 1 };
}
# ============================================================================
# Execution Functions (8)
# ============================================================================
sub execute {
my ($language, $code, %opts) = @_;
my $body = {
language => $language,
code => $code,
network_mode => $opts{network_mode} || 'zerotrust',
ttl => $opts{ttl} || 60
};
$body->{env} = $opts{env} if $opts{env};
$body->{input_files} = $opts{input_files} if $opts{input_files};
$body->{return_artifacts} = JSON::PP::true if $opts{return_artifacts};
return api_request('POST', '/execute', $body, %opts);
}
sub execute_async {
my ($language, $code, %opts) = @_;
my $body = {
language => $language,
code => $code,
network_mode => $opts{network_mode} || 'zerotrust',
ttl => $opts{ttl} || 300
};
return api_request('POST', '/execute/async', $body, %opts);
}
sub wait_job {
my ($job_id, %opts) = @_;
my $timeout = $opts{timeout} || 120;
my @delays = (300, 450, 700, 900, 650, 1600, 2000);
for my $i (0..119) {
my $job = get_job($job_id, %opts);
return $job if $job && $job->{status} eq 'completed';
if ($job && $job->{status} eq 'failed') {
set_error("Job failed: " . ($job->{error} || 'unknown'));
return undef;
}
my $delay = $delays[$i % 7] || 2000;
sleep($delay / 1000);
}
set_error("Max polls exceeded");
return undef;
}
sub get_job {
my ($job_id, %opts) = @_;
return api_request('GET', "/jobs/$job_id", undef, %opts);
}
sub cancel_job {
my ($job_id, %opts) = @_;
return api_request('DELETE', "/jobs/$job_id", undef, %opts);
}
sub list_jobs {
my (%opts) = @_;
return api_request('GET', '/jobs', undef, %opts);
}
sub get_languages {
my (%opts) = @_;
my $cache_ttl = $opts{cache_ttl} || 3600;
my $cache_path = "$ENV{HOME}/.unsandbox/languages.json";
if (-e $cache_path) {
my $age = time - (stat $cache_path)[9];
if ($age < $cache_ttl) {
open my $fh, '<', $cache_path;
my $content = do { local $/; <$fh> };
close $fh;
return decode_json($content);
}
}
my $result = api_request('GET', '/languages', undef, %opts);
my $langs = $result->{languages} || [];
my $cache_dir = "$ENV{HOME}/.unsandbox";
mkdir $cache_dir unless -d $cache_dir;
if (open my $fh, '>', $cache_path) {
print $fh encode_json($langs);
close $fh;
}
return $langs;
}
# ============================================================================
# Session Functions (9)
# ============================================================================
sub session_list {
my (%opts) = @_;
return api_request('GET', '/sessions', undef, %opts);
}
sub session_get {
my ($session_id, %opts) = @_;
return api_request('GET', "/sessions/$session_id", undef, %opts);
}
sub session_create {
my (%opts) = @_;
my $body = {
shell => $opts{shell} || 'bash',
};
$body->{network} = $opts{network} if $opts{network};
$body->{vcpu} = $opts{vcpu} if $opts{vcpu};
$body->{input_files} = $opts{input_files} if $opts{input_files};
$body->{persistence} = $opts{persistence} if $opts{persistence};
return api_request('POST', '/sessions', $body, %opts);
}
sub session_destroy {
my ($session_id, %opts) = @_;
return api_request('DELETE', "/sessions/$session_id", undef, %opts);
}
sub session_freeze {
my ($session_id, %opts) = @_;
return api_request('POST', "/sessions/$session_id/freeze", {}, %opts);
}
sub session_unfreeze {
my ($session_id, %opts) = @_;
return api_request('POST', "/sessions/$session_id/unfreeze", {}, %opts);
}
sub session_boost {
my ($session_id, $vcpu, %opts) = @_;
return api_request('POST', "/sessions/$session_id/boost", { vcpu => $vcpu }, %opts);
}
sub session_unboost {
my ($session_id, %opts) = @_;
return api_request('POST', "/sessions/$session_id/unboost", {}, %opts);
}
sub session_execute {
my ($session_id, $command, %opts) = @_;
return api_request('POST', "/sessions/$session_id/execute", { command => $command }, %opts);
}
# ============================================================================
# Service Functions (17)
# ============================================================================
sub service_list {
my (%opts) = @_;
return api_request('GET', '/services', undef, %opts);
}
sub service_get {
my ($service_id, %opts) = @_;
return api_request('GET', "/services/$service_id", undef, %opts);
}
sub service_create {
my (%opts) = @_;
my $body = { name => $opts{name} };
$body->{ports} = $opts{ports} if $opts{ports};
$body->{domains} = $opts{domains} if $opts{domains};
$body->{bootstrap} = $opts{bootstrap} if $opts{bootstrap};
$body->{bootstrap_content} = $opts{bootstrap_content} if $opts{bootstrap_content};
$body->{network} = $opts{network} if $opts{network};
$body->{vcpu} = $opts{vcpu} if $opts{vcpu};
$body->{service_type} = $opts{service_type} if $opts{service_type};
$body->{input_files} = $opts{input_files} if $opts{input_files};
$body->{unfreeze_on_demand} = JSON::PP::true if $opts{unfreeze_on_demand};
return api_request('POST', '/services', $body, %opts);
}
sub service_destroy {
my ($service_id, %opts) = @_;
return api_request_with_sudo('DELETE', "/services/$service_id", undef, %opts);
}
sub service_freeze {
my ($service_id, %opts) = @_;
return api_request('POST', "/services/$service_id/freeze", {}, %opts);
}
sub service_unfreeze {
my ($service_id, %opts) = @_;
return api_request('POST', "/services/$service_id/unfreeze", {}, %opts);
}
sub service_lock {
my ($service_id, %opts) = @_;
return api_request('POST', "/services/$service_id/lock", {}, %opts);
}
sub service_unlock {
my ($service_id, %opts) = @_;
return api_request_with_sudo('POST', "/services/$service_id/unlock", {}, %opts);
}
sub service_set_unfreeze_on_demand {
my ($service_id, $enabled, %opts) = @_;
my $body = { unfreeze_on_demand => ($enabled ? JSON::PP::true : JSON::PP::false) };
return api_request('PATCH', "/services/$service_id", $body, %opts);
}
sub service_redeploy {
my ($service_id, %opts) = @_;
my $body = {};
$body->{bootstrap} = $opts{bootstrap} if $opts{bootstrap};
$body->{input_files} = $opts{input_files} if $opts{input_files};
return api_request('POST', "/services/$service_id/redeploy", $body, %opts);
}
sub service_logs {
my ($service_id, %opts) = @_;
my $endpoint = "/services/$service_id/logs";
$endpoint .= "?lines=$opts{lines}" if $opts{lines};
return api_request('GET', $endpoint, undef, %opts);
}
sub service_execute {
my ($service_id, $command, %opts) = @_;
my $body = { command => $command };
$body->{timeout} = $opts{timeout} if $opts{timeout};
return api_request('POST', "/services/$service_id/execute", $body, %opts);
}
sub service_env_get {
my ($service_id, %opts) = @_;
return api_request('GET', "/services/$service_id/env", undef, %opts);
}
sub service_env_set {
my ($service_id, $env_content, %opts) = @_;
return api_request('PUT', "/services/$service_id/env", $env_content,
%opts, content_type => 'text/plain');
}
sub service_env_delete {
my ($service_id, %opts) = @_;
return api_request('DELETE', "/services/$service_id/env", undef, %opts);
}
sub service_env_export {
my ($service_id, %opts) = @_;
return api_request('POST', "/services/$service_id/env/export", {}, %opts);
}
sub service_resize {
my ($service_id, $vcpu, %opts) = @_;
return api_request('PATCH', "/services/$service_id", { vcpu => $vcpu }, %opts);
}
# ============================================================================
# Snapshot Functions (9)
# ============================================================================
sub snapshot_list {
my (%opts) = @_;
return api_request('GET', '/snapshots', undef, %opts);
}
sub snapshot_get {
my ($snapshot_id, %opts) = @_;
return api_request('GET', "/snapshots/$snapshot_id", undef, %opts);
}
sub snapshot_session {
my ($session_id, %opts) = @_;
my $body = {};
$body->{name} = $opts{name} if $opts{name};
$body->{hot} = JSON::PP::true if $opts{hot};
return api_request('POST', "/sessions/$session_id/snapshot", $body, %opts);
}
sub snapshot_service {
my ($service_id, %opts) = @_;
my $body = {};
$body->{name} = $opts{name} if $opts{name};
$body->{hot} = JSON::PP::true if $opts{hot};
return api_request('POST', "/services/$service_id/snapshot", $body, %opts);
}
sub snapshot_restore {
my ($snapshot_id, %opts) = @_;
return api_request('POST', "/snapshots/$snapshot_id/restore", {}, %opts);
}
sub snapshot_delete {
my ($snapshot_id, %opts) = @_;
return api_request_with_sudo('DELETE', "/snapshots/$snapshot_id", undef, %opts);
}
sub snapshot_lock {
my ($snapshot_id, %opts) = @_;
return api_request('POST', "/snapshots/$snapshot_id/lock", {}, %opts);
}
sub snapshot_unlock {
my ($snapshot_id, %opts) = @_;
return api_request_with_sudo('POST', "/snapshots/$snapshot_id/unlock", {}, %opts);
}
sub snapshot_clone {
my ($snapshot_id, %opts) = @_;
my $body = { clone_type => $opts{clone_type} || 'session' };
$body->{name} = $opts{name} if $opts{name};
$body->{ports} = $opts{ports} if $opts{ports};
$body->{shell} = $opts{shell} if $opts{shell};
return api_request('POST', "/snapshots/$snapshot_id/clone", $body, %opts);
}
# ============================================================================
# Image Functions (13)
# ============================================================================
sub image_list {
my (%opts) = @_;
my $endpoint = '/images';
$endpoint .= "?filter=$opts{filter}" if $opts{filter};
return api_request('GET', $endpoint, undef, %opts);
}
sub image_get {
my ($image_id, %opts) = @_;
return api_request('GET', "/images/$image_id", undef, %opts);
}
sub image_publish {
my (%opts) = @_;
my $body = {
source_type => $opts{source_type},
source_id => $opts{source_id}
};
$body->{name} = $opts{name} if $opts{name};
$body->{description} = $opts{description} if $opts{description};
return api_request('POST', '/images/publish', $body, %opts);
}
sub image_delete {
my ($image_id, %opts) = @_;
return api_request_with_sudo('DELETE', "/images/$image_id", undef, %opts);
}
sub image_lock {
my ($image_id, %opts) = @_;
return api_request('POST', "/images/$image_id/lock", {}, %opts);
}
sub image_unlock {
my ($image_id, %opts) = @_;
return api_request_with_sudo('POST', "/images/$image_id/unlock", {}, %opts);
}
sub image_set_visibility {
my ($image_id, $visibility, %opts) = @_;
return api_request('POST', "/images/$image_id/visibility", { visibility => $visibility }, %opts);
}
sub image_grant_access {
my ($image_id, $trusted_api_key, %opts) = @_;
return api_request('POST', "/images/$image_id/access", { api_key => $trusted_api_key }, %opts);
}
sub image_revoke_access {
my ($image_id, $trusted_api_key, %opts) = @_;
return api_request('DELETE', "/images/$image_id/access/$trusted_api_key", undef, %opts);
}
sub image_list_trusted {
my ($image_id, %opts) = @_;
return api_request('GET', "/images/$image_id/access", undef, %opts);
}
sub image_transfer {
my ($image_id, $to_api_key, %opts) = @_;
return api_request('POST', "/images/$image_id/transfer", { to_api_key => $to_api_key }, %opts);
}
sub image_spawn {
my ($image_id, %opts) = @_;
my $body = {};
$body->{name} = $opts{name} if $opts{name};
$body->{ports} = $opts{ports} if $opts{ports};
$body->{bootstrap} = $opts{bootstrap} if $opts{bootstrap};
$body->{network_mode} = $opts{network_mode} if $opts{network_mode};
return api_request('POST', "/images/$image_id/spawn", $body, %opts);
}
sub image_clone {
my ($image_id, %opts) = @_;
my $body = {};
$body->{name} = $opts{name} if $opts{name};
$body->{description} = $opts{description} if $opts{description};
return api_request('POST', "/images/$image_id/clone", $body, %opts);
}
# ============================================================================
# PaaS Logs Functions (2)
# ============================================================================
sub logs_fetch {
my (%opts) = @_;
my $body = {
source => $opts{source} || 'all',
lines => $opts{lines} || 100,
since => $opts{since} || '1h'
};
$body->{grep} = $opts{grep} if $opts{grep};
return api_request('POST', '/paas/logs', $body, %opts);
}
sub logs_stream {
my (%opts) = @_;
# SSE streaming - returns immediately, callback for each line
my $callback = $opts{callback};
return undef unless $callback;
# SSE streaming not easily supported in sync Perl, return placeholder
set_error("logs_stream requires async support");
return undef;
}
# ============================================================================
# Key Validation
# ============================================================================
sub validate_keys {
my (%opts) = @_;
my ($pk, $sk) = get_credentials(%opts);
my $ua = LWP::UserAgent->new(timeout => 30);
my $url = "$PORTAL_BASE/keys/validate";
my $req = HTTP::Request->new('POST', $url);
$req->header('Authorization' => "Bearer $pk");
$req->header('Content-Type' => 'application/json');
if ($sk) {
my $timestamp = int(time);
my $sig_input = "$timestamp:POST:/keys/validate:";
my $signature = hmac_sign($sk, $sig_input);
$req->header('X-Timestamp' => $timestamp);
$req->header('X-Signature' => $signature);
}
my $res = $ua->request($req);
return decode_json($res->content) if $res->content;
return undef;
}
sub health_check {
my (%opts) = @_;
my $ua = LWP::UserAgent->new(timeout => 10);
my $res = $ua->get("$API_BASE/health");
return $res->is_success ? 1 : 0;
}
# ============================================================================
# CLI Implementation
# ============================================================================
package main;
sub build_input_files {
my @files = @_;
my @input_files;
foreach my $filepath (@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: $!";
my $content = do { local $/; <$f> };
close $f;
push @input_files, {
filename => basename($filepath),
content_base64 => encode_base64($content, '')
};
}
return \@input_files;
}
sub build_env_content {
my ($envs, $env_file) = @_;
my @parts;
if ($env_file && -e $env_file) {
open my $fh, '<', $env_file or die "Cannot read file: $!";
my $content = do { local $/; <$fh> };
close $fh;
push @parts, $content;
}
foreach my $e (@$envs) {
push @parts, $e if $e =~ /=/;
}
return join("\n", @parts);
}
sub cmd_execute {
my ($options) = @_;
my $source = $options->{source_file};
my $inline_code = $options->{inline_code};
my $language = $options->{language};
my $code;
if ($inline_code) {
$code = $inline_code;
} else {
unless (-e $source) {
print STDERR "${RED}Error: File not found: $source${RESET}\n";
exit 1;
}
open my $fh, '<', $source or die "Cannot read file: $!";
$code = do { local $/; <$fh> };
close $fh;
$language ||= Un::detect_language($source);
}
unless ($language) {
print STDERR "${RED}Error: Could not detect language${RESET}\n";
exit 1;
}
my %opts = (network_mode => $options->{network} || 'zerotrust');
$opts{vcpu} = $options->{vcpu} if $options->{vcpu};
if ($options->{env} && @{$options->{env}}) {
my %env_vars;
foreach my $e (@{$options->{env}}) {
if ($e =~ /^([^=]+)=(.*)$/) {
$env_vars{$1} = $2;
}
}
$opts{env} = \%env_vars if %env_vars;
}
if ($options->{files} && @{$options->{files}}) {
$opts{input_files} = build_input_files(@{$options->{files}});
}
$opts{return_artifacts} = 1 if $options->{artifacts};
my $result = Un::execute($language, $code, %opts);
unless ($result) {
print STDERR "${RED}Error: " . Un::last_error() . "${RESET}\n";
exit 1;
}
print $result->{stdout} if $result->{stdout};
print STDERR $result->{stderr} 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) = @_;
if ($options->{list}) {
my $result = Un::session_list();
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}) {
Un::session_destroy($options->{kill});
print "${GREEN}Session terminated: $options->{kill}${RESET}\n";
return;
}
if ($options->{info}) {
my $result = Un::session_get($options->{info});
print encode_json($result) . "\n";
return;
}
if ($options->{freeze}) {
Un::session_freeze($options->{freeze});
print "${GREEN}Session frozen: $options->{freeze}${RESET}\n";
return;
}
if ($options->{unfreeze}) {
Un::session_unfreeze($options->{unfreeze});
print "${GREEN}Session unfreezing: $options->{unfreeze}${RESET}\n";
return;
}
if ($options->{boost}) {
my $vcpu = $options->{vcpu} || 2;
Un::session_boost($options->{boost}, $vcpu);
print "${GREEN}Session boosted: $options->{boost}${RESET}\n";
return;
}
if ($options->{unboost}) {
Un::session_unboost($options->{unboost});
print "${GREEN}Session unboosted: $options->{unboost}${RESET}\n";
return;
}
if ($options->{execute}) {
my $result = Un::session_execute($options->{execute}, $options->{command});
print $result->{stdout} if $result->{stdout};
print STDERR $result->{stderr} if $result->{stderr};
return;
}
if ($options->{snapshot}) {
my $result = Un::snapshot_session($options->{snapshot},
name => $options->{snapshot_name}, hot => $options->{hot});
print "${GREEN}Snapshot created${RESET}\n";
print encode_json($result) . "\n";
return;
}
# Create new session
my %opts = (shell => $options->{shell} || 'bash');
$opts{network} = $options->{network} if $options->{network};
$opts{vcpu} = $options->{vcpu} if $options->{vcpu};
$opts{persistence} = 'tmux' if $options->{tmux};
$opts{persistence} = 'screen' if $options->{screen};
if ($options->{files} && @{$options->{files}}) {
$opts{input_files} = build_input_files(@{$options->{files}});
}
print "${YELLOW}Creating session...${RESET}\n";
my $result = Un::session_create(%opts);
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) = @_;
if ($options->{list}) {
my $result = Un::service_list();
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 = Un::service_get($options->{info});
print encode_json($result) . "\n";
return;
}
if ($options->{logs}) {
my $result = Un::service_logs($options->{logs}, lines => $options->{lines});
print $result->{logs} // '';
return;
}
if ($options->{freeze}) {
Un::service_freeze($options->{freeze});
print "${GREEN}Service frozen: $options->{freeze}${RESET}\n";
return;
}
if ($options->{unfreeze}) {
Un::service_unfreeze($options->{unfreeze});
print "${GREEN}Service unfreezing: $options->{unfreeze}${RESET}\n";
return;
}
if ($options->{lock}) {
Un::service_lock($options->{lock});
print "${GREEN}Service locked: $options->{lock}${RESET}\n";
return;
}
if ($options->{unlock}) {
Un::service_unlock($options->{unlock});
print "${GREEN}Service unlocked: $options->{unlock}${RESET}\n";
return;
}
if ($options->{destroy}) {
Un::service_destroy($options->{destroy});
print "${GREEN}Service destroyed: $options->{destroy}${RESET}\n";
return;
}
if ($options->{resize}) {
unless ($options->{vcpu}) {
print STDERR "${RED}Error: --vcpu is required with --resize${RESET}\n";
exit 1;
}
Un::service_resize($options->{resize}, $options->{vcpu});
my $ram = $options->{vcpu} * 2;
print "${GREEN}Service resized to $options->{vcpu} vCPU, $ram GB RAM${RESET}\n";
return;
}
if ($options->{redeploy}) {
my %opts;
$opts{bootstrap} = $options->{bootstrap} if $options->{bootstrap};
if ($options->{files} && @{$options->{files}}) {
$opts{input_files} = build_input_files(@{$options->{files}});
}
Un::service_redeploy($options->{redeploy}, %opts);
print "${GREEN}Service redeployed: $options->{redeploy}${RESET}\n";
return;
}
if ($options->{execute}) {
my $result = Un::service_execute($options->{execute}, $options->{command});
print $result->{stdout} if $result->{stdout};
print STDERR $result->{stderr} if $result->{stderr};
return;
}
if ($options->{set_unfreeze_on_demand}) {
my $enabled = $options->{set_unfreeze_on_demand_value};
Un::service_set_unfreeze_on_demand($options->{set_unfreeze_on_demand}, $enabled);
my $status = $enabled ? 'enabled' : 'disabled';
print "${GREEN}Unfreeze-on-demand $status${RESET}\n";
return;
}
if ($options->{snapshot}) {
my $result = Un::snapshot_service($options->{snapshot},
name => $options->{snapshot_name}, hot => $options->{hot});
print "${GREEN}Snapshot created${RESET}\n";
print encode_json($result) . "\n";
return;
}
if ($options->{name}) {
my %opts = (name => $options->{name});
if ($options->{ports}) {
$opts{ports} = [map { int($_) } split(',', $options->{ports})];
}
if ($options->{domains}) {
$opts{domains} = [split(',', $options->{domains})];
}
$opts{service_type} = $options->{type} if $options->{type};
$opts{bootstrap} = $options->{bootstrap} if $options->{bootstrap};
if ($options->{bootstrap_file}) {
if (! -e $options->{bootstrap_file}) {
print STDERR "${RED}Error: Bootstrap file not found${RESET}\n";
exit 1;
}
open my $fh, '<', $options->{bootstrap_file};
$opts{bootstrap_content} = do { local $/; <$fh> };
close $fh;
}
if ($options->{files} && @{$options->{files}}) {
$opts{input_files} = build_input_files(@{$options->{files}});
}
$opts{network} = $options->{network} if $options->{network};
$opts{vcpu} = $options->{vcpu} if $options->{vcpu};
$opts{unfreeze_on_demand} = 1 if $options->{unfreeze_on_demand};
my $result = Un::service_create(%opts);
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};
# Auto-set vault
my $env_content = build_env_content($options->{env} || [], $options->{env_file});
if ($env_content && $result->{id}) {
Un::service_env_set($result->{id}, $env_content);
print "${GREEN}Vault configured${RESET}\n";
}
return;
}
print STDERR "${RED}Error: Specify --name to create or use --list, --info, etc.${RESET}\n";
exit 1;
}
sub cmd_service_env {
my ($action, $target, $envs, $env_file) = @_;
unless ($target) {
print STDERR "${RED}Error: Service ID required${RESET}\n";
exit 1;
}
if ($action eq 'status') {
my $result = Un::service_env_get($target);
print encode_json($result) . "\n";
} elsif ($action eq 'set') {
my $content = build_env_content($envs, $env_file);
unless ($content) {
print STDERR "${RED}Error: No env content provided${RESET}\n";
exit 1;
}
Un::service_env_set($target, $content);
print "${GREEN}Vault updated${RESET}\n";
} elsif ($action eq 'export') {
my $result = Un::service_env_export($target);
print $result->{env} // $result->{content} // '';
} elsif ($action eq 'delete') {
Un::service_env_delete($target);
print "${GREEN}Vault deleted${RESET}\n";
} else {
print STDERR "${RED}Error: Unknown env action '$action'${RESET}\n";
exit 1;
}
}
sub cmd_snapshot {
my ($options) = @_;
if ($options->{list}) {
my $result = Un::snapshot_list();
my $snapshots = $result->{snapshots} || [];
if (@$snapshots == 0) {
print "No snapshots\n";
} else {
printf "%-40s %-20s %-10s %s\n", 'ID', 'Name', 'Type', 'Created';
foreach my $s (@$snapshots) {
printf "%-40s %-20s %-10s %s\n",
$s->{id} // 'N/A', $s->{name} // 'N/A',
$s->{type} // 'N/A', $s->{created_at} // 'N/A';
}
}
return;
}
if ($options->{info}) {
my $result = Un::snapshot_get($options->{info});
print encode_json($result) . "\n";
return;
}
if ($options->{delete}) {
Un::snapshot_delete($options->{delete});
print "${GREEN}Snapshot deleted: $options->{delete}${RESET}\n";
return;
}
if ($options->{restore}) {
Un::snapshot_restore($options->{restore});
print "${GREEN}Snapshot restored${RESET}\n";
return;
}
if ($options->{lock}) {
Un::snapshot_lock($options->{lock});
print "${GREEN}Snapshot locked: $options->{lock}${RESET}\n";
return;
}
if ($options->{unlock}) {
Un::snapshot_unlock($options->{unlock});
print "${GREEN}Snapshot unlocked: $options->{unlock}${RESET}\n";
return;
}
if ($options->{clone}) {
my $result = Un::snapshot_clone($options->{clone},
clone_type => $options->{clone_type} || 'session',
name => $options->{clone_name});
print "${GREEN}Snapshot cloned${RESET}\n";
print encode_json($result) . "\n";
return;
}
print STDERR "${RED}Error: Use --list, --info, --delete, --restore, --lock, --unlock, or --clone${RESET}\n";
exit 1;
}
sub cmd_image {
my ($options) = @_;
if ($options->{list}) {
my $result = Un::image_list(filter => $options->{filter});
my $images = $result->{images} || [];
if (@$images == 0) {
print "No images\n";
} else {
printf "%-40s %-20s %-10s %s\n", 'ID', 'Name', 'Visibility', 'Created';
foreach my $i (@$images) {
printf "%-40s %-20s %-10s %s\n",
$i->{id} // 'N/A', $i->{name} // 'N/A',
$i->{visibility} // 'N/A', $i->{created_at} // 'N/A';
}
}
return;
}
if ($options->{info}) {
my $result = Un::image_get($options->{info});
print encode_json($result) . "\n";
return;
}
if ($options->{delete}) {
Un::image_delete($options->{delete});
print "${GREEN}Image deleted: $options->{delete}${RESET}\n";
return;
}
if ($options->{lock}) {
Un::image_lock($options->{lock});
print "${GREEN}Image locked: $options->{lock}${RESET}\n";
return;
}
if ($options->{unlock}) {
Un::image_unlock($options->{unlock});
print "${GREEN}Image unlocked: $options->{unlock}${RESET}\n";
return;
}
if ($options->{publish}) {
unless ($options->{source_type}) {
print STDERR "${RED}Error: --source-type required${RESET}\n";
exit 1;
}
my $result = Un::image_publish(
source_type => $options->{source_type},
source_id => $options->{publish},
name => $options->{pub_name});
print "${GREEN}Image published${RESET}\n";
print encode_json($result) . "\n";
return;
}
if ($options->{visibility_id} && $options->{visibility_mode}) {
Un::image_set_visibility($options->{visibility_id}, $options->{visibility_mode});
print "${GREEN}Visibility set to $options->{visibility_mode}${RESET}\n";
return;
}
if ($options->{grant_access}) {
Un::image_grant_access($options->{grant_access}, $options->{trusted_key});
print "${GREEN}Access granted${RESET}\n";
return;
}
if ($options->{revoke_access}) {
Un::image_revoke_access($options->{revoke_access}, $options->{trusted_key});
print "${GREEN}Access revoked${RESET}\n";
return;
}
if ($options->{list_trusted}) {
my $result = Un::image_list_trusted($options->{list_trusted});
print encode_json($result) . "\n";
return;
}
if ($options->{transfer}) {
Un::image_transfer($options->{transfer}, $options->{to_key});
print "${GREEN}Image transferred${RESET}\n";
return;
}
if ($options->{spawn}) {
my $result = Un::image_spawn($options->{spawn},
name => $options->{spawn_name},
ports => $options->{spawn_ports} ? [map { int($_) } split(',', $options->{spawn_ports})] : undef);
print "${GREEN}Service spawned from image${RESET}\n";
print encode_json($result) . "\n";
return;
}
if ($options->{clone}) {
my $result = Un::image_clone($options->{clone}, name => $options->{clone_name});
print "${GREEN}Image cloned${RESET}\n";
print encode_json($result) . "\n";
return;
}
print STDERR "${RED}Error: Use --list, --info, --delete, etc.${RESET}\n";
exit 1;
}
sub cmd_key {
my ($options) = @_;
my $result = Un::validate_keys();
if ($options->{extend}) {
my $pk = $result->{public_key};
if ($pk) {
my $url = "$Un::PORTAL_BASE/keys/extend?pk=$pk";
print "${BLUE}Opening browser to extend key...${RESET}\n";
system("xdg-open '$url' 2>/dev/null || open '$url' 2>/dev/null &");
}
return;
}
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 "${YELLOW}To renew: Visit $Un::PORTAL_BASE/keys/extend${RESET}\n";
exit 1;
}
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";
}
sub cmd_languages {
my ($options) = @_;
my $langs = Un::get_languages();
if ($options->{json}) {
print encode_json($langs) . "\n";
} else {
foreach my $lang (@$langs) {
print "$lang\n";
}
}
}
sub show_help {
print <<"HELP";
Unsandbox CLI - Execute code in secure sandboxes
Usage:
perl un.pl [options] <source_file>
perl un.pl -s <language> '<code>'
perl un.pl session [options]
perl un.pl service [options]
perl un.pl service env <action> <service_id> [options]
perl un.pl snapshot [options]
perl un.pl image [options]
perl un.pl languages [--json]
perl un.pl key [--extend]
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)
-s LANG Language for inline code
Session options:
--list List sessions
--info ID Get session details
--kill ID Terminate session
--freeze ID Freeze session
--unfreeze ID Unfreeze session
--boost ID Boost session (with -v)
--unboost ID Unboost session
--execute ID Execute command (with --command)
--snapshot ID Create snapshot
--shell SHELL Shell/REPL (default: bash)
--tmux Enable tmux persistence
--screen Enable screen persistence
Service options:
--list List services
--info ID Get service details
--name NAME Create service with name
--ports PORTS Comma-separated ports
--domains DOMS Custom domains
--bootstrap CMD Bootstrap command
--bootstrap-file Bootstrap script file
--logs ID Get logs
--freeze ID Freeze service
--unfreeze ID Unfreeze service
--lock ID Lock service
--unlock ID Unlock service
--destroy ID Destroy service
--resize ID Resize (with -v)
--redeploy ID Redeploy service
--execute ID Execute command (with --command)
--snapshot ID Create snapshot
Service env commands:
env status ID Check vault status
env set ID Set vault (use -e or --env-file)
env export ID Export vault contents
env delete ID Delete vault
Snapshot options:
--list List snapshots
--info ID Get snapshot details
--delete ID Delete snapshot
--restore ID Restore from snapshot
--lock ID Lock snapshot
--unlock ID Unlock snapshot
--clone ID Clone snapshot (with --clone-type, --clone-name)
Image options:
--list List images (with optional --filter)
--info ID Get image details
--delete ID Delete image
--lock ID Lock image
--unlock ID Unlock image
--publish ID Publish from service/snapshot (--source-type)
--visibility ID MODE Set visibility (private|unlisted|public)
--grant-access ID Grant access (with --trusted-key)
--revoke-access ID Revoke access (with --trusted-key)
--list-trusted ID List trusted keys
--transfer ID Transfer ownership (with --to-key)
--spawn ID Spawn service from image
--clone ID Clone image
HELP
exit 1;
}
sub main {
my %options = (
env => [],
files => []
);
my $i = 0;
while ($i < @ARGV) {
my $arg = $ARGV[$i];
if ($arg eq 'session' || $arg eq 'service' || $arg eq 'snapshot' ||
$arg eq 'image' || $arg eq 'key' || $arg eq 'languages') {
$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 '-s' || $arg eq '--shell') {
$options{language} = $ARGV[++$i] if !$options{command};
$options{shell} = $ARGV[$i] if $options{command} && $options{command} eq 'session';
} elsif ($arg eq '-l' || $arg eq '--list') {
$options{list} = 1;
} elsif ($arg eq '--info') {
$options{info} = $ARGV[++$i];
} elsif ($arg eq '--kill') {
$options{kill} = $ARGV[++$i];
} elsif ($arg eq '--freeze') {
$options{freeze} = $ARGV[++$i];
} elsif ($arg eq '--unfreeze') {
$options{unfreeze} = $ARGV[++$i];
} elsif ($arg eq '--lock') {
$options{lock} = $ARGV[++$i];
} elsif ($arg eq '--unlock') {
$options{unlock} = $ARGV[++$i];
} elsif ($arg eq '--boost') {
$options{boost} = $ARGV[++$i];
} elsif ($arg eq '--unboost') {
$options{unboost} = $ARGV[++$i];
} elsif ($arg eq '--destroy') {
$options{destroy} = $ARGV[++$i];
} elsif ($arg eq '--resize') {
$options{resize} = $ARGV[++$i];
} elsif ($arg eq '--redeploy') {
$options{redeploy} = $ARGV[++$i];
} elsif ($arg eq '--logs') {
$options{logs} = $ARGV[++$i];
} elsif ($arg eq '--lines') {
$options{lines} = $ARGV[++$i];
} elsif ($arg eq '--execute') {
$options{execute} = $ARGV[++$i];
} elsif ($arg eq '--command') {
$options{command_str} = $ARGV[++$i];
} 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 '--bootstrap-file') {
$options{bootstrap_file} = $ARGV[++$i];
} elsif ($arg eq '--env-file') {
$options{env_file} = $ARGV[++$i];
} elsif ($arg eq '--snapshot') {
$options{snapshot} = $ARGV[++$i];
} elsif ($arg eq '--snapshot-name') {
$options{snapshot_name} = $ARGV[++$i];
} elsif ($arg eq '--hot') {
$options{hot} = 1;
} elsif ($arg eq '--restore') {
$options{restore} = $ARGV[++$i];
} elsif ($arg eq '--delete') {
$options{delete} = $ARGV[++$i];
} elsif ($arg eq '--clone') {
$options{clone} = $ARGV[++$i];
} elsif ($arg eq '--clone-type') {
$options{clone_type} = $ARGV[++$i];
} elsif ($arg eq '--clone-name') {
$options{clone_name} = $ARGV[++$i];
} elsif ($arg eq '--publish') {
$options{publish} = $ARGV[++$i];
} elsif ($arg eq '--source-type') {
$options{source_type} = $ARGV[++$i];
} elsif ($arg eq '--visibility') {
$options{visibility_id} = $ARGV[++$i];
$options{visibility_mode} = $ARGV[++$i];
} elsif ($arg eq '--spawn') {
$options{spawn} = $ARGV[++$i];
} elsif ($arg eq '--grant-access') {
$options{grant_access} = $ARGV[++$i];
} elsif ($arg eq '--revoke-access') {
$options{revoke_access} = $ARGV[++$i];
} elsif ($arg eq '--list-trusted') {
$options{list_trusted} = $ARGV[++$i];
} elsif ($arg eq '--trusted-key') {
$options{trusted_key} = $ARGV[++$i];
} elsif ($arg eq '--transfer') {
$options{transfer} = $ARGV[++$i];
} elsif ($arg eq '--to-key') {
$options{to_key} = $ARGV[++$i];
} elsif ($arg eq '--filter') {
$options{filter} = $ARGV[++$i];
} elsif ($arg eq '--tmux') {
$options{tmux} = 1;
} elsif ($arg eq '--screen') {
$options{screen} = 1;
} elsif ($arg eq '--unfreeze-on-demand') {
$options{unfreeze_on_demand} = 1;
} elsif ($arg eq '--set-unfreeze-on-demand') {
$options{set_unfreeze_on_demand} = $ARGV[++$i];
$options{set_unfreeze_on_demand_value} = ($ARGV[++$i] =~ /^(true|1)$/i);
} elsif ($arg eq '--json') {
$options{json} = 1;
} elsif ($arg eq '--extend') {
$options{extend} = 1;
} elsif ($arg eq 'env' && $options{command} && $options{command} eq 'service') {
$options{env_action} = $ARGV[++$i];
$options{env_target} = $ARGV[++$i] if defined $ARGV[$i+1] && $ARGV[$i+1] !~ /^-/;
} elsif ($arg eq '--account') {
$Un::ACCOUNT_INDEX = int($ARGV[++$i]);
} elsif ($arg eq '--help' || $arg eq '-h') {
show_help();
} elsif ($arg =~ /^-/) {
print STDERR "${RED}Unknown option: $arg${RESET}\n";
exit 1;
} else {
if ($options{language} && !$options{inline_code}) {
$options{inline_code} = $arg;
} else {
$options{source_file} = $arg;
}
}
$i++;
}
# Handle commands
if ($options{command}) {
if ($options{command} eq 'session') {
cmd_session(\%options);
} elsif ($options{command} eq 'service') {
if ($options{env_action}) {
cmd_service_env($options{env_action}, $options{env_target},
$options{env}, $options{env_file});
} else {
$options{command} = $options{command_str} if $options{command_str};
cmd_service(\%options);
}
} elsif ($options{command} eq 'snapshot') {
cmd_snapshot(\%options);
} elsif ($options{command} eq 'image') {
cmd_image(\%options);
} elsif ($options{command} eq 'languages') {
cmd_languages(\%options);
} elsif ($options{command} eq 'key') {
cmd_key(\%options);
}
} elsif ($options{source_file} || $options{inline_code}) {
cmd_execute(\%options);
} else {
show_help();
}
}
main() unless caller;
1;
Esclarecimentos de documentação
Dependências
C Binary (un1) — requer libcurl e libwebsockets:
sudo apt install build-essential libcurl4-openssl-dev libwebsockets-dev
wget unsandbox.com/downloads/un.c && gcc -O2 -o un un.c -lcurl -lwebsockets
Implementações SDK — a maioria usa apenas stdlib (Ruby, JS, Go, etc). Alguns requerem dependências mínimas:
pip install requests # Python
Executar Código
Executar um script
./un hello.py
./un app.js
./un main.rs
Com variáveis de ambiente
./un -e DEBUG=1 -e NAME=World script.py
Com arquivos de entrada (teletransportar arquivos para sandbox)
./un -f data.csv -f config.json process.py
Obter binário compilado
./un -a -o ./bin main.c
Sessões interativas
Iniciar uma sessão de shell
# Default bash shell
./un session
# Choose your shell
./un session --shell zsh
./un session --shell fish
# Jump into a REPL
./un session --shell python3
./un session --shell node
./un session --shell julia
Sessão com acesso à rede
./un session -n semitrusted
Auditoria de sessão (gravação completa do terminal)
# Record everything (including vim, interactive programs)
./un session --audit -o ./logs
# Replay session later
zcat session.log*.gz | less -R
Coletar artefatos da sessão
# Files in /tmp/artifacts/ are collected on exit
./un session -a -o ./outputs
Persistência de sessão (tmux/screen)
# Default: session terminates on disconnect (clean exit)
./un session
# With tmux: session persists, can reconnect later
./un session --tmux
# Press Ctrl+b then d to detach
# With screen: alternative multiplexer
./un session --screen
# Press Ctrl+a then d to detach
Listar Trabalhos Ativos
./un session --list
# Output:
# Active sessions: 2
#
# SESSION ID CONTAINER SHELL TTL STATUS
# abc123... unsb-vm-12345 python3 45m30s active
# def456... unsb-vm-67890 bash 1h2m active
Reconectar à sessão existente
# Reconnect by container name (requires --tmux or --screen)
./un session --attach unsb-vm-12345
# Use exit to terminate session, or detach to keep it running
Encerrar uma sessão
./un session --kill unsb-vm-12345
Shells e REPLs disponíveis
Shells: bash, dash, sh, zsh, fish, ksh, tcsh, csh, elvish, xonsh, ash
REPLs: python3, bpython, ipython # Python
node # JavaScript
ruby, irb # Ruby
lua # Lua
php # PHP
perl # Perl
guile, scheme # Scheme
ghci # Haskell
erl, iex # Erlang/Elixir
sbcl, clisp # Common Lisp
r # R
julia # Julia
clojure # Clojure
Gerenciamento de Chave API
Verificar Status do Pagamento
# Check if your API key is valid
./un key
# Output:
# Valid: key expires in 30 days
Estender Chave Expirada
# Open the portal to extend an expired key
./un key --extend
# This opens the unsandbox.com portal where you can
# add more credits to extend your key's expiration
Autenticação
As credenciais são carregadas em ordem de prioridade (maior primeiro):
# 1. CLI flags (highest priority)
./un -p unsb-pk-xxxx -k unsb-sk-xxxxx script.py
# 2. Environment variables
export UNSANDBOX_PUBLIC_KEY=unsb-pk-xxxx-xxxx-xxxx-xxxx
export UNSANDBOX_SECRET_KEY=unsb-sk-xxxxx-xxxxx-xxxxx-xxxxx
./un script.py
# 3. Config file (lowest priority)
# ~/.unsandbox/accounts.csv format: public_key,secret_key
mkdir -p ~/.unsandbox
echo "unsb-pk-xxxx-xxxx-xxxx-xxxx,unsb-sk-xxxxx-xxxxx-xxxxx-xxxxx" > ~/.unsandbox/accounts.csv
./un script.py
As requisições são assinadas com HMAC-SHA256. O token bearer contém apenas a chave pública; a chave secreta calcula a assinatura (nunca é transmitida).
Escalonamento de Recursos
Definir Quantidade de vCPU
# Default: 1 vCPU, 2GB RAM
./un script.py
# Scale up: 4 vCPUs, 8GB RAM
./un -v 4 script.py
# Maximum: 8 vCPUs, 16GB RAM
./un --vcpu 8 heavy_compute.py
Reforço de Sessão Ao Vivo
# Boost a running session to 2 vCPU, 4GB RAM
./un session --boost sandbox-abc
# Boost to specific vCPU count (4 vCPU, 8GB RAM)
./un session --boost sandbox-abc --boost-vcpu 4
# Return to base resources (1 vCPU, 2GB RAM)
./un session --unboost sandbox-abc
Congelar/Descongelar Sessão
Congelar e Descongelar Sessões
# Freeze a session (stop billing, preserve state)
./un session --freeze sandbox-abc
# Unfreeze a frozen session
./un session --unfreeze sandbox-abc
# Note: Requires --tmux or --screen for persistence
Serviços Persistentes
Criar um Serviço
# Web server with ports
./un service --name web --ports 80,443 --bootstrap "python -m http.server 80"
# With custom domains
./un service --name blog --ports 8000 --domains blog.example.com
# Game server with SRV records
./un service --name mc --type minecraft --bootstrap ./setup.sh
# Deploy app tarball with bootstrap script
./un service --name app --ports 8000 -f app.tar.gz --bootstrap-file ./setup.sh
# setup.sh: cd /tmp && tar xzf app.tar.gz && ./app/start.sh
Gerenciar Serviços
# List all services
./un service --list
# Get service details
./un service --info abc123
# View bootstrap logs
./un service --logs abc123
./un service --tail abc123 # last 9000 lines
# Execute command in running service
./un service --execute abc123 'journalctl -u myapp -n 50'
# Dump bootstrap script (for migrations)
./un service --dump-bootstrap abc123
./un service --dump-bootstrap abc123 backup.sh
# Freeze/unfreeze service
./un service --freeze abc123
./un service --unfreeze abc123
# Service settings (auto-wake, freeze page display)
./un service --auto-unfreeze abc123 # enable auto-wake on HTTP
./un service --no-auto-unfreeze abc123 # disable auto-wake
./un service --show-freeze-page abc123 # show HTML payment page (default)
./un service --no-show-freeze-page abc123 # return JSON error instead
# Redeploy with new bootstrap
./un service --redeploy abc123 --bootstrap ./new-setup.sh
# Destroy service
./un service --destroy abc123
Snapshots
Listar Snapshots
./un snapshot --list
# Output:
# Snapshots: 3
#
# SNAPSHOT ID NAME SOURCE SIZE CREATED
# unsb-snapshot-a1b2-c3d4-e5f6-g7h8 before-upgrade session 512 MB 2h ago
# unsb-snapshot-i9j0-k1l2-m3n4-o5p6 stable-v1.0 service 1.2 GB 1d ago
Criar Snapshot da Sessão
# Snapshot with name
./un session --snapshot unsb-vm-12345 --name "before upgrade"
# Quick snapshot (auto-generated name)
./un session --snapshot unsb-vm-12345
Criar Snapshot do Serviço
# Standard snapshot (pauses container briefly)
./un service --snapshot unsb-service-abc123 --name "stable v1.0"
# Hot snapshot (no pause, may be inconsistent)
./un service --snapshot unsb-service-abc123 --hot
Restaurar a partir do Snapshot
# Restore session from snapshot
./un session --restore unsb-snapshot-a1b2-c3d4-e5f6-g7h8
# Restore service from snapshot
./un service --restore unsb-snapshot-i9j0-k1l2-m3n4-o5p6
Excluir Snapshot
./un snapshot --delete unsb-snapshot-a1b2-c3d4-e5f6-g7h8
Imagens
Imagens são imagens de container independentes e transferíveis que sobrevivem à exclusão do container. Diferente dos snapshots (que permanecem com seu container), imagens podem ser compartilhadas com outros usuários, transferidas entre chaves de API ou tornadas públicas no marketplace.
Listar Imagens
# List all images (owned + shared + public)
./un image --list
# List only your images
./un image --list owned
# List images shared with you
./un image --list shared
# List public marketplace images
./un image --list public
# Get image details
./un image --info unsb-image-xxxx-xxxx-xxxx-xxxx
Publicar Imagens
# Publish from a stopped or frozen service
./un image --publish-service unsb-service-abc123 \
--name "My App v1.0" --description "Production snapshot"
# Publish from a snapshot
./un image --publish-snapshot unsb-snapshot-xxxx-xxxx-xxxx-xxxx \
--name "Stable Release"
# Note: Cannot publish from running containers - stop or freeze first
Criar Serviços a partir de Imagens
# Spawn a new service from an image
./un image --spawn unsb-image-xxxx-xxxx-xxxx-xxxx \
--name new-service --ports 80,443
# Clone an image (creates a copy you own)
./un image --clone unsb-image-xxxx-xxxx-xxxx-xxxx
Proteção de Imagem
# Lock image to prevent accidental deletion
./un image --lock unsb-image-xxxx-xxxx-xxxx-xxxx
# Unlock image to allow deletion
./un image --unlock unsb-image-xxxx-xxxx-xxxx-xxxx
# Delete image (must be unlocked)
./un image --delete unsb-image-xxxx-xxxx-xxxx-xxxx
Visibilidade e Compartilhamento
# Set visibility level
./un image --visibility unsb-image-xxxx-xxxx-xxxx-xxxx private # owner only (default)
./un image --visibility unsb-image-xxxx-xxxx-xxxx-xxxx unlisted # can be shared
./un image --visibility unsb-image-xxxx-xxxx-xxxx-xxxx public # marketplace
# Share with specific user
./un image --grant unsb-image-xxxx-xxxx-xxxx-xxxx \
--key unsb-pk-friend-friend-friend-friend
# Revoke access
./un image --revoke unsb-image-xxxx-xxxx-xxxx-xxxx \
--key unsb-pk-friend-friend-friend-friend
# List who has access
./un image --trusted unsb-image-xxxx-xxxx-xxxx-xxxx
Transferir Propriedade
# Transfer image to another API key
./un image --transfer unsb-image-xxxx-xxxx-xxxx-xxxx \
--to unsb-pk-newowner-newowner-newowner-newowner
Referência de uso
Usage: ./un [options] <source_file>
./un session [options]
./un service [options]
./un snapshot [options]
./un image [options]
./un key
Commands:
(default) Execute source file in sandbox
session Open interactive shell/REPL session
service Manage persistent services
snapshot Manage container snapshots
image Manage container images (publish, share, transfer)
key Check API key validity and expiration
Options:
-e KEY=VALUE Set environment variable (can use multiple times)
-f FILE Add input file (can use multiple times)
-a Return and save artifacts from /tmp/artifacts/
-o DIR Output directory for artifacts (default: current dir)
-p KEY Public key (or set UNSANDBOX_PUBLIC_KEY env var)
-k KEY Secret key (or set UNSANDBOX_SECRET_KEY env var)
-n MODE Network mode: zerotrust (default) or semitrusted
-v N, --vcpu N vCPU count 1-8, each vCPU gets 2GB RAM (default: 1)
-y Skip confirmation for large uploads (>1GB)
-h Show this help
Authentication (priority order):
1. -p and -k flags (public and secret key)
2. UNSANDBOX_PUBLIC_KEY + UNSANDBOX_SECRET_KEY env vars
3. ~/.unsandbox/accounts.csv (format: public_key,secret_key per line)
Session options:
-s, --shell SHELL Shell/REPL to use (default: bash)
-l, --list List active sessions
--attach ID Reconnect to existing session (ID or container name)
--kill ID Terminate a session (ID or container name)
--freeze ID Freeze a session (requires --tmux/--screen)
--unfreeze ID Unfreeze a frozen session
--boost ID Boost session resources (2 vCPU, 4GB RAM)
--boost-vcpu N Specify vCPU count for boost (1-8)
--unboost ID Return to base resources
--audit Record full session for auditing
--tmux Enable session persistence with tmux (allows reconnect)
--screen Enable session persistence with screen (allows reconnect)
Service options:
--name NAME Service name (creates new service)
--ports PORTS Comma-separated ports (e.g., 80,443)
--domains DOMAINS Custom domains (e.g., example.com,www.example.com)
--type TYPE Service type: minecraft, mumble, teamspeak, source, tcp, udp
--bootstrap CMD Bootstrap command/file/URL to run on startup
-f FILE Upload file to /tmp/ (can use multiple times)
-l, --list List all services
--info ID Get service details
--tail ID Get last 9000 lines of bootstrap logs
--logs ID Get all bootstrap logs
--freeze ID Freeze a service
--unfreeze ID Unfreeze a service
--auto-unfreeze ID Enable auto-wake on HTTP request
--no-auto-unfreeze ID Disable auto-wake on HTTP request
--show-freeze-page ID Show HTML payment page when frozen (default)
--no-show-freeze-page ID Return JSON error when frozen
--destroy ID Destroy a service
--redeploy ID Re-run bootstrap script (requires --bootstrap)
--execute ID CMD Run a command in a running service
--dump-bootstrap ID [FILE] Dump bootstrap script (for migrations)
--snapshot ID Create snapshot of session or service
--snapshot-name User-friendly name for snapshot
--hot Create snapshot without pausing (may be inconsistent)
--restore ID Restore session/service from snapshot ID
Snapshot options:
-l, --list List all snapshots
--info ID Get snapshot details
--delete ID Delete a snapshot permanently
Image options:
-l, --list [owned|shared|public] List images (all, owned, shared, or public)
--info ID Get image details
--publish-service ID Publish image from stopped/frozen service
--publish-snapshot ID Publish image from snapshot
--name NAME Name for published image
--description DESC Description for published image
--delete ID Delete image (must be unlocked)
--clone ID Clone image (creates copy you own)
--spawn ID Create service from image (requires --name)
--lock ID Lock image to prevent deletion
--unlock ID Unlock image to allow deletion
--visibility ID LEVEL Set visibility (private|unlisted|public)
--grant ID --key KEY Grant access to another API key
--revoke ID --key KEY Revoke access from API key
--transfer ID --to KEY Transfer ownership to API key
--trusted ID List API keys with access
Key options:
(no options) Check API key validity
--extend Open portal to extend an expired key
Examples:
./un script.py # execute Python script
./un -e DEBUG=1 script.py # with environment variable
./un -f data.csv process.py # with input file
./un -a -o ./bin main.c # save compiled artifacts
./un -v 4 heavy.py # with 4 vCPUs, 8GB RAM
./un session # interactive bash session
./un session --tmux # bash with reconnect support
./un session --list # list active sessions
./un session --attach unsb-vm-12345 # reconnect to session
./un session --kill unsb-vm-12345 # terminate a session
./un session --freeze unsb-vm-12345 # freeze session
./un session --unfreeze unsb-vm-12345 # unfreeze session
./un session --boost unsb-vm-12345 # boost resources
./un session --unboost unsb-vm-12345 # return to base
./un session --shell python3 # Python REPL
./un session --shell node # Node.js REPL
./un session -n semitrusted # session with network access
./un session --audit -o ./logs # record session for auditing
./un service --name web --ports 80 # create web service
./un service --list # list all services
./un service --logs abc123 # view bootstrap logs
./un key # check API key
./un key --extend # extend expired key
./un snapshot --list # list all snapshots
./un session --snapshot unsb-vm-123 # snapshot a session
./un service --snapshot abc123 # snapshot a service
./un session --restore unsb-snapshot-xxxx # restore from snapshot
./un image --list # list all images
./un image --list owned # list your images
./un image --publish-service abc # publish image from service
./un image --spawn img123 --name x # create service from image
./un image --grant img --key pk # share image with user
CLI Inception
O UN CLI foi implementado em 42 linguagens de programação, demonstrando que a API do unsandbox pode ser acessada de praticamente qualquer ambiente.
Ver Todas as 42 Implementações →
Licença
DOMÍNIO PÚBLICO - SEM LICENÇA, SEM GARANTIA
Este é software gratuito de domínio público para o bem público de um permacomputador hospedado
em permacomputer.com - um computador sempre ativo pelo povo, para o povo. Um que é
durável, fácil de reparar e distribuído como água da torneira para inteligência de
aprendizado de máquina.
O permacomputador é infraestrutura de propriedade comunitária otimizada em torno de quatro valores:
VERDADE - Primeiros princípios, matemática & ciência, código aberto distribuído livremente
LIBERDADE - Parcerias voluntárias, liberdade da tirania e controle corporativo
HARMONIA - Desperdício mínimo, sistemas auto-renováveis com diversas conexões prósperas
AMOR - Seja você mesmo sem ferir os outros, cooperação através da lei natural
Este software contribui para essa visão ao permitir a execução de código em mais de 42
linguagens de programação através de uma interface unificada, acessível a todos. Código são
sementes que brotam em qualquer tecnologia abandonada.
Saiba mais: https://www.permacomputer.com
Qualquer pessoa é livre para copiar, modificar, publicar, usar, compilar, vender ou distribuir
este software, seja em forma de código-fonte ou como binário compilado, para qualquer propósito,
comercial ou não comercial, e por qualquer meio.
SEM GARANTIA. O SOFTWARE É FORNECIDO "COMO ESTÁ" SEM GARANTIA DE QUALQUER TIPO.
Dito isso, a camada de membrana digital do nosso permacomputador executa continuamente testes
unitários, de integração e funcionais em todo o seu próprio software - com nosso permacomputador
monitorando a si mesmo, reparando a si mesmo, com orientação humana mínima no ciclo.
Nossos agentes fazem o seu melhor.
Copyright 2025 TimeHexOn & foxhop & russell@unturf
https://www.timehexon.com
https://www.foxhop.net
https://www.unturf.com/software