CLI
Fast command-line client for code execution and interactive sessions. 42+ languages, 30+ shells/REPLs.
Official OpenAPI Swagger Docs ↗Quick Start — Fortran
# Download + setup
curl -O https://git.unturf.com/engineering/unturf/un-inception/-/raw/main/clients/fortran/sync/src/un.f90
export UNSANDBOX_PUBLIC_KEY="unsb-pk-xxxx-xxxx-xxxx-xxxx"
export UNSANDBOX_SECRET_KEY="unsb-sk-xxxxx-xxxxx-xxxxx-xxxxx"
# Run code
./un script.fortran
Downloads
Install Guide →Features
- 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
Integration Quickstart ⚡
Add unsandbox superpowers to your existing Fortran app:
curl -O https://git.unturf.com/engineering/unturf/un-inception/-/raw/main/clients/fortran/sync/src/un.f90
# 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 Fortran app:
program myapp
use un_module
type(result_type) :: result
result = execute_code("fortran", "print *, 'Hello from Fortran running on unsandbox!'")
print *, result%stdout ! Hello from Fortran running on unsandbox!
end program myapp
gfortran -o myapp main.f90 un.f90 -lcurl && ./myapp
6c7ebf45cbaa78f62137a75bada8c134
SHA256: bdcce3de8ecb539a8dd28ea7ad210497bd144723d9f3b8f17e5a199fe24a0d05
! 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
!==============================================================================
! unsandbox SDK for Fortran - Execute code in secure sandboxes
! https://unsandbox.com | https://api.unsandbox.com/openapi
!
! Library Usage:
! use unsandbox_sdk
!
! type(unsandbox_client) :: client
! type(execution_result) :: result
! integer :: status
!
! ! Initialize client (loads credentials from environment)
! call client%init(status)
!
! ! Execute code synchronously
! call client%execute("python", 'print("Hello")', result, status)
! print *, trim(result%stdout)
!
! ! Execute code asynchronously
! call client%execute_async("python", code, job_id, status)
! call client%wait(job_id, result, status)
!
! CLI Usage:
! ./un script.py
! ./un session [options]
! ./un service [options]
! ./un key [--extend]
!
! Authentication (in priority order):
! 1. Environment variables: UNSANDBOX_PUBLIC_KEY + UNSANDBOX_SECRET_KEY
! 2. Config file: ~/.unsandbox/accounts.csv (public_key,secret_key per line)
! 3. Legacy: UNSANDBOX_API_KEY (deprecated)
!
! Compile:
! gfortran -o un un.f90
!
!==============================================================================
!------------------------------------------------------------------------------
! Module: unsandbox_sdk
! Description: Unsandbox API client library for Fortran
!
! This module provides a type-safe interface to the unsandbox API for
! executing code in secure sandboxes. Due to Fortran's limited HTTP/JSON
! support, this implementation uses shell commands (curl/jq) for API calls.
!
! Types:
! unsandbox_client - Main client class with stored credentials
! execution_result - Result from code execution
! job_info - Information about an async job
!
! Functions:
! execute - Execute code synchronously
! execute_async - Execute code asynchronously, returns job_id
! get_job - Get status of an async job
! wait - Wait for async job completion
! cancel_job - Cancel a running job
! list_jobs - List all active jobs
! run - Execute code with shebang auto-detection
! run_async - Execute with auto-detection, returns job_id
! image - Generate image from text prompt
! languages - Get list of supported languages
!
!------------------------------------------------------------------------------
module unsandbox_sdk
implicit none
private
! Export public types and procedures
public :: unsandbox_client
public :: execution_result
public :: job_info
public :: get_credentials
public :: sign_request
public :: detect_language
! API configuration
character(len=*), parameter, public :: API_BASE = 'https://api.unsandbox.com'
character(len=*), parameter, public :: PORTAL_BASE = 'https://unsandbox.com'
integer, parameter, public :: DEFAULT_TTL = 60
integer, parameter, public :: DEFAULT_TIMEOUT = 300
integer, parameter, public :: LANGUAGES_CACHE_TTL = 3600 ! 1 hour cache TTL
!--------------------------------------------------------------------------
! Type: execution_result
! Description: Result from code execution
!
! Fields:
! success - Whether execution succeeded
! stdout - Standard output from execution
! stderr - Standard error from execution
! exit_code - Exit code from execution
! job_id - Job ID for async execution
! language - Detected or specified language
! time_ms - Execution time in milliseconds
!--------------------------------------------------------------------------
type :: execution_result
logical :: success = .false.
character(len=65536) :: stdout = ''
character(len=65536) :: stderr = ''
integer :: exit_code = 0
character(len=256) :: job_id = ''
character(len=64) :: language = ''
integer :: time_ms = 0
end type execution_result
!--------------------------------------------------------------------------
! Type: job_info
! Description: Information about an async job
!
! Fields:
! job_id - Unique job identifier
! status - Job status (pending, running, completed, failed, timeout, cancelled)
! language - Programming language
! submitted - Submission timestamp
!--------------------------------------------------------------------------
type :: job_info
character(len=256) :: job_id = ''
character(len=32) :: status = ''
character(len=64) :: language = ''
character(len=64) :: submitted = ''
end type job_info
!--------------------------------------------------------------------------
! Type: unsandbox_client
! Description: API client with stored credentials
!
! Use the client class when making multiple API calls to avoid
! repeated credential resolution.
!
! Example:
! type(unsandbox_client) :: client
! call client%init(status)
! call client%execute("python", code, result, status)
!--------------------------------------------------------------------------
type :: unsandbox_client
character(len=256) :: public_key = ''
character(len=256) :: secret_key = ''
logical :: initialized = .false.
contains
procedure :: init => client_init
procedure :: execute => client_execute
procedure :: execute_async => client_execute_async
procedure :: get_job => client_get_job
procedure :: wait => client_wait
procedure :: cancel_job => client_cancel_job
procedure :: list_jobs => client_list_jobs
procedure :: run => client_run
procedure :: run_async => client_run_async
procedure :: image => client_image
procedure :: languages => client_languages
end type unsandbox_client
contains
!--------------------------------------------------------------------------
! Subroutine: get_credentials
! Description: Get API credentials from environment or config file
!
! Priority order:
! 1. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY)
! 2. Config file (~/.unsandbox/accounts.csv)
! 3. Legacy UNSANDBOX_API_KEY (deprecated)
!
! Arguments:
! public_key - Output: API public key
! secret_key - Output: API secret key
! status - Output: 0 on success, non-zero on error
!--------------------------------------------------------------------------
subroutine get_credentials(public_key, secret_key, status)
character(len=*), intent(out) :: public_key, secret_key
integer, intent(out) :: status
character(len=1024) :: home_dir, accounts_path, line, api_key
integer :: unit_num, ios
logical :: file_exists
status = 0
public_key = ''
secret_key = ''
! Priority 1: Environment variables
call get_environment_variable('UNSANDBOX_PUBLIC_KEY', public_key, status=ios)
if (ios == 0 .and. len_trim(public_key) > 0) then
call get_environment_variable('UNSANDBOX_SECRET_KEY', secret_key, status=ios)
if (ios == 0 .and. len_trim(secret_key) > 0) then
return
end if
end if
! Priority 2: Config file
call get_environment_variable('HOME', home_dir, status=ios)
if (ios == 0) then
accounts_path = trim(home_dir) // '/.unsandbox/accounts.csv'
inquire(file=trim(accounts_path), exist=file_exists)
if (file_exists) then
open(newunit=unit_num, file=trim(accounts_path), status='old', &
action='read', iostat=ios)
if (ios == 0) then
do
read(unit_num, '(A)', iostat=ios) line
if (ios /= 0) exit
line = adjustl(line)
if (len_trim(line) == 0) cycle
if (line(1:1) == '#') cycle
! Parse CSV: public_key,secret_key
call parse_csv_line(line, public_key, secret_key)
if (len_trim(public_key) > 0 .and. len_trim(secret_key) > 0) then
if (public_key(1:8) == 'unsb-pk-' .and. &
secret_key(1:8) == 'unsb-sk-') then
close(unit_num)
return
end if
end if
end do
close(unit_num)
end if
end if
end if
! Priority 3: Legacy API key
call get_environment_variable('UNSANDBOX_API_KEY', api_key, status=ios)
if (ios == 0 .and. len_trim(api_key) > 0) then
public_key = api_key
secret_key = api_key
return
end if
! No credentials found
status = 1
end subroutine get_credentials
!--------------------------------------------------------------------------
! Subroutine: parse_csv_line
! Description: Parse a CSV line into two fields
!--------------------------------------------------------------------------
subroutine parse_csv_line(line, field1, field2)
character(len=*), intent(in) :: line
character(len=*), intent(out) :: field1, field2
integer :: comma_pos
field1 = ''
field2 = ''
comma_pos = index(line, ',')
if (comma_pos > 0) then
field1 = line(1:comma_pos-1)
field2 = line(comma_pos+1:)
end if
end subroutine parse_csv_line
!--------------------------------------------------------------------------
! Subroutine: sign_request
! Description: Generate HMAC-SHA256 signature for API request
!
! Signature format: HMAC-SHA256(secret_key, "timestamp:METHOD:path:body")
!
! Note: Uses openssl via shell command due to Fortran limitations
!
! Arguments:
! secret_key - API secret key
! timestamp - Unix timestamp as string
! method - HTTP method (GET, POST, etc.)
! path - API endpoint path
! body - Request body (empty string if none)
! signature - Output: Hex-encoded signature
!--------------------------------------------------------------------------
subroutine sign_request(secret_key, timestamp, method, path, body, signature)
character(len=*), intent(in) :: secret_key, timestamp, method, path, body
character(len=*), intent(out) :: signature
character(len=4096) :: cmd
integer :: ios
! Use shell to compute HMAC (Fortran lacks native crypto)
write(cmd, '(10A)') &
'echo -n "', trim(timestamp), ':', trim(method), ':', trim(path), ':', trim(body), &
'" | openssl dgst -sha256 -hmac "', trim(secret_key), '" | cut -d" " -f2'
! This would need to capture output - simplified for module use
signature = ''
end subroutine sign_request
!--------------------------------------------------------------------------
! Subroutine: detect_language
! Description: Detect programming language from file extension
!
! Arguments:
! filename - File path
! language - Output: Detected language name
! status - Output: 0 on success, 1 if unknown
!--------------------------------------------------------------------------
subroutine detect_language(filename, language, status)
character(len=*), intent(in) :: filename
character(len=*), intent(out) :: language
integer, intent(out) :: status
integer :: dot_pos
character(len=16) :: ext
status = 0
language = 'unknown'
dot_pos = index(trim(filename), '.', back=.true.)
if (dot_pos == 0) then
status = 1
return
end if
ext = filename(dot_pos:)
! Extension mapping
select case (trim(ext))
case ('.py')
language = 'python'
case ('.js')
language = 'javascript'
case ('.ts')
language = 'typescript'
case ('.rb')
language = 'ruby'
case ('.go')
language = 'go'
case ('.rs')
language = 'rust'
case ('.c')
language = 'c'
case ('.cpp', '.cc', '.cxx')
language = 'cpp'
case ('.java')
language = 'java'
case ('.kt')
language = 'kotlin'
case ('.cs')
language = 'csharp'
case ('.fs')
language = 'fsharp'
case ('.sh')
language = 'bash'
case ('.pl')
language = 'perl'
case ('.lua')
language = 'lua'
case ('.php')
language = 'php'
case ('.hs')
language = 'haskell'
case ('.ml')
language = 'ocaml'
case ('.clj')
language = 'clojure'
case ('.scm')
language = 'scheme'
case ('.lisp')
language = 'commonlisp'
case ('.erl')
language = 'erlang'
case ('.ex', '.exs')
language = 'elixir'
case ('.jl')
language = 'julia'
case ('.r', '.R')
language = 'r'
case ('.cr')
language = 'crystal'
case ('.f90', '.f95')
language = 'fortran'
case ('.cob')
language = 'cobol'
case ('.pro')
language = 'prolog'
case ('.forth', '.4th')
language = 'forth'
case ('.tcl')
language = 'tcl'
case ('.raku')
language = 'raku'
case ('.d')
language = 'd'
case ('.nim')
language = 'nim'
case ('.zig')
language = 'zig'
case ('.v')
language = 'v'
case ('.groovy')
language = 'groovy'
case ('.scala')
language = 'scala'
case ('.dart')
language = 'dart'
case ('.awk')
language = 'awk'
case ('.m')
language = 'objc'
case default
status = 1
end select
end subroutine detect_language
!--------------------------------------------------------------------------
! Client methods
!--------------------------------------------------------------------------
!--------------------------------------------------------------------------
! Subroutine: client_init
! Description: Initialize client with credentials
!
! Loads credentials from environment variables or config file.
!
! Arguments:
! self - Client instance
! status - Output: 0 on success, non-zero on error
!--------------------------------------------------------------------------
subroutine client_init(self, status)
class(unsandbox_client), intent(inout) :: self
integer, intent(out) :: status
call get_credentials(self%public_key, self%secret_key, status)
if (status == 0) then
self%initialized = .true.
end if
end subroutine client_init
!--------------------------------------------------------------------------
! Subroutine: client_execute
! Description: Execute code synchronously and return results
!
! Arguments:
! self - Client instance
! language - Programming language (python, javascript, etc.)
! code - Source code to execute
! result - Output: Execution result
! status - Output: 0 on success, non-zero on error
! network - Optional: Network mode (zerotrust/semitrusted)
! ttl - Optional: Timeout in seconds
! vcpu - Optional: vCPU count (1-8)
!--------------------------------------------------------------------------
subroutine client_execute(self, language, code, result, status, network, ttl, vcpu)
class(unsandbox_client), intent(in) :: self
character(len=*), intent(in) :: language, code
type(execution_result), intent(out) :: result
integer, intent(out) :: status
character(len=*), intent(in), optional :: network
integer, intent(in), optional :: ttl, vcpu
character(len=16384) :: cmd
character(len=32) :: net_mode
integer :: exec_ttl, exec_vcpu
status = 0
net_mode = 'zerotrust'
exec_ttl = DEFAULT_TTL
exec_vcpu = 1
if (present(network)) net_mode = network
if (present(ttl)) exec_ttl = ttl
if (present(vcpu)) exec_vcpu = vcpu
if (.not. self%initialized) then
status = 1
result%stderr = 'Client not initialized'
return
end if
! Build and execute shell command with HMAC auth
write(cmd, '(30A)') &
'TMPFILE=$(mktemp); ', &
'cat > "$TMPFILE" << ''CODEEOF''', char(10), trim(code), char(10), 'CODEEOF', char(10), &
'BODY=$(jq -Rs ''{language: "', trim(language), '", code: ., ', &
'network_mode: "', trim(net_mode), '", ttl: ', char(48+mod(exec_ttl/10,10)), char(48+mod(exec_ttl,10)), &
'}'' < "$TMPFILE"); ', &
'TS=$(date +%s); ', &
'SIG=$(echo -n "$TS:POST:/execute:$BODY" | openssl dgst -sha256 -hmac "', &
trim(self%secret_key), '" | cut -d" " -f2); ', &
'RESP=$(curl -s -X POST ', API_BASE, '/execute ', &
'-H "Content-Type: application/json" ', &
'-H "Authorization: Bearer ', trim(self%public_key), '" ', &
'-H "X-Timestamp: $TS" ', &
'-H "X-Signature: $SIG" ', &
'--data-binary "$BODY"); ', &
'rm -f "$TMPFILE"; ', &
'echo "$RESP" | jq -r ".stdout // empty"; ', &
'echo "$RESP" | jq -r ".stderr // empty" >&2; ', &
'echo "$RESP" | jq -r ".exit_code // 0"'
call execute_command_line(trim(cmd), wait=.true., exitstat=status)
result%success = (status == 0)
result%language = language
end subroutine client_execute
!--------------------------------------------------------------------------
! Subroutine: client_execute_async
! Description: Execute code asynchronously, returns job_id for polling
!
! Arguments:
! self - Client instance
! language - Programming language
! code - Source code to execute
! job_id - Output: Job ID for polling
! status - Output: 0 on success, non-zero on error
!--------------------------------------------------------------------------
subroutine client_execute_async(self, language, code, job_id, status)
class(unsandbox_client), intent(in) :: self
character(len=*), intent(in) :: language, code
character(len=*), intent(out) :: job_id
integer, intent(out) :: status
character(len=8192) :: cmd
status = 0
job_id = ''
if (.not. self%initialized) then
status = 1
return
end if
write(cmd, '(20A)') &
'TMPFILE=$(mktemp); ', &
'cat > "$TMPFILE" << ''CODEEOF''', char(10), trim(code), char(10), 'CODEEOF', char(10), &
'BODY=$(jq -Rs ''{language: "', trim(language), '", code: .}'' < "$TMPFILE"); ', &
'TS=$(date +%s); ', &
'SIG=$(echo -n "$TS:POST:/execute/async:$BODY" | openssl dgst -sha256 -hmac "', &
trim(self%secret_key), '" | cut -d" " -f2); ', &
'curl -s -X POST ', API_BASE, '/execute/async ', &
'-H "Content-Type: application/json" ', &
'-H "Authorization: Bearer ', trim(self%public_key), '" ', &
'-H "X-Timestamp: $TS" ', &
'-H "X-Signature: $SIG" ', &
'--data-binary "$BODY" | jq -r ".job_id // empty"; ', &
'rm -f "$TMPFILE"'
call execute_command_line(trim(cmd), wait=.true., exitstat=status)
end subroutine client_execute_async
!--------------------------------------------------------------------------
! Subroutine: client_get_job
! Description: Get status and results of an async job
!
! Arguments:
! self - Client instance
! job_id - Job ID from execute_async
! info - Output: Job information
! status - Output: 0 on success, non-zero on error
!--------------------------------------------------------------------------
subroutine client_get_job(self, job_id, info, status)
class(unsandbox_client), intent(in) :: self
character(len=*), intent(in) :: job_id
type(job_info), intent(out) :: info
integer, intent(out) :: status
character(len=4096) :: cmd
status = 0
info%job_id = job_id
write(cmd, '(15A)') &
'TS=$(date +%s); ', &
'SIG=$(echo -n "$TS:GET:/jobs/', trim(job_id), ':" | openssl dgst -sha256 -hmac "', &
trim(self%secret_key), '" | cut -d" " -f2); ', &
'curl -s -X GET ', API_BASE, '/jobs/', trim(job_id), ' ', &
'-H "Authorization: Bearer ', trim(self%public_key), '" ', &
'-H "X-Timestamp: $TS" ', &
'-H "X-Signature: $SIG" | jq .'
call execute_command_line(trim(cmd), wait=.true., exitstat=status)
end subroutine client_get_job
!--------------------------------------------------------------------------
! Subroutine: client_wait
! Description: Wait for async job completion with polling
!
! Arguments:
! self - Client instance
! job_id - Job ID from execute_async
! result - Output: Execution result
! status - Output: 0 on success, non-zero on error
! max_polls - Optional: Maximum poll attempts (default 100)
!--------------------------------------------------------------------------
subroutine client_wait(self, job_id, result, status, max_polls)
class(unsandbox_client), intent(in) :: self
character(len=*), intent(in) :: job_id
type(execution_result), intent(out) :: result
integer, intent(out) :: status
integer, intent(in), optional :: max_polls
character(len=8192) :: cmd
integer :: polls
polls = 100
if (present(max_polls)) polls = max_polls
status = 0
! Use shell loop for polling with exponential backoff
write(cmd, '(30A,I0,A)') &
'DELAYS=(300 450 700 900 650 1600 2000); ', &
'for i in $(seq 1 ', polls, '); do ', &
'sleep $(echo "scale=3; ${DELAYS[$(( (i-1) % 7 ))]}/1000" | bc); ', &
'TS=$(date +%s); ', &
'SIG=$(echo -n "$TS:GET:/jobs/', trim(job_id), ':" | openssl dgst -sha256 -hmac "', &
trim(self%secret_key), '" | cut -d" " -f2); ', &
'RESP=$(curl -s -X GET ', API_BASE, '/jobs/', trim(job_id), ' ', &
'-H "Authorization: Bearer ', trim(self%public_key), '" ', &
'-H "X-Timestamp: $TS" ', &
'-H "X-Signature: $SIG"); ', &
'STATUS=$(echo "$RESP" | jq -r ".status // empty"); ', &
'case "$STATUS" in ', &
'completed|failed|timeout|cancelled) ', &
'echo "$RESP" | jq -r ".stdout // empty"; ', &
'echo "$RESP" | jq -r ".stderr // empty" >&2; ', &
'exit 0;; ', &
'esac; ', &
'done; ', &
'echo "Timeout waiting for job" >&2; exit 1'
call execute_command_line(trim(cmd), wait=.true., exitstat=status)
result%success = (status == 0)
result%job_id = job_id
end subroutine client_wait
!--------------------------------------------------------------------------
! Subroutine: client_cancel_job
! Description: Cancel a running job
!
! Arguments:
! self - Client instance
! job_id - Job ID to cancel
! status - Output: 0 on success, non-zero on error
!--------------------------------------------------------------------------
subroutine client_cancel_job(self, job_id, status)
class(unsandbox_client), intent(in) :: self
character(len=*), intent(in) :: job_id
integer, intent(out) :: status
character(len=4096) :: cmd
write(cmd, '(15A)') &
'TS=$(date +%s); ', &
'SIG=$(echo -n "$TS:DELETE:/jobs/', trim(job_id), ':" | openssl dgst -sha256 -hmac "', &
trim(self%secret_key), '" | cut -d" " -f2); ', &
'curl -s -X DELETE ', API_BASE, '/jobs/', trim(job_id), ' ', &
'-H "Authorization: Bearer ', trim(self%public_key), '" ', &
'-H "X-Timestamp: $TS" ', &
'-H "X-Signature: $SIG" | jq .'
call execute_command_line(trim(cmd), wait=.true., exitstat=status)
end subroutine client_cancel_job
!--------------------------------------------------------------------------
! Subroutine: client_list_jobs
! Description: List all active jobs for this API key
!
! Arguments:
! self - Client instance
! status - Output: 0 on success, non-zero on error
!--------------------------------------------------------------------------
subroutine client_list_jobs(self, status)
class(unsandbox_client), intent(in) :: self
integer, intent(out) :: status
character(len=4096) :: cmd
write(cmd, '(15A)') &
'TS=$(date +%s); ', &
'SIG=$(echo -n "$TS:GET:/jobs:" | openssl dgst -sha256 -hmac "', &
trim(self%secret_key), '" | cut -d" " -f2); ', &
'curl -s -X GET ', API_BASE, '/jobs ', &
'-H "Authorization: Bearer ', trim(self%public_key), '" ', &
'-H "X-Timestamp: $TS" ', &
'-H "X-Signature: $SIG" | jq .'
call execute_command_line(trim(cmd), wait=.true., exitstat=status)
end subroutine client_list_jobs
!--------------------------------------------------------------------------
! Subroutine: client_run
! Description: Execute code with automatic language detection from shebang
!
! Arguments:
! self - Client instance
! code - Source code with shebang (e.g., #!/usr/bin/env python3)
! result - Output: Execution result
! status - Output: 0 on success, non-zero on error
!--------------------------------------------------------------------------
subroutine client_run(self, code, result, status)
class(unsandbox_client), intent(in) :: self
character(len=*), intent(in) :: code
type(execution_result), intent(out) :: result
integer, intent(out) :: status
character(len=8192) :: cmd
status = 0
write(cmd, '(20A)') &
'TMPFILE=$(mktemp); ', &
'cat > "$TMPFILE" << ''CODEEOF''', char(10), trim(code), char(10), 'CODEEOF', char(10), &
'BODY=$(cat "$TMPFILE"); ', &
'TS=$(date +%s); ', &
'SIG=$(echo -n "$TS:POST:/run:$BODY" | openssl dgst -sha256 -hmac "', &
trim(self%secret_key), '" | cut -d" " -f2); ', &
'curl -s -X POST ', API_BASE, '/run ', &
'-H "Content-Type: text/plain" ', &
'-H "Authorization: Bearer ', trim(self%public_key), '" ', &
'-H "X-Timestamp: $TS" ', &
'-H "X-Signature: $SIG" ', &
'--data-binary "@$TMPFILE" | jq .; ', &
'rm -f "$TMPFILE"'
call execute_command_line(trim(cmd), wait=.true., exitstat=status)
result%success = (status == 0)
end subroutine client_run
!--------------------------------------------------------------------------
! Subroutine: client_run_async
! Description: Execute with auto-detection asynchronously
!
! Arguments:
! self - Client instance
! code - Source code with shebang
! job_id - Output: Job ID for polling
! status - Output: 0 on success, non-zero on error
!--------------------------------------------------------------------------
subroutine client_run_async(self, code, job_id, status)
class(unsandbox_client), intent(in) :: self
character(len=*), intent(in) :: code
character(len=*), intent(out) :: job_id
integer, intent(out) :: status
character(len=8192) :: cmd
status = 0
job_id = ''
write(cmd, '(20A)') &
'TMPFILE=$(mktemp); ', &
'cat > "$TMPFILE" << ''CODEEOF''', char(10), trim(code), char(10), 'CODEEOF', char(10), &
'BODY=$(cat "$TMPFILE"); ', &
'TS=$(date +%s); ', &
'SIG=$(echo -n "$TS:POST:/run/async:$BODY" | openssl dgst -sha256 -hmac "', &
trim(self%secret_key), '" | cut -d" " -f2); ', &
'curl -s -X POST ', API_BASE, '/run/async ', &
'-H "Content-Type: text/plain" ', &
'-H "Authorization: Bearer ', trim(self%public_key), '" ', &
'-H "X-Timestamp: $TS" ', &
'-H "X-Signature: $SIG" ', &
'--data-binary "@$TMPFILE" | jq -r ".job_id // empty"; ', &
'rm -f "$TMPFILE"'
call execute_command_line(trim(cmd), wait=.true., exitstat=status)
end subroutine client_run_async
!--------------------------------------------------------------------------
! Subroutine: client_image
! Description: Generate image from text prompt
!
! Arguments:
! self - Client instance
! prompt - Text description of image to generate
! status - Output: 0 on success, non-zero on error
!--------------------------------------------------------------------------
subroutine client_image(self, prompt, status)
class(unsandbox_client), intent(in) :: self
character(len=*), intent(in) :: prompt
integer, intent(out) :: status
character(len=8192) :: cmd
write(cmd, '(15A)') &
'BODY=''{"prompt":"', trim(prompt), '","size":"1024x1024"}''; ', &
'TS=$(date +%s); ', &
'SIG=$(echo -n "$TS:POST:/image:$BODY" | openssl dgst -sha256 -hmac "', &
trim(self%secret_key), '" | cut -d" " -f2); ', &
'curl -s -X POST ', API_BASE, '/image ', &
'-H "Content-Type: application/json" ', &
'-H "Authorization: Bearer ', trim(self%public_key), '" ', &
'-H "X-Timestamp: $TS" ', &
'-H "X-Signature: $SIG" ', &
'-d "$BODY" | jq .'
call execute_command_line(trim(cmd), wait=.true., exitstat=status)
end subroutine client_image
!--------------------------------------------------------------------------
! Subroutine: client_languages
! Description: Get list of supported programming languages
!
! Arguments:
! self - Client instance
! status - Output: 0 on success, non-zero on error
!--------------------------------------------------------------------------
subroutine client_languages(self, status)
class(unsandbox_client), intent(in) :: self
integer, intent(out) :: status
character(len=4096) :: cmd
write(cmd, '(15A)') &
'TS=$(date +%s); ', &
'SIG=$(echo -n "$TS:GET:/languages:" | openssl dgst -sha256 -hmac "', &
trim(self%secret_key), '" | cut -d" " -f2); ', &
'curl -s -X GET ', API_BASE, '/languages ', &
'-H "Authorization: Bearer ', trim(self%public_key), '" ', &
'-H "X-Timestamp: $TS" ', &
'-H "X-Signature: $SIG" | jq .'
call execute_command_line(trim(cmd), wait=.true., exitstat=status)
end subroutine client_languages
end module unsandbox_sdk
!==============================================================================
! Main Program: unsandbox_cli
! Description: CLI interface for unsandbox API
!
! This is the command-line interface that uses the unsandbox_sdk module.
! Run without arguments for usage information.
!==============================================================================
program unsandbox_cli
use unsandbox_sdk
implicit none
character(len=2048) :: cmd_line, curl_cmd
character(len=1024) :: filename, language, api_key, ext, arg, subcommand
character(len=256) :: session_id, service_id
integer :: stat, i, nargs, dot_pos
logical :: list_flag, is_session, is_service, is_key
! Initialize
subcommand = ''
list_flag = .false.
is_session = .false.
is_service = .false.
is_key = .false.
session_id = ''
service_id = ''
! Get command line arguments count
nargs = command_argument_count()
if (nargs < 1) then
call print_help()
stop 1
end if
! Check for subcommands
call get_command_argument(1, arg, status=stat)
if (trim(arg) == '-h' .or. trim(arg) == '--help') then
call print_help()
stop 0
else if (trim(arg) == 'session') then
is_session = .true.
call handle_session()
stop 0
else if (trim(arg) == 'service') then
is_service = .true.
call handle_service()
stop 0
else if (trim(arg) == 'key') then
is_key = .true.
call handle_key()
stop 0
else if (trim(arg) == 'languages') then
call handle_languages()
stop 0
else if (trim(arg) == 'image') then
call handle_image()
stop 0
else if (trim(arg) == 'snapshot') then
call handle_snapshot()
stop 0
else
! Default execute command
filename = trim(arg)
call handle_execute(filename)
stop 0
end if
contains
subroutine print_help()
write(*, '(A)') 'unsandbox SDK for Fortran - Execute code in secure sandboxes'
write(*, '(A)') 'https://unsandbox.com | https://api.unsandbox.com/openapi'
write(*, '(A)') ''
write(*, '(A)') 'Usage: ./un [options] <source_file>'
write(*, '(A)') ' ./un session [options]'
write(*, '(A)') ' ./un service [options]'
write(*, '(A)') ' ./un snapshot [options]'
write(*, '(A)') ' ./un image [options]'
write(*, '(A)') ' ./un key [--extend]'
write(*, '(A)') ' ./un languages [--json]'
write(*, '(A)') ''
write(*, '(A)') 'Execute options:'
write(*, '(A)') ' -e KEY=VALUE Set environment variable'
write(*, '(A)') ' -f FILE Add input file'
write(*, '(A)') ' -n MODE Network mode (zerotrust/semitrusted)'
write(*, '(A)') ' -v N vCPU count (1-8)'
write(*, '(A)') ''
write(*, '(A)') 'Session options:'
write(*, '(A)') ' -l, --list List active sessions'
write(*, '(A)') ' --kill ID Terminate session'
write(*, '(A)') ''
write(*, '(A)') 'Service options:'
write(*, '(A)') ' -l, --list List services'
write(*, '(A)') ' --name NAME Service name (creates service)'
write(*, '(A)') ' --info ID Get service details'
write(*, '(A)') ' --logs ID Get service logs'
write(*, '(A)') ' --freeze ID Freeze service'
write(*, '(A)') ' --unfreeze ID Unfreeze service'
write(*, '(A)') ' --destroy ID Destroy service'
write(*, '(A)') ' --resize ID Resize service (with -v N)'
write(*, '(A)') ''
write(*, '(A)') 'Vault commands:'
write(*, '(A)') ' service env status <id> Check vault status'
write(*, '(A)') ' service env set <id> Set vault (-e KEY=VAL)'
write(*, '(A)') ' service env export <id> Export vault'
write(*, '(A)') ' service env delete <id> Delete vault'
write(*, '(A)') ''
write(*, '(A)') 'Snapshot options:'
write(*, '(A)') ' -l, --list List all snapshots'
write(*, '(A)') ' --info ID Get snapshot details'
write(*, '(A)') ' --delete ID Delete a snapshot'
write(*, '(A)') ' --lock ID Lock snapshot'
write(*, '(A)') ' --unlock ID Unlock snapshot'
write(*, '(A)') ' --restore ID Restore from snapshot'
write(*, '(A)') ' --clone ID Clone snapshot (requires --type)'
write(*, '(A)') ' --type TYPE Clone type: session or service'
write(*, '(A)') ' --name NAME Name for cloned resource'
write(*, '(A)') ' --shell SHELL Shell for cloned session'
write(*, '(A)') ' --ports PORTS Ports for cloned service'
write(*, '(A)') ''
write(*, '(A)') 'Image options:'
write(*, '(A)') ' -l, --list List all images'
write(*, '(A)') ' --info ID Get image details'
write(*, '(A)') ' --delete ID Delete an image'
write(*, '(A)') ' --lock ID Lock image'
write(*, '(A)') ' --unlock ID Unlock image'
write(*, '(A)') ' --publish ID Publish (requires --source-type)'
write(*, '(A)') ' --source-type TYPE Source: service or snapshot'
write(*, '(A)') ' --visibility ID MODE Set: private/unlisted/public'
write(*, '(A)') ' --spawn ID Spawn service from image'
write(*, '(A)') ' --clone ID Clone an image'
write(*, '(A)') ' --name NAME Name for spawn/clone'
write(*, '(A)') ' --ports PORTS Ports for spawn'
write(*, '(A)') ''
write(*, '(A)') 'Key options:'
write(*, '(A)') ' --extend Open browser to extend key'
write(*, '(A)') ''
write(*, '(A)') 'Languages options:'
write(*, '(A)') ' --json Output as JSON array'
write(*, '(A)') ''
write(*, '(A)') 'Library Usage:'
write(*, '(A)') ' use unsandbox_sdk'
write(*, '(A)') ' type(unsandbox_client) :: client'
write(*, '(A)') ' call client%init(status)'
write(*, '(A)') ' call client%execute("python", code, result, status)'
end subroutine print_help
subroutine handle_execute(fname)
character(len=*), intent(in) :: fname
character(len=4096) :: full_cmd
character(len=1024) :: env_opts, file_opts, net_opt, public_key, secret_key
integer :: i, arg_idx
logical :: artifacts, has_env, has_files
! Check if file exists
inquire(file=trim(fname), exist=stat)
if (.not. stat) then
write(0, '(A,A)') 'Error: File not found: ', trim(fname)
stop 1
end if
! Detect language from extension
call detect_language(fname, language, stat)
if (stat /= 0) then
write(0, '(A,A)') 'Error: Unknown language for file: ', trim(fname)
stop 1
end if
! Get API keys
call get_credentials(public_key, secret_key, stat)
if (stat /= 0) then
write(0, '(A)') 'Error: No credentials found. Set UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY'
stop 1
end if
! Build curl command with HMAC auth
write(full_cmd, '(30A)') &
'TS=$(date +%s); ', &
'BODY=$(jq -Rs ''{language: "', trim(language), '", code: .}'' < "', trim(fname), '"); ', &
'SIG=$(echo -n "$TS:POST:/execute:$BODY" | openssl dgst -sha256 -hmac "', trim(secret_key), '" | cut -d" " -f2); ', &
'curl -s -X POST https://api.unsandbox.com/execute ', &
'-H "Content-Type: application/json" ', &
'-H "Authorization: Bearer ', trim(public_key), '" ', &
'-H "X-Timestamp: $TS" ', &
'-H "X-Signature: $SIG" ', &
'--data-binary "$BODY" -o /tmp/unsandbox_resp.json; ', &
'RESP=$(cat /tmp/unsandbox_resp.json); ', &
'if echo "$RESP" | grep -q "timestamp" && ', &
'(echo "$RESP" | grep -Eq "(401|expired|invalid)"); then ', &
'echo -e "\x1b[31mError: Request timestamp expired (must be within 5 minutes of server time)\x1b[0m" >&2; ', &
'echo -e "\x1b[33mYour computer'\''s clock may have drifted.\x1b[0m" >&2; ', &
'echo "Check your system time and sync with NTP if needed:" >&2; ', &
'echo " Linux: sudo ntpdate -s time.nist.gov" >&2; ', &
'echo " macOS: sudo sntp -sS time.apple.com" >&2; ', &
'echo -e " Windows: w32tm /resync\x1b[0m" >&2; ', &
'rm -f /tmp/unsandbox_resp.json; exit 1; fi; ', &
'jq -r ".stdout // empty" /tmp/unsandbox_resp.json | ', &
'sed "s/^/\x1b[34m/" | sed "s/$/\x1b[0m/"; ', &
'jq -r ".stderr // empty" /tmp/unsandbox_resp.json | ', &
'sed "s/^/\x1b[31m/" | sed "s/$/\x1b[0m/" >&2; ', &
'rm -f /tmp/unsandbox_resp.json'
call execute_command_line(trim(full_cmd), wait=.true., exitstat=stat)
if (stat /= 0) then
write(0, '(A)') 'Error: Request failed'
stop 1
end if
end subroutine handle_execute
subroutine handle_session()
character(len=8192) :: full_cmd
character(len=256) :: arg, session_id
character(len=1024) :: public_key, secret_key, input_files
integer :: i, stat
logical :: list_mode, kill_mode
list_mode = .false.
kill_mode = .false.
session_id = ''
input_files = ''
! Parse session arguments
do i = 2, command_argument_count()
call get_command_argument(i, arg)
if (trim(arg) == '-l' .or. trim(arg) == '--list') then
list_mode = .true.
else if (trim(arg) == '--kill') then
kill_mode = .true.
if (i+1 <= command_argument_count()) then
call get_command_argument(i+1, session_id)
end if
else if (trim(arg) == '-f') then
if (i+1 <= command_argument_count()) then
call get_command_argument(i+1, arg)
if (len_trim(input_files) > 0) then
input_files = trim(input_files) // ',' // trim(arg)
else
input_files = trim(arg)
end if
end if
else
if (len_trim(arg) > 0) then
if (arg(1:1) == '-') then
write(0, '(A,A)') 'Unknown option: ', trim(arg)
write(0, '(A)') 'Usage: ./un session [options]'
stop 1
end if
end if
end if
end do
! Get API keys
call get_credentials(public_key, secret_key, stat)
if (stat /= 0) then
write(0, '(A)') 'Error: No credentials found'
stop 1
end if
if (list_mode) then
write(full_cmd, '(20A)') &
'TS=$(date +%s); ', &
'SIG=$(echo -n "$TS:GET:/sessions:" | openssl dgst -sha256 -hmac "', trim(secret_key), '" | cut -d" " -f2); ', &
'curl -s -X GET https://api.unsandbox.com/sessions ', &
'-H "Authorization: Bearer ', trim(public_key), '" ', &
'-H "X-Timestamp: $TS" ', &
'-H "X-Signature: $SIG" | ', &
'jq -r ''.sessions[] | "\(.id) \(.shell) \(.status) \(.created_at)"'' ', &
'2>/dev/null || echo "No active sessions"'
call execute_command_line(trim(full_cmd), wait=.true.)
else if (kill_mode .and. len_trim(session_id) > 0) then
write(full_cmd, '(20A)') &
'TS=$(date +%s); ', &
'SIG=$(echo -n "$TS:DELETE:/sessions/', trim(session_id), ':" | openssl dgst -sha256 -hmac "', trim(secret_key), '" | cut -d" " -f2); ', &
'curl -s -X DELETE https://api.unsandbox.com/sessions/', &
trim(session_id), ' ', &
'-H "Authorization: Bearer ', trim(public_key), '" ', &
'-H "X-Timestamp: $TS" ', &
'-H "X-Signature: $SIG" >/dev/null && ', &
'echo -e "\x1b[32mSession terminated: ', trim(session_id), '\x1b[0m"'
call execute_command_line(trim(full_cmd), wait=.true.)
else
if (len_trim(input_files) > 0) then
write(full_cmd, '(30A)') &
'INPUT_FILES=""; ', &
'IFS='','' read -ra FILES <<< "', trim(input_files), '"; ', &
'for f in "${FILES[@]}"; do ', &
'b64=$(base64 -w0 "$f" 2>/dev/null || base64 "$f"); ', &
'name=$(basename "$f"); ', &
'if [ -n "$INPUT_FILES" ]; then INPUT_FILES="$INPUT_FILES,"; fi; ', &
'INPUT_FILES="$INPUT_FILES{\"filename\":\"$name\",\"content\":\"$b64\"}"; ', &
'done; ', &
'BODY=''{"shell":"bash","input_files":[''"$INPUT_FILES"'']}''; ', &
'TS=$(date +%s); ', &
'SIG=$(echo -n "$TS:POST:/sessions:$BODY" | openssl dgst -sha256 -hmac "', trim(secret_key), '" | cut -d" " -f2); ', &
'curl -s -X POST https://api.unsandbox.com/sessions ', &
'-H "Content-Type: application/json" ', &
'-H "Authorization: Bearer ', trim(public_key), '" ', &
'-H "X-Timestamp: $TS" ', &
'-H "X-Signature: $SIG" ', &
'-d "$BODY" && ', &
'echo -e "\x1b[33mSession created (WebSocket required)\x1b[0m"'
else
write(full_cmd, '(20A)') &
'BODY=''{"shell":"bash"}''; ', &
'TS=$(date +%s); ', &
'SIG=$(echo -n "$TS:POST:/sessions:$BODY" | openssl dgst -sha256 -hmac "', trim(secret_key), '" | cut -d" " -f2); ', &
'curl -s -X POST https://api.unsandbox.com/sessions ', &
'-H "Content-Type: application/json" ', &
'-H "Authorization: Bearer ', trim(public_key), '" ', &
'-H "X-Timestamp: $TS" ', &
'-H "X-Signature: $SIG" ', &
'-d "$BODY" && ', &
'echo -e "\x1b[33mSession created (WebSocket required)\x1b[0m"'
end if
call execute_command_line(trim(full_cmd), wait=.true.)
end if
end subroutine handle_session
subroutine handle_service()
character(len=8192) :: full_cmd
character(len=256) :: arg, service_id, operation, service_type, service_name
character(len=1024) :: input_files, public_key, secret_key
character(len=2048) :: svc_envs, svc_env_file, env_action, env_target
integer :: i, stat, resize_vcpu
logical :: list_mode
list_mode = .false.
operation = ''
service_id = ''
service_type = ''
service_name = ''
input_files = ''
svc_envs = ''
svc_env_file = ''
env_action = ''
env_target = ''
resize_vcpu = 0
! Parse service arguments
i = 2
do while (i <= command_argument_count())
call get_command_argument(i, arg)
if (trim(arg) == '-l' .or. trim(arg) == '--list') then
list_mode = .true.
else if (trim(arg) == 'env') then
if (i+2 <= command_argument_count()) then
call get_command_argument(i+1, env_action)
call get_command_argument(i+2, env_target)
i = i + 2
end if
else if (trim(arg) == '--name') then
operation = 'create'
if (i+1 <= command_argument_count()) then
call get_command_argument(i+1, service_name)
i = i + 1
end if
else if (trim(arg) == '--type') then
if (i+1 <= command_argument_count()) then
call get_command_argument(i+1, service_type)
i = i + 1
end if
else if (trim(arg) == '-e') then
if (i+1 <= command_argument_count()) then
call get_command_argument(i+1, arg)
if (len_trim(svc_envs) > 0) then
svc_envs = trim(svc_envs) // char(10) // trim(arg)
else
svc_envs = trim(arg)
end if
i = i + 1
end if
else if (trim(arg) == '--env-file') then
if (i+1 <= command_argument_count()) then
call get_command_argument(i+1, svc_env_file)
i = i + 1
end if
else if (trim(arg) == '-f') then
if (i+1 <= command_argument_count()) then
call get_command_argument(i+1, arg)
if (len_trim(input_files) > 0) then
input_files = trim(input_files) // ',' // trim(arg)
else
input_files = trim(arg)
end if
i = i + 1
end if
else if (trim(arg) == '--info') then
operation = 'info'
if (i+1 <= command_argument_count()) then
call get_command_argument(i+1, service_id)
i = i + 1
end if
else if (trim(arg) == '--logs') then
operation = 'logs'
if (i+1 <= command_argument_count()) then
call get_command_argument(i+1, service_id)
i = i + 1
end if
else if (trim(arg) == '--freeze') then
operation = 'sleep'
if (i+1 <= command_argument_count()) then
call get_command_argument(i+1, service_id)
i = i + 1
end if
else if (trim(arg) == '--unfreeze') then
operation = 'wake'
if (i+1 <= command_argument_count()) then
call get_command_argument(i+1, service_id)
i = i + 1
end if
else if (trim(arg) == '--destroy') then
operation = 'destroy'
if (i+1 <= command_argument_count()) then
call get_command_argument(i+1, service_id)
i = i + 1
end if
else if (trim(arg) == '--resize') then
operation = 'resize'
if (i+1 <= command_argument_count()) then
call get_command_argument(i+1, service_id)
i = i + 1
end if
else if (trim(arg) == '--vcpu' .or. trim(arg) == '-v') then
if (i+1 <= command_argument_count()) then
call get_command_argument(i+1, arg)
read(arg, *) resize_vcpu
i = i + 1
end if
else if (trim(arg) == '--dump-bootstrap') then
operation = 'dump-bootstrap'
if (i+1 <= command_argument_count()) then
call get_command_argument(i+1, service_id)
i = i + 1
end if
else if (trim(arg) == '--dump-file') then
operation = 'dump-file'
if (i+1 <= command_argument_count()) then
call get_command_argument(i+1, service_type)
i = i + 1
end if
else if (trim(arg) == '--set-unfreeze-on-demand') then
operation = 'set-unfreeze-on-demand'
if (i+1 <= command_argument_count()) then
call get_command_argument(i+1, service_id)
i = i + 1
end if
if (i+1 <= command_argument_count()) then
call get_command_argument(i+1, service_type)
i = i + 1
end if
end if
i = i + 1
end do
! Get API keys
call get_credentials(public_key, secret_key, stat)
if (stat /= 0) then
write(0, '(A)') 'Error: No credentials found'
stop 1
end if
! Handle env subcommand
if (len_trim(env_action) > 0) then
if (trim(env_action) == 'status') then
write(full_cmd, '(20A)') &
'TS=$(date +%s); ', &
'SIG=$(echo -n "$TS:GET:/services/', trim(env_target), '/env:" | ', &
'openssl dgst -sha256 -hmac "', trim(secret_key), '" | cut -d" " -f2); ', &
'curl -s -X GET "https://api.unsandbox.com/services/', trim(env_target), '/env" ', &
'-H "Authorization: Bearer ', trim(public_key), '" ', &
'-H "X-Timestamp: $TS" ', &
'-H "X-Signature: $SIG" | jq .'
call execute_command_line(trim(full_cmd), wait=.true.)
return
else if (trim(env_action) == 'set') then
write(full_cmd, '(50A)') &
'ENV_CONTENT=""; ', &
'ENV_LINES="', trim(svc_envs), '"; ', &
'if [ -n "$ENV_LINES" ]; then ', &
'ENV_CONTENT="$ENV_LINES"; ', &
'fi; ', &
'ENV_FILE="', trim(svc_env_file), '"; ', &
'if [ -n "$ENV_FILE" ] && [ -f "$ENV_FILE" ]; then ', &
'while IFS= read -r line || [ -n "$line" ]; do ', &
'case "$line" in "#"*|"") continue ;; esac; ', &
'if [ -n "$ENV_CONTENT" ]; then ENV_CONTENT="$ENV_CONTENT', char(10), '"; fi; ', &
'ENV_CONTENT="$ENV_CONTENT$line"; ', &
'done < "$ENV_FILE"; fi; ', &
'if [ -z "$ENV_CONTENT" ]; then ', &
'echo -e "\x1b[31mError: No environment variables to set\x1b[0m" >&2; exit 1; fi; ', &
'TS=$(date +%s); ', &
'SIG=$(echo -n "$TS:PUT:/services/', trim(env_target), '/env:$ENV_CONTENT" | ', &
'openssl dgst -sha256 -hmac "', trim(secret_key), '" | cut -d" " -f2); ', &
'curl -s -X PUT "https://api.unsandbox.com/services/', trim(env_target), '/env" ', &
'-H "Authorization: Bearer ', trim(public_key), '" ', &
'-H "X-Timestamp: $TS" ', &
'-H "X-Signature: $SIG" ', &
'-H "Content-Type: text/plain" ', &
'--data-binary "$ENV_CONTENT" | jq .'
call execute_command_line(trim(full_cmd), wait=.true.)
return
else if (trim(env_action) == 'export') then
write(full_cmd, '(20A)') &
'TS=$(date +%s); ', &
'SIG=$(echo -n "$TS:POST:/services/', trim(env_target), '/env/export:" | ', &
'openssl dgst -sha256 -hmac "', trim(secret_key), '" | cut -d" " -f2); ', &
'curl -s -X POST "https://api.unsandbox.com/services/', trim(env_target), '/env/export" ', &
'-H "Authorization: Bearer ', trim(public_key), '" ', &
'-H "X-Timestamp: $TS" ', &
'-H "X-Signature: $SIG" | jq -r ".content // empty"'
call execute_command_line(trim(full_cmd), wait=.true.)
return
else if (trim(env_action) == 'delete') then
write(full_cmd, '(20A)') &
'TS=$(date +%s); ', &
'SIG=$(echo -n "$TS:DELETE:/services/', trim(env_target), '/env:" | ', &
'openssl dgst -sha256 -hmac "', trim(secret_key), '" | cut -d" " -f2); ', &
'curl -s -X DELETE "https://api.unsandbox.com/services/', trim(env_target), '/env" ', &
'-H "Authorization: Bearer ', trim(public_key), '" ', &
'-H "X-Timestamp: $TS" ', &
'-H "X-Signature: $SIG" >/dev/null && ', &
'echo -e "\x1b[32mVault deleted for: ', trim(env_target), '\x1b[0m"'
call execute_command_line(trim(full_cmd), wait=.true.)
return
else
write(0, '(A,A)') 'Error: Unknown env action: ', trim(env_action)
write(0, '(A)') 'Usage: ./un service env <status|set|export|delete> <service_id>'
stop 1
end if
end if
if (list_mode) then
write(full_cmd, '(20A)') &
'TS=$(date +%s); ', &
'SIG=$(echo -n "$TS:GET:/services:" | openssl dgst -sha256 -hmac "', trim(secret_key), '" | cut -d" " -f2); ', &
'curl -s -X GET https://api.unsandbox.com/services ', &
'-H "Authorization: Bearer ', trim(public_key), '" ', &
'-H "X-Timestamp: $TS" ', &
'-H "X-Signature: $SIG" | ', &
'jq -r ''.services[] | "\(.id) \(.name) \(.status)"'' ', &
'2>/dev/null || echo "No services"'
call execute_command_line(trim(full_cmd), wait=.true.)
else if (trim(operation) == 'info' .and. len_trim(service_id) > 0) then
write(full_cmd, '(20A)') &
'TS=$(date +%s); ', &
'SIG=$(echo -n "$TS:GET:/services/', trim(service_id), ':" | openssl dgst -sha256 -hmac "', trim(secret_key), '" | cut -d" " -f2); ', &
'curl -s -X GET https://api.unsandbox.com/services/', &
trim(service_id), ' ', &
'-H "Authorization: Bearer ', trim(public_key), '" ', &
'-H "X-Timestamp: $TS" ', &
'-H "X-Signature: $SIG" | jq .'
call execute_command_line(trim(full_cmd), wait=.true.)
else if (trim(operation) == 'logs' .and. len_trim(service_id) > 0) then
write(full_cmd, '(20A)') &
'TS=$(date +%s); ', &
'SIG=$(echo -n "$TS:GET:/services/', trim(service_id), '/logs:" | openssl dgst -sha256 -hmac "', trim(secret_key), '" | cut -d" " -f2); ', &
'curl -s -X GET https://api.unsandbox.com/services/', &
trim(service_id), '/logs ', &
'-H "Authorization: Bearer ', trim(public_key), '" ', &
'-H "X-Timestamp: $TS" ', &
'-H "X-Signature: $SIG" | jq -r ".logs"'
call execute_command_line(trim(full_cmd), wait=.true.)
else if (trim(operation) == 'sleep' .and. len_trim(service_id) > 0) then
write(full_cmd, '(20A)') &
'TS=$(date +%s); ', &
'SIG=$(echo -n "$TS:POST:/services/', trim(service_id), '/freeze:" | openssl dgst -sha256 -hmac "', trim(secret_key), '" | cut -d" " -f2); ', &
'curl -s -X POST https://api.unsandbox.com/services/', &
trim(service_id), '/freeze ', &
'-H "Authorization: Bearer ', trim(public_key), '" ', &
'-H "X-Timestamp: $TS" ', &
'-H "X-Signature: $SIG" >/dev/null && ', &
'echo -e "\x1b[32mService frozen: ', trim(service_id), '\x1b[0m"'
call execute_command_line(trim(full_cmd), wait=.true.)
else if (trim(operation) == 'wake' .and. len_trim(service_id) > 0) then
write(full_cmd, '(20A)') &
'TS=$(date +%s); ', &
'SIG=$(echo -n "$TS:POST:/services/', trim(service_id), '/unfreeze:" | openssl dgst -sha256 -hmac "', trim(secret_key), '" | cut -d" " -f2); ', &
'curl -s -X POST https://api.unsandbox.com/services/', &
trim(service_id), '/unfreeze ', &
'-H "Authorization: Bearer ', trim(public_key), '" ', &
'-H "X-Timestamp: $TS" ', &
'-H "X-Signature: $SIG" >/dev/null && ', &
'echo -e "\x1b[32mService unfreezing: ', trim(service_id), '\x1b[0m"'
call execute_command_line(trim(full_cmd), wait=.true.)
else if (trim(operation) == 'destroy' .and. len_trim(service_id) > 0) then
write(full_cmd, '(50A)') &
'TS=$(date +%s); ', &
'SIG=$(echo -n "$TS:DELETE:/services/', trim(service_id), ':" | openssl dgst -sha256 -hmac "', trim(secret_key), '" | cut -d" " -f2); ', &
'RESP=$(curl -s -w "\n%{http_code}" -X DELETE https://api.unsandbox.com/services/', &
trim(service_id), ' ', &
'-H "Authorization: Bearer ', trim(public_key), '" ', &
'-H "X-Timestamp: $TS" ', &
'-H "X-Signature: $SIG"); ', &
'HTTP_CODE=$(echo "$RESP" | tail -n1); ', &
'BODY=$(echo "$RESP" | sed ''$d''); ', &
'if [ "$HTTP_CODE" = "428" ]; then ', &
'CHALLENGE_ID=$(echo "$BODY" | jq -r ".challenge_id // empty"); ', &
'echo -e "\x1b[33mConfirmation required. Check your email for a one-time code.\x1b[0m" >&2; ', &
'echo -n "Enter OTP: " >&2; read OTP; ', &
'if [ -z "$OTP" ]; then echo -e "\x1b[31mError: Operation cancelled\x1b[0m" >&2; exit 1; fi; ', &
'TS2=$(date +%s); ', &
'SIG2=$(echo -n "$TS2:DELETE:/services/', trim(service_id), ':" | openssl dgst -sha256 -hmac "', trim(secret_key), '" | cut -d" " -f2); ', &
'curl -s -X DELETE https://api.unsandbox.com/services/', trim(service_id), ' ', &
'-H "Authorization: Bearer ', trim(public_key), '" ', &
'-H "X-Timestamp: $TS2" ', &
'-H "X-Signature: $SIG2" ', &
'-H "X-Sudo-OTP: $OTP" ', &
'-H "X-Sudo-Challenge: $CHALLENGE_ID" >/dev/null && ', &
'echo -e "\x1b[32mService destroyed: ', trim(service_id), '\x1b[0m"; ', &
'elif [ "$HTTP_CODE" = "200" ] || [ "$HTTP_CODE" = "204" ]; then ', &
'echo -e "\x1b[32mService destroyed: ', trim(service_id), '\x1b[0m"; ', &
'else echo -e "\x1b[31mError: HTTP $HTTP_CODE\x1b[0m" >&2; echo "$BODY" >&2; exit 1; fi'
call execute_command_line(trim(full_cmd), wait=.true.)
else if (trim(operation) == 'resize' .and. len_trim(service_id) > 0) then
if (resize_vcpu < 1 .or. resize_vcpu > 8) then
write(0, '(A)') char(27)//'[31mError: --vcpu must be between 1 and 8'//char(27)//'[0m'
stop 1
end if
write(full_cmd, '(30A,I0,A,I0,A,I0,A)') &
'VCPU=', resize_vcpu, '; ', &
'RAM=$((VCPU * 2)); ', &
'BODY=''{"vcpu":''$VCPU''}''; ', &
'TS=$(date +%s); ', &
'SIG=$(echo -n "$TS:PATCH:/services/', trim(service_id), ':$BODY" | openssl dgst -sha256 -hmac "', trim(secret_key), '" | cut -d" " -f2); ', &
'curl -s -X PATCH https://api.unsandbox.com/services/', &
trim(service_id), ' ', &
'-H "Content-Type: application/json" ', &
'-H "Authorization: Bearer ', trim(public_key), '" ', &
'-H "X-Timestamp: $TS" ', &
'-H "X-Signature: $SIG" ', &
'-d "$BODY" >/dev/null && ', &
'echo -e "\x1b[32mService resized to ', resize_vcpu, ' vCPU, ', resize_vcpu * 2, ' GB RAM\x1b[0m"'
call execute_command_line(trim(full_cmd), wait=.true.)
else if (trim(operation) == 'set-unfreeze-on-demand' .and. len_trim(service_id) > 0) then
! service_type holds the enabled value (true/false/1/0/yes/no/on/off)
write(full_cmd, '(30A)') &
'ENABLED_STR=$(echo "', trim(service_type), '" | tr "[:upper:]" "[:lower:]"); ', &
'case "$ENABLED_STR" in true|1|yes|on) ENABLED=true;; *) ENABLED=false;; esac; ', &
'BODY="{\"unfreeze_on_demand\":$ENABLED}"; ', &
'TS=$(date +%s); ', &
'SIG=$(echo -n "$TS:PATCH:/services/', trim(service_id), ':$BODY" | openssl dgst -sha256 -hmac "', trim(secret_key), '" | cut -d" " -f2); ', &
'curl -s -X PATCH https://api.unsandbox.com/services/', &
trim(service_id), ' ', &
'-H "Content-Type: application/json" ', &
'-H "Authorization: Bearer ', trim(public_key), '" ', &
'-H "X-Timestamp: $TS" ', &
'-H "X-Signature: $SIG" ', &
'-d "$BODY" >/dev/null && ', &
'echo -e "\x1b[32mService unfreeze_on_demand set to $ENABLED: ', trim(service_id), '\x1b[0m"'
call execute_command_line(trim(full_cmd), wait=.true.)
else if (trim(operation) == 'dump-bootstrap' .and. len_trim(service_id) > 0) then
write(full_cmd, '(30A)') &
'echo "Fetching bootstrap script from ', trim(service_id), '..." >&2; ', &
'BODY=''{"command":"cat /tmp/bootstrap.sh"}''; ', &
'TS=$(date +%s); ', &
'SIG=$(echo -n "$TS:POST:/services/', trim(service_id), '/execute:$BODY" | openssl dgst -sha256 -hmac "', trim(secret_key), '" | cut -d" " -f2); ', &
'RESP=$(curl -s -X POST https://api.unsandbox.com/services/', &
trim(service_id), '/execute ', &
'-H "Content-Type: application/json" ', &
'-H "Authorization: Bearer ', trim(public_key), '" ', &
'-H "X-Timestamp: $TS" ', &
'-H "X-Signature: $SIG" ', &
'-d "$BODY"); ', &
'STDOUT=$(echo "$RESP" | jq -r ".stdout // empty"); ', &
'if [ -n "$STDOUT" ]; then ', &
'if [ -n "', trim(service_type), '" ]; then ', &
'echo "$STDOUT" > "', trim(service_type), '" && chmod 755 "', trim(service_type), '" && ', &
'echo "Bootstrap saved to ', trim(service_type), '"; ', &
'else echo "$STDOUT"; fi; ', &
'else echo -e "\x1b[31mError: Failed to fetch bootstrap\x1b[0m" >&2; exit 1; fi'
call execute_command_line(trim(full_cmd), wait=.true.)
else if (trim(operation) == 'create' .and. len_trim(service_name) > 0) then
write(full_cmd, '(60A)') &
'BODY=''{"name":"', trim(service_name), '"}''; ', &
'TS=$(date +%s); ', &
'SIG=$(echo -n "$TS:POST:/services:$BODY" | openssl dgst -sha256 -hmac "', trim(secret_key), '" | cut -d" " -f2); ', &
'RESP=$(curl -s -X POST https://api.unsandbox.com/services ', &
'-H "Content-Type: application/json" ', &
'-H "Authorization: Bearer ', trim(public_key), '" ', &
'-H "X-Timestamp: $TS" ', &
'-H "X-Signature: $SIG" ', &
'-d "$BODY"); ', &
'SVC_ID=$(echo "$RESP" | jq -r ".id // empty"); ', &
'if [ -n "$SVC_ID" ]; then ', &
'echo -e "\x1b[32m$SVC_ID created\x1b[0m"; ', &
'ENV_CONTENT=""; ', &
'ENV_LINES="', trim(svc_envs), '"; ', &
'if [ -n "$ENV_LINES" ]; then ENV_CONTENT="$ENV_LINES"; fi; ', &
'ENV_FILE="', trim(svc_env_file), '"; ', &
'if [ -n "$ENV_FILE" ] && [ -f "$ENV_FILE" ]; then ', &
'while IFS= read -r line || [ -n "$line" ]; do ', &
'case "$line" in "#"*|"") continue ;; esac; ', &
'if [ -n "$ENV_CONTENT" ]; then ENV_CONTENT="$ENV_CONTENT', char(10), '"; fi; ', &
'ENV_CONTENT="$ENV_CONTENT$line"; ', &
'done < "$ENV_FILE"; fi; ', &
'if [ -n "$ENV_CONTENT" ]; then ', &
'TS2=$(date +%s); ', &
'SIG2=$(echo -n "$TS2:PUT:/services/$SVC_ID/env:$ENV_CONTENT" | openssl dgst -sha256 -hmac "', trim(secret_key), '" | cut -d" " -f2); ', &
'curl -s -X PUT "https://api.unsandbox.com/services/$SVC_ID/env" ', &
'-H "Authorization: Bearer ', trim(public_key), '" ', &
'-H "X-Timestamp: $TS2" ', &
'-H "X-Signature: $SIG2" ', &
'-H "Content-Type: text/plain" ', &
'--data-binary "$ENV_CONTENT" >/dev/null && ', &
'echo -e "\x1b[32mVault configured\x1b[0m"; fi; ', &
'else echo "$RESP" | jq .; fi'
call execute_command_line(trim(full_cmd), wait=.true.)
else
write(0, '(A)') 'Error: Use --list, --info, --logs, --freeze, --unfreeze, --destroy, --set-unfreeze-on-demand, --dump-bootstrap, --name, or env'
stop 1
end if
end subroutine handle_service
subroutine handle_snapshot()
character(len=8192) :: full_cmd
character(len=256) :: arg, snapshot_id, operation, clone_type, name, ports, shell
character(len=1024) :: public_key, secret_key
integer :: i, stat
logical :: list_mode
snapshot_id = ''
operation = ''
clone_type = ''
name = ''
ports = ''
shell = ''
list_mode = .false.
! Parse arguments
do i = 2, command_argument_count()
call get_command_argument(i, arg)
if (trim(arg) == '-l' .or. trim(arg) == '--list') then
list_mode = .true.
else if (trim(arg) == '--info') then
operation = 'info'
if (i < command_argument_count()) then
call get_command_argument(i + 1, snapshot_id)
end if
else if (trim(arg) == '--delete') then
operation = 'delete'
if (i < command_argument_count()) then
call get_command_argument(i + 1, snapshot_id)
end if
else if (trim(arg) == '--lock') then
operation = 'lock'
if (i < command_argument_count()) then
call get_command_argument(i + 1, snapshot_id)
end if
else if (trim(arg) == '--unlock') then
operation = 'unlock'
if (i < command_argument_count()) then
call get_command_argument(i + 1, snapshot_id)
end if
else if (trim(arg) == '--restore') then
operation = 'restore'
if (i < command_argument_count()) then
call get_command_argument(i + 1, snapshot_id)
end if
else if (trim(arg) == '--clone') then
operation = 'clone'
if (i < command_argument_count()) then
call get_command_argument(i + 1, snapshot_id)
end if
else if (trim(arg) == '--type') then
if (i < command_argument_count()) then
call get_command_argument(i + 1, clone_type)
end if
else if (trim(arg) == '--name') then
if (i < command_argument_count()) then
call get_command_argument(i + 1, name)
end if
else if (trim(arg) == '--ports') then
if (i < command_argument_count()) then
call get_command_argument(i + 1, ports)
end if
else if (trim(arg) == '--shell') then
if (i < command_argument_count()) then
call get_command_argument(i + 1, shell)
end if
end if
end do
! Get credentials
call get_credentials(public_key, secret_key, stat)
if (stat /= 0) then
write(0, '(A)') 'Error: No credentials found'
stop 1
end if
if (list_mode) then
! List snapshots
write(full_cmd, '(20A)') &
'TS=$(date +%s); ', &
'SIG=$(echo -n "$TS:GET:/snapshots:" | openssl dgst -sha256 -hmac "', trim(secret_key), '" | cut -d" " -f2); ', &
'curl -s -X GET https://api.unsandbox.com/snapshots ', &
'-H "Authorization: Bearer ', trim(public_key), '" ', &
'-H "X-Timestamp: $TS" ', &
'-H "X-Signature: $SIG" | jq .'
call execute_command_line(trim(full_cmd), wait=.true.)
else if (trim(operation) == 'info' .and. len_trim(snapshot_id) > 0) then
! Get snapshot info
write(full_cmd, '(20A)') &
'TS=$(date +%s); ', &
'SIG=$(echo -n "$TS:GET:/snapshots/', trim(snapshot_id), ':" | openssl dgst -sha256 -hmac "', trim(secret_key), '" | cut -d" " -f2); ', &
'curl -s -X GET https://api.unsandbox.com/snapshots/', trim(snapshot_id), ' ', &
'-H "Authorization: Bearer ', trim(public_key), '" ', &
'-H "X-Timestamp: $TS" ', &
'-H "X-Signature: $SIG" | jq .'
call execute_command_line(trim(full_cmd), wait=.true.)
else if (trim(operation) == 'delete' .and. len_trim(snapshot_id) > 0) then
! Delete snapshot (with sudo)
write(full_cmd, '(30A)') &
'TS=$(date +%s); ', &
'SIG=$(echo -n "$TS:DELETE:/snapshots/', trim(snapshot_id), ':" | openssl dgst -sha256 -hmac "', trim(secret_key), '" | cut -d" " -f2); ', &
'RESP=$(curl -s -w "\n%{http_code}" -X DELETE https://api.unsandbox.com/snapshots/', trim(snapshot_id), ' ', &
'-H "Authorization: Bearer ', trim(public_key), '" ', &
'-H "X-Timestamp: $TS" ', &
'-H "X-Signature: $SIG"); ', &
'HTTP_CODE=$(echo "$RESP" | tail -1); ', &
'BODY=$(echo "$RESP" | head -n -1); ', &
'if [ "$HTTP_CODE" = "428" ]; then ', &
'OTP=$(echo "$BODY" | jq -r ".otp // empty"); ', &
'if [ -n "$OTP" ]; then ', &
'TS2=$(date +%s); ', &
'SIG2=$(echo -n "$TS2:DELETE:/snapshots/', trim(snapshot_id), ':" | openssl dgst -sha256 -hmac "', trim(secret_key), '" | cut -d" " -f2); ', &
'curl -s -X DELETE https://api.unsandbox.com/snapshots/', trim(snapshot_id), ' ', &
'-H "Authorization: Bearer ', trim(public_key), '" ', &
'-H "X-Timestamp: $TS2" ', &
'-H "X-Signature: $SIG2" ', &
'-H "X-Sudo-OTP: $OTP" | jq .; ', &
'echo -e "\x1b[32mSnapshot deleted\x1b[0m"; fi; ', &
'else echo "$BODY" | jq .; fi'
call execute_command_line(trim(full_cmd), wait=.true.)
else if (trim(operation) == 'lock' .and. len_trim(snapshot_id) > 0) then
! Lock snapshot
write(full_cmd, '(20A)') &
'TS=$(date +%s); ', &
'SIG=$(echo -n "$TS:POST:/snapshots/', trim(snapshot_id), '/lock:" | openssl dgst -sha256 -hmac "', trim(secret_key), '" | cut -d" " -f2); ', &
'curl -s -X POST https://api.unsandbox.com/snapshots/', trim(snapshot_id), '/lock ', &
'-H "Authorization: Bearer ', trim(public_key), '" ', &
'-H "X-Timestamp: $TS" ', &
'-H "X-Signature: $SIG" | jq . && ', &
'echo -e "\x1b[32mSnapshot locked\x1b[0m"'
call execute_command_line(trim(full_cmd), wait=.true.)
else if (trim(operation) == 'unlock' .and. len_trim(snapshot_id) > 0) then
! Unlock snapshot (with sudo)
write(full_cmd, '(30A)') &
'TS=$(date +%s); ', &
'BODY="{}"; ', &
'SIG=$(echo -n "$TS:POST:/snapshots/', trim(snapshot_id), '/unlock:$BODY" | openssl dgst -sha256 -hmac "', trim(secret_key), '" | cut -d" " -f2); ', &
'RESP=$(curl -s -w "\n%{http_code}" -X POST https://api.unsandbox.com/snapshots/', trim(snapshot_id), '/unlock ', &
'-H "Authorization: Bearer ', trim(public_key), '" ', &
'-H "X-Timestamp: $TS" ', &
'-H "X-Signature: $SIG" ', &
'-H "Content-Type: application/json" ', &
'-d "$BODY"); ', &
'HTTP_CODE=$(echo "$RESP" | tail -1); ', &
'BODY_RESP=$(echo "$RESP" | head -n -1); ', &
'if [ "$HTTP_CODE" = "428" ]; then ', &
'OTP=$(echo "$BODY_RESP" | jq -r ".otp // empty"); ', &
'if [ -n "$OTP" ]; then ', &
'TS2=$(date +%s); ', &
'SIG2=$(echo -n "$TS2:POST:/snapshots/', trim(snapshot_id), '/unlock:$BODY" | openssl dgst -sha256 -hmac "', trim(secret_key), '" | cut -d" " -f2); ', &
'curl -s -X POST https://api.unsandbox.com/snapshots/', trim(snapshot_id), '/unlock ', &
'-H "Authorization: Bearer ', trim(public_key), '" ', &
'-H "X-Timestamp: $TS2" ', &
'-H "X-Signature: $SIG2" ', &
'-H "X-Sudo-OTP: $OTP" ', &
'-H "Content-Type: application/json" ', &
'-d "$BODY" | jq .; ', &
'echo -e "\x1b[32mSnapshot unlocked\x1b[0m"; fi; ', &
'else echo "$BODY_RESP" | jq .; fi'
call execute_command_line(trim(full_cmd), wait=.true.)
else if (trim(operation) == 'restore' .and. len_trim(snapshot_id) > 0) then
! Restore snapshot
write(full_cmd, '(20A)') &
'TS=$(date +%s); ', &
'BODY="{}"; ', &
'SIG=$(echo -n "$TS:POST:/snapshots/', trim(snapshot_id), '/restore:$BODY" | openssl dgst -sha256 -hmac "', trim(secret_key), '" | cut -d" " -f2); ', &
'curl -s -X POST https://api.unsandbox.com/snapshots/', trim(snapshot_id), '/restore ', &
'-H "Authorization: Bearer ', trim(public_key), '" ', &
'-H "X-Timestamp: $TS" ', &
'-H "X-Signature: $SIG" ', &
'-H "Content-Type: application/json" ', &
'-d "$BODY" | jq . && ', &
'echo -e "\x1b[32mSnapshot restored\x1b[0m"'
call execute_command_line(trim(full_cmd), wait=.true.)
else if (trim(operation) == 'clone' .and. len_trim(snapshot_id) > 0) then
! Clone snapshot
if (len_trim(clone_type) == 0) then
write(0, '(A)') 'Error: --type required for --clone (session or service)'
stop 1
end if
write(full_cmd, '(30A)') &
'TS=$(date +%s); ', &
'BODY=''{"type":"', trim(clone_type), '"'
if (len_trim(name) > 0) then
write(full_cmd, '(A,A)') trim(full_cmd), ',"name":"' // trim(name) // '"'
end if
if (len_trim(ports) > 0) then
write(full_cmd, '(A,A)') trim(full_cmd), ',"ports":[' // trim(ports) // ']'
end if
if (len_trim(shell) > 0) then
write(full_cmd, '(A,A)') trim(full_cmd), ',"shell":"' // trim(shell) // '"'
end if
write(full_cmd, '(A,20A)') trim(full_cmd), '}''; ', &
'SIG=$(echo -n "$TS:POST:/snapshots/', trim(snapshot_id), '/clone:$BODY" | openssl dgst -sha256 -hmac "', trim(secret_key), '" | cut -d" " -f2); ', &
'curl -s -X POST https://api.unsandbox.com/snapshots/', trim(snapshot_id), '/clone ', &
'-H "Authorization: Bearer ', trim(public_key), '" ', &
'-H "X-Timestamp: $TS" ', &
'-H "X-Signature: $SIG" ', &
'-H "Content-Type: application/json" ', &
'-d "$BODY" | jq . && ', &
'echo -e "\x1b[32mSnapshot cloned\x1b[0m"'
call execute_command_line(trim(full_cmd), wait=.true.)
else
write(0, '(A)') 'Error: Use --list, --info, --delete, --lock, --unlock, --restore, or --clone'
stop 1
end if
end subroutine handle_snapshot
subroutine handle_image()
character(len=8192) :: full_cmd
character(len=256) :: arg, image_id, operation, source_type, name, ports, visibility_mode
character(len=1024) :: public_key, secret_key
integer :: i, stat
logical :: list_mode
image_id = ''
operation = ''
source_type = ''
name = ''
ports = ''
visibility_mode = ''
list_mode = .false.
! Get credentials
call get_credentials(public_key, secret_key, stat)
if (stat /= 0) then
write(0, '(A)') 'Error: No credentials found'
stop 1
end if
! Parse image arguments
do i = 2, command_argument_count()
call get_command_argument(i, arg)
if (trim(arg) == '-l' .or. trim(arg) == '--list') then
list_mode = .true.
operation = 'list'
else if (trim(arg) == '--info') then
operation = 'info'
if (i < command_argument_count()) then
call get_command_argument(i+1, image_id)
end if
else if (trim(arg) == '--delete') then
operation = 'delete'
if (i < command_argument_count()) then
call get_command_argument(i+1, image_id)
end if
else if (trim(arg) == '--lock') then
operation = 'lock'
if (i < command_argument_count()) then
call get_command_argument(i+1, image_id)
end if
else if (trim(arg) == '--unlock') then
operation = 'unlock'
if (i < command_argument_count()) then
call get_command_argument(i+1, image_id)
end if
else if (trim(arg) == '--publish') then
operation = 'publish'
if (i < command_argument_count()) then
call get_command_argument(i+1, image_id)
end if
else if (trim(arg) == '--source-type') then
if (i < command_argument_count()) then
call get_command_argument(i+1, source_type)
end if
else if (trim(arg) == '--visibility') then
operation = 'visibility'
if (i < command_argument_count()) then
call get_command_argument(i+1, image_id)
end if
if (i+1 < command_argument_count()) then
call get_command_argument(i+2, visibility_mode)
end if
else if (trim(arg) == '--spawn') then
operation = 'spawn'
if (i < command_argument_count()) then
call get_command_argument(i+1, image_id)
end if
else if (trim(arg) == '--clone') then
operation = 'clone'
if (i < command_argument_count()) then
call get_command_argument(i+1, image_id)
end if
else if (trim(arg) == '--name') then
if (i < command_argument_count()) then
call get_command_argument(i+1, name)
end if
else if (trim(arg) == '--ports') then
if (i < command_argument_count()) then
call get_command_argument(i+1, ports)
end if
end if
end do
if (trim(operation) == 'list') then
write(full_cmd, '(20A)') &
'TS=$(date +%s); ', &
'SIG=$(echo -n "$TS:GET:/images:" | openssl dgst -sha256 -hmac "', trim(secret_key), '" | cut -d" " -f2); ', &
'curl -s -X GET https://api.unsandbox.com/images ', &
'-H "Authorization: Bearer ', trim(public_key), '" ', &
'-H "X-Timestamp: $TS" -H "X-Signature: $SIG" | jq .'
call execute_command_line(trim(full_cmd), wait=.true.)
else if (trim(operation) == 'info' .and. len_trim(image_id) > 0) then
write(full_cmd, '(20A)') &
'TS=$(date +%s); ', &
'SIG=$(echo -n "$TS:GET:/images/', trim(image_id), ':" | openssl dgst -sha256 -hmac "', trim(secret_key), '" | cut -d" " -f2); ', &
'curl -s -X GET https://api.unsandbox.com/images/', trim(image_id), ' ', &
'-H "Authorization: Bearer ', trim(public_key), '" ', &
'-H "X-Timestamp: $TS" -H "X-Signature: $SIG" | jq .'
call execute_command_line(trim(full_cmd), wait=.true.)
else if (trim(operation) == 'delete' .and. len_trim(image_id) > 0) then
write(full_cmd, '(50A)') &
'TS=$(date +%s); ', &
'SIG=$(echo -n "$TS:DELETE:/images/', trim(image_id), ':" | openssl dgst -sha256 -hmac "', trim(secret_key), '" | cut -d" " -f2); ', &
'RESP=$(curl -s -w "\n%{http_code}" -X DELETE https://api.unsandbox.com/images/', trim(image_id), ' ', &
'-H "Authorization: Bearer ', trim(public_key), '" ', &
'-H "X-Timestamp: $TS" -H "X-Signature: $SIG"); ', &
'HTTP_CODE=$(echo "$RESP" | tail -n1); ', &
'BODY=$(echo "$RESP" | sed ''$d''); ', &
'if [ "$HTTP_CODE" = "428" ]; then ', &
'CHALLENGE_ID=$(echo "$BODY" | jq -r ".challenge_id // empty"); ', &
'echo -e "\x1b[33mConfirmation required. Check your email for a one-time code.\x1b[0m" >&2; ', &
'echo -n "Enter OTP: " >&2; read OTP; ', &
'if [ -z "$OTP" ]; then echo -e "\x1b[31mError: Operation cancelled\x1b[0m" >&2; exit 1; fi; ', &
'TS2=$(date +%s); ', &
'SIG2=$(echo -n "$TS2:DELETE:/images/', trim(image_id), ':" | openssl dgst -sha256 -hmac "', trim(secret_key), '" | cut -d" " -f2); ', &
'curl -s -X DELETE https://api.unsandbox.com/images/', trim(image_id), ' ', &
'-H "Authorization: Bearer ', trim(public_key), '" ', &
'-H "X-Timestamp: $TS2" -H "X-Signature: $SIG2" ', &
'-H "X-Sudo-OTP: $OTP" -H "X-Sudo-Challenge: $CHALLENGE_ID" >/dev/null && ', &
'echo -e "\x1b[32mImage deleted: ', trim(image_id), '\x1b[0m"; ', &
'elif [ "$HTTP_CODE" = "200" ] || [ "$HTTP_CODE" = "204" ]; then ', &
'echo -e "\x1b[32mImage deleted: ', trim(image_id), '\x1b[0m"; ', &
'else echo -e "\x1b[31mError: HTTP $HTTP_CODE\x1b[0m" >&2; echo "$BODY" >&2; exit 1; fi'
call execute_command_line(trim(full_cmd), wait=.true.)
else if (trim(operation) == 'lock' .and. len_trim(image_id) > 0) then
write(full_cmd, '(20A)') &
'TS=$(date +%s); ', &
'SIG=$(echo -n "$TS:POST:/images/', trim(image_id), '/lock:" | openssl dgst -sha256 -hmac "', trim(secret_key), '" | cut -d" " -f2); ', &
'curl -s -X POST https://api.unsandbox.com/images/', trim(image_id), '/lock ', &
'-H "Authorization: Bearer ', trim(public_key), '" ', &
'-H "X-Timestamp: $TS" -H "X-Signature: $SIG" >/dev/null; ', &
'echo -e "\x1b[32mImage locked: ', trim(image_id), '\x1b[0m"'
call execute_command_line(trim(full_cmd), wait=.true.)
else if (trim(operation) == 'unlock' .and. len_trim(image_id) > 0) then
write(full_cmd, '(50A)') &
'TS=$(date +%s); BODY="{}"; ', &
'SIG=$(echo -n "$TS:POST:/images/', trim(image_id), '/unlock:$BODY" | openssl dgst -sha256 -hmac "', trim(secret_key), '" | cut -d" " -f2); ', &
'RESP=$(curl -s -w "\n%{http_code}" -X POST https://api.unsandbox.com/images/', trim(image_id), '/unlock ', &
'-H "Content-Type: application/json" ', &
'-H "Authorization: Bearer ', trim(public_key), '" ', &
'-H "X-Timestamp: $TS" -H "X-Signature: $SIG" -d "$BODY"); ', &
'HTTP_CODE=$(echo "$RESP" | tail -n1); ', &
'RESPBODY=$(echo "$RESP" | sed ''$d''); ', &
'if [ "$HTTP_CODE" = "428" ]; then ', &
'CHALLENGE_ID=$(echo "$RESPBODY" | jq -r ".challenge_id // empty"); ', &
'echo -e "\x1b[33mConfirmation required. Check your email for a one-time code.\x1b[0m" >&2; ', &
'echo -n "Enter OTP: " >&2; read OTP; ', &
'if [ -z "$OTP" ]; then echo -e "\x1b[31mError: Operation cancelled\x1b[0m" >&2; exit 1; fi; ', &
'TS2=$(date +%s); ', &
'SIG2=$(echo -n "$TS2:POST:/images/', trim(image_id), '/unlock:$BODY" | openssl dgst -sha256 -hmac "', trim(secret_key), '" | cut -d" " -f2); ', &
'curl -s -X POST https://api.unsandbox.com/images/', trim(image_id), '/unlock ', &
'-H "Content-Type: application/json" ', &
'-H "Authorization: Bearer ', trim(public_key), '" ', &
'-H "X-Timestamp: $TS2" -H "X-Signature: $SIG2" ', &
'-H "X-Sudo-OTP: $OTP" -H "X-Sudo-Challenge: $CHALLENGE_ID" -d "$BODY" >/dev/null && ', &
'echo -e "\x1b[32mImage unlocked: ', trim(image_id), '\x1b[0m"; ', &
'elif [ "$HTTP_CODE" = "200" ] || [ "$HTTP_CODE" = "204" ]; then ', &
'echo -e "\x1b[32mImage unlocked: ', trim(image_id), '\x1b[0m"; ', &
'else echo -e "\x1b[31mError: HTTP $HTTP_CODE\x1b[0m" >&2; echo "$RESPBODY" >&2; exit 1; fi'
call execute_command_line(trim(full_cmd), wait=.true.)
else if (trim(operation) == 'publish' .and. len_trim(image_id) > 0) then
if (len_trim(source_type) == 0) then
write(0, '(A)') 'Error: --source-type required (service or snapshot)'
stop 1
end if
if (len_trim(name) > 0) then
write(full_cmd, '(25A)') &
'BODY=''{"source_type":"', trim(source_type), '","source_id":"', trim(image_id), '","name":"', trim(name), '"}''; ', &
'TS=$(date +%s); ', &
'SIG=$(echo -n "$TS:POST:/images/publish:$BODY" | openssl dgst -sha256 -hmac "', trim(secret_key), '" | cut -d" " -f2); ', &
'curl -s -X POST https://api.unsandbox.com/images/publish ', &
'-H "Content-Type: application/json" ', &
'-H "Authorization: Bearer ', trim(public_key), '" ', &
'-H "X-Timestamp: $TS" -H "X-Signature: $SIG" ', &
'-d "$BODY" | jq .; ', &
'echo -e "\x1b[32mImage published\x1b[0m"'
else
write(full_cmd, '(25A)') &
'BODY=''{"source_type":"', trim(source_type), '","source_id":"', trim(image_id), '"}''; ', &
'TS=$(date +%s); ', &
'SIG=$(echo -n "$TS:POST:/images/publish:$BODY" | openssl dgst -sha256 -hmac "', trim(secret_key), '" | cut -d" " -f2); ', &
'curl -s -X POST https://api.unsandbox.com/images/publish ', &
'-H "Content-Type: application/json" ', &
'-H "Authorization: Bearer ', trim(public_key), '" ', &
'-H "X-Timestamp: $TS" -H "X-Signature: $SIG" ', &
'-d "$BODY" | jq .; ', &
'echo -e "\x1b[32mImage published\x1b[0m"'
end if
call execute_command_line(trim(full_cmd), wait=.true.)
else if (trim(operation) == 'visibility' .and. len_trim(image_id) > 0 .and. len_trim(visibility_mode) > 0) then
write(full_cmd, '(25A)') &
'BODY=''{"visibility":"', trim(visibility_mode), '"}''; ', &
'TS=$(date +%s); ', &
'SIG=$(echo -n "$TS:POST:/images/', trim(image_id), '/visibility:$BODY" | openssl dgst -sha256 -hmac "', trim(secret_key), '" | cut -d" " -f2); ', &
'curl -s -X POST https://api.unsandbox.com/images/', trim(image_id), '/visibility ', &
'-H "Content-Type: application/json" ', &
'-H "Authorization: Bearer ', trim(public_key), '" ', &
'-H "X-Timestamp: $TS" -H "X-Signature: $SIG" ', &
'-d "$BODY" >/dev/null; ', &
'echo -e "\x1b[32mImage visibility set to ', trim(visibility_mode), '\x1b[0m"'
call execute_command_line(trim(full_cmd), wait=.true.)
else if (trim(operation) == 'spawn' .and. len_trim(image_id) > 0) then
if (len_trim(name) > 0 .and. len_trim(ports) > 0) then
write(full_cmd, '(25A)') &
'BODY=''{"name":"', trim(name), '","ports":[', trim(ports), ']}''; ', &
'TS=$(date +%s); ', &
'SIG=$(echo -n "$TS:POST:/images/', trim(image_id), '/spawn:$BODY" | openssl dgst -sha256 -hmac "', trim(secret_key), '" | cut -d" " -f2); ', &
'curl -s -X POST https://api.unsandbox.com/images/', trim(image_id), '/spawn ', &
'-H "Content-Type: application/json" ', &
'-H "Authorization: Bearer ', trim(public_key), '" ', &
'-H "X-Timestamp: $TS" -H "X-Signature: $SIG" ', &
'-d "$BODY" | jq .; ', &
'echo -e "\x1b[32mService spawned from image\x1b[0m"'
else if (len_trim(name) > 0) then
write(full_cmd, '(25A)') &
'BODY=''{"name":"', trim(name), '"}''; ', &
'TS=$(date +%s); ', &
'SIG=$(echo -n "$TS:POST:/images/', trim(image_id), '/spawn:$BODY" | openssl dgst -sha256 -hmac "', trim(secret_key), '" | cut -d" " -f2); ', &
'curl -s -X POST https://api.unsandbox.com/images/', trim(image_id), '/spawn ', &
'-H "Content-Type: application/json" ', &
'-H "Authorization: Bearer ', trim(public_key), '" ', &
'-H "X-Timestamp: $TS" -H "X-Signature: $SIG" ', &
'-d "$BODY" | jq .; ', &
'echo -e "\x1b[32mService spawned from image\x1b[0m"'
else
write(full_cmd, '(25A)') &
'BODY=''{}''; ', &
'TS=$(date +%s); ', &
'SIG=$(echo -n "$TS:POST:/images/', trim(image_id), '/spawn:$BODY" | openssl dgst -sha256 -hmac "', trim(secret_key), '" | cut -d" " -f2); ', &
'curl -s -X POST https://api.unsandbox.com/images/', trim(image_id), '/spawn ', &
'-H "Content-Type: application/json" ', &
'-H "Authorization: Bearer ', trim(public_key), '" ', &
'-H "X-Timestamp: $TS" -H "X-Signature: $SIG" ', &
'-d "$BODY" | jq .; ', &
'echo -e "\x1b[32mService spawned from image\x1b[0m"'
end if
call execute_command_line(trim(full_cmd), wait=.true.)
else if (trim(operation) == 'clone' .and. len_trim(image_id) > 0) then
if (len_trim(name) > 0) then
write(full_cmd, '(25A)') &
'BODY=''{"name":"', trim(name), '"}''; ', &
'TS=$(date +%s); ', &
'SIG=$(echo -n "$TS:POST:/images/', trim(image_id), '/clone:$BODY" | openssl dgst -sha256 -hmac "', trim(secret_key), '" | cut -d" " -f2); ', &
'curl -s -X POST https://api.unsandbox.com/images/', trim(image_id), '/clone ', &
'-H "Content-Type: application/json" ', &
'-H "Authorization: Bearer ', trim(public_key), '" ', &
'-H "X-Timestamp: $TS" -H "X-Signature: $SIG" ', &
'-d "$BODY" | jq .; ', &
'echo -e "\x1b[32mImage cloned\x1b[0m"'
else
write(full_cmd, '(25A)') &
'BODY=''{}''; ', &
'TS=$(date +%s); ', &
'SIG=$(echo -n "$TS:POST:/images/', trim(image_id), '/clone:$BODY" | openssl dgst -sha256 -hmac "', trim(secret_key), '" | cut -d" " -f2); ', &
'curl -s -X POST https://api.unsandbox.com/images/', trim(image_id), '/clone ', &
'-H "Content-Type: application/json" ', &
'-H "Authorization: Bearer ', trim(public_key), '" ', &
'-H "X-Timestamp: $TS" -H "X-Signature: $SIG" ', &
'-d "$BODY" | jq .; ', &
'echo -e "\x1b[32mImage cloned\x1b[0m"'
end if
call execute_command_line(trim(full_cmd), wait=.true.)
else
write(0, '(A)') 'Error: Use --list, --info ID, --delete ID, --lock ID, --unlock ID, --publish ID, --visibility ID MODE, --spawn ID, or --clone ID'
stop 1
end if
end subroutine handle_image
subroutine handle_key()
character(len=4096) :: full_cmd
character(len=256) :: arg
character(len=1024) :: public_key, secret_key
integer :: i, stat
logical :: extend_mode
character(len=32) :: portal_base
portal_base = 'https://unsandbox.com'
extend_mode = .false.
! Check for --extend flag
do i = 2, command_argument_count()
call get_command_argument(i, arg)
if (trim(arg) == '--extend') then
extend_mode = .true.
end if
end do
! Get API key
call get_credentials(public_key, secret_key, stat)
if (stat /= 0) then
write(0, '(A)') 'Error: No credentials found'
stop 1
end if
if (extend_mode) then
write(full_cmd, '(30A)') &
'resp=$(curl -s -X POST ', trim(portal_base), '/keys/validate ', &
'-H "Content-Type: application/json" ', &
'-H "Authorization: Bearer ', trim(public_key), '" ', &
'-d "{}"); ', &
'status=$(echo "$resp" | jq -r ".status // empty"); ', &
'public_key=$(echo "$resp" | jq -r ".public_key // empty"); ', &
'tier=$(echo "$resp" | jq -r ".tier // empty"); ', &
'expires_at=$(echo "$resp" | jq -r ".expires_at // empty"); ', &
'time_remaining=$(echo "$resp" | jq -r ".time_remaining // empty"); ', &
'rate_limit=$(echo "$resp" | jq -r ".rate_limit // empty"); ', &
'burst=$(echo "$resp" | jq -r ".burst // empty"); ', &
'concurrency=$(echo "$resp" | jq -r ".concurrency // empty"); ', &
'if [ "$status" = "valid" ]; then ', &
'echo -e "\x1b[32mValid\x1b[0m"; ', &
'echo "Public Key: $public_key"; ', &
'echo "Tier: $tier"; ', &
'echo "Status: $status"; ', &
'echo "Expires: $expires_at"; ', &
'[ -n "$time_remaining" ] && echo "Time Remaining: $time_remaining"; ', &
'[ -n "$rate_limit" ] && echo "Rate Limit: $rate_limit"; ', &
'[ -n "$burst" ] && echo "Burst: $burst"; ', &
'[ -n "$concurrency" ] && echo "Concurrency: $concurrency"; ', &
'echo -e "\x1b[34mOpening browser to extend key...\x1b[0m"; ', &
'xdg-open "', trim(portal_base), '/keys/extend?pk=$public_key" 2>/dev/null || ', &
'sensible-browser "', trim(portal_base), '/keys/extend?pk=$public_key" 2>/dev/null &; ', &
'elif [ "$status" = "expired" ]; then ', &
'echo -e "\x1b[31mExpired\x1b[0m"; ', &
'echo "Public Key: $public_key"; ', &
'echo "Tier: $tier"; ', &
'echo "Expired: $expires_at"; ', &
'echo -e "\x1b[33mTo renew: Visit ', trim(portal_base), '/keys/extend\x1b[0m"; ', &
'echo -e "\x1b[34mOpening browser to extend key...\x1b[0m"; ', &
'xdg-open "', trim(portal_base), '/keys/extend?pk=$public_key" 2>/dev/null || ', &
'sensible-browser "', trim(portal_base), '/keys/extend?pk=$public_key" 2>/dev/null &; ', &
'else echo -e "\x1b[31mInvalid\x1b[0m"; fi'
else
write(full_cmd, '(30A)') &
'resp=$(curl -s -X POST ', trim(portal_base), '/keys/validate ', &
'-H "Content-Type: application/json" ', &
'-H "Authorization: Bearer ', trim(public_key), '" ', &
'-d "{}"); ', &
'status=$(echo "$resp" | jq -r ".status // empty"); ', &
'public_key=$(echo "$resp" | jq -r ".public_key // empty"); ', &
'tier=$(echo "$resp" | jq -r ".tier // empty"); ', &
'expires_at=$(echo "$resp" | jq -r ".expires_at // empty"); ', &
'time_remaining=$(echo "$resp" | jq -r ".time_remaining // empty"); ', &
'rate_limit=$(echo "$resp" | jq -r ".rate_limit // empty"); ', &
'burst=$(echo "$resp" | jq -r ".burst // empty"); ', &
'concurrency=$(echo "$resp" | jq -r ".concurrency // empty"); ', &
'if [ "$status" = "valid" ]; then ', &
'echo -e "\x1b[32mValid\x1b[0m"; ', &
'echo "Public Key: $public_key"; ', &
'echo "Tier: $tier"; ', &
'echo "Status: $status"; ', &
'echo "Expires: $expires_at"; ', &
'[ -n "$time_remaining" ] && echo "Time Remaining: $time_remaining"; ', &
'[ -n "$rate_limit" ] && echo "Rate Limit: $rate_limit"; ', &
'[ -n "$burst" ] && echo "Burst: $burst"; ', &
'[ -n "$concurrency" ] && echo "Concurrency: $concurrency"; ', &
'elif [ "$status" = "expired" ]; then ', &
'echo -e "\x1b[31mExpired\x1b[0m"; ', &
'echo "Public Key: $public_key"; ', &
'echo "Tier: $tier"; ', &
'echo "Expired: $expires_at"; ', &
'echo -e "\x1b[33mTo renew: Visit ', trim(portal_base), '/keys/extend\x1b[0m"; ', &
'else echo -e "\x1b[31mInvalid\x1b[0m"; fi'
end if
call execute_command_line(trim(full_cmd), wait=.true., exitstat=stat)
end subroutine handle_key
subroutine handle_languages()
character(len=8192) :: full_cmd
character(len=256) :: arg
character(len=1024) :: public_key, secret_key
integer :: i, stat
logical :: json_mode
json_mode = .false.
! Check for --json flag
do i = 2, command_argument_count()
call get_command_argument(i, arg)
if (trim(arg) == '--json') then
json_mode = .true.
end if
end do
! Get API keys
call get_credentials(public_key, secret_key, stat)
if (stat /= 0) then
write(0, '(A)') 'Error: No credentials found'
stop 1
end if
if (json_mode) then
! Output as JSON array with caching
write(full_cmd, '(40A)') &
'CACHE_TTL=3600; ', &
'CACHE_FILE="$HOME/.unsandbox/languages.json"; ', &
'if [ -f "$CACHE_FILE" ]; then ', &
'CACHE_TS=$(jq -r ".timestamp // 0" "$CACHE_FILE" 2>/dev/null); ', &
'CURRENT_TS=$(date +%s); ', &
'AGE=$((CURRENT_TS - CACHE_TS)); ', &
'if [ $AGE -lt $CACHE_TTL ]; then ', &
'jq -c ".languages" "$CACHE_FILE"; exit 0; fi; fi; ', &
'TS=$(date +%s); ', &
'SIG=$(echo -n "$TS:GET:/languages:" | openssl dgst -sha256 -hmac "', trim(secret_key), '" | cut -d" " -f2); ', &
'RESP=$(curl -s -X GET https://api.unsandbox.com/languages ', &
'-H "Authorization: Bearer ', trim(public_key), '" ', &
'-H "X-Timestamp: $TS" ', &
'-H "X-Signature: $SIG"); ', &
'LANGS=$(echo "$RESP" | jq -c ".languages // []"); ', &
'mkdir -p "$HOME/.unsandbox"; ', &
'echo "{\"languages\":$LANGS,\"timestamp\":$(date +%s)}" > "$CACHE_FILE"; ', &
'echo "$LANGS"'
else
! Output one language per line with caching
write(full_cmd, '(40A)') &
'CACHE_TTL=3600; ', &
'CACHE_FILE="$HOME/.unsandbox/languages.json"; ', &
'if [ -f "$CACHE_FILE" ]; then ', &
'CACHE_TS=$(jq -r ".timestamp // 0" "$CACHE_FILE" 2>/dev/null); ', &
'CURRENT_TS=$(date +%s); ', &
'AGE=$((CURRENT_TS - CACHE_TS)); ', &
'if [ $AGE -lt $CACHE_TTL ]; then ', &
'jq -r ".languages[]" "$CACHE_FILE"; exit 0; fi; fi; ', &
'TS=$(date +%s); ', &
'SIG=$(echo -n "$TS:GET:/languages:" | openssl dgst -sha256 -hmac "', trim(secret_key), '" | cut -d" " -f2); ', &
'RESP=$(curl -s -X GET https://api.unsandbox.com/languages ', &
'-H "Authorization: Bearer ', trim(public_key), '" ', &
'-H "X-Timestamp: $TS" ', &
'-H "X-Signature: $SIG"); ', &
'LANGS=$(echo "$RESP" | jq -c ".languages // []"); ', &
'mkdir -p "$HOME/.unsandbox"; ', &
'echo "{\"languages\":$LANGS,\"timestamp\":$(date +%s)}" > "$CACHE_FILE"; ', &
'echo "$RESP" | jq -r ".languages[]"'
end if
call execute_command_line(trim(full_cmd), wait=.true., exitstat=stat)
end subroutine handle_languages
end program unsandbox_cli
Documentation clarifications
Dependencies
C Binary (un1) — requires libcurl and 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
SDK Implementations — most use stdlib only (Ruby, JS, Go, etc). Some require minimal deps:
pip install requests # Python
Execute Code
Run a Script
./un hello.py
./un app.js
./un main.rs
With Environment Variables
./un -e DEBUG=1 -e NAME=World script.py
With Input Files (teleport files into sandbox)
./un -f data.csv -f config.json process.py
Get Compiled Binary (teleport artifacts out)
./un -a -o ./bin main.c
Interactive Sessions
Start a Shell Session
# 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
Session with Network Access
./un session -n semitrusted
Session Auditing (full terminal recording)
# Record everything (including vim, interactive programs)
./un session --audit -o ./logs
# Replay session later
zcat session.log*.gz | less -R
Collect Artifacts from Session
# Files in /tmp/artifacts/ are collected on exit
./un session -a -o ./outputs
Session Persistence (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
List Active Sessions
./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
Reconnect to Existing Session
# 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
Terminate a Session
./un session --kill unsb-vm-12345
Available Shells & REPLs
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
API Key Management
Check Key Status
# Check if your API key is valid
./un key
# Output:
# Valid: key expires in 30 days
Extend Expired Key
# 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
Authentication
Credentials are loaded in priority order (highest first):
# 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
Requests are signed with HMAC-SHA256. The bearer token contains only the public key; the secret key computes the signature (never transmitted).
Resource Scaling
Set vCPU Count
# 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
Live Session Boosting
# 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
Session Freeze/Unfreeze
Freeze and Unfreeze Sessions
# 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
Persistent Services
Create a Service
# 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
Manage Services
# 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
List 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
Create Session Snapshot
# Snapshot with name
./un session --snapshot unsb-vm-12345 --name "before upgrade"
# Quick snapshot (auto-generated name)
./un session --snapshot unsb-vm-12345
Create Service Snapshot
# 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
Restore from 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
Delete Snapshot
./un snapshot --delete unsb-snapshot-a1b2-c3d4-e5f6-g7h8
Images
Images are independent, transferable container images that survive container deletion. Unlike snapshots (which live with their container), images can be shared with other users, transferred between API keys, or made public in the marketplace.
List Images
# 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
Publish Images
# 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
Create Services from Images
# 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
Image Protection
# 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
Visibility & Sharing
# 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
Transfer Ownership
# Transfer image to another API key
./un image --transfer unsb-image-xxxx-xxxx-xxxx-xxxx \
--to unsb-pk-newowner-newowner-newowner-newowner
Usage Reference
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
The UN CLI has been implemented in 42 programming languages, demonstrating that the unsandbox API can be accessed from virtually any environment.
License
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
that 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 all 42
programming languages through a unified interface, accessible to everyone. 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 its own software - with our permacomputer
monitoring itself, repairing itself, with minimal human guidance in the loop.
Our agents do their best.
Copyright 2025 TimeHexOn & foxhop & russell@unturf
https://www.timehexon.com
https://www.foxhop.net
https://www.unturf.com/software