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