CLI
Cliente de linha de comando rápido para execução de código e sessões interativas. Mais de 42 linguagens, mais de 30 shells/REPLs.
Documentação Oficial OpenAPI Swagger ↗Início Rápido — 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
Baixar
Guia de Instalação →Características:
- 42+ languages - Python, JS, Go, Rust, C++, Java...
- Sessions - 30+ shells/REPLs, tmux persistence
- Files - Upload files, collect artifacts
- Services - Persistent containers with domains
- Snapshots - Point-in-time backups
- Images - Publish, share, transfer
Início Rápido de Integração ⚡
Adicione superpoderes unsandbox ao seu app Fortran existente:
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
a0e46d507ca9eccdba3919fba28317bf
SHA256: 4c6e446dd7971e1ea59178823449fa1647a66a8e04a686a0ba8b98135c4bcad6
! 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. --account N flag -> accounts.csv row N (bypasses env vars)
! 2. Environment variables: UNSANDBOX_PUBLIC_KEY + UNSANDBOX_SECRET_KEY
! 3. Config file: ~/.unsandbox/accounts.csv row 0 (or UNSANDBOX_ACCOUNT)
! 4. ./accounts.csv row 0
! 5. 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: load_csv_row
! Description: Load public_key,secret_key from a CSV file at row_index
! (0-based, skipping blank lines and '#' comments).
!
! Arguments:
! csv_path - Path to CSV file
! row_index - Zero-based data row to read
! public_key - Output: public key (empty if not found)
! secret_key - Output: secret key (empty if not found)
!--------------------------------------------------------------------------
subroutine load_csv_row(csv_path, row_index, public_key, secret_key)
character(len=*), intent(in) :: csv_path
integer, intent(in) :: row_index
character(len=*), intent(out) :: public_key, secret_key
character(len=1024) :: line
integer :: unit_num, ios, data_index
logical :: file_exists
public_key = ''
secret_key = ''
data_index = 0
inquire(file=trim(csv_path), exist=file_exists)
if (.not. file_exists) return
open(newunit=unit_num, file=trim(csv_path), status='old', action='read', iostat=ios)
if (ios /= 0) return
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
if (data_index == row_index) then
call parse_csv_line(line, public_key, secret_key)
close(unit_num)
return
end if
data_index = data_index + 1
end do
close(unit_num)
end subroutine load_csv_row
!--------------------------------------------------------------------------
! Subroutine: get_credentials
! Description: Get API credentials from environment or config file
!
! Priority order:
! 1. account_index >= 0 -> accounts.csv row N (bypasses env vars)
! 2. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY)
! 3. Config file (~/.unsandbox/accounts.csv row 0 or UNSANDBOX_ACCOUNT)
! 4. ./accounts.csv row 0
! 5. 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
! account_index - Optional input: if >= 0, load that CSV row directly
!--------------------------------------------------------------------------
subroutine get_credentials(public_key, secret_key, status, account_index)
character(len=*), intent(out) :: public_key, secret_key
integer, intent(out) :: status
integer, intent(in), optional :: account_index
character(len=1024) :: home_dir, accounts_path, api_key, acct_env
integer :: ios, acct_idx, default_index
status = 0
public_key = ''
secret_key = ''
! Priority 1: account_index >= 0 -> load that CSV row (bypasses env vars)
if (present(account_index)) then
if (account_index >= 0) then
acct_idx = account_index
call get_environment_variable('HOME', home_dir, status=ios)
if (ios == 0) then
accounts_path = trim(home_dir) // '/.unsandbox/accounts.csv'
call load_csv_row(accounts_path, acct_idx, public_key, secret_key)
if (len_trim(public_key) > 0) return
end if
call load_csv_row('accounts.csv', acct_idx, public_key, secret_key)
if (len_trim(public_key) > 0) return
status = 1
return
end if
end if
! Priority 2: 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 3: ~/.unsandbox/accounts.csv (default row)
call get_environment_variable('UNSANDBOX_ACCOUNT', acct_env, status=ios)
if (ios == 0 .and. len_trim(acct_env) > 0) then
read(acct_env, *, iostat=ios) default_index
if (ios /= 0) default_index = 0
else
default_index = 0
end if
call get_environment_variable('HOME', home_dir, status=ios)
if (ios == 0) then
accounts_path = trim(home_dir) // '/.unsandbox/accounts.csv'
call load_csv_row(accounts_path, default_index, public_key, secret_key)
if (len_trim(public_key) > 0) return
end if
! Priority 4: ./accounts.csv
call load_csv_row('accounts.csv', default_index, public_key, secret_key)
if (len_trim(public_key) > 0) return
! Priority 5: 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
integer :: account_index ! -1 = not set; >= 0 means use that CSV row
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 = ''
account_index = -1
! Get command line arguments count
nargs = command_argument_count()
if (nargs < 1) then
call print_help()
stop 1
end if
! Pre-scan all arguments for --account N
do i = 1, nargs - 1
call get_command_argument(i, arg)
if (trim(arg) == '--account') then
call get_command_argument(i + 1, arg)
read(arg, *, iostat=stat) account_index
if (stat /= 0) account_index = -1
exit
end if
end do
! 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)') 'Credential options (global):'
write(*, '(A)') ' --account N Use row N from accounts.csv (bypasses env vars)'
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, account_index)
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 (trim(arg) == '--account') then
! already processed in main pre-scan; skip this token and its value
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, account_index)
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, account_index)
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, account_index)
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, account_index)
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, account_index)
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, account_index)
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
Esclarecimentos de documentação
Dependências
C Binary (un1) — requer libcurl e libwebsockets:
sudo apt install build-essential libcurl4-openssl-dev libwebsockets-dev
wget unsandbox.com/downloads/un.c && gcc -O2 -o un un.c -lcurl -lwebsockets
Implementações SDK — a maioria usa apenas stdlib (Ruby, JS, Go, etc). Alguns requerem dependências mínimas:
pip install requests # Python
Executar Código
Executar um script
./un hello.py
./un app.js
./un main.rs
Com variáveis de ambiente
./un -e DEBUG=1 -e NAME=World script.py
Com arquivos de entrada (teletransportar arquivos para sandbox)
./un -f data.csv -f config.json process.py
Obter binário compilado
./un -a -o ./bin main.c
Sessões interativas
Iniciar uma sessão de shell
# Default bash shell
./un session
# Choose your shell
./un session --shell zsh
./un session --shell fish
# Jump into a REPL
./un session --shell python3
./un session --shell node
./un session --shell julia
Sessão com acesso à rede
./un session -n semitrusted
Auditoria de sessão (gravação completa do terminal)
# Record everything (including vim, interactive programs)
./un session --audit -o ./logs
# Replay session later
zcat session.log*.gz | less -R
Coletar artefatos da sessão
# Files in /tmp/artifacts/ are collected on exit
./un session -a -o ./outputs
Persistência de sessão (tmux/screen)
# Default: session terminates on disconnect (clean exit)
./un session
# With tmux: session persists, can reconnect later
./un session --tmux
# Press Ctrl+b then d to detach
# With screen: alternative multiplexer
./un session --screen
# Press Ctrl+a then d to detach
Listar Trabalhos Ativos
./un session --list
# Output:
# Active sessions: 2
#
# SESSION ID CONTAINER SHELL TTL STATUS
# abc123... unsb-vm-12345 python3 45m30s active
# def456... unsb-vm-67890 bash 1h2m active
Reconectar à sessão existente
# Reconnect by container name (requires --tmux or --screen)
./un session --attach unsb-vm-12345
# Use exit to terminate session, or detach to keep it running
Encerrar uma sessão
./un session --kill unsb-vm-12345
Shells e REPLs disponíveis
Shells: bash, dash, sh, zsh, fish, ksh, tcsh, csh, elvish, xonsh, ash
REPLs: python3, bpython, ipython # Python
node # JavaScript
ruby, irb # Ruby
lua # Lua
php # PHP
perl # Perl
guile, scheme # Scheme
ghci # Haskell
erl, iex # Erlang/Elixir
sbcl, clisp # Common Lisp
r # R
julia # Julia
clojure # Clojure
Gerenciamento de Chave API
Verificar Status do Pagamento
# Check if your API key is valid
./un key
# Output:
# Valid: key expires in 30 days
Estender Chave Expirada
# Open the portal to extend an expired key
./un key --extend
# This opens the unsandbox.com portal where you can
# add more credits to extend your key's expiration
Autenticação
As credenciais são carregadas em ordem de prioridade (maior primeiro):
# 1. CLI flags (highest priority)
./un -p unsb-pk-xxxx -k unsb-sk-xxxxx script.py
# 2. Environment variables
export UNSANDBOX_PUBLIC_KEY=unsb-pk-xxxx-xxxx-xxxx-xxxx
export UNSANDBOX_SECRET_KEY=unsb-sk-xxxxx-xxxxx-xxxxx-xxxxx
./un script.py
# 3. Config file (lowest priority)
# ~/.unsandbox/accounts.csv format: public_key,secret_key
mkdir -p ~/.unsandbox
echo "unsb-pk-xxxx-xxxx-xxxx-xxxx,unsb-sk-xxxxx-xxxxx-xxxxx-xxxxx" > ~/.unsandbox/accounts.csv
./un script.py
As requisições são assinadas com HMAC-SHA256. O token bearer contém apenas a chave pública; a chave secreta calcula a assinatura (nunca é transmitida).
Escalonamento de Recursos
Definir Quantidade de vCPU
# Default: 1 vCPU, 2GB RAM
./un script.py
# Scale up: 4 vCPUs, 8GB RAM
./un -v 4 script.py
# Maximum: 8 vCPUs, 16GB RAM
./un --vcpu 8 heavy_compute.py
Reforço de Sessão Ao Vivo
# Boost a running session to 2 vCPU, 4GB RAM
./un session --boost sandbox-abc
# Boost to specific vCPU count (4 vCPU, 8GB RAM)
./un session --boost sandbox-abc --boost-vcpu 4
# Return to base resources (1 vCPU, 2GB RAM)
./un session --unboost sandbox-abc
Congelar/Descongelar Sessão
Congelar e Descongelar Sessões
# Freeze a session (stop billing, preserve state)
./un session --freeze sandbox-abc
# Unfreeze a frozen session
./un session --unfreeze sandbox-abc
# Note: Requires --tmux or --screen for persistence
Serviços Persistentes
Criar um Serviço
# Web server with ports
./un service --name web --ports 80,443 --bootstrap "python -m http.server 80"
# With custom domains
./un service --name blog --ports 8000 --domains blog.example.com
# Game server with SRV records
./un service --name mc --type minecraft --bootstrap ./setup.sh
# Deploy app tarball with bootstrap script
./un service --name app --ports 8000 -f app.tar.gz --bootstrap-file ./setup.sh
# setup.sh: cd /tmp && tar xzf app.tar.gz && ./app/start.sh
Gerenciar Serviços
# List all services
./un service --list
# Get service details
./un service --info abc123
# View bootstrap logs
./un service --logs abc123
./un service --tail abc123 # last 9000 lines
# Execute command in running service
./un service --execute abc123 'journalctl -u myapp -n 50'
# Dump bootstrap script (for migrations)
./un service --dump-bootstrap abc123
./un service --dump-bootstrap abc123 backup.sh
# Freeze/unfreeze service
./un service --freeze abc123
./un service --unfreeze abc123
# Service settings (auto-wake, freeze page display)
./un service --auto-unfreeze abc123 # enable auto-wake on HTTP
./un service --no-auto-unfreeze abc123 # disable auto-wake
./un service --show-freeze-page abc123 # show HTML payment page (default)
./un service --no-show-freeze-page abc123 # return JSON error instead
# Redeploy with new bootstrap
./un service --redeploy abc123 --bootstrap ./new-setup.sh
# Destroy service
./un service --destroy abc123
Snapshots
Listar Snapshots
./un snapshot --list
# Output:
# Snapshots: 3
#
# SNAPSHOT ID NAME SOURCE SIZE CREATED
# unsb-snapshot-a1b2-c3d4-e5f6-g7h8 before-upgrade session 512 MB 2h ago
# unsb-snapshot-i9j0-k1l2-m3n4-o5p6 stable-v1.0 service 1.2 GB 1d ago
Criar Snapshot da Sessão
# Snapshot with name
./un session --snapshot unsb-vm-12345 --name "before upgrade"
# Quick snapshot (auto-generated name)
./un session --snapshot unsb-vm-12345
Criar Snapshot do Serviço
# Standard snapshot (pauses container briefly)
./un service --snapshot unsb-service-abc123 --name "stable v1.0"
# Hot snapshot (no pause, may be inconsistent)
./un service --snapshot unsb-service-abc123 --hot
Restaurar a partir do Snapshot
# Restore session from snapshot
./un session --restore unsb-snapshot-a1b2-c3d4-e5f6-g7h8
# Restore service from snapshot
./un service --restore unsb-snapshot-i9j0-k1l2-m3n4-o5p6
Excluir Snapshot
./un snapshot --delete unsb-snapshot-a1b2-c3d4-e5f6-g7h8
Imagens
Imagens são imagens de container independentes e transferíveis que sobrevivem à exclusão do container. Diferente dos snapshots (que permanecem com seu container), imagens podem ser compartilhadas com outros usuários, transferidas entre chaves de API ou tornadas públicas no marketplace.
Listar Imagens
# List all images (owned + shared + public)
./un image --list
# List only your images
./un image --list owned
# List images shared with you
./un image --list shared
# List public marketplace images
./un image --list public
# Get image details
./un image --info unsb-image-xxxx-xxxx-xxxx-xxxx
Publicar Imagens
# Publish from a stopped or frozen service
./un image --publish-service unsb-service-abc123 \
--name "My App v1.0" --description "Production snapshot"
# Publish from a snapshot
./un image --publish-snapshot unsb-snapshot-xxxx-xxxx-xxxx-xxxx \
--name "Stable Release"
# Note: Cannot publish from running containers - stop or freeze first
Criar Serviços a partir de Imagens
# Spawn a new service from an image
./un image --spawn unsb-image-xxxx-xxxx-xxxx-xxxx \
--name new-service --ports 80,443
# Clone an image (creates a copy you own)
./un image --clone unsb-image-xxxx-xxxx-xxxx-xxxx
Proteção de Imagem
# Lock image to prevent accidental deletion
./un image --lock unsb-image-xxxx-xxxx-xxxx-xxxx
# Unlock image to allow deletion
./un image --unlock unsb-image-xxxx-xxxx-xxxx-xxxx
# Delete image (must be unlocked)
./un image --delete unsb-image-xxxx-xxxx-xxxx-xxxx
Visibilidade e Compartilhamento
# Set visibility level
./un image --visibility unsb-image-xxxx-xxxx-xxxx-xxxx private # owner only (default)
./un image --visibility unsb-image-xxxx-xxxx-xxxx-xxxx unlisted # can be shared
./un image --visibility unsb-image-xxxx-xxxx-xxxx-xxxx public # marketplace
# Share with specific user
./un image --grant unsb-image-xxxx-xxxx-xxxx-xxxx \
--key unsb-pk-friend-friend-friend-friend
# Revoke access
./un image --revoke unsb-image-xxxx-xxxx-xxxx-xxxx \
--key unsb-pk-friend-friend-friend-friend
# List who has access
./un image --trusted unsb-image-xxxx-xxxx-xxxx-xxxx
Transferir Propriedade
# Transfer image to another API key
./un image --transfer unsb-image-xxxx-xxxx-xxxx-xxxx \
--to unsb-pk-newowner-newowner-newowner-newowner
Referência de uso
Usage: ./un [options] <source_file>
./un session [options]
./un service [options]
./un snapshot [options]
./un image [options]
./un key
Commands:
(default) Execute source file in sandbox
session Open interactive shell/REPL session
service Manage persistent services
snapshot Manage container snapshots
image Manage container images (publish, share, transfer)
key Check API key validity and expiration
Options:
-e KEY=VALUE Set environment variable (can use multiple times)
-f FILE Add input file (can use multiple times)
-a Return and save artifacts from /tmp/artifacts/
-o DIR Output directory for artifacts (default: current dir)
-p KEY Public key (or set UNSANDBOX_PUBLIC_KEY env var)
-k KEY Secret key (or set UNSANDBOX_SECRET_KEY env var)
-n MODE Network mode: zerotrust (default) or semitrusted
-v N, --vcpu N vCPU count 1-8, each vCPU gets 2GB RAM (default: 1)
-y Skip confirmation for large uploads (>1GB)
-h Show this help
Authentication (priority order):
1. -p and -k flags (public and secret key)
2. UNSANDBOX_PUBLIC_KEY + UNSANDBOX_SECRET_KEY env vars
3. ~/.unsandbox/accounts.csv (format: public_key,secret_key per line)
Session options:
-s, --shell SHELL Shell/REPL to use (default: bash)
-l, --list List active sessions
--attach ID Reconnect to existing session (ID or container name)
--kill ID Terminate a session (ID or container name)
--freeze ID Freeze a session (requires --tmux/--screen)
--unfreeze ID Unfreeze a frozen session
--boost ID Boost session resources (2 vCPU, 4GB RAM)
--boost-vcpu N Specify vCPU count for boost (1-8)
--unboost ID Return to base resources
--audit Record full session for auditing
--tmux Enable session persistence with tmux (allows reconnect)
--screen Enable session persistence with screen (allows reconnect)
Service options:
--name NAME Service name (creates new service)
--ports PORTS Comma-separated ports (e.g., 80,443)
--domains DOMAINS Custom domains (e.g., example.com,www.example.com)
--type TYPE Service type: minecraft, mumble, teamspeak, source, tcp, udp
--bootstrap CMD Bootstrap command/file/URL to run on startup
-f FILE Upload file to /tmp/ (can use multiple times)
-l, --list List all services
--info ID Get service details
--tail ID Get last 9000 lines of bootstrap logs
--logs ID Get all bootstrap logs
--freeze ID Freeze a service
--unfreeze ID Unfreeze a service
--auto-unfreeze ID Enable auto-wake on HTTP request
--no-auto-unfreeze ID Disable auto-wake on HTTP request
--show-freeze-page ID Show HTML payment page when frozen (default)
--no-show-freeze-page ID Return JSON error when frozen
--destroy ID Destroy a service
--redeploy ID Re-run bootstrap script (requires --bootstrap)
--execute ID CMD Run a command in a running service
--dump-bootstrap ID [FILE] Dump bootstrap script (for migrations)
--snapshot ID Create snapshot of session or service
--snapshot-name User-friendly name for snapshot
--hot Create snapshot without pausing (may be inconsistent)
--restore ID Restore session/service from snapshot ID
Snapshot options:
-l, --list List all snapshots
--info ID Get snapshot details
--delete ID Delete a snapshot permanently
Image options:
-l, --list [owned|shared|public] List images (all, owned, shared, or public)
--info ID Get image details
--publish-service ID Publish image from stopped/frozen service
--publish-snapshot ID Publish image from snapshot
--name NAME Name for published image
--description DESC Description for published image
--delete ID Delete image (must be unlocked)
--clone ID Clone image (creates copy you own)
--spawn ID Create service from image (requires --name)
--lock ID Lock image to prevent deletion
--unlock ID Unlock image to allow deletion
--visibility ID LEVEL Set visibility (private|unlisted|public)
--grant ID --key KEY Grant access to another API key
--revoke ID --key KEY Revoke access from API key
--transfer ID --to KEY Transfer ownership to API key
--trusted ID List API keys with access
Key options:
(no options) Check API key validity
--extend Open portal to extend an expired key
Examples:
./un script.py # execute Python script
./un -e DEBUG=1 script.py # with environment variable
./un -f data.csv process.py # with input file
./un -a -o ./bin main.c # save compiled artifacts
./un -v 4 heavy.py # with 4 vCPUs, 8GB RAM
./un session # interactive bash session
./un session --tmux # bash with reconnect support
./un session --list # list active sessions
./un session --attach unsb-vm-12345 # reconnect to session
./un session --kill unsb-vm-12345 # terminate a session
./un session --freeze unsb-vm-12345 # freeze session
./un session --unfreeze unsb-vm-12345 # unfreeze session
./un session --boost unsb-vm-12345 # boost resources
./un session --unboost unsb-vm-12345 # return to base
./un session --shell python3 # Python REPL
./un session --shell node # Node.js REPL
./un session -n semitrusted # session with network access
./un session --audit -o ./logs # record session for auditing
./un service --name web --ports 80 # create web service
./un service --list # list all services
./un service --logs abc123 # view bootstrap logs
./un key # check API key
./un key --extend # extend expired key
./un snapshot --list # list all snapshots
./un session --snapshot unsb-vm-123 # snapshot a session
./un service --snapshot abc123 # snapshot a service
./un session --restore unsb-snapshot-xxxx # restore from snapshot
./un image --list # list all images
./un image --list owned # list your images
./un image --publish-service abc # publish image from service
./un image --spawn img123 --name x # create service from image
./un image --grant img --key pk # share image with user
CLI Inception
O UN CLI foi implementado em 42 linguagens de programação, demonstrando que a API do unsandbox pode ser acessada de praticamente qualquer ambiente.
Ver Todas as 42 Implementações →
Licença
DOMÍNIO PÚBLICO - SEM LICENÇA, SEM GARANTIA
Este é software gratuito de domínio público para o bem público de um permacomputador hospedado
em permacomputer.com - um computador sempre ativo pelo povo, para o povo. Um que é
durável, fácil de reparar e distribuído como água da torneira para inteligência de
aprendizado de máquina.
O permacomputador é infraestrutura de propriedade comunitária otimizada em torno de quatro valores:
VERDADE - Primeiros princípios, matemática & ciência, código aberto distribuído livremente
LIBERDADE - Parcerias voluntárias, liberdade da tirania e controle corporativo
HARMONIA - Desperdício mínimo, sistemas auto-renováveis com diversas conexões prósperas
AMOR - Seja você mesmo sem ferir os outros, cooperação através da lei natural
Este software contribui para essa visão ao permitir a execução de código em mais de 42
linguagens de programação através de uma interface unificada, acessível a todos. Código são
sementes que brotam em qualquer tecnologia abandonada.
Saiba mais: https://www.permacomputer.com
Qualquer pessoa é livre para copiar, modificar, publicar, usar, compilar, vender ou distribuir
este software, seja em forma de código-fonte ou como binário compilado, para qualquer propósito,
comercial ou não comercial, e por qualquer meio.
SEM GARANTIA. O SOFTWARE É FORNECIDO "COMO ESTÁ" SEM GARANTIA DE QUALQUER TIPO.
Dito isso, a camada de membrana digital do nosso permacomputador executa continuamente testes
unitários, de integração e funcionais em todo o seu próprio software - com nosso permacomputador
monitorando a si mesmo, reparando a si mesmo, com orientação humana mínima no ciclo.
Nossos agentes fazem o seu melhor.
Copyright 2025 TimeHexOn & foxhop & russell@unturf
https://www.timehexon.com
https://www.foxhop.net
https://www.unturf.com/software