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 — Zig
# Download + setup
curl -O https://git.unturf.com/engineering/unturf/un-inception/-/raw/main/clients/zig/sync/src/un.zig
export UNSANDBOX_PUBLIC_KEY="unsb-pk-xxxx-xxxx-xxxx-xxxx"
export UNSANDBOX_SECRET_KEY="unsb-sk-xxxxx-xxxxx-xxxxx-xxxxx"
# Run code
./un script.zig
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 Zig existente:
curl -O https://git.unturf.com/engineering/unturf/un-inception/-/raw/main/clients/zig/sync/src/un.zig
# 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 Zig app:
const un = @import("un.zig");
const std = @import("std");
pub fn main() !void {
const result = try un.executeCode("zig", "std.debug.print(\"Hello from Zig running on unsandbox!\\n\", .{});");
std.debug.print("{s}", .{result.stdout}); // Hello from Zig running on unsandbox!
}
zig build-exe main.zig && ./main
76797534fa35c5677cd7cae012cc73c0
SHA256: 070989e87f5ee4ed775405494e80d73679b4ddb443aecd1fd044cf50550410f8
// PUBLIC DOMAIN - NO LICENSE, NO WARRANTY
//
// This is free public domain software for the public good of a permacomputer hosted
// at permacomputer.com - an always-on computer by the people, for the people. One
// which is durable, easy to repair, and distributed like tap water for machine
// learning intelligence.
//
// The permacomputer is community-owned infrastructure optimized around four values:
//
// TRUTH - First principles, math & science, open source code freely distributed
// FREEDOM - Voluntary partnerships, freedom from tyranny & corporate control
// HARMONY - Minimal waste, self-renewing systems with diverse thriving connections
// LOVE - Be yourself without hurting others, cooperation through natural law
//
// This software contributes to that vision by enabling code execution across 42+
// programming languages through a unified interface, accessible to all. Code is
// seeds to sprout on any abandoned technology.
//
// Learn more: https://www.permacomputer.com
//
// Anyone is free to copy, modify, publish, use, compile, sell, or distribute this
// software, either in source code form or as a compiled binary, for any purpose,
// commercial or non-commercial, and by any means.
//
// NO WARRANTY. THE SOFTWARE IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND.
//
// That said, our permacomputer's digital membrane stratum continuously runs unit,
// integration, and functional tests on all of it's own software - with our
// permacomputer monitoring itself, repairing itself, with minimal human in the
// loop guidance. Our agents do their best.
//
// Copyright 2025 TimeHexOn & foxhop & russell@unturf
// https://www.timehexon.com
// https://www.foxhop.net
// https://www.unturf.com/software
// UN CLI and Library - Zig Implementation (using curl subprocess for simplicity)
// Compile: zig build-exe un.zig -O ReleaseFast
//
// Library Usage (Zig):
// pub fn execute(allocator: std.mem.Allocator, language: []const u8, code: []const u8,
// public_key: []const u8, secret_key: []const u8) ![]const u8
// pub fn execute_async(allocator: std.mem.Allocator, language: []const u8, code: []const u8,
// public_key: []const u8, secret_key: []const u8) ![]const u8
// pub fn get_job(allocator: std.mem.Allocator, job_id: []const u8,
// public_key: []const u8, secret_key: []const u8) ![]const u8
// pub fn wait_for_job(allocator: std.mem.Allocator, job_id: []const u8,
// public_key: []const u8, secret_key: []const u8) ![]const u8
// pub fn cancel_job(allocator: std.mem.Allocator, job_id: []const u8,
// public_key: []const u8, secret_key: []const u8) ![]const u8
// pub fn list_jobs(allocator: std.mem.Allocator,
// public_key: []const u8, secret_key: []const u8) ![]const u8
// pub fn get_languages(allocator: std.mem.Allocator,
// public_key: []const u8, secret_key: []const u8) ![]const u8
// pub fn detect_language(filename: []const u8) ?[]const u8
//
// CLI Usage:
// un.zig script.py
// un.zig -e KEY=VALUE script.py
// un.zig session --list
// un.zig service --name web --ports 8080
//
// Note: This implementation uses system() to call curl for simplicity
// A production version would use Zig's HTTP client library
const std = @import("std");
const fs = std.fs;
const process = std.process;
const mem = std.mem;
const time = std.time;
const API_BASE = "https://api.unsandbox.com";
const PORTAL_BASE = "https://unsandbox.com";
const MAX_ENV_CONTENT_SIZE: usize = 65536;
const LANGUAGES_CACHE_TTL: i64 = 3600; // 1 hour in seconds
const GREEN = "\x1b[32m";
const RED = "\x1b[31m";
const YELLOW = "\x1b[33m";
const RESET = "\x1b[0m";
fn computeHmacCmd(allocator: std.mem.Allocator, secret_key: []const u8, message: []const u8) ![]const u8 {
return try std.fmt.allocPrint(allocator, "echo -n '{s}' | openssl dgst -sha256 -hmac '{s}' -hex 2>/dev/null | sed 's/.*= //'", .{ message, secret_key });
}
fn getTimestamp(allocator: std.mem.Allocator) ![]const u8 {
const timestamp = std.time.timestamp();
return try std.fmt.allocPrint(allocator, "{d}", .{timestamp});
}
fn buildAuthCmd(allocator: std.mem.Allocator, method: []const u8, path: []const u8, body: []const u8, public_key: []const u8, secret_key: []const u8) ![]const u8 {
if (secret_key.len == 0) {
// Legacy mode: use public_key as bearer token
return try std.fmt.allocPrint(allocator, "-H 'Authorization: Bearer {s}'", .{public_key});
}
// HMAC mode
const timestamp_str = try getTimestamp(allocator);
defer allocator.free(timestamp_str);
const message = try std.fmt.allocPrint(allocator, "{s}:{s}:{s}:{s}", .{ timestamp_str, method, path, body });
defer allocator.free(message);
const hmac_cmd = try computeHmacCmd(allocator, secret_key, message);
defer allocator.free(hmac_cmd);
// Execute HMAC command to get signature
var signature_buf: [256]u8 = undefined;
var fbs = std.io.fixedBufferStream(&signature_buf);
const signature_len = blk: {
const result = try std.process.Child.run(.{
.allocator = allocator,
.argv = &[_][]const u8{ "sh", "-c", hmac_cmd },
});
defer allocator.free(result.stdout);
defer allocator.free(result.stderr);
const trimmed = mem.trim(u8, result.stdout, &std.ascii.whitespace);
@memcpy(signature_buf[0..trimmed.len], trimmed);
break :blk trimmed.len;
};
const signature = signature_buf[0..signature_len];
return try std.fmt.allocPrint(allocator, "-H 'Authorization: Bearer {s}' -H 'X-Timestamp: {s}' -H 'X-Signature: {s}'", .{ public_key, timestamp_str, signature });
}
fn base64EncodeFile(allocator: std.mem.Allocator, filename: []const u8) ![]u8 {
const cmd = try std.fmt.allocPrint(allocator, "base64 -w0 '{s}'", .{filename});
defer allocator.free(cmd);
const result = std.process.Child.run(.{
.allocator = allocator,
.argv = &[_][]const u8{ "sh", "-c", cmd },
}) catch return try allocator.dupe(u8, "");
defer allocator.free(result.stdout);
defer allocator.free(result.stderr);
const trimmed = mem.trim(u8, result.stdout, &std.ascii.whitespace);
return try allocator.dupe(u8, trimmed);
}
fn readEnvFile(allocator: std.mem.Allocator, filename: []const u8) ![]u8 {
const content = fs.cwd().readFileAlloc(allocator, filename, MAX_ENV_CONTENT_SIZE) catch |err| {
std.debug.print("{s}Error: Cannot read env file: {s} ({s}){s}\n", .{ RED, filename, @errorName(err), RESET });
return try allocator.dupe(u8, "");
};
return content;
}
fn buildEnvContent(allocator: std.mem.Allocator, envs: std.ArrayList([]const u8), env_file: ?[]const u8) ![]u8 {
var list = std.ArrayList(u8).init(allocator);
errdefer list.deinit();
// Add environment variables from -e flags
for (envs.items) |env| {
try list.appendSlice(env);
try list.append('\n');
}
// Add content from env file
if (env_file) |ef| {
const file_content = try readEnvFile(allocator, ef);
defer allocator.free(file_content);
// Process line by line, skip comments and empty lines
var lines = mem.splitScalar(u8, file_content, '\n');
while (lines.next()) |line| {
const trimmed = mem.trim(u8, line, &std.ascii.whitespace);
if (trimmed.len == 0) continue;
if (trimmed[0] == '#') continue;
try list.appendSlice(trimmed);
try list.append('\n');
}
}
return list.toOwnedSlice();
}
fn extractJsonField(json: []const u8, field: []const u8) ?[]const u8 {
// Build search pattern: "field":"
var pattern_buf: [256]u8 = undefined;
const pattern = std.fmt.bufPrint(&pattern_buf, "\"{s}\":\"", .{field}) catch return null;
if (mem.indexOf(u8, json, pattern)) |start_idx| {
const value_start = start_idx + pattern.len;
if (mem.indexOfPos(u8, json, value_start, "\"")) |end_idx| {
return json[value_start..end_idx];
}
}
return null;
}
const CurlResult = struct {
body: []const u8,
status: i32,
};
fn execCurlWithStatus(allocator: std.mem.Allocator, method: []const u8, endpoint: []const u8, body: []const u8, public_key: []const u8, secret_key: []const u8, extra_headers: []const u8) !CurlResult {
const url = try std.fmt.allocPrint(allocator, "{s}{s}", .{ API_BASE, endpoint });
defer allocator.free(url);
const auth_headers = try buildAuthCmd(allocator, method, endpoint, body, public_key, secret_key);
defer allocator.free(auth_headers);
const response_file = "/tmp/unsandbox_curl_response.txt";
const status_file = "/tmp/unsandbox_curl_status.txt";
// Build curl command with status code output
const cmd = if (body.len > 0) blk: {
const body_file = "/tmp/unsandbox_curl_body.txt";
const file = try fs.cwd().createFile(body_file, .{});
try file.writeAll(body);
file.close();
break :blk try std.fmt.allocPrint(allocator, "curl -s -X {s} '{s}' -H 'Content-Type: application/json' {s} {s} --data-binary @{s} -o {s} -w '%{{http_code}}' > {s}", .{ method, url, auth_headers, extra_headers, body_file, response_file, status_file });
} else blk: {
break :blk try std.fmt.allocPrint(allocator, "curl -s -X {s} '{s}' {s} {s} -o {s} -w '%{{http_code}}' > {s}", .{ method, url, auth_headers, extra_headers, response_file, status_file });
};
defer allocator.free(cmd);
_ = std.c.system(cmd.ptr);
// Read response body
const response_body = fs.cwd().readFileAlloc(allocator, response_file, 1024 * 1024) catch try allocator.dupe(u8, "");
fs.cwd().deleteFile(response_file) catch {};
// Read status code
const status_str = fs.cwd().readFileAlloc(allocator, status_file, 16) catch try allocator.dupe(u8, "0");
defer allocator.free(status_str);
fs.cwd().deleteFile(status_file) catch {};
const trimmed_status = mem.trim(u8, status_str, &std.ascii.whitespace);
const status = std.fmt.parseInt(i32, trimmed_status, 10) catch 0;
return CurlResult{
.body = response_body,
.status = status,
};
}
fn handleSudoChallenge(allocator: std.mem.Allocator, method: []const u8, endpoint: []const u8, body: []const u8, public_key: []const u8, secret_key: []const u8, challenge_response: []const u8) !bool {
// Extract challenge_id from response
const challenge_id = extractJsonField(challenge_response, "challenge_id") orelse {
std.debug.print("{s}Error: No challenge_id in 428 response{s}\n", .{ RED, RESET });
return false;
};
// Prompt for OTP
std.debug.print("{s}Confirmation required. Check your email for a one-time code.{s}\n", .{ YELLOW, RESET });
std.debug.print("Enter OTP: ", .{});
// Read OTP from stdin
const stdin = std.io.getStdIn().reader();
var otp_buf: [64]u8 = undefined;
const otp_line = stdin.readUntilDelimiterOrEof(&otp_buf, '\n') catch null;
if (otp_line == null or otp_line.?.len == 0) {
std.debug.print("{s}Error: No OTP provided{s}\n", .{ RED, RESET });
return false;
}
const otp = mem.trim(u8, otp_line.?, &std.ascii.whitespace);
// Build sudo headers
const sudo_headers = try std.fmt.allocPrint(allocator, "-H 'X-Sudo-OTP: {s}' -H 'X-Sudo-Challenge: {s}'", .{ otp, challenge_id });
defer allocator.free(sudo_headers);
// Retry with sudo headers
const result = try execCurlWithStatus(allocator, method, endpoint, body, public_key, secret_key, sudo_headers);
defer allocator.free(result.body);
if (result.status >= 200 and result.status < 300) {
return true;
} else {
std.debug.print("{s}", .{result.body});
return false;
}
}
fn execDestructiveCurl(allocator: std.mem.Allocator, method: []const u8, endpoint: []const u8, body: []const u8, public_key: []const u8, secret_key: []const u8, success_msg: []const u8) !bool {
const result = try execCurlWithStatus(allocator, method, endpoint, body, public_key, secret_key, "");
if (result.status == 428) {
// Handle sudo challenge
const success = try handleSudoChallenge(allocator, method, endpoint, body, public_key, secret_key, result.body);
allocator.free(result.body);
if (success) {
std.debug.print("{s}{s}{s}\n", .{ GREEN, success_msg, RESET });
}
return success;
} else if (result.status >= 200 and result.status < 300) {
allocator.free(result.body);
std.debug.print("{s}{s}{s}\n", .{ GREEN, success_msg, RESET });
return true;
} else {
std.debug.print("{s}\n", .{result.body});
allocator.free(result.body);
return false;
}
}
fn execCurlPut(allocator: std.mem.Allocator, endpoint: []const u8, body: []const u8, public_key: []const u8, secret_key: []const u8) !bool {
const url = try std.fmt.allocPrint(allocator, "{s}{s}", .{ API_BASE, endpoint });
defer allocator.free(url);
const auth_headers = try buildAuthCmd(allocator, "PUT", endpoint, body, public_key, secret_key);
defer allocator.free(auth_headers);
// Write body to temp file to avoid shell escaping issues
const body_file = "/tmp/unsandbox_env_body.txt";
const file = try fs.cwd().createFile(body_file, .{});
try file.writeAll(body);
file.close();
defer fs.cwd().deleteFile(body_file) catch {};
const cmd = try std.fmt.allocPrint(allocator, "curl -s -X PUT '{s}' -H 'Content-Type: text/plain' {s} --data-binary @{s}", .{ url, auth_headers, body_file });
defer allocator.free(cmd);
const ret = std.c.system(cmd.ptr);
return ret == 0;
}
// ============================================================================
// Library Functions for Zig SDK (matching C reference un.h)
// ============================================================================
pub const SDK_VERSION = "4.2.0";
// Generic API request helper
fn makeApiRequest(allocator: std.mem.Allocator, method: []const u8, path: []const u8, body: []const u8, public_key: []const u8, secret_key: []const u8) ![]const u8 {
const result = try execCurlWithStatus(allocator, method, path, body, public_key, secret_key, "");
return result.body;
}
// Execute code synchronously
pub fn execute(allocator: std.mem.Allocator, language: []const u8, code: []const u8, public_key: []const u8, secret_key: []const u8) ![]const u8 {
const body = try std.fmt.allocPrint(allocator, "{{\"language\":\"{s}\",\"code\":\"{s}\"}}", .{ language, code });
defer allocator.free(body);
return try makeApiRequest(allocator, "POST", "/execute", body, public_key, secret_key);
}
// Execute code asynchronously (returns job_id)
pub fn executeAsync(allocator: std.mem.Allocator, language: []const u8, code: []const u8, public_key: []const u8, secret_key: []const u8) ![]const u8 {
const body = try std.fmt.allocPrint(allocator, "{{\"language\":\"{s}\",\"code\":\"{s}\",\"async\":true}}", .{ language, code });
defer allocator.free(body);
return try makeApiRequest(allocator, "POST", "/execute", body, public_key, secret_key);
}
// Get job status
pub fn getJob(allocator: std.mem.Allocator, job_id: []const u8, public_key: []const u8, secret_key: []const u8) ![]const u8 {
const path = try std.fmt.allocPrint(allocator, "/jobs/{s}", .{job_id});
defer allocator.free(path);
return try makeApiRequest(allocator, "GET", path, "", public_key, secret_key);
}
// Wait for job completion
pub fn waitForJob(allocator: std.mem.Allocator, job_id: []const u8, public_key: []const u8, secret_key: []const u8) ![]const u8 {
const poll_delays = [_]u64{ 300, 450, 700, 900, 650, 1600, 2000 };
var delay_idx: usize = 0;
while (true) {
const result = try getJob(allocator, job_id, public_key, secret_key);
// Check for terminal states
if (mem.indexOf(u8, result, "\"status\":\"completed\"") != null or
mem.indexOf(u8, result, "\"status\":\"failed\"") != null or
mem.indexOf(u8, result, "\"status\":\"timeout\"") != null or
mem.indexOf(u8, result, "\"status\":\"cancelled\"") != null)
{
return result;
}
allocator.free(result);
std.time.sleep(poll_delays[delay_idx] * std.time.ns_per_ms);
if (delay_idx < poll_delays.len - 1) delay_idx += 1;
}
}
// Cancel a job
pub fn cancelJob(allocator: std.mem.Allocator, job_id: []const u8, public_key: []const u8, secret_key: []const u8) ![]const u8 {
const path = try std.fmt.allocPrint(allocator, "/jobs/{s}/cancel", .{job_id});
defer allocator.free(path);
return try makeApiRequest(allocator, "POST", path, "", public_key, secret_key);
}
// List all jobs
pub fn listJobs(allocator: std.mem.Allocator, public_key: []const u8, secret_key: []const u8) ![]const u8 {
return try makeApiRequest(allocator, "GET", "/jobs", "", public_key, secret_key);
}
// Get supported languages
pub fn getLanguages(allocator: std.mem.Allocator, public_key: []const u8, secret_key: []const u8) ![]const u8 {
return try makeApiRequest(allocator, "GET", "/languages", "", public_key, secret_key);
}
// Detect language from filename
pub fn detectLanguage(filename: []const u8) ?[]const u8 {
const lang_map = [_]struct { ext: []const u8, lang: []const u8 }{
.{ .ext = ".py", .lang = "python" },
.{ .ext = ".js", .lang = "javascript" },
.{ .ext = ".ts", .lang = "typescript" },
.{ .ext = ".go", .lang = "go" },
.{ .ext = ".rs", .lang = "rust" },
.{ .ext = ".c", .lang = "c" },
.{ .ext = ".cpp", .lang = "cpp" },
.{ .ext = ".cc", .lang = "cpp" },
.{ .ext = ".d", .lang = "d" },
.{ .ext = ".zig", .lang = "zig" },
.{ .ext = ".rb", .lang = "ruby" },
.{ .ext = ".php", .lang = "php" },
.{ .ext = ".sh", .lang = "bash" },
.{ .ext = ".lua", .lang = "lua" },
.{ .ext = ".nim", .lang = "nim" },
.{ .ext = ".v", .lang = "v" },
};
const idx = mem.lastIndexOfScalar(u8, filename, '.') orelse return null;
const ext = filename[idx..];
for (lang_map) |entry| {
if (mem.eql(u8, ext, entry.ext)) {
return entry.lang;
}
}
return null;
}
// Session functions
pub fn sessionList(allocator: std.mem.Allocator, public_key: []const u8, secret_key: []const u8) ![]const u8 {
return try makeApiRequest(allocator, "GET", "/sessions", "", public_key, secret_key);
}
pub fn sessionGet(allocator: std.mem.Allocator, session_id: []const u8, public_key: []const u8, secret_key: []const u8) ![]const u8 {
const path = try std.fmt.allocPrint(allocator, "/sessions/{s}", .{session_id});
defer allocator.free(path);
return try makeApiRequest(allocator, "GET", path, "", public_key, secret_key);
}
pub fn sessionCreate(allocator: std.mem.Allocator, shell: ?[]const u8, network: ?[]const u8, public_key: []const u8, secret_key: []const u8) ![]const u8 {
var body_buf: [512]u8 = undefined;
var stream = std.io.fixedBufferStream(&body_buf);
const writer = stream.writer();
try writer.print("{{\"shell\":\"{s}\"", .{shell orelse "bash"});
if (network) |n| try writer.print(",\"network\":\"{s}\"", .{n});
try writer.writeAll("}");
const body = stream.getWritten();
return try makeApiRequest(allocator, "POST", "/sessions", body, public_key, secret_key);
}
pub fn sessionDestroy(allocator: std.mem.Allocator, session_id: []const u8, public_key: []const u8, secret_key: []const u8) ![]const u8 {
const path = try std.fmt.allocPrint(allocator, "/sessions/{s}", .{session_id});
defer allocator.free(path);
return try makeApiRequest(allocator, "DELETE", path, "", public_key, secret_key);
}
pub fn sessionFreeze(allocator: std.mem.Allocator, session_id: []const u8, public_key: []const u8, secret_key: []const u8) ![]const u8 {
const path = try std.fmt.allocPrint(allocator, "/sessions/{s}/freeze", .{session_id});
defer allocator.free(path);
return try makeApiRequest(allocator, "POST", path, "", public_key, secret_key);
}
pub fn sessionUnfreeze(allocator: std.mem.Allocator, session_id: []const u8, public_key: []const u8, secret_key: []const u8) ![]const u8 {
const path = try std.fmt.allocPrint(allocator, "/sessions/{s}/unfreeze", .{session_id});
defer allocator.free(path);
return try makeApiRequest(allocator, "POST", path, "", public_key, secret_key);
}
pub fn sessionBoost(allocator: std.mem.Allocator, session_id: []const u8, vcpu: ?u32, public_key: []const u8, secret_key: []const u8) ![]const u8 {
const path = try std.fmt.allocPrint(allocator, "/sessions/{s}/boost", .{session_id});
defer allocator.free(path);
var body: []const u8 = "{}";
if (vcpu) |v| {
body = try std.fmt.allocPrint(allocator, "{{\"vcpu\":{d}}}", .{v});
}
return try makeApiRequest(allocator, "POST", path, body, public_key, secret_key);
}
pub fn sessionUnboost(allocator: std.mem.Allocator, session_id: []const u8, public_key: []const u8, secret_key: []const u8) ![]const u8 {
const path = try std.fmt.allocPrint(allocator, "/sessions/{s}/unboost", .{session_id});
defer allocator.free(path);
return try makeApiRequest(allocator, "POST", path, "", public_key, secret_key);
}
pub fn sessionExecute(allocator: std.mem.Allocator, session_id: []const u8, command: []const u8, public_key: []const u8, secret_key: []const u8) ![]const u8 {
const path = try std.fmt.allocPrint(allocator, "/sessions/{s}/shell", .{session_id});
defer allocator.free(path);
const body = try std.fmt.allocPrint(allocator, "{{\"command\":\"{s}\"}}", .{command});
defer allocator.free(body);
return try makeApiRequest(allocator, "POST", path, body, public_key, secret_key);
}
// Service functions
pub fn serviceList(allocator: std.mem.Allocator, public_key: []const u8, secret_key: []const u8) ![]const u8 {
return try makeApiRequest(allocator, "GET", "/services", "", public_key, secret_key);
}
pub fn serviceGet(allocator: std.mem.Allocator, service_id: []const u8, public_key: []const u8, secret_key: []const u8) ![]const u8 {
const path = try std.fmt.allocPrint(allocator, "/services/{s}", .{service_id});
defer allocator.free(path);
return try makeApiRequest(allocator, "GET", path, "", public_key, secret_key);
}
pub fn serviceDestroy(allocator: std.mem.Allocator, service_id: []const u8, public_key: []const u8, secret_key: []const u8) ![]const u8 {
const path = try std.fmt.allocPrint(allocator, "/services/{s}", .{service_id});
defer allocator.free(path);
return try makeApiRequest(allocator, "DELETE", path, "", public_key, secret_key);
}
pub fn serviceFreeze(allocator: std.mem.Allocator, service_id: []const u8, public_key: []const u8, secret_key: []const u8) ![]const u8 {
const path = try std.fmt.allocPrint(allocator, "/services/{s}/freeze", .{service_id});
defer allocator.free(path);
return try makeApiRequest(allocator, "POST", path, "", public_key, secret_key);
}
pub fn serviceUnfreeze(allocator: std.mem.Allocator, service_id: []const u8, public_key: []const u8, secret_key: []const u8) ![]const u8 {
const path = try std.fmt.allocPrint(allocator, "/services/{s}/unfreeze", .{service_id});
defer allocator.free(path);
return try makeApiRequest(allocator, "POST", path, "", public_key, secret_key);
}
pub fn serviceLock(allocator: std.mem.Allocator, service_id: []const u8, public_key: []const u8, secret_key: []const u8) ![]const u8 {
const path = try std.fmt.allocPrint(allocator, "/services/{s}/lock", .{service_id});
defer allocator.free(path);
return try makeApiRequest(allocator, "POST", path, "", public_key, secret_key);
}
pub fn serviceUnlock(allocator: std.mem.Allocator, service_id: []const u8, public_key: []const u8, secret_key: []const u8) ![]const u8 {
const path = try std.fmt.allocPrint(allocator, "/services/{s}/unlock", .{service_id});
defer allocator.free(path);
return try makeApiRequest(allocator, "POST", path, "", public_key, secret_key);
}
pub fn serviceRedeploy(allocator: std.mem.Allocator, service_id: []const u8, bootstrap: ?[]const u8, public_key: []const u8, secret_key: []const u8) ![]const u8 {
const path = try std.fmt.allocPrint(allocator, "/services/{s}/redeploy", .{service_id});
defer allocator.free(path);
const body = if (bootstrap) |b| try std.fmt.allocPrint(allocator, "{{\"bootstrap\":\"{s}\"}}", .{b}) else try allocator.dupe(u8, "{}");
defer allocator.free(body);
return try makeApiRequest(allocator, "POST", path, body, public_key, secret_key);
}
pub fn serviceLogs(allocator: std.mem.Allocator, service_id: []const u8, all: bool, public_key: []const u8, secret_key: []const u8) ![]const u8 {
const path = if (all)
try std.fmt.allocPrint(allocator, "/services/{s}/logs?all=true", .{service_id})
else
try std.fmt.allocPrint(allocator, "/services/{s}/logs", .{service_id});
defer allocator.free(path);
return try makeApiRequest(allocator, "GET", path, "", public_key, secret_key);
}
pub fn serviceExecute(allocator: std.mem.Allocator, service_id: []const u8, command: []const u8, timeout_ms: ?u32, public_key: []const u8, secret_key: []const u8) ![]const u8 {
const path = try std.fmt.allocPrint(allocator, "/services/{s}/execute", .{service_id});
defer allocator.free(path);
const body = if (timeout_ms) |t|
try std.fmt.allocPrint(allocator, "{{\"command\":\"{s}\",\"timeout\":{d}}}", .{ command, t })
else
try std.fmt.allocPrint(allocator, "{{\"command\":\"{s}\"}}", .{command});
defer allocator.free(body);
return try makeApiRequest(allocator, "POST", path, body, public_key, secret_key);
}
pub fn serviceResize(allocator: std.mem.Allocator, service_id: []const u8, vcpu: u32, public_key: []const u8, secret_key: []const u8) ![]const u8 {
const path = try std.fmt.allocPrint(allocator, "/services/{s}/resize", .{service_id});
defer allocator.free(path);
const body = try std.fmt.allocPrint(allocator, "{{\"vcpu\":{d}}}", .{vcpu});
defer allocator.free(body);
return try makeApiRequest(allocator, "POST", path, body, public_key, secret_key);
}
// Snapshot functions
pub fn snapshotList(allocator: std.mem.Allocator, public_key: []const u8, secret_key: []const u8) ![]const u8 {
return try makeApiRequest(allocator, "GET", "/snapshots", "", public_key, secret_key);
}
pub fn snapshotGet(allocator: std.mem.Allocator, snapshot_id: []const u8, public_key: []const u8, secret_key: []const u8) ![]const u8 {
const path = try std.fmt.allocPrint(allocator, "/snapshots/{s}", .{snapshot_id});
defer allocator.free(path);
return try makeApiRequest(allocator, "GET", path, "", public_key, secret_key);
}
pub fn snapshotRestore(allocator: std.mem.Allocator, snapshot_id: []const u8, public_key: []const u8, secret_key: []const u8) ![]const u8 {
const path = try std.fmt.allocPrint(allocator, "/snapshots/{s}/restore", .{snapshot_id});
defer allocator.free(path);
return try makeApiRequest(allocator, "POST", path, "", public_key, secret_key);
}
pub fn snapshotDelete(allocator: std.mem.Allocator, snapshot_id: []const u8, public_key: []const u8, secret_key: []const u8) ![]const u8 {
const path = try std.fmt.allocPrint(allocator, "/snapshots/{s}", .{snapshot_id});
defer allocator.free(path);
return try makeApiRequest(allocator, "DELETE", path, "", public_key, secret_key);
}
pub fn snapshotLock(allocator: std.mem.Allocator, snapshot_id: []const u8, public_key: []const u8, secret_key: []const u8) ![]const u8 {
const path = try std.fmt.allocPrint(allocator, "/snapshots/{s}/lock", .{snapshot_id});
defer allocator.free(path);
return try makeApiRequest(allocator, "POST", path, "", public_key, secret_key);
}
pub fn snapshotUnlock(allocator: std.mem.Allocator, snapshot_id: []const u8, public_key: []const u8, secret_key: []const u8) ![]const u8 {
const path = try std.fmt.allocPrint(allocator, "/snapshots/{s}/unlock", .{snapshot_id});
defer allocator.free(path);
return try makeApiRequest(allocator, "POST", path, "", public_key, secret_key);
}
// Image functions
pub fn imageList(allocator: std.mem.Allocator, filter: ?[]const u8, public_key: []const u8, secret_key: []const u8) ![]const u8 {
const path = if (filter) |f|
try std.fmt.allocPrint(allocator, "/images?filter={s}", .{f})
else
try allocator.dupe(u8, "/images");
defer allocator.free(path);
return try makeApiRequest(allocator, "GET", path, "", public_key, secret_key);
}
pub fn imageGet(allocator: std.mem.Allocator, image_id: []const u8, public_key: []const u8, secret_key: []const u8) ![]const u8 {
const path = try std.fmt.allocPrint(allocator, "/images/{s}", .{image_id});
defer allocator.free(path);
return try makeApiRequest(allocator, "GET", path, "", public_key, secret_key);
}
pub fn imageDelete(allocator: std.mem.Allocator, image_id: []const u8, public_key: []const u8, secret_key: []const u8) ![]const u8 {
const path = try std.fmt.allocPrint(allocator, "/images/{s}", .{image_id});
defer allocator.free(path);
return try makeApiRequest(allocator, "DELETE", path, "", public_key, secret_key);
}
pub fn imageLock(allocator: std.mem.Allocator, image_id: []const u8, public_key: []const u8, secret_key: []const u8) ![]const u8 {
const path = try std.fmt.allocPrint(allocator, "/images/{s}/lock", .{image_id});
defer allocator.free(path);
return try makeApiRequest(allocator, "POST", path, "", public_key, secret_key);
}
pub fn imageUnlock(allocator: std.mem.Allocator, image_id: []const u8, public_key: []const u8, secret_key: []const u8) ![]const u8 {
const path = try std.fmt.allocPrint(allocator, "/images/{s}/unlock", .{image_id});
defer allocator.free(path);
return try makeApiRequest(allocator, "POST", path, "", public_key, secret_key);
}
// PaaS Logs functions
pub fn logsFetch(allocator: std.mem.Allocator, source: ?[]const u8, lines: ?u32, since: ?[]const u8, grep: ?[]const u8, public_key: []const u8, secret_key: []const u8) ![]const u8 {
var path_buf: [512]u8 = undefined;
var stream = std.io.fixedBufferStream(&path_buf);
const writer = stream.writer();
try writer.writeAll("/paas/logs?");
var has_param = false;
if (source) |s| {
try writer.print("source={s}", .{s});
has_param = true;
}
if (lines) |l| {
if (has_param) try writer.writeAll("&");
try writer.print("lines={d}", .{l});
has_param = true;
}
if (since) |s| {
if (has_param) try writer.writeAll("&");
try writer.print("since={s}", .{s});
has_param = true;
}
if (grep) |g| {
if (has_param) try writer.writeAll("&");
try writer.print("grep={s}", .{g});
}
const path = stream.getWritten();
return try makeApiRequest(allocator, "GET", path, "", public_key, secret_key);
}
// Key validation
pub fn validateKeys(allocator: std.mem.Allocator, public_key: []const u8, secret_key: []const u8) ![]const u8 {
return try makeApiRequest(allocator, "POST", "/keys/validate", "", public_key, secret_key);
}
// Utility functions
pub fn hmacSign(allocator: std.mem.Allocator, secret_key: []const u8, message: []const u8) ![]const u8 {
const hmac_cmd = try computeHmacCmd(allocator, secret_key, message);
defer allocator.free(hmac_cmd);
const result = try std.process.Child.run(.{
.allocator = allocator,
.argv = &[_][]const u8{ "sh", "-c", hmac_cmd },
});
defer allocator.free(result.stderr);
const trimmed = mem.trim(u8, result.stdout, &std.ascii.whitespace);
const signature = try allocator.dupe(u8, trimmed);
allocator.free(result.stdout);
return signature;
}
pub fn healthCheck(allocator: std.mem.Allocator) !bool {
const cmd = try std.fmt.allocPrint(allocator, "curl -s -o /dev/null -w '%{{http_code}}' '{s}/health' 2>/dev/null", .{API_BASE});
defer allocator.free(cmd);
const result = try std.process.Child.run(.{
.allocator = allocator,
.argv = &[_][]const u8{ "sh", "-c", cmd },
});
defer allocator.free(result.stdout);
defer allocator.free(result.stderr);
const trimmed = mem.trim(u8, result.stdout, &std.ascii.whitespace);
return mem.eql(u8, trimmed, "200");
}
pub fn version() []const u8 {
return SDK_VERSION;
}
var last_error_msg: []const u8 = "";
pub fn setLastError(msg: []const u8) void {
last_error_msg = msg;
}
pub fn lastError() []const u8 {
return last_error_msg;
}
fn cmdServiceEnv(allocator: std.mem.Allocator, action: []const u8, target: []const u8, envs: std.ArrayList([]const u8), env_file: ?[]const u8, public_key: []const u8, secret_key: []const u8) !void {
if (mem.eql(u8, action, "status")) {
const path = try std.fmt.allocPrint(allocator, "/services/{s}/env", .{target});
defer allocator.free(path);
const auth_headers = try buildAuthCmd(allocator, "GET", path, "", public_key, secret_key);
defer allocator.free(auth_headers);
const cmd = try std.fmt.allocPrint(allocator, "curl -s -X GET '{s}{s}' {s}", .{ API_BASE, path, auth_headers });
defer allocator.free(cmd);
_ = std.c.system(cmd.ptr);
std.debug.print("\n", .{});
} else if (mem.eql(u8, action, "set")) {
if (envs.items.len == 0 and env_file == null) {
std.debug.print("{s}Error: No environment variables specified. Use -e KEY=VALUE or --env-file FILE{s}\n", .{ RED, RESET });
return;
}
const content = try buildEnvContent(allocator, envs, env_file);
defer allocator.free(content);
if (content.len > MAX_ENV_CONTENT_SIZE) {
std.debug.print("{s}Error: Environment content exceeds 64KB limit{s}\n", .{ RED, RESET });
return;
}
const path = try std.fmt.allocPrint(allocator, "/services/{s}/env", .{target});
defer allocator.free(path);
_ = try execCurlPut(allocator, path, content, public_key, secret_key);
std.debug.print("\n{s}Vault updated for service {s}{s}\n", .{ GREEN, target, RESET });
} else if (mem.eql(u8, action, "export")) {
const path = try std.fmt.allocPrint(allocator, "/services/{s}/env/export", .{target});
defer allocator.free(path);
const auth_headers = try buildAuthCmd(allocator, "POST", path, "", public_key, secret_key);
defer allocator.free(auth_headers);
const cmd = try std.fmt.allocPrint(allocator, "curl -s -X POST '{s}{s}' {s}", .{ API_BASE, path, auth_headers });
defer allocator.free(cmd);
_ = std.c.system(cmd.ptr);
std.debug.print("\n", .{});
} else if (mem.eql(u8, action, "delete")) {
const path = try std.fmt.allocPrint(allocator, "/services/{s}/env", .{target});
defer allocator.free(path);
const auth_headers = try buildAuthCmd(allocator, "DELETE", path, "", public_key, secret_key);
defer allocator.free(auth_headers);
const cmd = try std.fmt.allocPrint(allocator, "curl -s -X DELETE '{s}{s}' {s}", .{ API_BASE, path, auth_headers });
defer allocator.free(cmd);
_ = std.c.system(cmd.ptr);
std.debug.print("\n{s}Vault deleted for service {s}{s}\n", .{ GREEN, target, RESET });
} else {
std.debug.print("{s}Error: Unknown env action: {s}{s}\n", .{ RED, action, RESET });
std.debug.print("Usage: un service env <status|set|export|delete> <service_id>\n", .{});
}
}
fn serviceEnvSet(allocator: std.mem.Allocator, service_id: []const u8, content: []const u8, public_key: []const u8, secret_key: []const u8) !bool {
const path = try std.fmt.allocPrint(allocator, "/services/{s}/env", .{service_id});
defer allocator.free(path);
return try execCurlPut(allocator, path, content, public_key, secret_key);
}
fn buildInputFilesJson(allocator: std.mem.Allocator, files: std.ArrayList([]const u8)) ![]u8 {
if (files.items.len == 0) {
return try allocator.dupe(u8, "");
}
var list = std.ArrayList(u8).init(allocator);
defer list.deinit();
try list.appendSlice(",\"input_files\":[");
for (files.items, 0..) |file, i| {
if (i > 0) try list.append(',');
// Get basename
var basename: []const u8 = file;
if (mem.lastIndexOfScalar(u8, file, '/')) |idx| {
basename = file[idx + 1 ..];
}
// Base64 encode file content
const content = try base64EncodeFile(allocator, file);
defer allocator.free(content);
const entry = try std.fmt.allocPrint(allocator, "{{\"filename\":\"{s}\",\"content\":\"{s}\"}}", .{ basename, content });
defer allocator.free(entry);
try list.appendSlice(entry);
}
try list.append(']');
return list.toOwnedSlice();
}
/// Load credentials from a CSV file at the given 0-based row index,
/// skipping blank lines and lines starting with '#'.
/// Returns allocated pk and sk slices, or null if not found.
fn loadCsvRow(allocator: std.mem.Allocator, csv_path: []const u8, row_index: usize) !?struct { pk: []const u8, sk: []const u8 } {
const file = fs.cwd().openFile(csv_path, .{}) catch return null;
defer file.close();
var buf_reader = std.io.bufferedReader(file.reader());
var reader = buf_reader.reader();
var line_buf: [4096]u8 = undefined;
var data_index: usize = 0;
while (true) {
const line = reader.readUntilDelimiterOrEof(&line_buf, '\n') catch break orelse break;
const trimmed = std.mem.trim(u8, line, " \t\r\n");
if (trimmed.len == 0 or trimmed[0] == '#') continue;
if (data_index == row_index) {
if (std.mem.indexOfScalar(u8, trimmed, ',')) |comma_pos| {
const pk = std.mem.trim(u8, trimmed[0..comma_pos], " \t");
const sk = std.mem.trim(u8, trimmed[comma_pos + 1 ..], " \t");
if (pk.len > 0 and sk.len > 0) {
return .{ .pk = try allocator.dupe(u8, pk), .sk = try allocator.dupe(u8, sk) };
}
}
return null;
}
data_index += 1;
}
return null;
}
/// Resolve credentials using 5-tier priority:
/// 1. arg_pk / arg_sk (explicit -p/-k flags) if both non-empty
/// 2. account_index >= 0 -> accounts.csv row N (bypasses env vars)
/// 3. UNSANDBOX_PUBLIC_KEY / UNSANDBOX_SECRET_KEY env vars
/// 4. ~/.unsandbox/accounts.csv row 0 (or UNSANDBOX_ACCOUNT env var)
/// 5. ./accounts.csv row 0
/// Returns allocated pk and sk slices.
fn resolveCredentials(allocator: std.mem.Allocator, arg_pk: []const u8, arg_sk: []const u8, account_index: i64) !struct { pk: []u8, sk: []u8 } {
// Tier 1: explicit key flags
if (arg_pk.len > 0 and arg_sk.len > 0) {
return .{ .pk = try allocator.dupe(u8, arg_pk), .sk = try allocator.dupe(u8, arg_sk) };
}
// Tier 2: --account N bypasses env vars
if (account_index >= 0) {
const acct_idx: usize = @intCast(account_index);
// try ~/.unsandbox/accounts.csv
const home_opt = process.getEnvVarOwned(allocator, "HOME") catch null;
if (home_opt) |home| {
defer allocator.free(home);
const home_csv = try std.fmt.allocPrint(allocator, "{s}/.unsandbox/accounts.csv", .{home});
defer allocator.free(home_csv);
if (try loadCsvRow(allocator, home_csv, acct_idx)) |creds| {
return .{ .pk = @constCast(creds.pk), .sk = @constCast(creds.sk) };
}
}
// try ./accounts.csv
if (try loadCsvRow(allocator, "accounts.csv", acct_idx)) |creds| {
return .{ .pk = @constCast(creds.pk), .sk = @constCast(creds.sk) };
}
const stderr = std.io.getStdErr().writer();
try stderr.print("{s}Error: No credentials found for account index {d} in accounts.csv{s}\n", .{ RED, account_index, RESET });
std.process.exit(1);
}
// Tier 3: env vars
const env_pk = process.getEnvVarOwned(allocator, "UNSANDBOX_PUBLIC_KEY") catch blk: {
break :blk process.getEnvVarOwned(allocator, "UNSANDBOX_API_KEY") catch try allocator.dupe(u8, "");
};
const env_sk = process.getEnvVarOwned(allocator, "UNSANDBOX_SECRET_KEY") catch try allocator.dupe(u8, "");
if (env_pk.len > 0 and env_sk.len > 0) {
return .{ .pk = env_pk, .sk = env_sk };
}
// Tier 4: ~/.unsandbox/accounts.csv (default row)
const default_index_str = process.getEnvVarOwned(allocator, "UNSANDBOX_ACCOUNT") catch try allocator.dupe(u8, "0");
defer allocator.free(default_index_str);
const default_index = std.fmt.parseInt(usize, std.mem.trim(u8, default_index_str, " \t"), 10) catch 0;
const home_opt = process.getEnvVarOwned(allocator, "HOME") catch null;
if (home_opt) |home| {
defer allocator.free(home);
const home_csv = try std.fmt.allocPrint(allocator, "{s}/.unsandbox/accounts.csv", .{home});
defer allocator.free(home_csv);
if (try loadCsvRow(allocator, home_csv, default_index)) |creds| {
allocator.free(env_pk);
allocator.free(env_sk);
return .{ .pk = @constCast(creds.pk), .sk = @constCast(creds.sk) };
}
}
// Tier 5: ./accounts.csv
if (try loadCsvRow(allocator, "accounts.csv", default_index)) |creds| {
allocator.free(env_pk);
allocator.free(env_sk);
return .{ .pk = @constCast(creds.pk), .sk = @constCast(creds.sk) };
}
if (env_pk.len > 0) {
return .{ .pk = env_pk, .sk = env_sk };
}
allocator.free(env_pk);
allocator.free(env_sk);
const stderr = std.io.getStdErr().writer();
try stderr.print("{s}Error: No credentials found. Set UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY{s}\n", .{ RED, RESET });
std.process.exit(1);
}
pub fn main() !u8 {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
const args = try process.argsAlloc(allocator);
defer process.argsFree(allocator, args);
if (args.len < 2) {
std.debug.print("Usage: {s} [options] <source_file>\n", .{args[0]});
std.debug.print(" {s} session [options]\n", .{args[0]});
std.debug.print(" {s} service [options]\n", .{args[0]});
std.debug.print(" {s} service env <action> <service_id> [options]\n", .{args[0]});
std.debug.print(" {s} image [options]\n", .{args[0]});
std.debug.print(" {s} key [--extend]\n", .{args[0]});
std.debug.print(" {s} languages [--json]\n", .{args[0]});
std.debug.print("\nVault commands:\n", .{});
std.debug.print(" service env status <id> Check vault status\n", .{});
std.debug.print(" service env set <id> Set vault (-e KEY=VAL or --env-file FILE)\n", .{});
std.debug.print(" service env export <id> Export vault contents\n", .{});
std.debug.print(" service env delete <id> Delete vault\n", .{});
std.debug.print("\nImage commands:\n", .{});
std.debug.print(" image --list List all images\n", .{});
std.debug.print(" image --info ID Get image details\n", .{});
std.debug.print(" image --delete ID Delete an image\n", .{});
std.debug.print(" image --lock ID Lock image to prevent deletion\n", .{});
std.debug.print(" image --unlock ID Unlock image\n", .{});
std.debug.print(" image --publish ID --source-type TYPE Publish from service/snapshot\n", .{});
std.debug.print(" image --visibility ID MODE Set visibility (private/unlisted/public)\n", .{});
std.debug.print(" image --spawn ID --name NAME Spawn service from image\n", .{});
std.debug.print(" image --clone ID --name NAME Clone an image\n", .{});
std.debug.print("\nCredential options (global):\n", .{});
std.debug.print(" -p PUBLIC_KEY Explicit public key\n", .{});
std.debug.print(" -k SECRET_KEY Explicit secret key\n", .{});
std.debug.print(" --account N Use row N from accounts.csv (bypasses env vars)\n", .{});
return 1;
}
// Pre-scan for --account N, -p, and -k before subcommand dispatch
var account_index: i64 = -1;
var arg_pk: []const u8 = "";
var arg_sk: []const u8 = "";
{
var scan_i: usize = 1;
while (scan_i < args.len) : (scan_i += 1) {
if (mem.eql(u8, args[scan_i], "--account") and scan_i + 1 < args.len) {
scan_i += 1;
account_index = std.fmt.parseInt(i64, args[scan_i], 10) catch -1;
} else if (mem.eql(u8, args[scan_i], "-p") and scan_i + 1 < args.len) {
scan_i += 1;
arg_pk = args[scan_i];
} else if (mem.eql(u8, args[scan_i], "-k") and scan_i + 1 < args.len) {
scan_i += 1;
arg_sk = args[scan_i];
}
}
}
const creds = try resolveCredentials(allocator, arg_pk, arg_sk, account_index);
var public_key = creds.pk;
defer allocator.free(public_key);
const secret_key = creds.sk;
defer allocator.free(secret_key);
// Handle session command
if (mem.eql(u8, args[1], "session")) {
var list = false;
var kill: ?[]const u8 = null;
var shell: ?[]const u8 = null;
var input_files = std.ArrayList([]const u8).init(allocator);
defer input_files.deinit();
var i: usize = 2;
while (i < args.len) : (i += 1) {
if (mem.eql(u8, args[i], "--list")) {
list = true;
} else if (mem.eql(u8, args[i], "--kill") and i + 1 < args.len) {
i += 1;
kill = args[i];
} else if (mem.eql(u8, args[i], "--shell") and i + 1 < args.len) {
i += 1;
shell = args[i];
} else if (mem.eql(u8, args[i], "-k") and i + 1 < args.len) {
i += 1;
allocator.free(public_key);
public_key = try allocator.dupe(u8, args[i]);
} else if (mem.eql(u8, args[i], "-f") and i + 1 < args.len) {
i += 1;
const file = args[i];
// Check if file exists
fs.cwd().access(file, .{}) catch {
std.debug.print("Error: File not found: {s}\n", .{file});
return 1;
};
try input_files.append(file);
}
}
if (list) {
const auth_headers = try buildAuthCmd(allocator, "GET", "/sessions", "", public_key, secret_key);
defer allocator.free(auth_headers);
const cmd = try std.fmt.allocPrint(allocator, "curl -s -X GET '{s}/sessions' {s}", .{ API_BASE, auth_headers });
defer allocator.free(cmd);
_ = std.c.system(cmd.ptr);
std.debug.print("\n", .{});
} else if (kill) |k| {
const path = try std.fmt.allocPrint(allocator, "/sessions/{s}", .{k});
defer allocator.free(path);
const auth_headers = try buildAuthCmd(allocator, "DELETE", path, "", public_key, secret_key);
defer allocator.free(auth_headers);
const cmd = try std.fmt.allocPrint(allocator, "curl -s -X DELETE '{s}/sessions/{s}' {s}", .{ API_BASE, k, auth_headers });
defer allocator.free(cmd);
_ = std.c.system(cmd.ptr);
std.debug.print("\x1b[32mSession terminated: {s}\x1b[0m\n", .{k});
} else {
const sh = shell orelse "bash";
const input_files_json = try buildInputFilesJson(allocator, input_files);
defer allocator.free(input_files_json);
const json = try std.fmt.allocPrint(allocator, "{{\"shell\":\"{s}\"{s}}}", .{ sh, input_files_json });
defer allocator.free(json);
const auth_headers = try buildAuthCmd(allocator, "POST", "/sessions", json, public_key, secret_key);
defer allocator.free(auth_headers);
const cmd = try std.fmt.allocPrint(allocator, "curl -s -X POST '{s}/sessions' -H 'Content-Type: application/json' {s} -d '{s}'", .{ API_BASE, auth_headers, json });
defer allocator.free(cmd);
std.debug.print("\x1b[33mCreating session...\x1b[0m\n", .{});
_ = std.c.system(cmd.ptr);
std.debug.print("\n", .{});
}
return 0;
}
// Handle service command
if (mem.eql(u8, args[1], "service")) {
var list = false;
var name: ?[]const u8 = null;
var ports: ?[]const u8 = null;
var service_type: ?[]const u8 = null;
var bootstrap: ?[]const u8 = null;
var bootstrap_file: ?[]const u8 = null;
var info: ?[]const u8 = null;
var execute: ?[]const u8 = null;
var command: ?[]const u8 = null;
var dump_bootstrap: ?[]const u8 = null;
var dump_file: ?[]const u8 = null;
var resize: ?[]const u8 = null;
var vcpu: i32 = 0;
var unfreeze_on_demand = false;
var set_unfreeze_on_demand_id: ?[]const u8 = null;
var set_unfreeze_on_demand_enabled: ?[]const u8 = null;
var input_files = std.ArrayList([]const u8).init(allocator);
defer input_files.deinit();
var svc_envs = std.ArrayList([]const u8).init(allocator);
defer svc_envs.deinit();
var svc_env_file: ?[]const u8 = null;
var env_action: ?[]const u8 = null;
var env_target: ?[]const u8 = null;
var i: usize = 2;
while (i < args.len) : (i += 1) {
if (mem.eql(u8, args[i], "--list")) {
list = true;
} else if (mem.eql(u8, args[i], "env") and i + 2 < args.len) {
// service env <action> <service_id>
i += 1;
env_action = args[i];
i += 1;
env_target = args[i];
} else if (mem.eql(u8, args[i], "--name") and i + 1 < args.len) {
i += 1;
name = args[i];
} else if (mem.eql(u8, args[i], "--ports") and i + 1 < args.len) {
i += 1;
ports = args[i];
} else if (mem.eql(u8, args[i], "--type") and i + 1 < args.len) {
i += 1;
service_type = args[i];
} else if (mem.eql(u8, args[i], "--bootstrap") and i + 1 < args.len) {
i += 1;
bootstrap = args[i];
} else if (mem.eql(u8, args[i], "--bootstrap-file") and i + 1 < args.len) {
i += 1;
bootstrap_file = args[i];
} else if (mem.eql(u8, args[i], "--info") and i + 1 < args.len) {
i += 1;
info = args[i];
} else if (mem.eql(u8, args[i], "--execute") and i + 1 < args.len) {
i += 1;
execute = args[i];
} else if (mem.eql(u8, args[i], "--command") and i + 1 < args.len) {
i += 1;
command = args[i];
} else if (mem.eql(u8, args[i], "--dump-bootstrap") and i + 1 < args.len) {
i += 1;
dump_bootstrap = args[i];
} else if (mem.eql(u8, args[i], "--dump-file") and i + 1 < args.len) {
i += 1;
dump_file = args[i];
} else if (mem.eql(u8, args[i], "--resize") and i + 1 < args.len) {
i += 1;
resize = args[i];
} else if (mem.eql(u8, args[i], "-v") and i + 1 < args.len) {
i += 1;
vcpu = std.fmt.parseInt(i32, args[i], 10) catch 0;
} else if (mem.eql(u8, args[i], "-e") and i + 1 < args.len) {
i += 1;
try svc_envs.append(args[i]);
} else if (mem.eql(u8, args[i], "--env-file") and i + 1 < args.len) {
i += 1;
svc_env_file = args[i];
} else if (mem.eql(u8, args[i], "--unfreeze-on-demand")) {
unfreeze_on_demand = true;
} else if (mem.eql(u8, args[i], "--set-unfreeze-on-demand") and i + 2 < args.len) {
i += 1;
set_unfreeze_on_demand_id = args[i];
i += 1;
set_unfreeze_on_demand_enabled = args[i];
} else if (mem.eql(u8, args[i], "-k") and i + 1 < args.len) {
i += 1;
allocator.free(public_key);
public_key = try allocator.dupe(u8, args[i]);
} else if (mem.eql(u8, args[i], "-f") and i + 1 < args.len) {
i += 1;
const file = args[i];
// Check if file exists
fs.cwd().access(file, .{}) catch {
std.debug.print("Error: File not found: {s}\n", .{file});
return 1;
};
try input_files.append(file);
}
}
// Handle env subcommand
if (env_action) |action| {
if (env_target) |target| {
try cmdServiceEnv(allocator, action, target, svc_envs, svc_env_file, public_key, secret_key);
return 0;
}
}
if (list) {
const auth_headers = try buildAuthCmd(allocator, "GET", "/services", "", public_key, secret_key);
defer allocator.free(auth_headers);
const cmd = try std.fmt.allocPrint(allocator, "curl -s -X GET '{s}/services' {s}", .{ API_BASE, auth_headers });
defer allocator.free(cmd);
_ = std.c.system(cmd.ptr);
std.debug.print("\n", .{});
} else if (info) |inf| {
const path = try std.fmt.allocPrint(allocator, "/services/{s}", .{inf});
defer allocator.free(path);
const auth_headers = try buildAuthCmd(allocator, "GET", path, "", public_key, secret_key);
defer allocator.free(auth_headers);
const cmd = try std.fmt.allocPrint(allocator, "curl -s -X GET '{s}/services/{s}' {s}", .{ API_BASE, inf, auth_headers });
defer allocator.free(cmd);
_ = std.c.system(cmd.ptr);
std.debug.print("\n", .{});
} else if (execute) |exec_id| {
const cmd_text = command orelse "";
const json = try std.fmt.allocPrint(allocator, "{{\"command\":\"{s}\"}}", .{cmd_text});
defer allocator.free(json);
const path = try std.fmt.allocPrint(allocator, "/services/{s}/execute", .{exec_id});
defer allocator.free(path);
const auth_headers = try buildAuthCmd(allocator, "POST", path, json, public_key, secret_key);
defer allocator.free(auth_headers);
const cmd = try std.fmt.allocPrint(allocator, "curl -s -X POST '{s}/services/{s}/execute' -H 'Content-Type: application/json' {s} -d '{s}'", .{ API_BASE, exec_id, auth_headers, json });
defer allocator.free(cmd);
_ = std.c.system(cmd.ptr);
std.debug.print("\n", .{});
} else if (dump_bootstrap) |bootstrap_id| {
std.debug.print("Fetching bootstrap script from {s}...\n", .{bootstrap_id});
const tmp_file = "/tmp/unsandbox_bootstrap_dump.txt";
const json = "{{\"command\":\"cat /tmp/bootstrap.sh\"}}";
const path = try std.fmt.allocPrint(allocator, "/services/{s}/execute", .{bootstrap_id});
defer allocator.free(path);
const auth_headers = try buildAuthCmd(allocator, "POST", path, json, public_key, secret_key);
defer allocator.free(auth_headers);
const cmd = try std.fmt.allocPrint(allocator, "curl -s -X POST '{s}/services/{s}/execute' -H 'Content-Type: application/json' {s} -d '{s}' -o {s}", .{ API_BASE, bootstrap_id, auth_headers, json, tmp_file });
defer allocator.free(cmd);
_ = std.c.system(cmd.ptr);
// Read the JSON response
const json_content = fs.cwd().readFileAlloc(allocator, tmp_file, 1024 * 1024) catch |err| {
std.debug.print("\x1b[31mError reading response: {}\x1b[0m\n", .{err});
std.fs.cwd().deleteFile(tmp_file) catch {};
return 1;
};
defer allocator.free(json_content);
std.fs.cwd().deleteFile(tmp_file) catch {};
// Extract stdout from JSON (simple string search)
const stdout_prefix = "\"stdout\":\"";
if (mem.indexOf(u8, json_content, stdout_prefix)) |start_idx| {
const value_start = start_idx + stdout_prefix.len;
if (mem.indexOfPos(u8, json_content, value_start, "\"")) |end_idx| {
const bootstrap_content = json_content[value_start..end_idx];
if (dump_file) |file_path| {
const file = try std.fs.cwd().createFile(file_path, .{});
defer file.close();
try file.writeAll(bootstrap_content);
// Set permissions (Unix only)
if (@import("builtin").os.tag != .windows) {
const chmod_cmd = try std.fmt.allocPrint(allocator, "chmod 755 {s}", .{file_path});
defer allocator.free(chmod_cmd);
_ = std.c.system(chmod_cmd.ptr);
}
std.debug.print("Bootstrap saved to {s}\n", .{file_path});
} else {
std.debug.print("{s}", .{bootstrap_content});
}
} else {
std.debug.print("\x1b[31mError: Failed to parse bootstrap response\x1b[0m\n", .{});
return 1;
}
} else {
std.debug.print("\x1b[31mError: Failed to fetch bootstrap (service not running or no bootstrap file)\x1b[0m\n", .{});
return 1;
}
} else if (resize) |resize_id| {
// Validate vcpu
if (vcpu < 1 or vcpu > 8) {
std.debug.print("{s}Error: --resize requires -v N (1-8){s}\n", .{ RED, RESET });
return 1;
}
// Build JSON body
var vcpu_buf: [16]u8 = undefined;
const vcpu_str = std.fmt.bufPrint(&vcpu_buf, "{d}", .{vcpu}) catch "0";
const json = try std.fmt.allocPrint(allocator, "{{\"vcpu\":{s}}}", .{vcpu_str});
defer allocator.free(json);
// Build path
const path = try std.fmt.allocPrint(allocator, "/services/{s}", .{resize_id});
defer allocator.free(path);
// Build auth headers
const auth_headers = try buildAuthCmd(allocator, "PATCH", path, json, public_key, secret_key);
defer allocator.free(auth_headers);
// Execute PATCH request
const cmd = try std.fmt.allocPrint(allocator, "curl -s -X PATCH '{s}/services/{s}' -H 'Content-Type: application/json' {s} -d '{s}'", .{ API_BASE, resize_id, auth_headers, json });
defer allocator.free(cmd);
_ = std.c.system(cmd.ptr);
// Calculate RAM
const ram = vcpu * 2;
std.debug.print("\n{s}Service resized to {d} vCPU, {d} GB RAM{s}\n", .{ GREEN, vcpu, ram, RESET });
} else if (set_unfreeze_on_demand_id) |svc_id| {
// Handle set-unfreeze-on-demand
const enabled = if (set_unfreeze_on_demand_enabled) |e|
mem.eql(u8, e, "true") or mem.eql(u8, e, "1")
else
false;
const enabled_str = if (enabled) "true" else "false";
const json = try std.fmt.allocPrint(allocator, "{{\"unfreeze_on_demand\":{s}}}", .{enabled_str});
defer allocator.free(json);
const path = try std.fmt.allocPrint(allocator, "/services/{s}", .{svc_id});
defer allocator.free(path);
const auth_headers = try buildAuthCmd(allocator, "PATCH", path, json, public_key, secret_key);
defer allocator.free(auth_headers);
const cmd = try std.fmt.allocPrint(allocator, "curl -s -X PATCH '{s}/services/{s}' -H 'Content-Type: application/json' {s} -d '{s}'", .{ API_BASE, svc_id, auth_headers, json });
defer allocator.free(cmd);
_ = std.c.system(cmd.ptr);
if (enabled) {
std.debug.print("\n{s}Unfreeze-on-demand enabled for service {s}{s}\n", .{ GREEN, svc_id, RESET });
} else {
std.debug.print("\n{s}Unfreeze-on-demand disabled for service {s}{s}\n", .{ GREEN, svc_id, RESET });
}
} else if (name) |n| {
var json_buf: [65536]u8 = undefined;
var json_stream = std.io.fixedBufferStream(&json_buf);
const writer = json_stream.writer();
try writer.print("{{\"name\":\"{s}\"", .{n});
if (ports) |p| {
try writer.print(",\"ports\":[{s}]", .{p});
}
if (service_type) |t| {
try writer.print(",\"service_type\":\"{s}\"", .{t});
}
if (bootstrap) |b| {
try writer.writeAll(",\"bootstrap\":\"");
// Escape JSON
for (b) |c| {
switch (c) {
'"' => try writer.writeAll("\\\""),
'\\' => try writer.writeAll("\\\\"),
'\n' => try writer.writeAll("\\n"),
'\r' => try writer.writeAll("\\r"),
'\t' => try writer.writeAll("\\t"),
else => try writer.writeByte(c),
}
}
try writer.writeAll("\"");
}
if (bootstrap_file) |bf| {
const boot_content = fs.cwd().readFileAlloc(allocator, bf, 10 * 1024 * 1024) catch |err| {
std.debug.print("\x1b[31mError: Bootstrap file not found: {s} ({})\x1b[0m\n", .{ bf, err });
return 1;
};
defer allocator.free(boot_content);
try writer.writeAll(",\"bootstrap_content\":\"");
// Escape JSON
for (boot_content) |c| {
switch (c) {
'"' => try writer.writeAll("\\\""),
'\\' => try writer.writeAll("\\\\"),
'\n' => try writer.writeAll("\\n"),
'\r' => try writer.writeAll("\\r"),
'\t' => try writer.writeAll("\\t"),
else => try writer.writeByte(c),
}
}
try writer.writeAll("\"");
}
if (unfreeze_on_demand) {
try writer.writeAll(",\"unfreeze_on_demand\":true");
}
// Add input_files JSON
const input_files_json = try buildInputFilesJson(allocator, input_files);
defer allocator.free(input_files_json);
try writer.writeAll(input_files_json);
try writer.writeAll("}");
const json_str = json_stream.getWritten();
const auth_headers = try buildAuthCmd(allocator, "POST", "/services", json_str, public_key, secret_key);
defer allocator.free(auth_headers);
// Check if we need auto-vault
const has_env = svc_envs.items.len > 0 or svc_env_file != null;
if (has_env) {
// Capture response to temp file to extract service_id
const response_file = "/tmp/unsandbox_service_create.json";
const cmd = try std.fmt.allocPrint(allocator, "curl -s -X POST '{s}/services' -H 'Content-Type: application/json' {s} -d '{s}' -o {s}", .{ API_BASE, auth_headers, json_str, response_file });
defer allocator.free(cmd);
std.debug.print("{s}Creating service...{s}\n", .{ YELLOW, RESET });
_ = std.c.system(cmd.ptr);
// Read response
const response_content = fs.cwd().readFileAlloc(allocator, response_file, 1024 * 1024) catch {
std.debug.print("{s}Error: Failed to read service creation response{s}\n", .{ RED, RESET });
return 1;
};
defer allocator.free(response_content);
fs.cwd().deleteFile(response_file) catch {};
// Print the response
std.debug.print("{s}\n", .{response_content});
// Extract service_id and auto-set vault
if (extractJsonField(response_content, "service_id")) |service_id| {
const env_content = try buildEnvContent(allocator, svc_envs, svc_env_file);
defer allocator.free(env_content);
if (env_content.len > 0) {
if (try serviceEnvSet(allocator, service_id, env_content, public_key, secret_key)) {
std.debug.print("\n{s}Vault configured for service {s}{s}\n", .{ GREEN, service_id, RESET });
}
}
}
} else {
const cmd = try std.fmt.allocPrint(allocator, "curl -s -X POST '{s}/services' -H 'Content-Type: application/json' {s} -d '{s}'", .{ API_BASE, auth_headers, json_str });
defer allocator.free(cmd);
std.debug.print("{s}Creating service...{s}\n", .{ YELLOW, RESET });
_ = std.c.system(cmd.ptr);
std.debug.print("\n", .{});
}
}
return 0;
}
// Handle languages command
if (mem.eql(u8, args[1], "languages")) {
var json_output = false;
var i: usize = 2;
while (i < args.len) : (i += 1) {
if (mem.eql(u8, args[i], "--json")) {
json_output = true;
} else if (mem.eql(u8, args[i], "-k") and i + 1 < args.len) {
i += 1;
allocator.free(public_key);
public_key = try allocator.dupe(u8, args[i]);
}
}
// Get cache path (~/.unsandbox/languages.json)
const home_env = std.process.getEnvVarOwned(allocator, "HOME") catch try allocator.dupe(u8, "/tmp");
defer allocator.free(home_env);
const cache_dir = try std.fmt.allocPrint(allocator, "{s}/.unsandbox", .{home_env});
defer allocator.free(cache_dir);
const cache_file = try std.fmt.allocPrint(allocator, "{s}/languages.json", .{cache_dir});
defer allocator.free(cache_file);
// Ensure cache directory exists
fs.cwd().makeDir(cache_dir) catch |err| switch (err) {
error.PathAlreadyExists => {},
else => {},
};
// Check cache first
var use_cache = false;
const cache_content = fs.cwd().readFileAlloc(allocator, cache_file, 1024 * 1024) catch null;
defer if (cache_content) |cc| allocator.free(cc);
if (cache_content) |cc| {
// Check timestamp
const ts_prefix = "\"timestamp\":";
if (mem.indexOf(u8, cc, ts_prefix)) |ts_start_idx| {
const ts_value_start = ts_start_idx + ts_prefix.len;
var ts_value_end = ts_value_start;
while (ts_value_end < cc.len and (cc[ts_value_end] >= '0' and cc[ts_value_end] <= '9')) {
ts_value_end += 1;
}
if (ts_value_end > ts_value_start) {
const ts_str = cc[ts_value_start..ts_value_end];
const cache_timestamp = std.fmt.parseInt(i64, ts_str, 10) catch 0;
const current_timestamp = std.time.timestamp();
if (current_timestamp - cache_timestamp < LANGUAGES_CACHE_TTL) {
use_cache = true;
}
}
}
}
var json_content: []const u8 = undefined;
var json_content_owned = false;
if (use_cache) {
json_content = cache_content.?;
} else {
// Fetch languages from API
const json_file = "/tmp/unsandbox_languages_tmp.json";
const auth_headers = try buildAuthCmd(allocator, "GET", "/languages", "", public_key, secret_key);
defer allocator.free(auth_headers);
const cmd = try std.fmt.allocPrint(allocator, "curl -s -X GET '{s}/languages' {s} -o {s}", .{ API_BASE, auth_headers, json_file });
defer allocator.free(cmd);
_ = std.c.system(cmd.ptr);
// Read the JSON response
const api_content = fs.cwd().readFileAlloc(allocator, json_file, 1024 * 1024) catch |err| {
std.debug.print("{s}Error reading languages response: {}{s}\n", .{ RED, err, RESET });
std.fs.cwd().deleteFile(json_file) catch {};
return 1;
};
json_content = api_content;
json_content_owned = true;
std.fs.cwd().deleteFile(json_file) catch {};
// Save to cache with timestamp
const arr_prefix = "\"languages\":[";
if (mem.indexOf(u8, api_content, arr_prefix)) |start_idx| {
const arr_start = start_idx + arr_prefix.len - 1; // Include the '['
if (mem.indexOfPos(u8, api_content, arr_start, "]")) |end_idx| {
const languages_array = api_content[arr_start .. end_idx + 1];
const current_ts = std.time.timestamp();
const cache_data = try std.fmt.allocPrint(allocator, "{{\"languages\":{s},\"timestamp\":{d}}}", .{ languages_array, current_ts });
defer allocator.free(cache_data);
// Write cache file
const cache_file_handle = fs.cwd().createFile(cache_file, .{}) catch null;
if (cache_file_handle) |fh| {
fh.writeAll(cache_data) catch {};
fh.close();
}
}
}
}
defer if (json_content_owned) allocator.free(json_content);
if (json_output) {
// Find and print just the languages array
const arr_prefix = "\"languages\":[";
if (mem.indexOf(u8, json_content, arr_prefix)) |start_idx| {
const arr_start = start_idx + arr_prefix.len - 1; // Include the '['
if (mem.indexOfPos(u8, json_content, arr_start, "]")) |end_idx| {
std.debug.print("{s}\n", .{json_content[arr_start .. end_idx + 1]});
} else {
std.debug.print("{s}\n", .{json_content});
}
} else {
std.debug.print("{s}\n", .{json_content});
}
} else {
// Parse and print one language per line
const arr_prefix = "\"languages\":[";
if (mem.indexOf(u8, json_content, arr_prefix)) |start_idx| {
const arr_start = start_idx + arr_prefix.len;
if (mem.indexOfPos(u8, json_content, arr_start, "]")) |end_idx| {
const arr_content = json_content[arr_start..end_idx];
// Split by comma and extract language names
var it = mem.splitSequence(u8, arr_content, ",");
while (it.next()) |item| {
// Trim whitespace and quotes
const trimmed = mem.trim(u8, item, &std.ascii.whitespace);
if (trimmed.len > 2 and trimmed[0] == '"') {
// Remove quotes
const lang = trimmed[1 .. trimmed.len - 1];
std.debug.print("{s}\n", .{lang});
}
}
} else {
std.debug.print("{s}\n", .{json_content});
}
} else {
std.debug.print("{s}\n", .{json_content});
}
}
return 0;
}
// Handle image command
if (mem.eql(u8, args[1], "image")) {
var list = false;
var info: ?[]const u8 = null;
var delete: ?[]const u8 = null;
var lock: ?[]const u8 = null;
var unlock: ?[]const u8 = null;
var publish: ?[]const u8 = null;
var source_type: ?[]const u8 = null;
var visibility_id: ?[]const u8 = null;
var visibility_mode: ?[]const u8 = null;
var spawn: ?[]const u8 = null;
var clone: ?[]const u8 = null;
var name: ?[]const u8 = null;
var ports: ?[]const u8 = null;
var i: usize = 2;
while (i < args.len) : (i += 1) {
if (mem.eql(u8, args[i], "--list") or mem.eql(u8, args[i], "-l")) {
list = true;
} else if (mem.eql(u8, args[i], "--info") and i + 1 < args.len) {
i += 1;
info = args[i];
} else if (mem.eql(u8, args[i], "--delete") and i + 1 < args.len) {
i += 1;
delete = args[i];
} else if (mem.eql(u8, args[i], "--lock") and i + 1 < args.len) {
i += 1;
lock = args[i];
} else if (mem.eql(u8, args[i], "--unlock") and i + 1 < args.len) {
i += 1;
unlock = args[i];
} else if (mem.eql(u8, args[i], "--publish") and i + 1 < args.len) {
i += 1;
publish = args[i];
} else if (mem.eql(u8, args[i], "--source-type") and i + 1 < args.len) {
i += 1;
source_type = args[i];
} else if (mem.eql(u8, args[i], "--visibility") and i + 2 < args.len) {
i += 1;
visibility_id = args[i];
i += 1;
visibility_mode = args[i];
} else if (mem.eql(u8, args[i], "--spawn") and i + 1 < args.len) {
i += 1;
spawn = args[i];
} else if (mem.eql(u8, args[i], "--clone") and i + 1 < args.len) {
i += 1;
clone = args[i];
} else if (mem.eql(u8, args[i], "--name") and i + 1 < args.len) {
i += 1;
name = args[i];
} else if (mem.eql(u8, args[i], "--ports") and i + 1 < args.len) {
i += 1;
ports = args[i];
} else if (mem.eql(u8, args[i], "-k") and i + 1 < args.len) {
i += 1;
allocator.free(public_key);
public_key = try allocator.dupe(u8, args[i]);
}
}
if (list) {
const auth_headers = try buildAuthCmd(allocator, "GET", "/images", "", public_key, secret_key);
defer allocator.free(auth_headers);
const cmd = try std.fmt.allocPrint(allocator, "curl -s -X GET '{s}/images' {s}", .{ API_BASE, auth_headers });
defer allocator.free(cmd);
_ = std.c.system(cmd.ptr);
std.debug.print("\n", .{});
} else if (info) |inf| {
const path = try std.fmt.allocPrint(allocator, "/images/{s}", .{inf});
defer allocator.free(path);
const auth_headers = try buildAuthCmd(allocator, "GET", path, "", public_key, secret_key);
defer allocator.free(auth_headers);
const cmd = try std.fmt.allocPrint(allocator, "curl -s -X GET '{s}/images/{s}' {s}", .{ API_BASE, inf, auth_headers });
defer allocator.free(cmd);
_ = std.c.system(cmd.ptr);
std.debug.print("\n", .{});
} else if (delete) |del_id| {
const path = try std.fmt.allocPrint(allocator, "/images/{s}", .{del_id});
defer allocator.free(path);
const success_msg = try std.fmt.allocPrint(allocator, "Image deleted: {s}", .{del_id});
defer allocator.free(success_msg);
_ = try execDestructiveCurl(allocator, "DELETE", path, "", public_key, secret_key, success_msg);
} else if (lock) |lock_id| {
const path = try std.fmt.allocPrint(allocator, "/images/{s}/lock", .{lock_id});
defer allocator.free(path);
const auth_headers = try buildAuthCmd(allocator, "POST", path, "", public_key, secret_key);
defer allocator.free(auth_headers);
const cmd = try std.fmt.allocPrint(allocator, "curl -s -X POST '{s}/images/{s}/lock' {s}", .{ API_BASE, lock_id, auth_headers });
defer allocator.free(cmd);
_ = std.c.system(cmd.ptr);
std.debug.print("\n{s}Image locked: {s}{s}\n", .{ GREEN, lock_id, RESET });
} else if (unlock) |unlock_id| {
const path = try std.fmt.allocPrint(allocator, "/images/{s}/unlock", .{unlock_id});
defer allocator.free(path);
const success_msg = try std.fmt.allocPrint(allocator, "Image unlocked: {s}", .{unlock_id});
defer allocator.free(success_msg);
_ = try execDestructiveCurl(allocator, "POST", path, "", public_key, secret_key, success_msg);
} else if (publish) |pub_id| {
if (source_type == null) {
std.debug.print("{s}Error: --publish requires --source-type (service or snapshot){s}\n", .{ RED, RESET });
return 1;
}
const nm = name orelse "";
const json = try std.fmt.allocPrint(allocator, "{{\"source_type\":\"{s}\",\"source_id\":\"{s}\",\"name\":\"{s}\"}}", .{ source_type.?, pub_id, nm });
defer allocator.free(json);
const auth_headers = try buildAuthCmd(allocator, "POST", "/images/publish", json, public_key, secret_key);
defer allocator.free(auth_headers);
const cmd = try std.fmt.allocPrint(allocator, "curl -s -X POST '{s}/images/publish' -H 'Content-Type: application/json' {s} -d '{s}'", .{ API_BASE, auth_headers, json });
defer allocator.free(cmd);
_ = std.c.system(cmd.ptr);
std.debug.print("\n", .{});
} else if (visibility_id) |vis_id| {
if (visibility_mode == null) {
std.debug.print("{s}Error: --visibility requires a mode (private, unlisted, or public){s}\n", .{ RED, RESET });
return 1;
}
const json = try std.fmt.allocPrint(allocator, "{{\"visibility\":\"{s}\"}}", .{visibility_mode.?});
defer allocator.free(json);
const path = try std.fmt.allocPrint(allocator, "/images/{s}/visibility", .{vis_id});
defer allocator.free(path);
const auth_headers = try buildAuthCmd(allocator, "POST", path, json, public_key, secret_key);
defer allocator.free(auth_headers);
const cmd = try std.fmt.allocPrint(allocator, "curl -s -X POST '{s}/images/{s}/visibility' -H 'Content-Type: application/json' {s} -d '{s}'", .{ API_BASE, vis_id, auth_headers, json });
defer allocator.free(cmd);
_ = std.c.system(cmd.ptr);
std.debug.print("\n{s}Image visibility set to {s}: {s}{s}\n", .{ GREEN, visibility_mode.?, vis_id, RESET });
} else if (spawn) |spawn_id| {
const nm = name orelse "";
const pt = ports orelse "";
const json = try std.fmt.allocPrint(allocator, "{{\"name\":\"{s}\",\"ports\":[{s}]}}", .{ nm, pt });
defer allocator.free(json);
const path = try std.fmt.allocPrint(allocator, "/images/{s}/spawn", .{spawn_id});
defer allocator.free(path);
const auth_headers = try buildAuthCmd(allocator, "POST", path, json, public_key, secret_key);
defer allocator.free(auth_headers);
const cmd = try std.fmt.allocPrint(allocator, "curl -s -X POST '{s}/images/{s}/spawn' -H 'Content-Type: application/json' {s} -d '{s}'", .{ API_BASE, spawn_id, auth_headers, json });
defer allocator.free(cmd);
_ = std.c.system(cmd.ptr);
std.debug.print("\n", .{});
} else if (clone) |clone_id| {
const nm = name orelse "";
const json = try std.fmt.allocPrint(allocator, "{{\"name\":\"{s}\"}}", .{nm});
defer allocator.free(json);
const path = try std.fmt.allocPrint(allocator, "/images/{s}/clone", .{clone_id});
defer allocator.free(path);
const auth_headers = try buildAuthCmd(allocator, "POST", path, json, public_key, secret_key);
defer allocator.free(auth_headers);
const cmd = try std.fmt.allocPrint(allocator, "curl -s -X POST '{s}/images/{s}/clone' -H 'Content-Type: application/json' {s} -d '{s}'", .{ API_BASE, clone_id, auth_headers, json });
defer allocator.free(cmd);
_ = std.c.system(cmd.ptr);
std.debug.print("\n", .{});
} else {
std.debug.print("{s}Error: No image action specified. Use --list, --info, --delete, --publish, etc.{s}\n", .{ RED, RESET });
return 1;
}
return 0;
}
// Handle key command
if (mem.eql(u8, args[1], "key")) {
var extend = false;
var i: usize = 2;
while (i < args.len) : (i += 1) {
if (mem.eql(u8, args[i], "--extend")) {
extend = true;
} else if (mem.eql(u8, args[i], "-k") and i + 1 < args.len) {
i += 1;
allocator.free(public_key);
public_key = try allocator.dupe(u8, args[i]);
}
}
if (extend) {
// First validate to get the public_key
const json_file = "/tmp/unsandbox_key_validate.json";
const auth_headers = try buildAuthCmd(allocator, "POST", "/keys/validate", "", public_key, secret_key);
defer allocator.free(auth_headers);
const cmd_validate = try std.fmt.allocPrint(allocator, "curl -s -X POST '{s}/keys/validate' -H 'Content-Type: application/json' {s} -o {s}", .{ PORTAL_BASE, auth_headers, json_file });
defer allocator.free(cmd_validate);
_ = std.c.system(cmd_validate.ptr);
// Read the JSON response to extract public_key
const json_content = fs.cwd().readFileAlloc(allocator, json_file, 1024 * 1024) catch |err| {
std.debug.print("\x1b[31mError reading validation response: {}\x1b[0m\n", .{err});
std.fs.cwd().deleteFile(json_file) catch {};
return 1;
};
defer allocator.free(json_content);
std.fs.cwd().deleteFile(json_file) catch {};
// Check for clock drift errors
if (mem.indexOf(u8, json_content, "timestamp") != null and
(mem.indexOf(u8, json_content, "401") != null or
mem.indexOf(u8, json_content, "expired") != null or
mem.indexOf(u8, json_content, "invalid") != null))
{
std.debug.print("\x1b[31mError: Request timestamp expired (must be within 5 minutes of server time)\x1b[0m\n", .{});
std.debug.print("\x1b[33mYour computer's clock may have drifted.\x1b[0m\n", .{});
std.debug.print("\x1b[33mCheck your system time and sync with NTP if needed:\x1b[0m\n", .{});
std.debug.print("\x1b[33m Linux: sudo ntpdate -s time.nist.gov\x1b[0m\n", .{});
std.debug.print("\x1b[33m macOS: sudo sntp -sS time.apple.com\x1b[0m\n", .{});
std.debug.print("\x1b[33m Windows: w32tm /resync\x1b[0m\n", .{});
return 1;
}
// Simple JSON parsing to find public_key (looking for "public_key":"value")
const pk_prefix = "\"public_key\":\"";
var public_key_value: ?[]const u8 = null;
if (mem.indexOf(u8, json_content, pk_prefix)) |start_idx| {
const value_start = start_idx + pk_prefix.len;
if (mem.indexOfPos(u8, json_content, value_start, "\"")) |end_idx| {
public_key_value = json_content[value_start..end_idx];
}
}
if (public_key_value) |pk| {
const url = try std.fmt.allocPrint(allocator, "{s}/keys/extend?pk={s}", .{ PORTAL_BASE, pk });
defer allocator.free(url);
std.debug.print("\x1b[33mOpening browser to extend key...\x1b[0m\n", .{});
const open_cmd = try std.fmt.allocPrint(allocator, "xdg-open '{s}' 2>/dev/null || open '{s}' 2>/dev/null || start '{s}' 2>/dev/null", .{ url, url, url });
defer allocator.free(open_cmd);
_ = std.c.system(open_cmd.ptr);
} else {
std.debug.print("\x1b[31mError: Could not extract public_key from response\x1b[0m\n", .{});
return 1;
}
} else {
// Regular validation
const json_file = "/tmp/unsandbox_key_validate.json";
const auth_headers = try buildAuthCmd(allocator, "POST", "/keys/validate", "", public_key, secret_key);
defer allocator.free(auth_headers);
const cmd = try std.fmt.allocPrint(allocator, "curl -s -X POST '{s}/keys/validate' -H 'Content-Type: application/json' {s} -o {s}", .{ PORTAL_BASE, auth_headers, json_file });
defer allocator.free(cmd);
_ = std.c.system(cmd.ptr);
// Read and parse the response
const json_content = fs.cwd().readFileAlloc(allocator, json_file, 1024 * 1024) catch |err| {
std.debug.print("\x1b[31mError reading validation response: {}\x1b[0m\n", .{err});
std.fs.cwd().deleteFile(json_file) catch {};
return 1;
};
defer allocator.free(json_content);
std.fs.cwd().deleteFile(json_file) catch {};
// Check for clock drift errors
if (mem.indexOf(u8, json_content, "timestamp") != null and
(mem.indexOf(u8, json_content, "401") != null or
mem.indexOf(u8, json_content, "expired") != null or
mem.indexOf(u8, json_content, "invalid") != null))
{
std.debug.print("\x1b[31mError: Request timestamp expired (must be within 5 minutes of server time)\x1b[0m\n", .{});
std.debug.print("\x1b[33mYour computer's clock may have drifted.\x1b[0m\n", .{});
std.debug.print("\x1b[33mCheck your system time and sync with NTP if needed:\x1b[0m\n", .{});
std.debug.print("\x1b[33m Linux: sudo ntpdate -s time.nist.gov\x1b[0m\n", .{});
std.debug.print("\x1b[33m macOS: sudo sntp -sS time.apple.com\x1b[0m\n", .{});
std.debug.print("\x1b[33m Windows: w32tm /resync\x1b[0m\n", .{});
return 1;
}
// Simple JSON parsing (looking for specific fields)
const status_prefix = "\"status\":\"";
var status: ?[]const u8 = null;
if (mem.indexOf(u8, json_content, status_prefix)) |start_idx| {
const value_start = start_idx + status_prefix.len;
if (mem.indexOfPos(u8, json_content, value_start, "\"")) |end_idx| {
status = json_content[value_start..end_idx];
}
}
if (status == null) {
std.debug.print("\x1b[31mError: Invalid response from server\x1b[0m\n", .{});
return 1;
}
// Extract other fields
var pub_key: ?[]const u8 = null;
var tier: ?[]const u8 = null;
var expires_at: ?[]const u8 = null;
const pk_prefix = "\"public_key\":\"";
if (mem.indexOf(u8, json_content, pk_prefix)) |start_idx| {
const value_start = start_idx + pk_prefix.len;
if (mem.indexOfPos(u8, json_content, value_start, "\"")) |end_idx| {
pub_key = json_content[value_start..end_idx];
}
}
const tier_prefix = "\"tier\":\"";
if (mem.indexOf(u8, json_content, tier_prefix)) |start_idx| {
const value_start = start_idx + tier_prefix.len;
if (mem.indexOfPos(u8, json_content, value_start, "\"")) |end_idx| {
tier = json_content[value_start..end_idx];
}
}
const expires_prefix = "\"expires_at\":\"";
if (mem.indexOf(u8, json_content, expires_prefix)) |start_idx| {
const value_start = start_idx + expires_prefix.len;
if (mem.indexOfPos(u8, json_content, value_start, "\"")) |end_idx| {
expires_at = json_content[value_start..end_idx];
}
}
// Display results based on status
if (status) |s| {
if (mem.eql(u8, s, "valid")) {
std.debug.print("\x1b[32mValid\x1b[0m\n", .{});
if (pub_key) |pk| std.debug.print("Public Key: {s}\n", .{pk});
if (tier) |t| std.debug.print("Tier: {s}\n", .{t});
if (expires_at) |exp| std.debug.print("Expires: {s}\n", .{exp});
} else if (mem.eql(u8, s, "expired")) {
std.debug.print("\x1b[31mExpired\x1b[0m\n", .{});
if (pub_key) |pk| std.debug.print("Public Key: {s}\n", .{pk});
if (tier) |t| std.debug.print("Tier: {s}\n", .{t});
if (expires_at) |exp| std.debug.print("Expired: {s}\n", .{exp});
std.debug.print("\x1b[33mTo renew: Visit {s}/keys/extend\x1b[0m\n", .{PORTAL_BASE});
} else if (mem.eql(u8, s, "invalid")) {
std.debug.print("\x1b[31mInvalid\x1b[0m\n", .{});
} else {
std.debug.print("Status: {s}\n", .{s});
}
}
}
return 0;
}
// Execute mode - find source file (skip known flags and their values)
var source_file: ?[]const u8 = null;
{
var exec_i: usize = 1;
while (exec_i < args.len) : (exec_i += 1) {
const arg = args[exec_i];
if (mem.eql(u8, arg, "--account") or mem.eql(u8, arg, "-p") or
mem.eql(u8, arg, "-k") or mem.eql(u8, arg, "-e") or
mem.eql(u8, arg, "-n") or mem.eql(u8, arg, "-v"))
{
exec_i += 1; // skip value
} else if (mem.eql(u8, arg, "-a")) {
// no value
} else if (mem.startsWith(u8, arg, "-")) {
const stderr = std.io.getStdErr().writer();
stderr.print("{s}Unknown option: {s}{s}\n", .{ RED, arg, RESET }) catch {};
std.os.exit(1);
} else {
source_file = arg;
break;
}
}
}
if (source_file == null) {
std.debug.print("\x1b[31mError: No source file specified\x1b[0m\n", .{});
return 1;
}
const filename = source_file.?;
// Detect language
const ext = fs.path.extension(filename);
const lang = blk: {
if (mem.eql(u8, ext, ".py")) break :blk "python";
if (mem.eql(u8, ext, ".js")) break :blk "javascript";
if (mem.eql(u8, ext, ".go")) break :blk "go";
if (mem.eql(u8, ext, ".rs")) break :blk "rust";
if (mem.eql(u8, ext, ".c")) break :blk "c";
if (mem.eql(u8, ext, ".cpp")) break :blk "cpp";
if (mem.eql(u8, ext, ".d")) break :blk "d";
if (mem.eql(u8, ext, ".zig")) break :blk "zig";
if (mem.eql(u8, ext, ".nim")) break :blk "nim";
if (mem.eql(u8, ext, ".v")) break :blk "v";
std.debug.print("\x1b[31mError: Cannot detect language\x1b[0m\n", .{});
return 1;
};
// Read source file
const code = fs.cwd().readFileAlloc(allocator, filename, 10 * 1024 * 1024) catch |err| {
std.debug.print("\x1b[31mError reading file: {}\x1b[0m\n", .{err});
return 1;
};
defer allocator.free(code);
// Build JSON (simplified - doesn't handle all escape sequences)
const json_file = "/tmp/unsandbox_request.json";
const file = try std.fs.cwd().createFile(json_file, .{});
defer file.close();
const writer = file.writer();
try writer.print("{{\"language\":\"{s}\",\"code\":\"", .{lang});
// Escape JSON
for (code) |c| {
switch (c) {
'"' => try writer.writeAll("\\\""),
'\\' => try writer.writeAll("\\\\"),
'\n' => try writer.writeAll("\\n"),
'\r' => try writer.writeAll("\\r"),
'\t' => try writer.writeAll("\\t"),
else => try writer.writeByte(c),
}
}
try writer.writeAll("\"}");
// Read back the JSON to compute HMAC
const json_content = try fs.cwd().readFileAlloc(allocator, json_file, 10 * 1024 * 1024);
defer allocator.free(json_content);
// Execute with curl
const auth_headers = try buildAuthCmd(allocator, "POST", "/execute", json_content, public_key, secret_key);
defer allocator.free(auth_headers);
const response_file = "/tmp/unsandbox_response.json";
const cmd = try std.fmt.allocPrint(allocator, "curl -s -X POST '{s}/execute' -H 'Content-Type: application/json' {s} -d @{s} -o {s}", .{ API_BASE, auth_headers, json_file, response_file });
defer allocator.free(cmd);
_ = std.c.system(cmd.ptr);
// Read response to check for clock drift errors
const response_content = fs.cwd().readFileAlloc(allocator, response_file, 10 * 1024 * 1024) catch |err| {
std.debug.print("\x1b[31mError reading response: {}\x1b[0m\n", .{err});
std.fs.cwd().deleteFile(json_file) catch {};
std.fs.cwd().deleteFile(response_file) catch {};
return 1;
};
defer allocator.free(response_content);
// Check for clock drift errors
if (mem.indexOf(u8, response_content, "timestamp") != null and
(mem.indexOf(u8, response_content, "401") != null or
mem.indexOf(u8, response_content, "expired") != null or
mem.indexOf(u8, response_content, "invalid") != null))
{
std.debug.print("\x1b[31mError: Request timestamp expired (must be within 5 minutes of server time)\x1b[0m\n", .{});
std.debug.print("\x1b[33mYour computer's clock may have drifted.\x1b[0m\n", .{});
std.debug.print("\x1b[33mCheck your system time and sync with NTP if needed:\x1b[0m\n", .{});
std.debug.print("\x1b[33m Linux: sudo ntpdate -s time.nist.gov\x1b[0m\n", .{});
std.debug.print("\x1b[33m macOS: sudo sntp -sS time.apple.com\x1b[0m\n", .{});
std.debug.print("\x1b[33m Windows: w32tm /resync\x1b[0m\n", .{});
std.fs.cwd().deleteFile(json_file) catch {};
std.fs.cwd().deleteFile(response_file) catch {};
return 1;
}
// Print response
std.debug.print("{s}\n", .{response_content});
// Cleanup
std.fs.cwd().deleteFile(json_file) catch {};
std.fs.cwd().deleteFile(response_file) catch {};
return 0;
}
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