CLI
Fast command-line client for code execution and interactive sessions. 42+ languages, 30+ shells/REPLs.
Official OpenAPI Swagger Docs ↗Quick Start — Java
# Download + setup
curl -O https://git.unturf.com/engineering/unturf/un-inception/-/raw/main/clients/java/sync/src/Un.java
export UNSANDBOX_PUBLIC_KEY="unsb-pk-xxxx-xxxx-xxxx-xxxx"
export UNSANDBOX_SECRET_KEY="unsb-sk-xxxxx-xxxxx-xxxxx-xxxxx"
# Run code
./un script.java
Downloads
Install Guide →Features
- 42+ languages - Python, JS, Go, Rust, C++, Java...
- Sessions - 30+ shells/REPLs, tmux persistence
- Files - Upload files, collect artifacts
- Services - Persistent containers with domains
- Snapshots - Point-in-time backups
- Images - Publish, share, transfer
Integration Quickstart ⚡
Add unsandbox superpowers to your existing Java app:
curl -O https://git.unturf.com/engineering/unturf/un-inception/-/raw/main/clients/java/sync/src/Un.java
# 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 Java app:
import static Un.*;
public class MyApp {
public static void main(String[] args) {
var result = executeCode("java", "System.out.println(\"Hello from Java running on unsandbox!\");");
System.out.println(result.get("stdout")); // Hello from Java running on unsandbox!
}
}
javac MyApp.java Un.java && java MyApp
26af1326319e8cec2fb3a7ffc8cdf63f
SHA256: 2eb883e4a1ef28a9f7870d2d74e91ba1999b7b8009ebcaf7d641bc5b117e7ebe
/* PUBLIC DOMAIN - NO LICENSE, NO WARRANTY
*
* unsandbox.com Java SDK (Synchronous)
*
* Library Usage:
* import Un;
* import java.util.Map;
* import java.util.List;
*
* // Execute code synchronously
* Map<String, Object> result = Un.executeCode("python", "print('hello')", publicKey, secretKey);
*
* // Execute asynchronously
* String jobId = Un.executeAsync("javascript", "console.log('hello')", publicKey, secretKey);
*
* // Wait for job completion with exponential backoff
* Map<String, Object> result = Un.waitForJob(jobId, publicKey, secretKey, 60000);
*
* // List all jobs
* List<Map<String, Object>> jobs = Un.listJobs(publicKey, secretKey);
*
* // Get supported languages
* List<String> languages = Un.getLanguages(publicKey, secretKey);
*
* // Snapshot operations
* String snapshotId = Un.sessionSnapshot(sessionId, publicKey, secretKey, "my-snapshot", false);
*
* Authentication Priority (4-tier):
* 1. Method arguments (publicKey, secretKey)
* 2. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY)
* 3. Config file (~/.unsandbox/accounts.csv, line 0 by default)
* 4. Local directory (./accounts.csv, line 0 by default)
*
* Request Authentication (HMAC-SHA256):
* Authorization: Bearer <public_key>
* X-Timestamp: <unix_seconds>
* X-Signature: HMAC-SHA256(secret_key, "timestamp:METHOD:path:body")
*
* Languages Cache:
* - Cached in ~/.unsandbox/languages.json
* - TTL: 1 hour
* - Updated on successful API calls
*/
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.*;
/**
* Un SDK - Synchronous Java client for the unsandbox.com API.
*
* <p>This class provides methods to execute code in secure sandboxed environments,
* manage jobs, and work with snapshots.
*
* <p>Example usage:
* <pre>{@code
* Map<String, Object> result = Un.executeCode("python", "print('hello')", null, null);
* System.out.println(result.get("stdout"));
* }</pre>
*
* @see <a href="https://unsandbox.com">unsandbox.com</a>
*/
public class Un {
private static final String API_BASE = "https://api.unsandbox.com";
private static final int[] POLL_DELAYS_MS = {300, 450, 700, 900, 650, 1600, 2000};
private static final long LANGUAGES_CACHE_TTL_MS = 3600 * 1000; // 1 hour
private static final int DEFAULT_TIMEOUT_MS = 120000; // 2 minutes
/**
* Exception thrown when credentials cannot be found or are invalid.
*/
public static class CredentialsException extends RuntimeException {
public CredentialsException(String message) {
super(message);
}
}
/**
* Exception thrown when an API request fails.
*/
public static class ApiException extends RuntimeException {
private final int statusCode;
private final String responseBody;
public ApiException(String message, int statusCode, String responseBody) {
super(message);
this.statusCode = statusCode;
this.responseBody = responseBody;
}
public int getStatusCode() {
return statusCode;
}
public String getResponseBody() {
return responseBody;
}
}
/**
* Exception thrown when a 428 sudo challenge is received.
* This indicates a destructive operation requires OTP confirmation.
*/
public static class SudoChallengeException extends RuntimeException {
private final String challengeId;
private final String responseBody;
public SudoChallengeException(String challengeId, String responseBody) {
super("Sudo challenge required");
this.challengeId = challengeId;
this.responseBody = responseBody;
}
public String getChallengeId() {
return challengeId;
}
public String getResponseBody() {
return responseBody;
}
}
// ========================================================================
// Credential Resolution
// ========================================================================
private static Path getUnsandboxDir() {
String home = System.getProperty("user.home");
Path unsandboxDir = Paths.get(home, ".unsandbox");
try {
if (!Files.exists(unsandboxDir)) {
Files.createDirectories(unsandboxDir);
}
} catch (IOException e) {
// Ignore - will fail later if needed
}
return unsandboxDir;
}
private static String[] loadCredentialsFromCsv(Path csvPath, int accountIndex) {
if (!Files.exists(csvPath)) {
return null;
}
try (BufferedReader reader = Files.newBufferedReader(csvPath)) {
String line;
int lineIndex = 0;
while ((line = reader.readLine()) != null) {
line = line.trim();
if (line.isEmpty() || line.startsWith("#")) {
continue;
}
if (lineIndex == accountIndex) {
String[] parts = line.split(",");
if (parts.length >= 2) {
return new String[]{parts[0].trim(), parts[1].trim()};
}
}
lineIndex++;
}
} catch (IOException e) {
// Ignore - will try next source
}
return null;
}
private static String[] resolveCredentials(String publicKey, String secretKey) {
// Tier 1: Method arguments
if (publicKey != null && !publicKey.isEmpty() && secretKey != null && !secretKey.isEmpty()) {
return new String[]{publicKey, secretKey};
}
// Tier 2: Environment variables
String envPk = System.getenv("UNSANDBOX_PUBLIC_KEY");
String envSk = System.getenv("UNSANDBOX_SECRET_KEY");
if (envPk != null && !envPk.isEmpty() && envSk != null && !envSk.isEmpty()) {
return new String[]{envPk, envSk};
}
// Determine account index
int accountIndex = 0;
String accountEnv = System.getenv("UNSANDBOX_ACCOUNT");
if (accountEnv != null && !accountEnv.isEmpty()) {
try {
accountIndex = Integer.parseInt(accountEnv);
} catch (NumberFormatException e) {
// Use default
}
}
// Tier 3: ~/.unsandbox/accounts.csv
Path unsandboxDir = getUnsandboxDir();
String[] creds = loadCredentialsFromCsv(unsandboxDir.resolve("accounts.csv"), accountIndex);
if (creds != null) {
return creds;
}
// Tier 4: ./accounts.csv
creds = loadCredentialsFromCsv(Paths.get("accounts.csv"), accountIndex);
if (creds != null) {
return creds;
}
throw new CredentialsException(
"No credentials found. Please provide via:\n" +
" 1. Method arguments (publicKey, secretKey)\n" +
" 2. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY)\n" +
" 3. ~/.unsandbox/accounts.csv\n" +
" 4. ./accounts.csv"
);
}
// ========================================================================
// HMAC-SHA256 Signing
// ========================================================================
private static String signRequest(String secretKey, long timestamp, String method, String path, String body) {
try {
String bodyStr = (body != null) ? body : "";
String message = timestamp + ":" + method + ":" + path + ":" + bodyStr;
Mac mac = Mac.getInstance("HmacSHA256");
SecretKeySpec secretKeySpec = new SecretKeySpec(
secretKey.getBytes(StandardCharsets.UTF_8),
"HmacSHA256"
);
mac.init(secretKeySpec);
byte[] hash = mac.doFinal(message.getBytes(StandardCharsets.UTF_8));
// Convert to lowercase hex
StringBuilder hexString = new StringBuilder();
for (byte b : hash) {
hexString.append(String.format("%02x", b));
}
return hexString.toString();
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
throw new RuntimeException("Failed to compute HMAC-SHA256", e);
}
}
// ========================================================================
// HTTP Request
// ========================================================================
private static Map<String, Object> makeRequest(
String method,
String path,
String publicKey,
String secretKey,
Map<String, Object> data
) throws IOException {
String url = API_BASE + path;
long timestamp = System.currentTimeMillis() / 1000;
String body = (data != null) ? mapToJson(data) : "";
String signature = signRequest(secretKey, timestamp, method, path, data != null ? body : null);
HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
conn.setRequestMethod(method);
conn.setConnectTimeout(DEFAULT_TIMEOUT_MS);
conn.setReadTimeout(DEFAULT_TIMEOUT_MS);
conn.setRequestProperty("Authorization", "Bearer " + publicKey);
conn.setRequestProperty("X-Timestamp", String.valueOf(timestamp));
conn.setRequestProperty("X-Signature", signature);
conn.setRequestProperty("Content-Type", "application/json");
if ("POST".equals(method) && data != null) {
conn.setDoOutput(true);
try (OutputStream os = conn.getOutputStream()) {
os.write(body.getBytes(StandardCharsets.UTF_8));
}
}
int responseCode = conn.getResponseCode();
String responseBody;
InputStream inputStream = (responseCode >= 200 && responseCode < 300)
? conn.getInputStream()
: conn.getErrorStream();
if (inputStream == null) {
responseBody = "";
} else {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) {
StringBuilder sb = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
sb.append(line);
}
responseBody = sb.toString();
}
}
if (responseCode == 428) {
// Extract challenge_id from response
String challengeId = null;
try {
Map<String, Object> errorJson = parseJson(responseBody);
Object cid = errorJson.get("challenge_id");
if (cid != null) {
challengeId = cid.toString();
}
} catch (Exception e) {
// Ignore parse errors
}
throw new SudoChallengeException(challengeId, responseBody);
}
if (responseCode < 200 || responseCode >= 300) {
throw new ApiException(
"API request failed with status " + responseCode,
responseCode,
responseBody
);
}
return parseJson(responseBody);
}
// ========================================================================
// Sudo Challenge Handling
// ========================================================================
/**
* Make an HTTP request with sudo headers for OTP verification.
*/
private static Map<String, Object> makeRequestWithSudo(
String method,
String path,
String publicKey,
String secretKey,
Map<String, Object> data,
String otp,
String challengeId
) throws IOException {
String url = API_BASE + path;
long timestamp = System.currentTimeMillis() / 1000;
String body = (data != null) ? mapToJson(data) : "";
String signature = signRequest(secretKey, timestamp, method, path, data != null ? body : null);
HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
conn.setRequestMethod(method);
conn.setConnectTimeout(DEFAULT_TIMEOUT_MS);
conn.setReadTimeout(DEFAULT_TIMEOUT_MS);
conn.setRequestProperty("Authorization", "Bearer " + publicKey);
conn.setRequestProperty("X-Timestamp", String.valueOf(timestamp));
conn.setRequestProperty("X-Signature", signature);
conn.setRequestProperty("Content-Type", "application/json");
conn.setRequestProperty("X-Sudo-OTP", otp);
if (challengeId != null) {
conn.setRequestProperty("X-Sudo-Challenge", challengeId);
}
if ("POST".equals(method) && data != null) {
conn.setDoOutput(true);
try (OutputStream os = conn.getOutputStream()) {
os.write(body.getBytes(StandardCharsets.UTF_8));
}
} else if ("DELETE".equals(method)) {
conn.setRequestMethod("DELETE");
}
int responseCode = conn.getResponseCode();
String responseBody;
InputStream inputStream = (responseCode >= 200 && responseCode < 300)
? conn.getInputStream()
: conn.getErrorStream();
if (inputStream == null) {
responseBody = "";
} else {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) {
StringBuilder sb = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
sb.append(line);
}
responseBody = sb.toString();
}
}
if (responseCode < 200 || responseCode >= 300) {
throw new ApiException(
"API request failed with status " + responseCode,
responseCode,
responseBody
);
}
return parseJson(responseBody);
}
/**
* Prompt user for OTP and retry a destructive operation.
* Called when a 428 Sudo Challenge is received.
*
* @param challengeId The challenge ID from the 428 response
* @param method HTTP method (DELETE or POST)
* @param path API endpoint path
* @param publicKey API public key
* @param secretKey API secret key
* @param data Request body data (can be null)
* @return Response map on success
* @throws IOException on network errors
*/
private static Map<String, Object> handleSudoChallenge(
String challengeId,
String method,
String path,
String publicKey,
String secretKey,
Map<String, Object> data
) throws IOException {
System.err.println("\033[33mConfirmation required. Check your email for a one-time code.\033[0m");
System.err.print("Enter OTP: ");
System.err.flush();
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
String otp = reader.readLine();
if (otp == null || otp.trim().isEmpty()) {
throw new RuntimeException("Operation cancelled - no OTP provided");
}
otp = otp.trim();
return makeRequestWithSudo(method, path, publicKey, secretKey, data, otp, challengeId);
}
/**
* Execute a destructive operation with 428 sudo challenge handling.
* If the API returns 428, prompts for OTP and retries.
*
* @param method HTTP method
* @param path API endpoint path
* @param publicKey API public key
* @param secretKey API secret key
* @param data Request body data (can be null)
* @return Response map on success
* @throws IOException on network errors
*/
private static Map<String, Object> makeDestructiveRequest(
String method,
String path,
String publicKey,
String secretKey,
Map<String, Object> data
) throws IOException {
try {
return makeRequest(method, path, publicKey, secretKey, data);
} catch (SudoChallengeException e) {
return handleSudoChallenge(e.getChallengeId(), method, path, publicKey, secretKey, data);
}
}
// ========================================================================
// Simple JSON Serialization/Deserialization
// ========================================================================
@SuppressWarnings("unchecked")
private static Map<String, Object> parseJson(String json) {
if (json == null || json.trim().isEmpty()) {
return new HashMap<>();
}
json = json.trim();
if (!json.startsWith("{")) {
throw new RuntimeException("Invalid JSON: expected object");
}
return (Map<String, Object>) parseValue(json, new int[]{0});
}
private static Object parseValue(String json, int[] pos) {
skipWhitespace(json, pos);
char c = json.charAt(pos[0]);
if (c == '{') {
return parseObject(json, pos);
} else if (c == '[') {
return parseArray(json, pos);
} else if (c == '"') {
return parseString(json, pos);
} else if (c == 't' || c == 'f') {
return parseBoolean(json, pos);
} else if (c == 'n') {
return parseNull(json, pos);
} else if (c == '-' || Character.isDigit(c)) {
return parseNumber(json, pos);
}
throw new RuntimeException("Unexpected character at position " + pos[0]);
}
private static void skipWhitespace(String json, int[] pos) {
while (pos[0] < json.length() && Character.isWhitespace(json.charAt(pos[0]))) {
pos[0]++;
}
}
private static Map<String, Object> parseObject(String json, int[] pos) {
Map<String, Object> result = new LinkedHashMap<>();
pos[0]++; // skip '{'
skipWhitespace(json, pos);
if (json.charAt(pos[0]) == '}') {
pos[0]++;
return result;
}
while (true) {
skipWhitespace(json, pos);
String key = parseString(json, pos);
skipWhitespace(json, pos);
if (json.charAt(pos[0]) != ':') {
throw new RuntimeException("Expected ':' at position " + pos[0]);
}
pos[0]++;
Object value = parseValue(json, pos);
result.put(key, value);
skipWhitespace(json, pos);
char c = json.charAt(pos[0]);
if (c == '}') {
pos[0]++;
break;
} else if (c == ',') {
pos[0]++;
} else {
throw new RuntimeException("Expected ',' or '}' at position " + pos[0]);
}
}
return result;
}
private static List<Object> parseArray(String json, int[] pos) {
List<Object> result = new ArrayList<>();
pos[0]++; // skip '['
skipWhitespace(json, pos);
if (json.charAt(pos[0]) == ']') {
pos[0]++;
return result;
}
while (true) {
result.add(parseValue(json, pos));
skipWhitespace(json, pos);
char c = json.charAt(pos[0]);
if (c == ']') {
pos[0]++;
break;
} else if (c == ',') {
pos[0]++;
} else {
throw new RuntimeException("Expected ',' or ']' at position " + pos[0]);
}
}
return result;
}
private static String parseString(String json, int[] pos) {
pos[0]++; // skip opening quote
StringBuilder sb = new StringBuilder();
while (pos[0] < json.length()) {
char c = json.charAt(pos[0]);
if (c == '"') {
pos[0]++;
return sb.toString();
} else if (c == '\\') {
pos[0]++;
if (pos[0] < json.length()) {
char escaped = json.charAt(pos[0]);
switch (escaped) {
case '"': sb.append('"'); break;
case '\\': sb.append('\\'); break;
case '/': sb.append('/'); break;
case 'b': sb.append('\b'); break;
case 'f': sb.append('\f'); break;
case 'n': sb.append('\n'); break;
case 'r': sb.append('\r'); break;
case 't': sb.append('\t'); break;
case 'u':
String hex = json.substring(pos[0] + 1, pos[0] + 5);
sb.append((char) Integer.parseInt(hex, 16));
pos[0] += 4;
break;
default: sb.append(escaped);
}
}
} else {
sb.append(c);
}
pos[0]++;
}
throw new RuntimeException("Unterminated string");
}
private static Object parseNumber(String json, int[] pos) {
int start = pos[0];
boolean isDouble = false;
if (json.charAt(pos[0]) == '-') pos[0]++;
while (pos[0] < json.length() && Character.isDigit(json.charAt(pos[0]))) pos[0]++;
if (pos[0] < json.length() && json.charAt(pos[0]) == '.') {
isDouble = true;
pos[0]++;
while (pos[0] < json.length() && Character.isDigit(json.charAt(pos[0]))) pos[0]++;
}
if (pos[0] < json.length() && (json.charAt(pos[0]) == 'e' || json.charAt(pos[0]) == 'E')) {
isDouble = true;
pos[0]++;
if (pos[0] < json.length() && (json.charAt(pos[0]) == '+' || json.charAt(pos[0]) == '-')) pos[0]++;
while (pos[0] < json.length() && Character.isDigit(json.charAt(pos[0]))) pos[0]++;
}
String numStr = json.substring(start, pos[0]);
if (isDouble) {
return Double.parseDouble(numStr);
} else {
long value = Long.parseLong(numStr);
if (value >= Integer.MIN_VALUE && value <= Integer.MAX_VALUE) {
return (int) value;
}
return value;
}
}
private static Boolean parseBoolean(String json, int[] pos) {
if (json.startsWith("true", pos[0])) {
pos[0] += 4;
return true;
} else if (json.startsWith("false", pos[0])) {
pos[0] += 5;
return false;
}
throw new RuntimeException("Invalid boolean at position " + pos[0]);
}
private static Object parseNull(String json, int[] pos) {
if (json.startsWith("null", pos[0])) {
pos[0] += 4;
return null;
}
throw new RuntimeException("Invalid null at position " + pos[0]);
}
private static String mapToJson(Map<String, Object> map) {
StringBuilder sb = new StringBuilder();
sb.append("{");
boolean first = true;
for (Map.Entry<String, Object> entry : map.entrySet()) {
if (!first) sb.append(",");
first = false;
sb.append("\"").append(escapeJsonString(entry.getKey())).append("\":");
sb.append(valueToJson(entry.getValue()));
}
sb.append("}");
return sb.toString();
}
private static String valueToJson(Object value) {
if (value == null) {
return "null";
} else if (value instanceof String) {
return "\"" + escapeJsonString((String) value) + "\"";
} else if (value instanceof Number) {
return value.toString();
} else if (value instanceof Boolean) {
return value.toString();
} else if (value instanceof Map) {
@SuppressWarnings("unchecked")
Map<String, Object> map = (Map<String, Object>) value;
return mapToJson(map);
} else if (value instanceof List) {
StringBuilder sb = new StringBuilder();
sb.append("[");
boolean first = true;
for (Object item : (List<?>) value) {
if (!first) sb.append(",");
first = false;
sb.append(valueToJson(item));
}
sb.append("]");
return sb.toString();
}
return "\"" + escapeJsonString(value.toString()) + "\"";
}
private static String escapeJsonString(String s) {
StringBuilder sb = new StringBuilder();
for (char c : s.toCharArray()) {
switch (c) {
case '"': sb.append("\\\""); break;
case '\\': sb.append("\\\\"); break;
case '\b': sb.append("\\b"); break;
case '\f': sb.append("\\f"); break;
case '\n': sb.append("\\n"); break;
case '\r': sb.append("\\r"); break;
case '\t': sb.append("\\t"); break;
default:
if (c < 0x20) {
sb.append(String.format("\\u%04x", (int) c));
} else {
sb.append(c);
}
}
}
return sb.toString();
}
// ========================================================================
// Languages Cache
// ========================================================================
private static Path getLanguagesCachePath() {
return getUnsandboxDir().resolve("languages.json");
}
@SuppressWarnings("unchecked")
private static List<String> loadLanguagesCache() {
Path cachePath = getLanguagesCachePath();
if (!Files.exists(cachePath)) {
return null;
}
try {
long mtime = Files.getLastModifiedTime(cachePath).toMillis();
long ageMs = System.currentTimeMillis() - mtime;
if (ageMs >= LANGUAGES_CACHE_TTL_MS) {
return null;
}
String content = new String(Files.readAllBytes(cachePath), StandardCharsets.UTF_8);
Map<String, Object> data = parseJson(content);
Object languages = data.get("languages");
if (languages instanceof List) {
List<String> result = new ArrayList<>();
for (Object item : (List<?>) languages) {
if (item instanceof String) {
result.add((String) item);
}
}
return result;
}
} catch (IOException e) {
// Cache failure is non-fatal
}
return null;
}
private static void saveLanguagesCache(List<String> languages) {
try {
Path cachePath = getLanguagesCachePath();
Map<String, Object> data = new LinkedHashMap<>();
data.put("languages", languages);
data.put("timestamp", System.currentTimeMillis() / 1000);
Files.write(cachePath, mapToJson(data).getBytes(StandardCharsets.UTF_8));
} catch (IOException e) {
// Cache failure is non-fatal
}
}
// ========================================================================
// Language Detection
// ========================================================================
private static final Map<String, String> LANGUAGE_MAP = new HashMap<>();
static {
LANGUAGE_MAP.put("py", "python");
LANGUAGE_MAP.put("js", "javascript");
LANGUAGE_MAP.put("ts", "typescript");
LANGUAGE_MAP.put("rb", "ruby");
LANGUAGE_MAP.put("php", "php");
LANGUAGE_MAP.put("pl", "perl");
LANGUAGE_MAP.put("sh", "bash");
LANGUAGE_MAP.put("r", "r");
LANGUAGE_MAP.put("lua", "lua");
LANGUAGE_MAP.put("go", "go");
LANGUAGE_MAP.put("rs", "rust");
LANGUAGE_MAP.put("c", "c");
LANGUAGE_MAP.put("cpp", "cpp");
LANGUAGE_MAP.put("cc", "cpp");
LANGUAGE_MAP.put("cxx", "cpp");
LANGUAGE_MAP.put("java", "java");
LANGUAGE_MAP.put("kt", "kotlin");
LANGUAGE_MAP.put("m", "objc");
LANGUAGE_MAP.put("cs", "csharp");
LANGUAGE_MAP.put("fs", "fsharp");
LANGUAGE_MAP.put("hs", "haskell");
LANGUAGE_MAP.put("ml", "ocaml");
LANGUAGE_MAP.put("clj", "clojure");
LANGUAGE_MAP.put("scm", "scheme");
LANGUAGE_MAP.put("ss", "scheme");
LANGUAGE_MAP.put("erl", "erlang");
LANGUAGE_MAP.put("ex", "elixir");
LANGUAGE_MAP.put("exs", "elixir");
LANGUAGE_MAP.put("jl", "julia");
LANGUAGE_MAP.put("d", "d");
LANGUAGE_MAP.put("nim", "nim");
LANGUAGE_MAP.put("zig", "zig");
LANGUAGE_MAP.put("v", "v");
LANGUAGE_MAP.put("cr", "crystal");
LANGUAGE_MAP.put("dart", "dart");
LANGUAGE_MAP.put("groovy", "groovy");
LANGUAGE_MAP.put("f90", "fortran");
LANGUAGE_MAP.put("f95", "fortran");
LANGUAGE_MAP.put("lisp", "commonlisp");
LANGUAGE_MAP.put("lsp", "commonlisp");
LANGUAGE_MAP.put("cob", "cobol");
LANGUAGE_MAP.put("tcl", "tcl");
LANGUAGE_MAP.put("raku", "raku");
LANGUAGE_MAP.put("pro", "prolog");
LANGUAGE_MAP.put("p", "prolog");
LANGUAGE_MAP.put("4th", "forth");
LANGUAGE_MAP.put("forth", "forth");
LANGUAGE_MAP.put("fth", "forth");
}
/**
* Detect programming language from filename extension.
*
* @param filename Filename to detect language from (e.g., "script.py")
* @return Language identifier (e.g., "python") or null if unknown
*/
public static String detectLanguage(String filename) {
if (filename == null || !filename.contains(".")) {
return null;
}
String ext = filename.substring(filename.lastIndexOf('.') + 1).toLowerCase();
return LANGUAGE_MAP.get(ext);
}
// ========================================================================
// Public API Methods
// ========================================================================
/**
* Execute code synchronously (blocks until completion).
*
* @param language Programming language (e.g., "python", "javascript", "go")
* @param code Source code to execute
* @param publicKey Optional API key (uses credentials resolution if null)
* @param secretKey Optional API secret (uses credentials resolution if null)
* @return Response map containing stdout, stderr, exit code, etc.
* @throws IOException on network errors
* @throws CredentialsException if credentials cannot be found
* @throws ApiException if API returns an error
*/
public static Map<String, Object> executeCode(
String language,
String code,
String publicKey,
String secretKey
) throws IOException {
String[] creds = resolveCredentials(publicKey, secretKey);
publicKey = creds[0];
secretKey = creds[1];
Map<String, Object> data = new LinkedHashMap<>();
data.put("language", language);
data.put("code", code);
Map<String, Object> response = makeRequest("POST", "/execute", publicKey, secretKey, data);
// If we got a job_id, poll until completion
Object jobIdObj = response.get("job_id");
Object statusObj = response.get("status");
if (jobIdObj != null && statusObj != null) {
String status = statusObj.toString();
if ("pending".equals(status) || "running".equals(status)) {
return waitForJob(jobIdObj.toString(), publicKey, secretKey, 0);
}
}
return response;
}
/**
* Execute code asynchronously (returns immediately with job ID).
*
* @param language Programming language (e.g., "python", "javascript")
* @param code Source code to execute
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @return Job ID string
* @throws IOException on network errors
* @throws CredentialsException if credentials cannot be found
* @throws ApiException if API returns an error
*/
public static String executeAsync(
String language,
String code,
String publicKey,
String secretKey
) throws IOException {
String[] creds = resolveCredentials(publicKey, secretKey);
publicKey = creds[0];
secretKey = creds[1];
Map<String, Object> data = new LinkedHashMap<>();
data.put("language", language);
data.put("code", code);
Map<String, Object> response = makeRequest("POST", "/execute", publicKey, secretKey, data);
Object jobId = response.get("job_id");
return jobId != null ? jobId.toString() : null;
}
/**
* Get current status/result of a job (single poll, no waiting).
*
* @param jobId Job ID from executeAsync()
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @return Job response map
* @throws IOException on network errors
* @throws CredentialsException if credentials cannot be found
* @throws ApiException if API returns an error
*/
public static Map<String, Object> getJob(
String jobId,
String publicKey,
String secretKey
) throws IOException {
String[] creds = resolveCredentials(publicKey, secretKey);
return makeRequest("GET", "/jobs/" + jobId, creds[0], creds[1], null);
}
/**
* Wait for job completion with exponential backoff polling.
*
* <p>Polling delays (ms): [300, 450, 700, 900, 650, 1600, 2000, ...]
*
* @param jobId Job ID from executeAsync()
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @param timeoutMs Maximum time to wait (0 for no timeout)
* @return Final job result when status is terminal (completed, failed, timeout, cancelled)
* @throws IOException on network errors
* @throws CredentialsException if credentials cannot be found
* @throws ApiException if API returns an error
* @throws RuntimeException if timeout exceeded
*/
public static Map<String, Object> waitForJob(
String jobId,
String publicKey,
String secretKey,
long timeoutMs
) throws IOException {
String[] creds = resolveCredentials(publicKey, secretKey);
publicKey = creds[0];
secretKey = creds[1];
long startTime = System.currentTimeMillis();
int pollCount = 0;
while (true) {
// Sleep before polling
int delayIdx = Math.min(pollCount, POLL_DELAYS_MS.length - 1);
try {
Thread.sleep(POLL_DELAYS_MS[delayIdx]);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("Wait interrupted", e);
}
pollCount++;
// Check timeout
if (timeoutMs > 0 && System.currentTimeMillis() - startTime > timeoutMs) {
throw new RuntimeException("Timeout waiting for job " + jobId);
}
Map<String, Object> response = getJob(jobId, publicKey, secretKey);
Object statusObj = response.get("status");
if (statusObj != null) {
String status = statusObj.toString();
if ("completed".equals(status) || "failed".equals(status) ||
"timeout".equals(status) || "cancelled".equals(status)) {
return response;
}
}
// Still running, continue polling
}
}
/**
* Cancel a running job.
*
* @param jobId Job ID to cancel
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @return Response map with cancellation confirmation
* @throws IOException on network errors
* @throws CredentialsException if credentials cannot be found
* @throws ApiException if API returns an error
*/
public static Map<String, Object> cancelJob(
String jobId,
String publicKey,
String secretKey
) throws IOException {
String[] creds = resolveCredentials(publicKey, secretKey);
return makeRequest("DELETE", "/jobs/" + jobId, creds[0], creds[1], null);
}
/**
* List all jobs for the authenticated account.
*
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @return List of job maps
* @throws IOException on network errors
* @throws CredentialsException if credentials cannot be found
* @throws ApiException if API returns an error
*/
@SuppressWarnings("unchecked")
public static List<Map<String, Object>> listJobs(
String publicKey,
String secretKey
) throws IOException {
String[] creds = resolveCredentials(publicKey, secretKey);
Map<String, Object> response = makeRequest("GET", "/jobs", creds[0], creds[1], null);
Object jobs = response.get("jobs");
if (jobs instanceof List) {
List<Map<String, Object>> result = new ArrayList<>();
for (Object item : (List<?>) jobs) {
if (item instanceof Map) {
result.add((Map<String, Object>) item);
}
}
return result;
}
return new ArrayList<>();
}
/**
* Get list of supported programming languages.
*
* <p>Results are cached for 1 hour in ~/.unsandbox/languages.json
*
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @return List of language identifiers (e.g., ["python", "javascript", "go", ...])
* @throws IOException on network errors
* @throws CredentialsException if credentials cannot be found
* @throws ApiException if API returns an error
*/
public static List<String> getLanguages(
String publicKey,
String secretKey
) throws IOException {
// Try cache first
List<String> cached = loadLanguagesCache();
if (cached != null) {
return cached;
}
String[] creds = resolveCredentials(publicKey, secretKey);
Map<String, Object> response = makeRequest("GET", "/languages", creds[0], creds[1], null);
Object languages = response.get("languages");
List<String> result = new ArrayList<>();
if (languages instanceof List) {
for (Object item : (List<?>) languages) {
if (item instanceof String) {
result.add((String) item);
}
}
}
// Cache the result
saveLanguagesCache(result);
return result;
}
/**
* Create a snapshot of a session.
*
* @param sessionId Session ID to snapshot
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @param name Optional snapshot name
* @param ephemeral If true, snapshot is ephemeral (hot snapshot)
* @return Snapshot ID
* @throws IOException on network errors
* @throws CredentialsException if credentials cannot be found
* @throws ApiException if API returns an error
*/
public static String sessionSnapshot(
String sessionId,
String publicKey,
String secretKey,
String name,
boolean ephemeral
) throws IOException {
String[] creds = resolveCredentials(publicKey, secretKey);
Map<String, Object> data = new LinkedHashMap<>();
data.put("session_id", sessionId);
data.put("hot", ephemeral);
if (name != null && !name.isEmpty()) {
data.put("name", name);
}
Map<String, Object> response = makeRequest("POST", "/snapshots", creds[0], creds[1], data);
Object snapshotId = response.get("snapshot_id");
return snapshotId != null ? snapshotId.toString() : null;
}
/**
* Create a snapshot of a service.
*
* @param serviceId Service ID to snapshot
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @param name Optional snapshot name
* @return Snapshot ID
* @throws IOException on network errors
* @throws CredentialsException if credentials cannot be found
* @throws ApiException if API returns an error
*/
public static String serviceSnapshot(
String serviceId,
String publicKey,
String secretKey,
String name
) throws IOException {
String[] creds = resolveCredentials(publicKey, secretKey);
Map<String, Object> data = new LinkedHashMap<>();
data.put("service_id", serviceId);
data.put("hot", false);
if (name != null && !name.isEmpty()) {
data.put("name", name);
}
Map<String, Object> response = makeRequest("POST", "/snapshots", creds[0], creds[1], data);
Object snapshotId = response.get("snapshot_id");
return snapshotId != null ? snapshotId.toString() : null;
}
/**
* List all snapshots.
*
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @return List of snapshot maps
* @throws IOException on network errors
* @throws CredentialsException if credentials cannot be found
* @throws ApiException if API returns an error
*/
@SuppressWarnings("unchecked")
public static List<Map<String, Object>> listSnapshots(
String publicKey,
String secretKey
) throws IOException {
String[] creds = resolveCredentials(publicKey, secretKey);
Map<String, Object> response = makeRequest("GET", "/snapshots", creds[0], creds[1], null);
Object snapshots = response.get("snapshots");
if (snapshots instanceof List) {
List<Map<String, Object>> result = new ArrayList<>();
for (Object item : (List<?>) snapshots) {
if (item instanceof Map) {
result.add((Map<String, Object>) item);
}
}
return result;
}
return new ArrayList<>();
}
/**
* Restore a snapshot.
*
* @param snapshotId Snapshot ID to restore
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @return Response map with restored resource info
* @throws IOException on network errors
* @throws CredentialsException if credentials cannot be found
* @throws ApiException if API returns an error
*/
public static Map<String, Object> restoreSnapshot(
String snapshotId,
String publicKey,
String secretKey
) throws IOException {
String[] creds = resolveCredentials(publicKey, secretKey);
return makeRequest("POST", "/snapshots/" + snapshotId + "/restore", creds[0], creds[1], new LinkedHashMap<>());
}
/**
* Delete a snapshot.
*
* @param snapshotId Snapshot ID to delete
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @return Response map with deletion confirmation
* @throws IOException on network errors
* @throws CredentialsException if credentials cannot be found
* @throws ApiException if API returns an error
*/
public static Map<String, Object> deleteSnapshot(
String snapshotId,
String publicKey,
String secretKey
) throws IOException {
String[] creds = resolveCredentials(publicKey, secretKey);
return makeDestructiveRequest("DELETE", "/snapshots/" + snapshotId, creds[0], creds[1], null);
}
// ========================================================================
// Session API Methods
// ========================================================================
/**
* List all active sessions for the authenticated account.
*
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @return List of session maps containing id, container_name, shell, status, remaining_ttl
* @throws IOException on network errors
* @throws CredentialsException if credentials cannot be found
* @throws ApiException if API returns an error
*/
@SuppressWarnings("unchecked")
public static List<Map<String, Object>> listSessions(
String publicKey,
String secretKey
) throws IOException {
String[] creds = resolveCredentials(publicKey, secretKey);
Map<String, Object> response = makeRequest("GET", "/sessions", creds[0], creds[1], null);
Object sessions = response.get("sessions");
if (sessions instanceof List) {
List<Map<String, Object>> result = new ArrayList<>();
for (Object item : (List<?>) sessions) {
if (item instanceof Map) {
result.add((Map<String, Object>) item);
}
}
return result;
}
return new ArrayList<>();
}
/**
* Get details of a specific session.
*
* @param sessionId Session ID to retrieve
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @return Session details map
* @throws IOException on network errors
* @throws CredentialsException if credentials cannot be found
* @throws ApiException if API returns an error
*/
public static Map<String, Object> getSession(
String sessionId,
String publicKey,
String secretKey
) throws IOException {
String[] creds = resolveCredentials(publicKey, secretKey);
return makeRequest("GET", "/sessions/" + sessionId, creds[0], creds[1], null);
}
/**
* Create a new interactive session.
*
* @param language Programming language/shell for the session (e.g., "bash", "python3")
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @param opts Optional parameters: network_mode, ttl, shell, multiplexer, vcpu
* @return Response map containing session_id, container_name
* @throws IOException on network errors
* @throws CredentialsException if credentials cannot be found
* @throws ApiException if API returns an error
*/
public static Map<String, Object> createSession(
String language,
String publicKey,
String secretKey,
Map<String, Object> opts
) throws IOException {
String[] creds = resolveCredentials(publicKey, secretKey);
Map<String, Object> data = new LinkedHashMap<>();
data.put("network_mode", "zerotrust");
data.put("ttl", 3600);
if (language != null && !language.isEmpty()) {
data.put("shell", language);
}
if (opts != null) {
data.putAll(opts);
}
return makeRequest("POST", "/sessions", creds[0], creds[1], data);
}
/**
* Delete (terminate) a session.
*
* @param sessionId Session ID to terminate
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @return Response map with termination confirmation
* @throws IOException on network errors
* @throws CredentialsException if credentials cannot be found
* @throws ApiException if API returns an error
*/
public static Map<String, Object> deleteSession(
String sessionId,
String publicKey,
String secretKey
) throws IOException {
String[] creds = resolveCredentials(publicKey, secretKey);
return makeRequest("DELETE", "/sessions/" + sessionId, creds[0], creds[1], null);
}
/**
* Freeze a session (pause execution, reduce resource consumption).
*
* @param sessionId Session ID to freeze
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @return Response map with freeze confirmation
* @throws IOException on network errors
* @throws CredentialsException if credentials cannot be found
* @throws ApiException if API returns an error
*/
public static Map<String, Object> freezeSession(
String sessionId,
String publicKey,
String secretKey
) throws IOException {
String[] creds = resolveCredentials(publicKey, secretKey);
return makeRequest("POST", "/sessions/" + sessionId + "/freeze", creds[0], creds[1], new LinkedHashMap<>());
}
/**
* Unfreeze a session (resume execution).
*
* @param sessionId Session ID to unfreeze
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @return Response map with unfreeze confirmation
* @throws IOException on network errors
* @throws CredentialsException if credentials cannot be found
* @throws ApiException if API returns an error
*/
public static Map<String, Object> unfreezeSession(
String sessionId,
String publicKey,
String secretKey
) throws IOException {
String[] creds = resolveCredentials(publicKey, secretKey);
return makeRequest("POST", "/sessions/" + sessionId + "/unfreeze", creds[0], creds[1], new LinkedHashMap<>());
}
/**
* Boost a session's resources (increase vCPU and memory).
*
* @param sessionId Session ID to boost
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @return Response map with boost confirmation
* @throws IOException on network errors
* @throws CredentialsException if credentials cannot be found
* @throws ApiException if API returns an error
*/
public static Map<String, Object> boostSession(
String sessionId,
String publicKey,
String secretKey
) throws IOException {
String[] creds = resolveCredentials(publicKey, secretKey);
Map<String, Object> data = new LinkedHashMap<>();
data.put("vcpu", 2);
return makeRequest("POST", "/sessions/" + sessionId + "/boost", creds[0], creds[1], data);
}
/**
* Remove boost from a session (return to base resources).
*
* @param sessionId Session ID to unboost
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @return Response map with unboost confirmation
* @throws IOException on network errors
* @throws CredentialsException if credentials cannot be found
* @throws ApiException if API returns an error
*/
public static Map<String, Object> unboostSession(
String sessionId,
String publicKey,
String secretKey
) throws IOException {
String[] creds = resolveCredentials(publicKey, secretKey);
return makeRequest("POST", "/sessions/" + sessionId + "/unboost", creds[0], creds[1], new LinkedHashMap<>());
}
/**
* Execute a shell command in an existing session.
*
* @param sessionId Session ID to execute command in
* @param command Shell command to execute
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @return Response map with command output
* @throws IOException on network errors
* @throws CredentialsException if credentials cannot be found
* @throws ApiException if API returns an error
*/
public static Map<String, Object> shellSession(
String sessionId,
String command,
String publicKey,
String secretKey
) throws IOException {
String[] creds = resolveCredentials(publicKey, secretKey);
Map<String, Object> data = new LinkedHashMap<>();
data.put("command", command);
return makeRequest("POST", "/sessions/" + sessionId + "/shell", creds[0], creds[1], data);
}
// ========================================================================
// Service API Methods
// ========================================================================
/**
* List all services for the authenticated account.
*
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @return List of service maps containing id, name, state, ports, disk_used
* @throws IOException on network errors
* @throws CredentialsException if credentials cannot be found
* @throws ApiException if API returns an error
*/
@SuppressWarnings("unchecked")
public static List<Map<String, Object>> listServices(
String publicKey,
String secretKey
) throws IOException {
String[] creds = resolveCredentials(publicKey, secretKey);
Map<String, Object> response = makeRequest("GET", "/services", creds[0], creds[1], null);
Object services = response.get("services");
if (services instanceof List) {
List<Map<String, Object>> result = new ArrayList<>();
for (Object item : (List<?>) services) {
if (item instanceof Map) {
result.add((Map<String, Object>) item);
}
}
return result;
}
return new ArrayList<>();
}
/**
* Create a new persistent service.
*
* @param name Service name
* @param ports Comma-separated list of ports to expose (e.g., "80,443")
* @param bootstrap Bootstrap script or URL to run on service creation
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @return Response map containing service_id
* @throws IOException on network errors
* @throws CredentialsException if credentials cannot be found
* @throws ApiException if API returns an error
*/
public static Map<String, Object> createService(
String name,
String ports,
String bootstrap,
String publicKey,
String secretKey
) throws IOException {
String[] creds = resolveCredentials(publicKey, secretKey);
Map<String, Object> data = new LinkedHashMap<>();
data.put("name", name);
if (ports != null && !ports.isEmpty()) {
// Parse ports string into list
List<Integer> portList = new ArrayList<>();
for (String p : ports.split(",")) {
try {
portList.add(Integer.parseInt(p.trim()));
} catch (NumberFormatException e) {
// Skip invalid port
}
}
data.put("ports", portList);
}
if (bootstrap != null && !bootstrap.isEmpty()) {
if (bootstrap.startsWith("http://") || bootstrap.startsWith("https://")) {
data.put("bootstrap_url", bootstrap);
} else {
data.put("bootstrap", bootstrap);
}
}
return makeRequest("POST", "/services", creds[0], creds[1], data);
}
/**
* Create a new service (long-running container) with unfreeze-on-demand option.
*
* @param name Service name (used for hostname)
* @param ports Comma-separated list of ports to expose (e.g., "80,443")
* @param bootstrap Bootstrap script or URL to run on service creation
* @param unfreezeOnDemand If true, frozen service will auto-wake on HTTP request
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @return Response map containing service_id
* @throws IOException on network errors
* @throws CredentialsException if credentials cannot be found
* @throws ApiException if API returns an error
*/
public static Map<String, Object> createService(
String name,
String ports,
String bootstrap,
boolean unfreezeOnDemand,
String publicKey,
String secretKey
) throws IOException {
String[] creds = resolveCredentials(publicKey, secretKey);
Map<String, Object> data = new LinkedHashMap<>();
data.put("name", name);
if (ports != null && !ports.isEmpty()) {
// Parse ports string into list
List<Integer> portList = new ArrayList<>();
for (String p : ports.split(",")) {
try {
portList.add(Integer.parseInt(p.trim()));
} catch (NumberFormatException e) {
// Skip invalid port
}
}
data.put("ports", portList);
}
if (bootstrap != null && !bootstrap.isEmpty()) {
if (bootstrap.startsWith("http://") || bootstrap.startsWith("https://")) {
data.put("bootstrap_url", bootstrap);
} else {
data.put("bootstrap", bootstrap);
}
}
if (unfreezeOnDemand) {
data.put("unfreeze_on_demand", true);
}
return makeRequest("POST", "/services", creds[0], creds[1], data);
}
/**
* Get details of a specific service.
*
* @param serviceId Service ID to retrieve
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @return Service details map
* @throws IOException on network errors
* @throws CredentialsException if credentials cannot be found
* @throws ApiException if API returns an error
*/
public static Map<String, Object> getService(
String serviceId,
String publicKey,
String secretKey
) throws IOException {
String[] creds = resolveCredentials(publicKey, secretKey);
return makeRequest("GET", "/services/" + serviceId, creds[0], creds[1], null);
}
/**
* Update a service's configuration.
*
* @param serviceId Service ID to update
* @param opts Update options (e.g., vcpu for resizing)
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @return Response map with update confirmation
* @throws IOException on network errors
* @throws CredentialsException if credentials cannot be found
* @throws ApiException if API returns an error
*/
public static Map<String, Object> updateService(
String serviceId,
Map<String, Object> opts,
String publicKey,
String secretKey
) throws IOException {
String[] creds = resolveCredentials(publicKey, secretKey);
return makeRequestWithMethod("PATCH", "/services/" + serviceId, creds[0], creds[1], opts);
}
/**
* Delete (destroy) a service.
*
* @param serviceId Service ID to destroy
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @return Response map with deletion confirmation
* @throws IOException on network errors
* @throws CredentialsException if credentials cannot be found
* @throws ApiException if API returns an error
*/
public static Map<String, Object> deleteService(
String serviceId,
String publicKey,
String secretKey
) throws IOException {
String[] creds = resolveCredentials(publicKey, secretKey);
return makeDestructiveRequest("DELETE", "/services/" + serviceId, creds[0], creds[1], null);
}
/**
* Freeze a service (pause execution, reduce resource consumption).
*
* @param serviceId Service ID to freeze
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @return Response map with freeze confirmation
* @throws IOException on network errors
* @throws CredentialsException if credentials cannot be found
* @throws ApiException if API returns an error
*/
public static Map<String, Object> freezeService(
String serviceId,
String publicKey,
String secretKey
) throws IOException {
String[] creds = resolveCredentials(publicKey, secretKey);
return makeRequest("POST", "/services/" + serviceId + "/freeze", creds[0], creds[1], new LinkedHashMap<>());
}
/**
* Unfreeze a service (resume execution).
*
* @param serviceId Service ID to unfreeze
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @return Response map with unfreeze confirmation
* @throws IOException on network errors
* @throws CredentialsException if credentials cannot be found
* @throws ApiException if API returns an error
*/
public static Map<String, Object> unfreezeService(
String serviceId,
String publicKey,
String secretKey
) throws IOException {
String[] creds = resolveCredentials(publicKey, secretKey);
return makeRequest("POST", "/services/" + serviceId + "/unfreeze", creds[0], creds[1], new LinkedHashMap<>());
}
/**
* Lock a service (prevent modifications and termination).
*
* @param serviceId Service ID to lock
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @return Response map with lock confirmation
* @throws IOException on network errors
* @throws CredentialsException if credentials cannot be found
* @throws ApiException if API returns an error
*/
public static Map<String, Object> lockService(
String serviceId,
String publicKey,
String secretKey
) throws IOException {
String[] creds = resolveCredentials(publicKey, secretKey);
return makeRequest("POST", "/services/" + serviceId + "/lock", creds[0], creds[1], new LinkedHashMap<>());
}
/**
* Unlock a service (allow modifications and termination).
*
* @param serviceId Service ID to unlock
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @return Response map with unlock confirmation
* @throws IOException on network errors
* @throws CredentialsException if credentials cannot be found
* @throws ApiException if API returns an error
*/
public static Map<String, Object> unlockService(
String serviceId,
String publicKey,
String secretKey
) throws IOException {
String[] creds = resolveCredentials(publicKey, secretKey);
return makeDestructiveRequest("POST", "/services/" + serviceId + "/unlock", creds[0], creds[1], new LinkedHashMap<>());
}
/**
* Set unfreeze-on-demand for a service.
*
* <p>When enabled, a frozen service will automatically wake up when it receives
* an HTTP request, without requiring an explicit unfreeze API call.
*
* @param serviceId Service ID to configure
* @param enabled True to enable unfreeze-on-demand, false to disable
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @return Response map with update confirmation
* @throws IOException on network errors
* @throws CredentialsException if credentials cannot be found
* @throws ApiException if API returns an error
*/
public static Map<String, Object> setUnfreezeOnDemand(
String serviceId,
boolean enabled,
String publicKey,
String secretKey
) throws IOException {
String[] creds = resolveCredentials(publicKey, secretKey);
Map<String, Object> data = new LinkedHashMap<>();
data.put("unfreeze_on_demand", enabled);
return makeRequestWithMethod("PATCH", "/services/" + serviceId, creds[0], creds[1], data);
}
/**
* Set show-freeze-page for a service.
*
* <p>When enabled, visitors to a frozen service will see a branded "frozen" page
* instead of an error. This improves UX for services that use unfreeze-on-demand.
*
* @param serviceId Service ID to configure
* @param enabled True to show freeze page, false to hide it
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @return Response map with update confirmation
* @throws IOException on network errors
* @throws CredentialsException if credentials cannot be found
* @throws ApiException if API returns an error
*/
public static Map<String, Object> setShowFreezePage(
String serviceId,
boolean enabled,
String publicKey,
String secretKey
) throws IOException {
String[] creds = resolveCredentials(publicKey, secretKey);
Map<String, Object> data = new LinkedHashMap<>();
data.put("show_freeze_page", enabled);
return makeRequestWithMethod("PATCH", "/services/" + serviceId, creds[0], creds[1], data);
}
/**
* Get bootstrap logs for a service.
*
* @param serviceId Service ID to get logs for
* @param all If true, return all logs; if false, return recent logs only
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @return Response map containing logs
* @throws IOException on network errors
* @throws CredentialsException if credentials cannot be found
* @throws ApiException if API returns an error
*/
public static Map<String, Object> getServiceLogs(
String serviceId,
boolean all,
String publicKey,
String secretKey
) throws IOException {
String[] creds = resolveCredentials(publicKey, secretKey);
String path = "/services/" + serviceId + "/logs";
if (all) {
path += "?all=true";
}
return makeRequest("GET", path, creds[0], creds[1], null);
}
/**
* Get environment vault status for a service.
*
* @param serviceId Service ID to get env status for
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @return Response map containing has_vault, count, updated_at
* @throws IOException on network errors
* @throws CredentialsException if credentials cannot be found
* @throws ApiException if API returns an error
*/
public static Map<String, Object> getServiceEnv(
String serviceId,
String publicKey,
String secretKey
) throws IOException {
String[] creds = resolveCredentials(publicKey, secretKey);
return makeRequest("GET", "/services/" + serviceId + "/env", creds[0], creds[1], null);
}
/**
* Set environment vault for a service.
*
* @param serviceId Service ID to set env for
* @param env Environment variables map (KEY=VALUE pairs)
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @return Response map with set confirmation
* @throws IOException on network errors
* @throws CredentialsException if credentials cannot be found
* @throws ApiException if API returns an error
*/
public static Map<String, Object> setServiceEnv(
String serviceId,
Map<String, String> env,
String publicKey,
String secretKey
) throws IOException {
String[] creds = resolveCredentials(publicKey, secretKey);
Map<String, Object> data = new LinkedHashMap<>();
data.put("env", env);
return makeRequest("POST", "/services/" + serviceId + "/env", creds[0], creds[1], data);
}
/**
* Delete environment variables from a service's vault.
*
* @param serviceId Service ID to delete env from
* @param keys List of keys to delete (null to delete entire vault)
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @return Response map with deletion confirmation
* @throws IOException on network errors
* @throws CredentialsException if credentials cannot be found
* @throws ApiException if API returns an error
*/
public static Map<String, Object> deleteServiceEnv(
String serviceId,
List<String> keys,
String publicKey,
String secretKey
) throws IOException {
String[] creds = resolveCredentials(publicKey, secretKey);
String path = "/services/" + serviceId + "/env";
if (keys != null && !keys.isEmpty()) {
// URL encode keys parameter
path += "?keys=" + String.join(",", keys);
}
return makeRequest("DELETE", path, creds[0], creds[1], null);
}
/**
* Export environment vault for a service (returns decrypted values).
*
* @param serviceId Service ID to export env from
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @return Response map containing exported environment variables
* @throws IOException on network errors
* @throws CredentialsException if credentials cannot be found
* @throws ApiException if API returns an error
*/
public static Map<String, Object> exportServiceEnv(
String serviceId,
String publicKey,
String secretKey
) throws IOException {
String[] creds = resolveCredentials(publicKey, secretKey);
return makeRequest("POST", "/services/" + serviceId + "/env/export", creds[0], creds[1], new LinkedHashMap<>());
}
/**
* Redeploy a service (re-run bootstrap script).
*
* @param serviceId Service ID to redeploy
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @return Response map with redeploy confirmation
* @throws IOException on network errors
* @throws CredentialsException if credentials cannot be found
* @throws ApiException if API returns an error
*/
public static Map<String, Object> redeployService(
String serviceId,
String publicKey,
String secretKey
) throws IOException {
String[] creds = resolveCredentials(publicKey, secretKey);
return makeRequest("POST", "/services/" + serviceId + "/redeploy", creds[0], creds[1], new LinkedHashMap<>());
}
/**
* Execute a command in a service container.
*
* @param serviceId Service ID to execute command in
* @param command Command to execute
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @return Response map containing stdout, stderr, exit_code
* @throws IOException on network errors
* @throws CredentialsException if credentials cannot be found
* @throws ApiException if API returns an error
*/
public static Map<String, Object> executeInService(
String serviceId,
String command,
String publicKey,
String secretKey
) throws IOException {
String[] creds = resolveCredentials(publicKey, secretKey);
Map<String, Object> data = new LinkedHashMap<>();
data.put("command", command);
return makeRequest("POST", "/services/" + serviceId + "/execute", creds[0], creds[1], data);
}
// ========================================================================
// Additional Snapshot API Methods
// ========================================================================
/**
* Lock a snapshot (prevent deletion).
*
* @param snapshotId Snapshot ID to lock
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @return Response map with lock confirmation
* @throws IOException on network errors
* @throws CredentialsException if credentials cannot be found
* @throws ApiException if API returns an error
*/
public static Map<String, Object> lockSnapshot(
String snapshotId,
String publicKey,
String secretKey
) throws IOException {
String[] creds = resolveCredentials(publicKey, secretKey);
return makeRequest("POST", "/snapshots/" + snapshotId + "/lock", creds[0], creds[1], new LinkedHashMap<>());
}
/**
* Unlock a snapshot (allow deletion).
*
* @param snapshotId Snapshot ID to unlock
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @return Response map with unlock confirmation
* @throws IOException on network errors
* @throws CredentialsException if credentials cannot be found
* @throws ApiException if API returns an error
*/
public static Map<String, Object> unlockSnapshot(
String snapshotId,
String publicKey,
String secretKey
) throws IOException {
String[] creds = resolveCredentials(publicKey, secretKey);
return makeDestructiveRequest("POST", "/snapshots/" + snapshotId + "/unlock", creds[0], creds[1], new LinkedHashMap<>());
}
/**
* Clone a snapshot to create a new snapshot with a different name.
*
* @param snapshotId Snapshot ID to clone
* @param name Name for the cloned snapshot
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @return Response map containing new snapshot_id
* @throws IOException on network errors
* @throws CredentialsException if credentials cannot be found
* @throws ApiException if API returns an error
*/
public static Map<String, Object> cloneSnapshot(
String snapshotId,
String name,
String publicKey,
String secretKey
) throws IOException {
String[] creds = resolveCredentials(publicKey, secretKey);
Map<String, Object> data = new LinkedHashMap<>();
if (name != null && !name.isEmpty()) {
data.put("name", name);
}
return makeRequest("POST", "/snapshots/" + snapshotId + "/clone", creds[0], creds[1], data);
}
// ========================================================================
// Images API Methods (LXD Container Images)
// ========================================================================
/**
* Publish a session or service as a reusable LXD container image.
*
* @param sourceType Source type: "session" or "service"
* @param sourceId ID of the session or service to publish
* @param name Name for the image
* @param description Optional description for the image
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @return Response map containing image_id
* @throws IOException on network errors
* @throws CredentialsException if credentials cannot be found
* @throws ApiException if API returns an error
*/
public static Map<String, Object> imagePublish(
String sourceType,
String sourceId,
String name,
String description,
String publicKey,
String secretKey
) throws IOException {
String[] creds = resolveCredentials(publicKey, secretKey);
Map<String, Object> data = new LinkedHashMap<>();
data.put("source_type", sourceType);
data.put("source_id", sourceId);
data.put("name", name);
if (description != null && !description.isEmpty()) {
data.put("description", description);
}
return makeRequest("POST", "/images", creds[0], creds[1], data);
}
/**
* List container images with optional filtering.
*
* @param filterType Optional filter: "own", "shared", "public", or null for all
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @return List of image maps containing id, name, description, visibility, etc.
* @throws IOException on network errors
* @throws CredentialsException if credentials cannot be found
* @throws ApiException if API returns an error
*/
@SuppressWarnings("unchecked")
public static List<Map<String, Object>> listImages(
String filterType,
String publicKey,
String secretKey
) throws IOException {
String[] creds = resolveCredentials(publicKey, secretKey);
String path = "/images";
if (filterType != null && !filterType.isEmpty()) {
path = "/images/" + filterType;
}
Map<String, Object> response = makeRequest("GET", path, creds[0], creds[1], null);
Object images = response.get("images");
if (images instanceof List) {
List<Map<String, Object>> result = new ArrayList<>();
for (Object item : (List<?>) images) {
if (item instanceof Map) {
result.add((Map<String, Object>) item);
}
}
return result;
}
return new ArrayList<>();
}
/**
* Get details of a specific image.
*
* @param imageId Image ID to retrieve
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @return Image details map
* @throws IOException on network errors
* @throws CredentialsException if credentials cannot be found
* @throws ApiException if API returns an error
*/
public static Map<String, Object> getImage(
String imageId,
String publicKey,
String secretKey
) throws IOException {
String[] creds = resolveCredentials(publicKey, secretKey);
return makeRequest("GET", "/images/" + imageId, creds[0], creds[1], null);
}
/**
* Delete an image.
*
* @param imageId Image ID to delete
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @return Response map with deletion confirmation
* @throws IOException on network errors
* @throws CredentialsException if credentials cannot be found
* @throws ApiException if API returns an error
*/
public static Map<String, Object> deleteImage(
String imageId,
String publicKey,
String secretKey
) throws IOException {
String[] creds = resolveCredentials(publicKey, secretKey);
return makeDestructiveRequest("DELETE", "/images/" + imageId, creds[0], creds[1], null);
}
/**
* Lock an image (prevent deletion and modifications).
*
* @param imageId Image ID to lock
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @return Response map with lock confirmation
* @throws IOException on network errors
* @throws CredentialsException if credentials cannot be found
* @throws ApiException if API returns an error
*/
public static Map<String, Object> lockImage(
String imageId,
String publicKey,
String secretKey
) throws IOException {
String[] creds = resolveCredentials(publicKey, secretKey);
return makeRequest("POST", "/images/" + imageId + "/lock", creds[0], creds[1], new LinkedHashMap<>());
}
/**
* Unlock an image (allow deletion and modifications).
*
* @param imageId Image ID to unlock
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @return Response map with unlock confirmation
* @throws IOException on network errors
* @throws CredentialsException if credentials cannot be found
* @throws ApiException if API returns an error
*/
public static Map<String, Object> unlockImage(
String imageId,
String publicKey,
String secretKey
) throws IOException {
String[] creds = resolveCredentials(publicKey, secretKey);
return makeDestructiveRequest("POST", "/images/" + imageId + "/unlock", creds[0], creds[1], new LinkedHashMap<>());
}
/**
* Set the visibility of an image.
*
* @param imageId Image ID to modify
* @param visibility Visibility level: "private", "shared", or "public"
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @return Response map with visibility change confirmation
* @throws IOException on network errors
* @throws CredentialsException if credentials cannot be found
* @throws ApiException if API returns an error
*/
public static Map<String, Object> setImageVisibility(
String imageId,
String visibility,
String publicKey,
String secretKey
) throws IOException {
String[] creds = resolveCredentials(publicKey, secretKey);
Map<String, Object> data = new LinkedHashMap<>();
data.put("visibility", visibility);
return makeRequest("POST", "/images/" + imageId + "/visibility", creds[0], creds[1], data);
}
/**
* Publish an image from a service or snapshot.
*
* @param sourceType Source type: "service" or "snapshot"
* @param sourceId Source ID (service_id or snapshot_id)
* @param name Name for the new image (optional)
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @return Response map containing new image_id
* @throws IOException on network errors
* @throws CredentialsException if credentials cannot be found
* @throws ApiException if API returns an error
*/
public static Map<String, Object> publishImage(
String sourceType,
String sourceId,
String name,
String publicKey,
String secretKey
) throws IOException {
String[] creds = resolveCredentials(publicKey, secretKey);
Map<String, Object> data = new LinkedHashMap<>();
data.put("source_type", sourceType);
data.put("source_id", sourceId);
if (name != null && !name.isEmpty()) {
data.put("name", name);
}
return makeRequest("POST", "/images/publish", creds[0], creds[1], data);
}
/**
* Grant access to an image for another API key (for shared images).
*
* @param imageId Image ID to grant access to
* @param trustedApiKey Public API key to grant access to
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @return Response map with grant confirmation
* @throws IOException on network errors
* @throws CredentialsException if credentials cannot be found
* @throws ApiException if API returns an error
*/
public static Map<String, Object> grantImageAccess(
String imageId,
String trustedApiKey,
String publicKey,
String secretKey
) throws IOException {
String[] creds = resolveCredentials(publicKey, secretKey);
Map<String, Object> data = new LinkedHashMap<>();
data.put("trusted_api_key", trustedApiKey);
return makeRequest("POST", "/images/" + imageId + "/grant", creds[0], creds[1], data);
}
/**
* Revoke access to an image from another API key.
*
* @param imageId Image ID to revoke access from
* @param trustedApiKey Public API key to revoke access from
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @return Response map with revoke confirmation
* @throws IOException on network errors
* @throws CredentialsException if credentials cannot be found
* @throws ApiException if API returns an error
*/
public static Map<String, Object> revokeImageAccess(
String imageId,
String trustedApiKey,
String publicKey,
String secretKey
) throws IOException {
String[] creds = resolveCredentials(publicKey, secretKey);
Map<String, Object> data = new LinkedHashMap<>();
data.put("trusted_api_key", trustedApiKey);
return makeRequest("POST", "/images/" + imageId + "/revoke", creds[0], creds[1], data);
}
/**
* List API keys that have been granted access to an image.
*
* @param imageId Image ID to list trusted keys for
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @return List of trusted API key maps
* @throws IOException on network errors
* @throws CredentialsException if credentials cannot be found
* @throws ApiException if API returns an error
*/
@SuppressWarnings("unchecked")
public static List<Map<String, Object>> listImageTrusted(
String imageId,
String publicKey,
String secretKey
) throws IOException {
String[] creds = resolveCredentials(publicKey, secretKey);
Map<String, Object> response = makeRequest("GET", "/images/" + imageId + "/trusted", creds[0], creds[1], null);
Object trusted = response.get("trusted");
if (trusted instanceof List) {
List<Map<String, Object>> result = new ArrayList<>();
for (Object item : (List<?>) trusted) {
if (item instanceof Map) {
result.add((Map<String, Object>) item);
}
}
return result;
}
return new ArrayList<>();
}
/**
* Transfer ownership of an image to another API key.
*
* @param imageId Image ID to transfer
* @param toApiKey Public API key of the new owner
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @return Response map with transfer confirmation
* @throws IOException on network errors
* @throws CredentialsException if credentials cannot be found
* @throws ApiException if API returns an error
*/
public static Map<String, Object> transferImage(
String imageId,
String toApiKey,
String publicKey,
String secretKey
) throws IOException {
String[] creds = resolveCredentials(publicKey, secretKey);
Map<String, Object> data = new LinkedHashMap<>();
data.put("to_api_key", toApiKey);
return makeRequest("POST", "/images/" + imageId + "/transfer", creds[0], creds[1], data);
}
/**
* Spawn a new service from an image.
*
* @param imageId Image ID to spawn from
* @param name Name for the new service
* @param ports Comma-separated list of ports to expose (e.g., "80,443")
* @param bootstrap Optional bootstrap script or URL
* @param networkMode Network mode: "zerotrust" or "semitrusted"
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @return Response map containing service_id
* @throws IOException on network errors
* @throws CredentialsException if credentials cannot be found
* @throws ApiException if API returns an error
*/
public static Map<String, Object> spawnFromImage(
String imageId,
String name,
String ports,
String bootstrap,
String networkMode,
String publicKey,
String secretKey
) throws IOException {
String[] creds = resolveCredentials(publicKey, secretKey);
Map<String, Object> data = new LinkedHashMap<>();
data.put("name", name);
if (ports != null && !ports.isEmpty()) {
List<Integer> portList = new ArrayList<>();
for (String p : ports.split(",")) {
try {
portList.add(Integer.parseInt(p.trim()));
} catch (NumberFormatException e) {
// Skip invalid port
}
}
data.put("ports", portList);
}
if (bootstrap != null && !bootstrap.isEmpty()) {
if (bootstrap.startsWith("http://") || bootstrap.startsWith("https://")) {
data.put("bootstrap_url", bootstrap);
} else {
data.put("bootstrap", bootstrap);
}
}
if (networkMode != null && !networkMode.isEmpty()) {
data.put("network_mode", networkMode);
}
return makeRequest("POST", "/images/" + imageId + "/spawn", creds[0], creds[1], data);
}
/**
* Clone an image to create a new image with a different name.
*
* @param imageId Image ID to clone
* @param name Name for the cloned image
* @param description Optional description for the cloned image
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @return Response map containing new image_id
* @throws IOException on network errors
* @throws CredentialsException if credentials cannot be found
* @throws ApiException if API returns an error
*/
public static Map<String, Object> cloneImage(
String imageId,
String name,
String description,
String publicKey,
String secretKey
) throws IOException {
String[] creds = resolveCredentials(publicKey, secretKey);
Map<String, Object> data = new LinkedHashMap<>();
if (name != null && !name.isEmpty()) {
data.put("name", name);
}
if (description != null && !description.isEmpty()) {
data.put("description", description);
}
return makeRequest("POST", "/images/" + imageId + "/clone", creds[0], creds[1], data);
}
// ========================================================================
// PaaS Logs API (2)
// ========================================================================
/**
* Fetch batch logs from portal.
*
* @param source Log source: "all", "api", "portal", "pool/cammy", "pool/ai"
* @param lines Number of lines (1-10000)
* @param since Time window: "1m", "5m", "1h", "1d"
* @param grep Optional filter pattern (null for no filter)
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @return Response map containing logs
* @throws IOException on network errors
* @throws CredentialsException if credentials cannot be found
* @throws ApiException if API returns an error
*/
public static Map<String, Object> logsFetch(
String source,
int lines,
String since,
String grep,
String publicKey,
String secretKey
) throws IOException {
String[] creds = resolveCredentials(publicKey, secretKey);
StringBuilder path = new StringBuilder("/paas/logs?source=");
path.append(source != null ? source : "all");
path.append("&lines=").append(lines > 0 ? lines : 100);
if (since != null && !since.isEmpty()) {
path.append("&since=").append(since);
}
if (grep != null && !grep.isEmpty()) {
path.append("&grep=").append(java.net.URLEncoder.encode(grep, "UTF-8"));
}
return makeRequest("GET", path.toString(), creds[0], creds[1], null);
}
/**
* Interface for receiving streamed log lines.
*/
public interface LogCallback {
/**
* Called for each log line received.
*
* @param source The log source (e.g., "api", "portal")
* @param line The log line content
*/
void onLogLine(String source, String line);
}
/**
* Stream logs via SSE. Blocks until interrupted or server closes connection.
*
* @param source Log source: "all", "api", "portal", "pool/cammy", "pool/ai"
* @param grep Optional filter pattern (null for no filter)
* @param callback Callback for each log line received
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @return true on clean shutdown, false on error
* @throws IOException on network errors
* @throws CredentialsException if credentials cannot be found
*/
public static boolean logsStream(
String source,
String grep,
LogCallback callback,
String publicKey,
String secretKey
) throws IOException {
String[] creds = resolveCredentials(publicKey, secretKey);
StringBuilder path = new StringBuilder("/paas/logs/stream?source=");
path.append(source != null ? source : "all");
if (grep != null && !grep.isEmpty()) {
path.append("&grep=").append(java.net.URLEncoder.encode(grep, "UTF-8"));
}
String url = API_BASE + path.toString();
long timestamp = System.currentTimeMillis() / 1000;
String signature = signRequest(creds[1], timestamp, "GET", path.toString(), null);
HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
conn.setRequestMethod("GET");
conn.setConnectTimeout(30000);
conn.setReadTimeout(0); // No timeout for streaming
conn.setRequestProperty("Authorization", "Bearer " + creds[0]);
conn.setRequestProperty("X-Timestamp", String.valueOf(timestamp));
conn.setRequestProperty("X-Signature", signature);
conn.setRequestProperty("Accept", "text/event-stream");
int responseCode = conn.getResponseCode();
if (responseCode != 200) {
return false;
}
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8))) {
String line;
String currentSource = source != null ? source : "all";
while ((line = reader.readLine()) != null) {
if (line.startsWith("data: ")) {
String data = line.substring(6);
if (callback != null) {
callback.onLogLine(currentSource, data);
}
} else if (line.startsWith("event: ")) {
currentSource = line.substring(7);
}
}
return true;
} catch (Exception e) {
return false;
}
}
// ========================================================================
// Key Validation API
// ========================================================================
/**
* Validate API key credentials.
*
* @param publicKey API public key to validate
* @param secretKey API secret key to validate
* @return Response map with validation result (valid, tier, etc.)
* @throws IOException on network errors
* @throws ApiException if API returns an error (including invalid credentials)
*/
public static Map<String, Object> validateKeys(
String publicKey,
String secretKey
) throws IOException {
String[] creds = resolveCredentials(publicKey, secretKey);
// Note: validateKeys uses POST to /keys/validate
return makeRequest("POST", "/keys/validate", creds[0], creds[1], new LinkedHashMap<>());
}
// ========================================================================
// Utility Functions
// ========================================================================
/**
* Get SDK version string.
*
* @return Version string (e.g., "4.2.0")
*/
public static String version() {
return "4.2.0";
}
/**
* Check API health status.
*
* @return true if API is healthy, false otherwise
*/
public static boolean healthCheck() {
try {
HttpURLConnection conn = (HttpURLConnection) new URL(API_BASE + "/health").openConnection();
conn.setRequestMethod("GET");
conn.setConnectTimeout(5000);
conn.setReadTimeout(5000);
return conn.getResponseCode() == 200;
} catch (Exception e) {
return false;
}
}
/**
* Generate HMAC-SHA256 signature.
*
* @param secretKey Secret key for HMAC
* @param message Message to sign
* @return Lowercase hex-encoded signature
*/
public static String hmacSign(String secretKey, String message) {
try {
Mac mac = Mac.getInstance("HmacSHA256");
SecretKeySpec secretKeySpec = new SecretKeySpec(
secretKey.getBytes(StandardCharsets.UTF_8),
"HmacSHA256"
);
mac.init(secretKeySpec);
byte[] hash = mac.doFinal(message.getBytes(StandardCharsets.UTF_8));
StringBuilder hexString = new StringBuilder();
for (byte b : hash) {
hexString.append(String.format("%02x", b));
}
return hexString.toString();
} catch (Exception e) {
return null;
}
}
/**
* Get details of a specific snapshot.
*
* @param snapshotId Snapshot ID to retrieve
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @return Snapshot details map
* @throws IOException on network errors
* @throws CredentialsException if credentials cannot be found
* @throws ApiException if API returns an error
*/
public static Map<String, Object> getSnapshot(
String snapshotId,
String publicKey,
String secretKey
) throws IOException {
String[] creds = resolveCredentials(publicKey, secretKey);
return makeRequest("GET", "/snapshots/" + snapshotId, creds[0], creds[1], null);
}
/**
* Resize a service (change vCPU allocation).
*
* @param serviceId Service ID to resize
* @param vcpu New vCPU count (1-8)
* @param publicKey Optional API key
* @param secretKey Optional API secret
* @return Response map with resize confirmation
* @throws IOException on network errors
* @throws CredentialsException if credentials cannot be found
* @throws ApiException if API returns an error
*/
public static Map<String, Object> resizeService(
String serviceId,
int vcpu,
String publicKey,
String secretKey
) throws IOException {
String[] creds = resolveCredentials(publicKey, secretKey);
Map<String, Object> data = new LinkedHashMap<>();
data.put("vcpu", vcpu);
return makeRequestWithMethod("PATCH", "/services/" + serviceId, creds[0], creds[1], data);
}
// ========================================================================
// Image Generation API
// ========================================================================
/**
* Generate images from text prompt using AI.
*
* @param prompt Text description of the image to generate
* @param model Model to use (optional, can be null)
* @param size Image size (e.g., "1024x1024")
* @param quality "standard" or "hd"
* @param n Number of images to generate
* @param publicKey API public key (optional)
* @param secretKey API secret key (optional)
* @return Map containing "images" (List of base64/URLs) and "created_at"
* @throws IOException on network errors
* @throws CredentialsException if credentials cannot be found
* @throws ApiException if API returns an error
*/
public static Map<String, Object> image(
String prompt,
String model,
String size,
String quality,
int n,
String publicKey,
String secretKey
) throws IOException {
String[] creds = resolveCredentials(publicKey, secretKey);
if (size == null) size = "1024x1024";
if (quality == null) quality = "standard";
if (n <= 0) n = 1;
Map<String, Object> data = new LinkedHashMap<>();
data.put("prompt", prompt);
data.put("size", size);
data.put("quality", quality);
data.put("n", n);
if (model != null && !model.isEmpty()) {
data.put("model", model);
}
return makeRequest("POST", "/image", creds[0], creds[1], data);
}
// ========================================================================
// HTTP Request Helpers
// ========================================================================
/**
* Make an HTTP request with a specified method (supports PATCH).
*/
private static Map<String, Object> makeRequestWithMethod(
String method,
String path,
String publicKey,
String secretKey,
Map<String, Object> data
) throws IOException {
String url = API_BASE + path;
long timestamp = System.currentTimeMillis() / 1000;
String body = (data != null) ? mapToJson(data) : "";
String signature = signRequest(secretKey, timestamp, method, path, data != null ? body : null);
HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
conn.setRequestMethod(method);
conn.setConnectTimeout(DEFAULT_TIMEOUT_MS);
conn.setReadTimeout(DEFAULT_TIMEOUT_MS);
conn.setRequestProperty("Authorization", "Bearer " + publicKey);
conn.setRequestProperty("X-Timestamp", String.valueOf(timestamp));
conn.setRequestProperty("X-Signature", signature);
conn.setRequestProperty("Content-Type", "application/json");
if (data != null) {
conn.setDoOutput(true);
try (OutputStream os = conn.getOutputStream()) {
os.write(body.getBytes(StandardCharsets.UTF_8));
}
}
int responseCode = conn.getResponseCode();
String responseBody;
InputStream inputStream = (responseCode >= 200 && responseCode < 300)
? conn.getInputStream()
: conn.getErrorStream();
if (inputStream == null) {
responseBody = "";
} else {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) {
StringBuilder sb = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
sb.append(line);
}
responseBody = sb.toString();
}
}
if (responseCode < 200 || responseCode >= 300) {
throw new ApiException(
"API request failed with status " + responseCode,
responseCode,
responseBody
);
}
return parseJson(responseBody);
}
// ========================================================================
// CLI Implementation
// ========================================================================
/**
* CLI entry point.
*/
public static void main(String[] args) {
try {
cliMain(args);
} catch (Exception e) {
System.err.println("Error: " + e.getMessage());
System.exit(1);
}
}
/**
* CLI main implementation.
*/
public static void cliMain(String[] args) throws Exception {
if (args.length == 0) {
printHelp();
System.exit(0);
}
// Parse global options
String publicKey = null;
String secretKey = null;
String language = null;
String networkMode = "zerotrust";
int vcpu = 1;
List<String> envVars = new ArrayList<>();
List<String> files = new ArrayList<>();
List<String> positionalArgs = new ArrayList<>();
boolean showHelp = false;
int i = 0;
while (i < args.length) {
String arg = args[i];
if (arg.equals("-h") || arg.equals("--help")) {
showHelp = true;
i++;
} else if (arg.equals("-s") || arg.equals("--shell")) {
if (i + 1 >= args.length) {
System.err.println("Error: -s/--shell requires an argument");
System.exit(2);
}
language = args[++i];
i++;
} else if (arg.equals("-p") || arg.equals("--public-key")) {
if (i + 1 >= args.length) {
System.err.println("Error: -p/--public-key requires an argument");
System.exit(2);
}
publicKey = args[++i];
i++;
} else if (arg.equals("-k") || arg.equals("--secret-key")) {
if (i + 1 >= args.length) {
System.err.println("Error: -k/--secret-key requires an argument");
System.exit(2);
}
secretKey = args[++i];
i++;
} else if (arg.equals("-n") || arg.equals("--network")) {
if (i + 1 >= args.length) {
System.err.println("Error: -n/--network requires an argument");
System.exit(2);
}
networkMode = args[++i];
i++;
} else if (arg.equals("-v") || arg.equals("--vcpu")) {
if (i + 1 >= args.length) {
System.err.println("Error: -v/--vcpu requires an argument");
System.exit(2);
}
vcpu = Integer.parseInt(args[++i]);
i++;
} else if (arg.equals("-e") || arg.equals("--env")) {
if (i + 1 >= args.length) {
System.err.println("Error: -e/--env requires an argument");
System.exit(2);
}
envVars.add(args[++i]);
i++;
} else if (arg.equals("-f") || arg.equals("--file")) {
if (i + 1 >= args.length) {
System.err.println("Error: -f/--file requires an argument");
System.exit(2);
}
files.add(args[++i]);
i++;
} else if (arg.startsWith("-")) {
System.err.println("Error: Unknown option: " + arg);
System.exit(2);
} else {
positionalArgs.add(arg);
i++;
}
}
if (showHelp || positionalArgs.isEmpty()) {
printHelp();
System.exit(0);
}
String command = positionalArgs.get(0);
// Route to subcommand handlers
switch (command) {
case "session":
handleSession(positionalArgs, publicKey, secretKey, networkMode, vcpu, language);
break;
case "service":
handleService(positionalArgs, publicKey, secretKey, networkMode, vcpu, envVars);
break;
case "snapshot":
handleSnapshot(positionalArgs, publicKey, secretKey);
break;
case "image":
handleImage(positionalArgs, publicKey, secretKey);
break;
case "key":
handleKey(publicKey, secretKey);
break;
case "languages":
handleLanguages(positionalArgs, publicKey, secretKey);
break;
default:
// Default: execute code
handleExecute(positionalArgs, publicKey, secretKey, language, networkMode, vcpu, envVars, files);
break;
}
}
private static void printHelp() {
System.out.println("Un - unsandbox.com CLI (Java Synchronous SDK)");
System.out.println();
System.out.println("Usage:");
System.out.println(" java Un [options] <source_file> Execute code file");
System.out.println(" java Un [options] -s LANG 'code' Execute inline code");
System.out.println(" java Un session [options] Interactive session");
System.out.println(" java Un service [options] Manage services");
System.out.println(" java Un snapshot [options] Manage snapshots");
System.out.println(" java Un image [options] Manage images");
System.out.println(" java Un key Check API key");
System.out.println(" java Un languages [--json] List supported languages");
System.out.println();
System.out.println("Global Options:");
System.out.println(" -s, --shell LANG Language for inline code");
System.out.println(" -e, --env KEY=VAL Set environment variable");
System.out.println(" -f, --file FILE Add input file to /tmp/");
System.out.println(" -p, --public-key KEY API public key");
System.out.println(" -k, --secret-key KEY API secret key");
System.out.println(" -n, --network MODE Network: zerotrust or semitrusted");
System.out.println(" -v, --vcpu N vCPU count (1-8)");
System.out.println(" -h, --help Show help");
System.out.println();
System.out.println("Session Options:");
System.out.println(" --list, -l List active sessions");
System.out.println(" --attach ID Reconnect to session");
System.out.println(" --kill ID Terminate session");
System.out.println(" --freeze ID Pause session");
System.out.println(" --unfreeze ID Resume session");
System.out.println(" --boost ID Add resources");
System.out.println(" --unboost ID Remove boost");
System.out.println(" --snapshot ID Create snapshot");
System.out.println(" --tmux Enable persistence with tmux");
System.out.println(" --screen Enable persistence with screen");
System.out.println(" --shell SHELL Shell/REPL to use");
System.out.println();
System.out.println("Service Options:");
System.out.println(" --list, -l List all services");
System.out.println(" --name NAME Service name (creates new)");
System.out.println(" --ports PORTS Comma-separated ports");
System.out.println(" --bootstrap CMD Bootstrap command");
System.out.println(" --info ID Get service details");
System.out.println(" --logs ID Get all logs");
System.out.println(" --freeze ID Pause service");
System.out.println(" --unfreeze ID Resume service");
System.out.println(" --destroy ID Delete service");
System.out.println(" --lock ID Prevent deletion");
System.out.println(" --unlock ID Allow deletion");
System.out.println(" --execute ID CMD Run command in service");
System.out.println(" --redeploy ID Re-run bootstrap");
System.out.println(" --snapshot ID Create snapshot");
System.out.println();
System.out.println("Service Env Subcommand:");
System.out.println(" java Un service env status ID Show vault status");
System.out.println(" java Un service env set ID Set from stdin");
System.out.println(" java Un service env export ID Export to stdout");
System.out.println(" java Un service env delete ID Delete vault");
System.out.println();
System.out.println("Snapshot Options:");
System.out.println(" --list, -l List all snapshots");
System.out.println(" --info ID Get snapshot details");
System.out.println(" --delete ID Delete snapshot");
System.out.println(" --lock ID Prevent deletion");
System.out.println(" --unlock ID Allow deletion");
System.out.println(" --clone ID Clone snapshot");
System.out.println();
System.out.println("Image Options:");
System.out.println(" --list, -l List all images");
System.out.println(" --info ID Get image details");
System.out.println(" --delete ID Delete an image");
System.out.println(" --lock ID Lock image to prevent deletion");
System.out.println(" --unlock ID Unlock image");
System.out.println(" --publish ID Publish from service/snapshot (requires --source-type)");
System.out.println(" --source-type TYPE Source type: service or snapshot");
System.out.println(" --visibility ID MODE Set visibility (private/unlisted/public)");
System.out.println(" --spawn ID Spawn service from image");
System.out.println(" --clone ID Clone an image");
System.out.println(" --name NAME Name for spawned service or cloned image");
System.out.println(" --ports PORTS Ports for spawned service");
System.out.println();
System.out.println("Languages Options:");
System.out.println(" --json Output as JSON array (for scripts)");
}
private static void handleExecute(
List<String> args,
String publicKey,
String secretKey,
String language,
String networkMode,
int vcpu,
List<String> envVars,
List<String> files
) throws Exception {
String codeOrFile = args.get(0);
String code;
if (language != null) {
// Inline code mode: -s python 'print(1)'
code = codeOrFile;
} else {
// File mode: script.py
Path filePath = Paths.get(codeOrFile);
if (!Files.exists(filePath)) {
System.err.println("Error: File not found: " + codeOrFile);
System.exit(1);
}
code = new String(Files.readAllBytes(filePath), StandardCharsets.UTF_8);
language = detectLanguage(codeOrFile);
if (language == null) {
System.err.println("Error: Cannot detect language from file extension: " + codeOrFile);
System.exit(1);
}
}
Map<String, Object> result = executeCode(language, code, publicKey, secretKey);
// Print output
Object stdout = result.get("stdout");
if (stdout != null && !stdout.toString().isEmpty()) {
System.out.print(stdout);
}
Object stderr = result.get("stderr");
if (stderr != null && !stderr.toString().isEmpty()) {
System.err.print(stderr);
}
System.out.println("---");
Object exitCode = result.get("exit_code");
System.out.println("Exit code: " + (exitCode != null ? exitCode : "0"));
Object executionTime = result.get("execution_time_ms");
if (executionTime != null) {
System.out.println("Execution time: " + executionTime + "ms");
}
// Exit with the code's exit code
if (exitCode != null && exitCode instanceof Number) {
int code_exit = ((Number) exitCode).intValue();
if (code_exit != 0) {
System.exit(code_exit);
}
}
}
private static void handleSession(
List<String> args,
String publicKey,
String secretKey,
String networkMode,
int vcpu,
String shell
) throws Exception {
// Parse session-specific options
boolean list = false;
String attachId = null;
String killId = null;
String freezeId = null;
String unfreezeId = null;
String boostId = null;
String unboostId = null;
String snapshotId = null;
String snapshotName = null;
boolean hot = false;
boolean useTmux = false;
boolean useScreen = false;
int i = 1; // Skip "session" command
while (i < args.size()) {
String arg = args.get(i);
if (arg.equals("--list") || arg.equals("-l")) {
list = true;
i++;
} else if (arg.equals("--attach")) {
if (i + 1 >= args.size()) {
System.err.println("Error: --attach requires an ID");
System.exit(2);
}
attachId = args.get(++i);
i++;
} else if (arg.equals("--kill")) {
if (i + 1 >= args.size()) {
System.err.println("Error: --kill requires an ID");
System.exit(2);
}
killId = args.get(++i);
i++;
} else if (arg.equals("--freeze")) {
if (i + 1 >= args.size()) {
System.err.println("Error: --freeze requires an ID");
System.exit(2);
}
freezeId = args.get(++i);
i++;
} else if (arg.equals("--unfreeze")) {
if (i + 1 >= args.size()) {
System.err.println("Error: --unfreeze requires an ID");
System.exit(2);
}
unfreezeId = args.get(++i);
i++;
} else if (arg.equals("--boost")) {
if (i + 1 >= args.size()) {
System.err.println("Error: --boost requires an ID");
System.exit(2);
}
boostId = args.get(++i);
i++;
} else if (arg.equals("--unboost")) {
if (i + 1 >= args.size()) {
System.err.println("Error: --unboost requires an ID");
System.exit(2);
}
unboostId = args.get(++i);
i++;
} else if (arg.equals("--snapshot")) {
if (i + 1 >= args.size()) {
System.err.println("Error: --snapshot requires an ID");
System.exit(2);
}
snapshotId = args.get(++i);
i++;
} else if (arg.equals("--snapshot-name")) {
if (i + 1 >= args.size()) {
System.err.println("Error: --snapshot-name requires a name");
System.exit(2);
}
snapshotName = args.get(++i);
i++;
} else if (arg.equals("--hot")) {
hot = true;
i++;
} else if (arg.equals("--tmux")) {
useTmux = true;
i++;
} else if (arg.equals("--screen")) {
useScreen = true;
i++;
} else if (arg.equals("--shell")) {
if (i + 1 >= args.size()) {
System.err.println("Error: --shell requires a value");
System.exit(2);
}
shell = args.get(++i);
i++;
} else {
i++;
}
}
if (list) {
List<Map<String, Object>> sessions = listSessions(publicKey, secretKey);
printSessionList(sessions);
} else if (attachId != null) {
Map<String, Object> session = getSession(attachId, publicKey, secretKey);
System.out.println("Session: " + attachId);
printMap(session);
} else if (killId != null) {
deleteSession(killId, publicKey, secretKey);
System.out.println("Session terminated: " + killId);
} else if (freezeId != null) {
freezeSession(freezeId, publicKey, secretKey);
System.out.println("Session frozen: " + freezeId);
} else if (unfreezeId != null) {
unfreezeSession(unfreezeId, publicKey, secretKey);
System.out.println("Session unfrozen: " + unfreezeId);
} else if (boostId != null) {
boostSession(boostId, publicKey, secretKey);
System.out.println("Session boosted: " + boostId);
} else if (unboostId != null) {
unboostSession(unboostId, publicKey, secretKey);
System.out.println("Session unboosted: " + unboostId);
} else if (snapshotId != null) {
String snapId = sessionSnapshot(snapshotId, publicKey, secretKey, snapshotName, hot);
System.out.println("Snapshot created: " + snapId);
} else {
// Create new session
Map<String, Object> opts = new LinkedHashMap<>();
opts.put("network_mode", networkMode);
if (vcpu > 1) {
opts.put("vcpu", vcpu);
}
if (useTmux) {
opts.put("multiplexer", "tmux");
} else if (useScreen) {
opts.put("multiplexer", "screen");
}
Map<String, Object> result = createSession(shell != null ? shell : "bash", publicKey, secretKey, opts);
System.out.println("Session created:");
printMap(result);
}
}
private static void handleService(
List<String> args,
String publicKey,
String secretKey,
String networkMode,
int vcpu,
List<String> envVars
) throws Exception {
// Check for "env" subcommand
if (args.size() > 1 && args.get(1).equals("env")) {
handleServiceEnv(args, publicKey, secretKey);
return;
}
// Parse service-specific options
boolean list = false;
String name = null;
String ports = null;
String bootstrap = null;
String infoId = null;
String logsId = null;
String freezeId = null;
String unfreezeId = null;
String destroyId = null;
String lockId = null;
String unlockId = null;
String executeId = null;
String executeCmd = null;
String redeployId = null;
String snapshotId = null;
int i = 1; // Skip "service" command
while (i < args.size()) {
String arg = args.get(i);
if (arg.equals("--list") || arg.equals("-l")) {
list = true;
i++;
} else if (arg.equals("--name")) {
if (i + 1 >= args.size()) {
System.err.println("Error: --name requires a value");
System.exit(2);
}
name = args.get(++i);
i++;
} else if (arg.equals("--ports")) {
if (i + 1 >= args.size()) {
System.err.println("Error: --ports requires a value");
System.exit(2);
}
ports = args.get(++i);
i++;
} else if (arg.equals("--bootstrap")) {
if (i + 1 >= args.size()) {
System.err.println("Error: --bootstrap requires a value");
System.exit(2);
}
bootstrap = args.get(++i);
i++;
} else if (arg.equals("--info")) {
if (i + 1 >= args.size()) {
System.err.println("Error: --info requires an ID");
System.exit(2);
}
infoId = args.get(++i);
i++;
} else if (arg.equals("--logs")) {
if (i + 1 >= args.size()) {
System.err.println("Error: --logs requires an ID");
System.exit(2);
}
logsId = args.get(++i);
i++;
} else if (arg.equals("--freeze")) {
if (i + 1 >= args.size()) {
System.err.println("Error: --freeze requires an ID");
System.exit(2);
}
freezeId = args.get(++i);
i++;
} else if (arg.equals("--unfreeze")) {
if (i + 1 >= args.size()) {
System.err.println("Error: --unfreeze requires an ID");
System.exit(2);
}
unfreezeId = args.get(++i);
i++;
} else if (arg.equals("--destroy")) {
if (i + 1 >= args.size()) {
System.err.println("Error: --destroy requires an ID");
System.exit(2);
}
destroyId = args.get(++i);
i++;
} else if (arg.equals("--lock")) {
if (i + 1 >= args.size()) {
System.err.println("Error: --lock requires an ID");
System.exit(2);
}
lockId = args.get(++i);
i++;
} else if (arg.equals("--unlock")) {
if (i + 1 >= args.size()) {
System.err.println("Error: --unlock requires an ID");
System.exit(2);
}
unlockId = args.get(++i);
i++;
} else if (arg.equals("--execute")) {
if (i + 2 >= args.size()) {
System.err.println("Error: --execute requires ID and command");
System.exit(2);
}
executeId = args.get(++i);
executeCmd = args.get(++i);
i++;
} else if (arg.equals("--redeploy")) {
if (i + 1 >= args.size()) {
System.err.println("Error: --redeploy requires an ID");
System.exit(2);
}
redeployId = args.get(++i);
i++;
} else if (arg.equals("--snapshot")) {
if (i + 1 >= args.size()) {
System.err.println("Error: --snapshot requires an ID");
System.exit(2);
}
snapshotId = args.get(++i);
i++;
} else {
i++;
}
}
if (list) {
List<Map<String, Object>> services = listServices(publicKey, secretKey);
printServiceList(services);
} else if (infoId != null) {
Map<String, Object> service = getService(infoId, publicKey, secretKey);
printMap(service);
} else if (logsId != null) {
Map<String, Object> logs = getServiceLogs(logsId, true, publicKey, secretKey);
Object content = logs.get("logs");
if (content != null) {
System.out.println(content);
}
} else if (freezeId != null) {
freezeService(freezeId, publicKey, secretKey);
System.out.println("Service frozen: " + freezeId);
} else if (unfreezeId != null) {
unfreezeService(unfreezeId, publicKey, secretKey);
System.out.println("Service unfrozen: " + unfreezeId);
} else if (destroyId != null) {
deleteService(destroyId, publicKey, secretKey);
System.out.println("Service destroyed: " + destroyId);
} else if (lockId != null) {
lockService(lockId, publicKey, secretKey);
System.out.println("Service locked: " + lockId);
} else if (unlockId != null) {
unlockService(unlockId, publicKey, secretKey);
System.out.println("Service unlocked: " + unlockId);
} else if (executeId != null && executeCmd != null) {
Map<String, Object> result = executeInService(executeId, executeCmd, publicKey, secretKey);
Object stdout = result.get("stdout");
if (stdout != null) {
System.out.print(stdout);
}
Object stderr = result.get("stderr");
if (stderr != null && !stderr.toString().isEmpty()) {
System.err.print(stderr);
}
} else if (redeployId != null) {
redeployService(redeployId, publicKey, secretKey);
System.out.println("Service redeployed: " + redeployId);
} else if (snapshotId != null) {
String snapId = serviceSnapshot(snapshotId, publicKey, secretKey, null);
System.out.println("Snapshot created: " + snapId);
} else if (name != null) {
// Create new service
Map<String, Object> result = createService(name, ports, bootstrap, publicKey, secretKey);
System.out.println("Service created:");
printMap(result);
} else {
System.err.println("Error: No service action specified. Use --list, --name, --info, etc.");
System.exit(2);
}
}
private static void handleServiceEnv(
List<String> args,
String publicKey,
String secretKey
) throws Exception {
if (args.size() < 4) {
System.err.println("Error: service env requires action and service ID");
System.err.println("Usage: java Un service env <status|set|export|delete> <service_id>");
System.exit(2);
}
String action = args.get(2);
String serviceId = args.get(3);
switch (action) {
case "status":
Map<String, Object> status = getServiceEnv(serviceId, publicKey, secretKey);
printMap(status);
break;
case "set":
// Read env from stdin
Map<String, String> env = new LinkedHashMap<>();
try (BufferedReader reader = new BufferedReader(new InputStreamReader(System.in))) {
String line;
while ((line = reader.readLine()) != null) {
line = line.trim();
if (line.isEmpty() || line.startsWith("#")) {
continue;
}
int eqIdx = line.indexOf('=');
if (eqIdx > 0) {
String key = line.substring(0, eqIdx);
String value = line.substring(eqIdx + 1);
env.put(key, value);
}
}
}
if (!env.isEmpty()) {
setServiceEnv(serviceId, env, publicKey, secretKey);
System.out.println("Environment set for service: " + serviceId);
} else {
System.err.println("Error: No environment variables provided");
System.exit(1);
}
break;
case "export":
Map<String, Object> exported = exportServiceEnv(serviceId, publicKey, secretKey);
Object envData = exported.get("env");
if (envData instanceof Map) {
@SuppressWarnings("unchecked")
Map<String, Object> envMap = (Map<String, Object>) envData;
for (Map.Entry<String, Object> entry : envMap.entrySet()) {
System.out.println(entry.getKey() + "=" + entry.getValue());
}
}
break;
case "delete":
deleteServiceEnv(serviceId, null, publicKey, secretKey);
System.out.println("Environment deleted for service: " + serviceId);
break;
default:
System.err.println("Error: Unknown env action: " + action);
System.err.println("Valid actions: status, set, export, delete");
System.exit(2);
}
}
private static void handleSnapshot(
List<String> args,
String publicKey,
String secretKey
) throws Exception {
// Parse snapshot-specific options
boolean list = false;
String infoId = null;
String deleteId = null;
String lockId = null;
String unlockId = null;
String cloneId = null;
String cloneName = null;
String cloneType = null;
String clonePorts = null;
int i = 1; // Skip "snapshot" command
while (i < args.size()) {
String arg = args.get(i);
if (arg.equals("--list") || arg.equals("-l")) {
list = true;
i++;
} else if (arg.equals("--info")) {
if (i + 1 >= args.size()) {
System.err.println("Error: --info requires an ID");
System.exit(2);
}
infoId = args.get(++i);
i++;
} else if (arg.equals("--delete")) {
if (i + 1 >= args.size()) {
System.err.println("Error: --delete requires an ID");
System.exit(2);
}
deleteId = args.get(++i);
i++;
} else if (arg.equals("--lock")) {
if (i + 1 >= args.size()) {
System.err.println("Error: --lock requires an ID");
System.exit(2);
}
lockId = args.get(++i);
i++;
} else if (arg.equals("--unlock")) {
if (i + 1 >= args.size()) {
System.err.println("Error: --unlock requires an ID");
System.exit(2);
}
unlockId = args.get(++i);
i++;
} else if (arg.equals("--clone")) {
if (i + 1 >= args.size()) {
System.err.println("Error: --clone requires an ID");
System.exit(2);
}
cloneId = args.get(++i);
i++;
} else if (arg.equals("--name")) {
if (i + 1 >= args.size()) {
System.err.println("Error: --name requires a value");
System.exit(2);
}
cloneName = args.get(++i);
i++;
} else if (arg.equals("--type")) {
if (i + 1 >= args.size()) {
System.err.println("Error: --type requires a value");
System.exit(2);
}
cloneType = args.get(++i);
i++;
} else if (arg.equals("--ports")) {
if (i + 1 >= args.size()) {
System.err.println("Error: --ports requires a value");
System.exit(2);
}
clonePorts = args.get(++i);
i++;
} else {
i++;
}
}
if (list) {
List<Map<String, Object>> snapshots = listSnapshots(publicKey, secretKey);
printSnapshotList(snapshots);
} else if (infoId != null) {
// Get snapshot info via restore API (or just list and filter)
List<Map<String, Object>> snapshots = listSnapshots(publicKey, secretKey);
for (Map<String, Object> snap : snapshots) {
Object id = snap.get("id");
if (id != null && id.toString().equals(infoId)) {
printMap(snap);
return;
}
}
System.err.println("Error: Snapshot not found: " + infoId);
System.exit(1);
} else if (deleteId != null) {
deleteSnapshot(deleteId, publicKey, secretKey);
System.out.println("Snapshot deleted: " + deleteId);
} else if (lockId != null) {
lockSnapshot(lockId, publicKey, secretKey);
System.out.println("Snapshot locked: " + lockId);
} else if (unlockId != null) {
unlockSnapshot(unlockId, publicKey, secretKey);
System.out.println("Snapshot unlocked: " + unlockId);
} else if (cloneId != null) {
Map<String, Object> result = cloneSnapshot(cloneId, cloneName, publicKey, secretKey);
System.out.println("Snapshot cloned:");
printMap(result);
} else {
System.err.println("Error: No snapshot action specified. Use --list, --info, --delete, etc.");
System.exit(2);
}
}
private static void handleImage(
List<String> args,
String publicKey,
String secretKey
) throws Exception {
// Parse image-specific options
boolean list = false;
String infoId = null;
String deleteId = null;
String lockId = null;
String unlockId = null;
String publishId = null;
String sourceType = null;
String visibilityId = null;
String visibilityMode = null;
String spawnId = null;
String cloneId = null;
String name = null;
String ports = null;
int i = 1; // Skip "image" command
while (i < args.size()) {
String arg = args.get(i);
if (arg.equals("--list") || arg.equals("-l")) {
list = true;
i++;
} else if (arg.equals("--info")) {
if (i + 1 >= args.size()) {
System.err.println("Error: --info requires an ID");
System.exit(2);
}
infoId = args.get(++i);
i++;
} else if (arg.equals("--delete")) {
if (i + 1 >= args.size()) {
System.err.println("Error: --delete requires an ID");
System.exit(2);
}
deleteId = args.get(++i);
i++;
} else if (arg.equals("--lock")) {
if (i + 1 >= args.size()) {
System.err.println("Error: --lock requires an ID");
System.exit(2);
}
lockId = args.get(++i);
i++;
} else if (arg.equals("--unlock")) {
if (i + 1 >= args.size()) {
System.err.println("Error: --unlock requires an ID");
System.exit(2);
}
unlockId = args.get(++i);
i++;
} else if (arg.equals("--publish")) {
if (i + 1 >= args.size()) {
System.err.println("Error: --publish requires an ID");
System.exit(2);
}
publishId = args.get(++i);
i++;
} else if (arg.equals("--source-type")) {
if (i + 1 >= args.size()) {
System.err.println("Error: --source-type requires a value");
System.exit(2);
}
sourceType = args.get(++i);
i++;
} else if (arg.equals("--visibility")) {
if (i + 2 >= args.size()) {
System.err.println("Error: --visibility requires ID and MODE");
System.exit(2);
}
visibilityId = args.get(++i);
visibilityMode = args.get(++i);
i++;
} else if (arg.equals("--spawn")) {
if (i + 1 >= args.size()) {
System.err.println("Error: --spawn requires an ID");
System.exit(2);
}
spawnId = args.get(++i);
i++;
} else if (arg.equals("--clone")) {
if (i + 1 >= args.size()) {
System.err.println("Error: --clone requires an ID");
System.exit(2);
}
cloneId = args.get(++i);
i++;
} else if (arg.equals("--name")) {
if (i + 1 >= args.size()) {
System.err.println("Error: --name requires a value");
System.exit(2);
}
name = args.get(++i);
i++;
} else if (arg.equals("--ports")) {
if (i + 1 >= args.size()) {
System.err.println("Error: --ports requires a value");
System.exit(2);
}
ports = args.get(++i);
i++;
} else {
i++;
}
}
if (list) {
List<Map<String, Object>> images = listImages(null, publicKey, secretKey);
printImageList(images);
} else if (infoId != null) {
Map<String, Object> image = getImage(infoId, publicKey, secretKey);
printMap(image);
} else if (deleteId != null) {
deleteImage(deleteId, publicKey, secretKey);
System.out.println("Image deleted: " + deleteId);
} else if (lockId != null) {
lockImage(lockId, publicKey, secretKey);
System.out.println("Image locked: " + lockId);
} else if (unlockId != null) {
unlockImage(unlockId, publicKey, secretKey);
System.out.println("Image unlocked: " + unlockId);
} else if (publishId != null) {
if (sourceType == null) {
System.err.println("Error: --publish requires --source-type (service or snapshot)");
System.exit(2);
}
Map<String, Object> result = publishImage(sourceType, publishId, name, publicKey, secretKey);
System.out.println("Image published:");
printMap(result);
} else if (visibilityId != null) {
if (visibilityMode == null) {
System.err.println("Error: --visibility requires a mode (private, unlisted, or public)");
System.exit(2);
}
setImageVisibility(visibilityId, visibilityMode, publicKey, secretKey);
System.out.println("Image visibility set to " + visibilityMode + ": " + visibilityId);
} else if (spawnId != null) {
String svcName = name != null ? name : "spawned-service";
Map<String, Object> result = spawnFromImage(spawnId, svcName, ports, null, null, publicKey, secretKey);
System.out.println("Service spawned:");
printMap(result);
} else if (cloneId != null) {
String imgName = name != null ? name : cloneId + "-clone";
Map<String, Object> result = cloneImage(cloneId, imgName, null, publicKey, secretKey);
System.out.println("Image cloned:");
printMap(result);
} else {
System.err.println("Error: No image action specified. Use --list, --info, --delete, --publish, etc.");
System.exit(2);
}
}
private static void printImageList(List<Map<String, Object>> images) {
System.out.printf("%-40s %-20s %-10s %-20s%n", "ID", "NAME", "VISIBILITY", "CREATED");
for (Map<String, Object> image : images) {
String id = getStr(image, "image_id", "id");
String name = getStr(image, "name", "");
String visibility = getStr(image, "visibility", "");
String created = getStr(image, "created_at", "created");
System.out.printf("%-40s %-20s %-10s %-20s%n", id, name, visibility, created);
}
}
private static void handleKey(String publicKey, String secretKey) throws Exception {
Map<String, Object> result = validateKeys(publicKey, secretKey);
printMap(result);
}
private static void handleLanguages(List<String> args, String publicKey, String secretKey) throws Exception {
boolean jsonOutput = false;
// Parse options
int i = 1; // Skip "languages" command
while (i < args.size()) {
String arg = args.get(i);
if (arg.equals("--json")) {
jsonOutput = true;
}
i++;
}
List<String> languages = getLanguages(publicKey, secretKey);
if (jsonOutput) {
// Output as JSON array
StringBuilder sb = new StringBuilder();
sb.append("[");
for (int j = 0; j < languages.size(); j++) {
if (j > 0) sb.append(",");
sb.append("\"").append(escapeJsonString(languages.get(j))).append("\"");
}
sb.append("]");
System.out.println(sb.toString());
} else {
// Output one language per line
for (String lang : languages) {
System.out.println(lang);
}
}
}
private static void printSessionList(List<Map<String, Object>> sessions) {
System.out.printf("%-40s %-20s %-10s %-20s%n", "ID", "NAME", "STATUS", "CREATED");
for (Map<String, Object> session : sessions) {
String id = getStr(session, "id", "session_id");
String name = getStr(session, "name", "container_name");
String status = getStr(session, "status", "state");
String created = getStr(session, "created_at", "created");
System.out.printf("%-40s %-20s %-10s %-20s%n", id, name, status, created);
}
}
private static void printServiceList(List<Map<String, Object>> services) {
System.out.printf("%-40s %-20s %-10s %-20s%n", "ID", "NAME", "STATUS", "CREATED");
for (Map<String, Object> service : services) {
String id = getStr(service, "id", "service_id");
String name = getStr(service, "name", "");
String status = getStr(service, "state", "status");
String created = getStr(service, "created_at", "created");
System.out.printf("%-40s %-20s %-10s %-20s%n", id, name, status, created);
}
}
private static void printSnapshotList(List<Map<String, Object>> snapshots) {
System.out.printf("%-40s %-20s %-10s %-20s%n", "ID", "NAME", "TYPE", "CREATED");
for (Map<String, Object> snapshot : snapshots) {
String id = getStr(snapshot, "id", "snapshot_id");
String name = getStr(snapshot, "name", "");
String type = getStr(snapshot, "type", "source_type");
String created = getStr(snapshot, "created_at", "created");
System.out.printf("%-40s %-20s %-10s %-20s%n", id, name, type, created);
}
}
private static String getStr(Map<String, Object> map, String key1, String key2) {
Object val = map.get(key1);
if (val != null) {
return val.toString();
}
val = map.get(key2);
if (val != null) {
return val.toString();
}
return "";
}
private static void printMap(Map<String, Object> map) {
for (Map.Entry<String, Object> entry : map.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}
}
}
Documentation clarifications
Dependencies
C Binary (un1) — requires libcurl and libwebsockets:
sudo apt install build-essential libcurl4-openssl-dev libwebsockets-dev
wget unsandbox.com/downloads/un.c && gcc -O2 -o un un.c -lcurl -lwebsockets
SDK Implementations — most use stdlib only (Ruby, JS, Go, etc). Some require minimal deps:
pip install requests # Python
Execute Code
Run a Script
./un hello.py
./un app.js
./un main.rs
With Environment Variables
./un -e DEBUG=1 -e NAME=World script.py
With Input Files (teleport files into sandbox)
./un -f data.csv -f config.json process.py
Get Compiled Binary (teleport artifacts out)
./un -a -o ./bin main.c
Interactive Sessions
Start a Shell Session
# Default bash shell
./un session
# Choose your shell
./un session --shell zsh
./un session --shell fish
# Jump into a REPL
./un session --shell python3
./un session --shell node
./un session --shell julia
Session with Network Access
./un session -n semitrusted
Session Auditing (full terminal recording)
# Record everything (including vim, interactive programs)
./un session --audit -o ./logs
# Replay session later
zcat session.log*.gz | less -R
Collect Artifacts from Session
# Files in /tmp/artifacts/ are collected on exit
./un session -a -o ./outputs
Session Persistence (tmux/screen)
# Default: session terminates on disconnect (clean exit)
./un session
# With tmux: session persists, can reconnect later
./un session --tmux
# Press Ctrl+b then d to detach
# With screen: alternative multiplexer
./un session --screen
# Press Ctrl+a then d to detach
List Active Sessions
./un session --list
# Output:
# Active sessions: 2
#
# SESSION ID CONTAINER SHELL TTL STATUS
# abc123... unsb-vm-12345 python3 45m30s active
# def456... unsb-vm-67890 bash 1h2m active
Reconnect to Existing Session
# Reconnect by container name (requires --tmux or --screen)
./un session --attach unsb-vm-12345
# Use exit to terminate session, or detach to keep it running
Terminate a Session
./un session --kill unsb-vm-12345
Available Shells & REPLs
Shells: bash, dash, sh, zsh, fish, ksh, tcsh, csh, elvish, xonsh, ash
REPLs: python3, bpython, ipython # Python
node # JavaScript
ruby, irb # Ruby
lua # Lua
php # PHP
perl # Perl
guile, scheme # Scheme
ghci # Haskell
erl, iex # Erlang/Elixir
sbcl, clisp # Common Lisp
r # R
julia # Julia
clojure # Clojure
API Key Management
Check Key Status
# Check if your API key is valid
./un key
# Output:
# Valid: key expires in 30 days
Extend Expired Key
# Open the portal to extend an expired key
./un key --extend
# This opens the unsandbox.com portal where you can
# add more credits to extend your key's expiration
Authentication
Credentials are loaded in priority order (highest first):
# 1. CLI flags (highest priority)
./un -p unsb-pk-xxxx -k unsb-sk-xxxxx script.py
# 2. Environment variables
export UNSANDBOX_PUBLIC_KEY=unsb-pk-xxxx-xxxx-xxxx-xxxx
export UNSANDBOX_SECRET_KEY=unsb-sk-xxxxx-xxxxx-xxxxx-xxxxx
./un script.py
# 3. Config file (lowest priority)
# ~/.unsandbox/accounts.csv format: public_key,secret_key
mkdir -p ~/.unsandbox
echo "unsb-pk-xxxx-xxxx-xxxx-xxxx,unsb-sk-xxxxx-xxxxx-xxxxx-xxxxx" > ~/.unsandbox/accounts.csv
./un script.py
Requests are signed with HMAC-SHA256. The bearer token contains only the public key; the secret key computes the signature (never transmitted).
Resource Scaling
Set vCPU Count
# Default: 1 vCPU, 2GB RAM
./un script.py
# Scale up: 4 vCPUs, 8GB RAM
./un -v 4 script.py
# Maximum: 8 vCPUs, 16GB RAM
./un --vcpu 8 heavy_compute.py
Live Session Boosting
# Boost a running session to 2 vCPU, 4GB RAM
./un session --boost sandbox-abc
# Boost to specific vCPU count (4 vCPU, 8GB RAM)
./un session --boost sandbox-abc --boost-vcpu 4
# Return to base resources (1 vCPU, 2GB RAM)
./un session --unboost sandbox-abc
Session Freeze/Unfreeze
Freeze and Unfreeze Sessions
# Freeze a session (stop billing, preserve state)
./un session --freeze sandbox-abc
# Unfreeze a frozen session
./un session --unfreeze sandbox-abc
# Note: Requires --tmux or --screen for persistence
Persistent Services
Create a Service
# Web server with ports
./un service --name web --ports 80,443 --bootstrap "python -m http.server 80"
# With custom domains
./un service --name blog --ports 8000 --domains blog.example.com
# Game server with SRV records
./un service --name mc --type minecraft --bootstrap ./setup.sh
# Deploy app tarball with bootstrap script
./un service --name app --ports 8000 -f app.tar.gz --bootstrap-file ./setup.sh
# setup.sh: cd /tmp && tar xzf app.tar.gz && ./app/start.sh
Manage Services
# List all services
./un service --list
# Get service details
./un service --info abc123
# View bootstrap logs
./un service --logs abc123
./un service --tail abc123 # last 9000 lines
# Execute command in running service
./un service --execute abc123 'journalctl -u myapp -n 50'
# Dump bootstrap script (for migrations)
./un service --dump-bootstrap abc123
./un service --dump-bootstrap abc123 backup.sh
# Freeze/unfreeze service
./un service --freeze abc123
./un service --unfreeze abc123
# Service settings (auto-wake, freeze page display)
./un service --auto-unfreeze abc123 # enable auto-wake on HTTP
./un service --no-auto-unfreeze abc123 # disable auto-wake
./un service --show-freeze-page abc123 # show HTML payment page (default)
./un service --no-show-freeze-page abc123 # return JSON error instead
# Redeploy with new bootstrap
./un service --redeploy abc123 --bootstrap ./new-setup.sh
# Destroy service
./un service --destroy abc123
Snapshots
List Snapshots
./un snapshot --list
# Output:
# Snapshots: 3
#
# SNAPSHOT ID NAME SOURCE SIZE CREATED
# unsb-snapshot-a1b2-c3d4-e5f6-g7h8 before-upgrade session 512 MB 2h ago
# unsb-snapshot-i9j0-k1l2-m3n4-o5p6 stable-v1.0 service 1.2 GB 1d ago
Create Session Snapshot
# Snapshot with name
./un session --snapshot unsb-vm-12345 --name "before upgrade"
# Quick snapshot (auto-generated name)
./un session --snapshot unsb-vm-12345
Create Service Snapshot
# Standard snapshot (pauses container briefly)
./un service --snapshot unsb-service-abc123 --name "stable v1.0"
# Hot snapshot (no pause, may be inconsistent)
./un service --snapshot unsb-service-abc123 --hot
Restore from Snapshot
# Restore session from snapshot
./un session --restore unsb-snapshot-a1b2-c3d4-e5f6-g7h8
# Restore service from snapshot
./un service --restore unsb-snapshot-i9j0-k1l2-m3n4-o5p6
Delete Snapshot
./un snapshot --delete unsb-snapshot-a1b2-c3d4-e5f6-g7h8
Images
Images are independent, transferable container images that survive container deletion. Unlike snapshots (which live with their container), images can be shared with other users, transferred between API keys, or made public in the marketplace.
List Images
# List all images (owned + shared + public)
./un image --list
# List only your images
./un image --list owned
# List images shared with you
./un image --list shared
# List public marketplace images
./un image --list public
# Get image details
./un image --info unsb-image-xxxx-xxxx-xxxx-xxxx
Publish Images
# Publish from a stopped or frozen service
./un image --publish-service unsb-service-abc123 \
--name "My App v1.0" --description "Production snapshot"
# Publish from a snapshot
./un image --publish-snapshot unsb-snapshot-xxxx-xxxx-xxxx-xxxx \
--name "Stable Release"
# Note: Cannot publish from running containers - stop or freeze first
Create Services from Images
# Spawn a new service from an image
./un image --spawn unsb-image-xxxx-xxxx-xxxx-xxxx \
--name new-service --ports 80,443
# Clone an image (creates a copy you own)
./un image --clone unsb-image-xxxx-xxxx-xxxx-xxxx
Image Protection
# Lock image to prevent accidental deletion
./un image --lock unsb-image-xxxx-xxxx-xxxx-xxxx
# Unlock image to allow deletion
./un image --unlock unsb-image-xxxx-xxxx-xxxx-xxxx
# Delete image (must be unlocked)
./un image --delete unsb-image-xxxx-xxxx-xxxx-xxxx
Visibility & Sharing
# Set visibility level
./un image --visibility unsb-image-xxxx-xxxx-xxxx-xxxx private # owner only (default)
./un image --visibility unsb-image-xxxx-xxxx-xxxx-xxxx unlisted # can be shared
./un image --visibility unsb-image-xxxx-xxxx-xxxx-xxxx public # marketplace
# Share with specific user
./un image --grant unsb-image-xxxx-xxxx-xxxx-xxxx \
--key unsb-pk-friend-friend-friend-friend
# Revoke access
./un image --revoke unsb-image-xxxx-xxxx-xxxx-xxxx \
--key unsb-pk-friend-friend-friend-friend
# List who has access
./un image --trusted unsb-image-xxxx-xxxx-xxxx-xxxx
Transfer Ownership
# Transfer image to another API key
./un image --transfer unsb-image-xxxx-xxxx-xxxx-xxxx \
--to unsb-pk-newowner-newowner-newowner-newowner
Usage Reference
Usage: ./un [options] <source_file>
./un session [options]
./un service [options]
./un snapshot [options]
./un image [options]
./un key
Commands:
(default) Execute source file in sandbox
session Open interactive shell/REPL session
service Manage persistent services
snapshot Manage container snapshots
image Manage container images (publish, share, transfer)
key Check API key validity and expiration
Options:
-e KEY=VALUE Set environment variable (can use multiple times)
-f FILE Add input file (can use multiple times)
-a Return and save artifacts from /tmp/artifacts/
-o DIR Output directory for artifacts (default: current dir)
-p KEY Public key (or set UNSANDBOX_PUBLIC_KEY env var)
-k KEY Secret key (or set UNSANDBOX_SECRET_KEY env var)
-n MODE Network mode: zerotrust (default) or semitrusted
-v N, --vcpu N vCPU count 1-8, each vCPU gets 2GB RAM (default: 1)
-y Skip confirmation for large uploads (>1GB)
-h Show this help
Authentication (priority order):
1. -p and -k flags (public and secret key)
2. UNSANDBOX_PUBLIC_KEY + UNSANDBOX_SECRET_KEY env vars
3. ~/.unsandbox/accounts.csv (format: public_key,secret_key per line)
Session options:
-s, --shell SHELL Shell/REPL to use (default: bash)
-l, --list List active sessions
--attach ID Reconnect to existing session (ID or container name)
--kill ID Terminate a session (ID or container name)
--freeze ID Freeze a session (requires --tmux/--screen)
--unfreeze ID Unfreeze a frozen session
--boost ID Boost session resources (2 vCPU, 4GB RAM)
--boost-vcpu N Specify vCPU count for boost (1-8)
--unboost ID Return to base resources
--audit Record full session for auditing
--tmux Enable session persistence with tmux (allows reconnect)
--screen Enable session persistence with screen (allows reconnect)
Service options:
--name NAME Service name (creates new service)
--ports PORTS Comma-separated ports (e.g., 80,443)
--domains DOMAINS Custom domains (e.g., example.com,www.example.com)
--type TYPE Service type: minecraft, mumble, teamspeak, source, tcp, udp
--bootstrap CMD Bootstrap command/file/URL to run on startup
-f FILE Upload file to /tmp/ (can use multiple times)
-l, --list List all services
--info ID Get service details
--tail ID Get last 9000 lines of bootstrap logs
--logs ID Get all bootstrap logs
--freeze ID Freeze a service
--unfreeze ID Unfreeze a service
--auto-unfreeze ID Enable auto-wake on HTTP request
--no-auto-unfreeze ID Disable auto-wake on HTTP request
--show-freeze-page ID Show HTML payment page when frozen (default)
--no-show-freeze-page ID Return JSON error when frozen
--destroy ID Destroy a service
--redeploy ID Re-run bootstrap script (requires --bootstrap)
--execute ID CMD Run a command in a running service
--dump-bootstrap ID [FILE] Dump bootstrap script (for migrations)
--snapshot ID Create snapshot of session or service
--snapshot-name User-friendly name for snapshot
--hot Create snapshot without pausing (may be inconsistent)
--restore ID Restore session/service from snapshot ID
Snapshot options:
-l, --list List all snapshots
--info ID Get snapshot details
--delete ID Delete a snapshot permanently
Image options:
-l, --list [owned|shared|public] List images (all, owned, shared, or public)
--info ID Get image details
--publish-service ID Publish image from stopped/frozen service
--publish-snapshot ID Publish image from snapshot
--name NAME Name for published image
--description DESC Description for published image
--delete ID Delete image (must be unlocked)
--clone ID Clone image (creates copy you own)
--spawn ID Create service from image (requires --name)
--lock ID Lock image to prevent deletion
--unlock ID Unlock image to allow deletion
--visibility ID LEVEL Set visibility (private|unlisted|public)
--grant ID --key KEY Grant access to another API key
--revoke ID --key KEY Revoke access from API key
--transfer ID --to KEY Transfer ownership to API key
--trusted ID List API keys with access
Key options:
(no options) Check API key validity
--extend Open portal to extend an expired key
Examples:
./un script.py # execute Python script
./un -e DEBUG=1 script.py # with environment variable
./un -f data.csv process.py # with input file
./un -a -o ./bin main.c # save compiled artifacts
./un -v 4 heavy.py # with 4 vCPUs, 8GB RAM
./un session # interactive bash session
./un session --tmux # bash with reconnect support
./un session --list # list active sessions
./un session --attach unsb-vm-12345 # reconnect to session
./un session --kill unsb-vm-12345 # terminate a session
./un session --freeze unsb-vm-12345 # freeze session
./un session --unfreeze unsb-vm-12345 # unfreeze session
./un session --boost unsb-vm-12345 # boost resources
./un session --unboost unsb-vm-12345 # return to base
./un session --shell python3 # Python REPL
./un session --shell node # Node.js REPL
./un session -n semitrusted # session with network access
./un session --audit -o ./logs # record session for auditing
./un service --name web --ports 80 # create web service
./un service --list # list all services
./un service --logs abc123 # view bootstrap logs
./un key # check API key
./un key --extend # extend expired key
./un snapshot --list # list all snapshots
./un session --snapshot unsb-vm-123 # snapshot a session
./un service --snapshot abc123 # snapshot a service
./un session --restore unsb-snapshot-xxxx # restore from snapshot
./un image --list # list all images
./un image --list owned # list your images
./un image --publish-service abc # publish image from service
./un image --spawn img123 --name x # create service from image
./un image --grant img --key pk # share image with user
CLI Inception
The UN CLI has been implemented in 42 programming languages, demonstrating that the unsandbox API can be accessed from virtually any environment.
License
PUBLIC DOMAIN - NO LICENSE, NO WARRANTY
This is free public domain software for the public good of a permacomputer hosted
at permacomputer.com - an always-on computer by the people, for the people. One
that is durable, easy to repair, and distributed like tap water for machine
learning intelligence.
The permacomputer is community-owned infrastructure optimized around four values:
TRUTH - First principles, math & science, open source code freely distributed
FREEDOM - Voluntary partnerships, freedom from tyranny & corporate control
HARMONY - Minimal waste, self-renewing systems with diverse thriving connections
LOVE - Be yourself without hurting others, cooperation through natural law
This software contributes to that vision by enabling code execution across all 42
programming languages through a unified interface, accessible to everyone. Code is
seeds to sprout on any abandoned technology.
Learn more: https://www.permacomputer.com
Anyone is free to copy, modify, publish, use, compile, sell, or distribute this
software, either in source code form or as a compiled binary, for any purpose,
commercial or non-commercial, and by any means.
NO WARRANTY. THE SOFTWARE IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND.
That said, our permacomputer's digital membrane stratum continuously runs unit,
integration, and functional tests on all its own software - with our permacomputer
monitoring itself, repairing itself, with minimal human guidance in the loop.
Our agents do their best.
Copyright 2025 TimeHexOn & foxhop & russell@unturf
https://www.timehexon.com
https://www.foxhop.net
https://www.unturf.com/software