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 — Objective-C
# Download + setup
curl -O https://git.unturf.com/engineering/unturf/un-inception/-/raw/main/clients/objective-c/sync/src/un.m && chmod +x un.m && ln -sf un.m un
export UNSANDBOX_PUBLIC_KEY="unsb-pk-xxxx-xxxx-xxxx-xxxx"
export UNSANDBOX_SECRET_KEY="unsb-sk-xxxxx-xxxxx-xxxxx-xxxxx"
# Run code
./un script.objective-c
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 Objective-C existente:
curl -O https://git.unturf.com/engineering/unturf/un-inception/-/raw/main/clients/objective-c/sync/src/un.m
# 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 Objective-C app:
#import "Un.h"
int main() {
NSDictionary *result = [Un executeCode:@"objc"
code:@"NSLog(@\"Hello from Objective-C running on unsandbox!\");"];
NSLog(@"%@", result[@"stdout"]); // Hello from Objective-C running on unsandbox!
}
./un.m
0a5217653a9b3880ba7e0a3364d29e2d
SHA256: cb0e02f92053f9c686e1288f08189f3d02fd148ff39e221646082aee469615bb
// PUBLIC DOMAIN - NO LICENSE, NO WARRANTY
//
// This is free public domain software for the public good of a permacomputer hosted
// at permacomputer.com - an always-on computer by the people, for the people. One
// which is durable, easy to repair, and distributed like tap water for machine
// learning intelligence.
//
// The permacomputer is community-owned infrastructure optimized around four values:
//
// TRUTH - First principles, math & science, open source code freely distributed
// FREEDOM - Voluntary partnerships, freedom from tyranny & corporate control
// HARMONY - Minimal waste, self-renewing systems with diverse thriving connections
// LOVE - Be yourself without hurting others, cooperation through natural law
//
// This software contributes to that vision by enabling code execution across 42+
// programming languages through a unified interface, accessible to all. Code is
// seeds to sprout on any abandoned technology.
//
// Learn more: https://www.permacomputer.com
//
// Anyone is free to copy, modify, publish, use, compile, sell, or distribute this
// software, either in source code form or as a compiled binary, for any purpose,
// commercial or non-commercial, and by any means.
//
// NO WARRANTY. THE SOFTWARE IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND.
//
// That said, our permacomputer's digital membrane stratum continuously runs unit,
// integration, and functional tests on all of it's own software - with our
// permacomputer monitoring itself, repairing itself, with minimal human in the
// loop guidance. Our agents do their best.
//
// Copyright 2025 TimeHexOn & foxhop & russell@unturf
// https://www.timehexon.com
// https://www.foxhop.net
// https://www.unturf.com/software
//
// unsandbox SDK for Objective-C - Execute code in secure sandboxes
// https://unsandbox.com | https://api.unsandbox.com/openapi
//
// Library Usage:
// #import "un.m" // or as header
// UNClient *client = [[UNClient alloc] init];
// NSDictionary *result = [client execute:@"python" code:@"print('Hello')"];
// NSLog(@"%@", result[@"stdout"]);
//
// CLI Usage:
// ./un.m script.py
// ./un.m -s python 'print("Hello")'
// ./un.m session --shell python3
//
// Authentication (in priority order):
// 1. UNClient initWithPublicKey:secretKey: constructor arguments
// 2. --account N flag -> accounts.csv row N (bypasses env vars)
// 3. Environment variables: UNSANDBOX_PUBLIC_KEY + UNSANDBOX_SECRET_KEY
// 4. Config file: ~/.unsandbox/accounts.csv row 0 (or UNSANDBOX_ACCOUNT)
// 5. ./accounts.csv row 0
#!/usr/bin/env -S clang -x objective-c -framework Foundation -o /tmp/un_objc && /tmp/un_objc
#import <Foundation/Foundation.h>
#import <CommonCrypto/CommonHMAC.h>
// ============================================================================
// Configuration
// ============================================================================
static NSString* const UN_API_BASE = @"https://api.unsandbox.com";
static NSString* const UN_PORTAL_BASE = @"https://unsandbox.com";
static const NSInteger UN_DEFAULT_TIMEOUT = 300;
static const NSInteger UN_DEFAULT_TTL = 60;
static const NSInteger UN_LANGUAGES_CACHE_TTL = 3600; // 1 hour in seconds
// Polling delays (ms) - exponential backoff
static const int UN_POLL_DELAYS[] = {300, 450, 700, 900, 650, 1600, 2000};
static const int UN_POLL_DELAYS_COUNT = 7;
// ANSI colors
static NSString* const BLUE = @"\033[34m";
static NSString* const RED = @"\033[31m";
static NSString* const GREEN = @"\033[32m";
static NSString* const YELLOW = @"\033[33m";
static NSString* const RESET = @"\033[0m";
// ============================================================================
// Extension to Language Mapping
// ============================================================================
/**
* Returns mapping from file extensions to language identifiers.
*/
NSDictionary* UNGetExtMap(void) {
return @{
@"py": @"python", @"js": @"javascript", @"ts": @"typescript",
@"rb": @"ruby", @"php": @"php", @"pl": @"perl", @"lua": @"lua",
@"sh": @"bash", @"go": @"go", @"rs": @"rust", @"c": @"c",
@"cpp": @"cpp", @"cc": @"cpp", @"cxx": @"cpp",
@"java": @"java", @"kt": @"kotlin", @"cs": @"csharp", @"fs": @"fsharp",
@"hs": @"haskell", @"ml": @"ocaml", @"clj": @"clojure", @"scm": @"scheme",
@"lisp": @"commonlisp", @"erl": @"erlang", @"ex": @"elixir", @"exs": @"elixir",
@"jl": @"julia", @"r": @"r", @"R": @"r", @"cr": @"crystal",
@"d": @"d", @"nim": @"nim", @"zig": @"zig", @"v": @"vlang",
@"dart": @"dart", @"groovy": @"groovy", @"scala": @"scala",
@"f90": @"fortran", @"f95": @"fortran", @"cob": @"cobol",
@"pro": @"prolog", @"forth": @"forth", @"4th": @"forth",
@"tcl": @"tcl", @"raku": @"raku", @"pl6": @"raku", @"p6": @"raku",
@"m": @"objc", @"awk": @"awk"
};
}
// ============================================================================
// Error Classes
// ============================================================================
/**
* UNError - Base error class for unsandbox SDK errors.
*/
@interface UNError : NSError
+ (instancetype)errorWithMessage:(NSString*)message;
@end
@implementation UNError
+ (instancetype)errorWithMessage:(NSString*)message {
return [self errorWithDomain:@"com.unsandbox" code:1 userInfo:@{NSLocalizedDescriptionKey: message}];
}
@end
/**
* UNAuthenticationError - Invalid or missing credentials.
*/
@interface UNAuthenticationError : UNError
@end
@implementation UNAuthenticationError
@end
/**
* UNExecutionError - Code execution failed.
*/
@interface UNExecutionError : UNError
@property (nonatomic) int exitCode;
@property (nonatomic, strong) NSString* stderr;
@end
@implementation UNExecutionError
@end
/**
* UNAPIError - API request failed.
*/
@interface UNAPIError : UNError
@property (nonatomic) NSInteger statusCode;
@property (nonatomic, strong) NSString* response;
@end
@implementation UNAPIError
@end
/**
* UNTimeoutError - Execution or polling timed out.
*/
@interface UNTimeoutError : UNError
@end
@implementation UNTimeoutError
@end
// ============================================================================
// HMAC Authentication
// ============================================================================
/**
* Generate HMAC-SHA256 signature in hex format.
*
* @param key The secret key for HMAC
* @param message The message to sign
* @return Hex-encoded signature string
*/
NSString* UNHmacSha256Hex(NSString* key, NSString* message) {
const char* cKey = [key UTF8String];
const char* cMessage = [message UTF8String];
unsigned char digest[CC_SHA256_DIGEST_LENGTH];
CCHmac(kCCHmacAlgSHA256, cKey, strlen(cKey), cMessage, strlen(cMessage), digest);
NSMutableString* hex = [NSMutableString stringWithCapacity:CC_SHA256_DIGEST_LENGTH * 2];
for (int i = 0; i < CC_SHA256_DIGEST_LENGTH; i++) {
[hex appendFormat:@"%02x", digest[i]];
}
return hex;
}
/**
* Compute API request signature.
* Signature = HMAC-SHA256(secret_key, "timestamp:METHOD:path:body")
*
* @param secretKey API secret key
* @param timestamp Unix timestamp
* @param method HTTP method (GET, POST, etc.)
* @param path API endpoint path
* @param body Request body (empty string if none)
* @return Hex-encoded signature
*/
NSString* UNComputeSignature(NSString* secretKey, long timestamp, NSString* method, NSString* path, NSString* body) {
NSString* message = [NSString stringWithFormat:@"%ld:%@:%@:%@", timestamp, method, path, body ?: @""];
return UNHmacSha256Hex(secretKey, message);
}
// ============================================================================
// Credentials Loading
// ============================================================================
// Global account index: -1 means not set (use env vars / default CSV row).
// Set by main() when --account N is parsed.
static NSInteger g_accountIndex = -1;
/**
* Load public_key,secret_key from a CSV file at the given row index (0-based,
* skipping blank lines and comment lines starting with '#').
*
* @param csvPath Path to the CSV file
* @param rowIndex Zero-based data row to read
* @param outPk Output: public key string (nil if not found)
* @param outSk Output: secret key string (nil if not found)
*/
void UNLoadCredentialsFromCSV(NSString* csvPath, NSInteger rowIndex, NSString** outPk, NSString** outSk) {
*outPk = nil;
*outSk = nil;
NSFileManager* fm = [NSFileManager defaultManager];
if (![fm fileExistsAtPath:csvPath]) return;
NSString* content = [NSString stringWithContentsOfFile:csvPath encoding:NSUTF8StringEncoding error:nil];
if (!content) return;
NSArray* lines = [content componentsSeparatedByString:@"\n"];
NSInteger dataIndex = 0;
for (NSString* line in lines) {
NSString* trimmed = [line stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
if ([trimmed length] == 0 || [trimmed hasPrefix:@"#"]) continue;
if (dataIndex == rowIndex) {
NSArray* parts = [trimmed componentsSeparatedByString:@","];
if ([parts count] >= 2) {
*outPk = [parts[0] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
*outSk = [parts[1] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
}
return;
}
dataIndex++;
}
}
/**
* Get API credentials from environment or config file.
*
* Priority order:
* 1. Function arguments (argPublicKey / argSecretKey)
* 2. g_accountIndex >= 0 -> accounts.csv row N (bypasses env vars)
* 3. Environment variables: UNSANDBOX_PUBLIC_KEY + UNSANDBOX_SECRET_KEY
* 4. ~/.unsandbox/accounts.csv row 0 (or UNSANDBOX_ACCOUNT env var)
* 5. ./accounts.csv row 0
*
* @param publicKey Output public key
* @param secretKey Output secret key
* @param argPublicKey Optional public key from arguments
* @param argSecretKey Optional secret key from arguments
* @param error Error output
* @return YES if credentials found, NO otherwise
*/
BOOL UNGetCredentials(NSString** publicKey, NSString** secretKey, NSString* argPublicKey, NSString* argSecretKey, NSError** error) {
// Priority 1: Function arguments
if (argPublicKey && argSecretKey && [argPublicKey length] > 0 && [argSecretKey length] > 0) {
*publicKey = argPublicKey;
*secretKey = argSecretKey;
return YES;
}
// Priority 2: --account N -> accounts.csv row N (bypasses env vars)
if (g_accountIndex >= 0) {
NSString* home = NSHomeDirectory();
NSString* homeCsv = [home stringByAppendingPathComponent:@".unsandbox/accounts.csv"];
UNLoadCredentialsFromCSV(homeCsv, g_accountIndex, publicKey, secretKey);
if (*publicKey && [*publicKey length] > 0) return YES;
UNLoadCredentialsFromCSV(@"accounts.csv", g_accountIndex, publicKey, secretKey);
if (*publicKey && [*publicKey length] > 0) return YES;
if (error) {
*error = [UNAuthenticationError errorWithMessage:
[NSString stringWithFormat:@"No credentials found for account index %ld in accounts.csv", (long)g_accountIndex]];
}
return NO;
}
// Priority 3: Environment variables
*publicKey = [[[NSProcessInfo processInfo] environment] objectForKey:@"UNSANDBOX_PUBLIC_KEY"];
*secretKey = [[[NSProcessInfo processInfo] environment] objectForKey:@"UNSANDBOX_SECRET_KEY"];
if (*publicKey && *secretKey && [*publicKey length] > 0 && [*secretKey length] > 0) {
return YES;
}
// Fall back to legacy UNSANDBOX_API_KEY
NSString* oldKey = [[[NSProcessInfo processInfo] environment] objectForKey:@"UNSANDBOX_API_KEY"];
if (oldKey && [oldKey length] > 0) {
*publicKey = oldKey;
*secretKey = oldKey;
return YES;
}
// Priority 4: Config file ~/.unsandbox/accounts.csv (default row)
NSString* home = NSHomeDirectory();
NSString* accountIndexStr = [[[NSProcessInfo processInfo] environment] objectForKey:@"UNSANDBOX_ACCOUNT"];
NSInteger defaultIndex = accountIndexStr ? [accountIndexStr integerValue] : 0;
NSString* homeCsv = [home stringByAppendingPathComponent:@".unsandbox/accounts.csv"];
UNLoadCredentialsFromCSV(homeCsv, defaultIndex, publicKey, secretKey);
if (*publicKey && [*publicKey length] > 0) return YES;
// Priority 5: ./accounts.csv
UNLoadCredentialsFromCSV(@"accounts.csv", defaultIndex, publicKey, secretKey);
if (*publicKey && [*publicKey length] > 0) return YES;
if (error) {
*error = [UNAuthenticationError errorWithMessage:
@"No credentials found. Set UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY, "
"or create ~/.unsandbox/accounts.csv, or pass credentials to initializer."];
}
return NO;
}
/**
* Get API keys for CLI commands (exits on failure).
*/
void UNGetApiKeysCLI(NSString** publicKey, NSString** secretKey) {
NSError* error = nil;
if (!UNGetCredentials(publicKey, secretKey, nil, nil, &error)) {
fprintf(stderr, "%s%s%s\n", [RED UTF8String], [[error localizedDescription] UTF8String], [RESET UTF8String]);
exit(1);
}
}
// ============================================================================
// Clock Drift Detection
// ============================================================================
/**
* Check response for timestamp/clock drift errors.
*/
void UNCheckClockDrift(NSString* response) {
NSString* responseLower = [response lowercaseString];
if ([responseLower rangeOfString:@"timestamp"].location != NSNotFound &&
([responseLower rangeOfString:@"401"].location != NSNotFound ||
[responseLower rangeOfString:@"expired"].location != NSNotFound ||
[responseLower rangeOfString:@"invalid"].location != NSNotFound)) {
fprintf(stderr, "%sError: Request timestamp expired (must be within 5 minutes of server time)%s\n",
[RED UTF8String], [RESET UTF8String]);
fprintf(stderr, "%sYour computer's clock may have drifted.%s\n",
[YELLOW UTF8String], [RESET UTF8String]);
fprintf(stderr, "Check your system time and sync with NTP if needed:\n");
fprintf(stderr, " Linux: sudo ntpdate -s time.nist.gov\n");
fprintf(stderr, " macOS: sudo sntp -sS time.apple.com\n");
fprintf(stderr, " Windows: w32tm /resync\n");
exit(1);
}
}
// ============================================================================
// Languages Cache
// ============================================================================
/**
* Get path to languages cache file.
*/
NSString* UNLanguagesCachePath(void) {
NSString* home = NSHomeDirectory();
return [home stringByAppendingPathComponent:@".unsandbox/languages.json"];
}
/**
* Check if languages cache is valid (less than 1 hour old).
*/
BOOL UNIsCacheValid(void) {
NSFileManager* fm = [NSFileManager defaultManager];
NSString* cachePath = UNLanguagesCachePath();
if (![fm fileExistsAtPath:cachePath]) {
return NO;
}
NSError* error = nil;
NSDictionary* attrs = [fm attributesOfItemAtPath:cachePath error:&error];
if (error) {
return NO;
}
NSDate* modDate = attrs[NSFileModificationDate];
NSTimeInterval age = -[modDate timeIntervalSinceNow];
return age < UN_LANGUAGES_CACHE_TTL;
}
/**
* Read languages from cache file.
*/
NSDictionary* UNReadLanguagesCache(void) {
NSString* cachePath = UNLanguagesCachePath();
NSFileManager* fm = [NSFileManager defaultManager];
if (![fm fileExistsAtPath:cachePath]) {
return nil;
}
NSData* data = [NSData dataWithContentsOfFile:cachePath];
if (!data) {
return nil;
}
NSError* error = nil;
NSDictionary* result = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
return error ? nil : result;
}
/**
* Write languages to cache file.
*/
void UNWriteLanguagesCache(NSDictionary* data) {
NSString* cachePath = UNLanguagesCachePath();
NSString* cacheDir = [cachePath stringByDeletingLastPathComponent];
NSFileManager* fm = [NSFileManager defaultManager];
// Create directory if needed
if (![fm fileExistsAtPath:cacheDir]) {
[fm createDirectoryAtPath:cacheDir withIntermediateDirectories:YES attributes:nil error:nil];
}
NSError* error = nil;
NSData* jsonData = [NSJSONSerialization dataWithJSONObject:data options:0 error:&error];
if (!error && jsonData) {
[jsonData writeToFile:cachePath atomically:YES];
}
}
// ============================================================================
// Language Detection
// ============================================================================
/**
* Detect programming language from file extension or shebang.
*
* @param filename Path to source file
* @return Language identifier or nil if undetected
*/
NSString* UNDetectLanguage(NSString* filename) {
NSString* ext = [filename pathExtension];
NSDictionary* langMap = UNGetExtMap();
NSString* language = langMap[ext];
if (language) {
return language;
}
// Try reading shebang
NSFileManager* fm = [NSFileManager defaultManager];
if ([fm fileExistsAtPath:filename]) {
NSString* content = [NSString stringWithContentsOfFile:filename encoding:NSUTF8StringEncoding error:nil];
if (content) {
NSString* firstLine = [[content componentsSeparatedByString:@"\n"] firstObject];
if ([firstLine hasPrefix:@"#!"]) {
if ([firstLine rangeOfString:@"python"].location != NSNotFound) return @"python";
if ([firstLine rangeOfString:@"node"].location != NSNotFound) return @"javascript";
if ([firstLine rangeOfString:@"ruby"].location != NSNotFound) return @"ruby";
if ([firstLine rangeOfString:@"perl"].location != NSNotFound) return @"perl";
if ([firstLine rangeOfString:@"bash"].location != NSNotFound ||
[firstLine rangeOfString:@"/sh"].location != NSNotFound) return @"bash";
if ([firstLine rangeOfString:@"lua"].location != NSNotFound) return @"lua";
if ([firstLine rangeOfString:@"php"].location != NSNotFound) return @"php";
}
}
}
return nil;
}
// ============================================================================
// UNClient Class - Main SDK Interface
// ============================================================================
/**
* UNClient - Unsandbox API client with stored credentials.
*
* Example usage:
* UNClient *client = [[UNClient alloc] init];
* NSDictionary *result = [client execute:@"python" code:@"print('Hello')"];
* NSLog(@"Output: %@", result[@"stdout"]);
*
* // Or with explicit credentials:
* UNClient *client = [[UNClient alloc] initWithPublicKey:@"unsb-pk-..." secretKey:@"unsb-sk-..."];
*/
@interface UNClient : NSObject
@property (nonatomic, strong, readonly) NSString* publicKey;
@property (nonatomic, strong, readonly) NSString* secretKey;
/**
* Initialize client with automatic credential loading.
* Loads from environment variables or ~/.unsandbox/accounts.csv
*/
- (instancetype)init;
/**
* Initialize client with explicit credentials.
*
* @param publicKey API public key (unsb-pk-...)
* @param secretKey API secret key (unsb-sk-...)
*/
- (instancetype)initWithPublicKey:(NSString*)publicKey secretKey:(NSString*)secretKey;
/**
* Execute code synchronously.
*
* @param language Programming language (python, javascript, go, rust, etc.)
* @param code Source code to execute
* @return Dictionary with stdout, stderr, exit_code, job_id
*/
- (NSDictionary*)execute:(NSString*)language code:(NSString*)code;
/**
* Execute code with options.
*
* @param language Programming language
* @param code Source code
* @param options Dictionary with optional keys: env, input_files, network_mode, ttl, vcpu, return_artifact
* @return Dictionary with stdout, stderr, exit_code, job_id
*/
- (NSDictionary*)execute:(NSString*)language code:(NSString*)code options:(NSDictionary*)options;
/**
* Execute code asynchronously. Returns immediately with job_id.
*
* @param language Programming language
* @param code Source code
* @param options Optional execution options
* @return Dictionary with job_id, status ("pending")
*/
- (NSDictionary*)executeAsync:(NSString*)language code:(NSString*)code options:(NSDictionary*)options;
/**
* Execute code with automatic language detection from shebang.
*
* @param code Source code with shebang (e.g., #!/usr/bin/env python3)
* @return Dictionary with detected_language, stdout, stderr, etc.
*/
- (NSDictionary*)run:(NSString*)code;
/**
* Execute with auto-detect, asynchronously.
*
* @param code Source code with shebang
* @return Dictionary with job_id, detected_language, status
*/
- (NSDictionary*)runAsync:(NSString*)code;
/**
* Get job status and results.
*
* @param jobId Job ID from executeAsync or runAsync
* @return Dictionary with job_id, status, result (if completed)
*/
- (NSDictionary*)getJob:(NSString*)jobId;
/**
* Wait for job completion with exponential backoff polling.
*
* @param jobId Job ID to wait for
* @return Final job result dictionary
*/
- (NSDictionary*)wait:(NSString*)jobId;
/**
* Wait for job with max polls limit.
*
* @param jobId Job ID to wait for
* @param maxPolls Maximum number of poll attempts
* @return Final job result dictionary
*/
- (NSDictionary*)wait:(NSString*)jobId maxPolls:(int)maxPolls;
/**
* Cancel a running job.
*
* @param jobId Job ID to cancel
* @return Dictionary with partial output collected before cancellation
*/
- (NSDictionary*)cancelJob:(NSString*)jobId;
/**
* List all active jobs for this API key.
*
* @return Array of job summary dictionaries
*/
- (NSArray*)listJobs;
/**
* Generate images from text prompt.
*
* @param prompt Text description of the image to generate
* @return Dictionary with images array, created_at
*/
- (NSDictionary*)image:(NSString*)prompt;
/**
* Generate images with options.
*
* @param prompt Text prompt
* @param options Dictionary with optional keys: model, size, quality, n
* @return Dictionary with images array
*/
- (NSDictionary*)image:(NSString*)prompt options:(NSDictionary*)options;
/**
* Get list of supported programming languages.
* Results are cached in ~/.unsandbox/languages.json for 1 hour.
*
* @return Dictionary with languages array, count, aliases
*/
- (NSDictionary*)languages;
/**
* Make authenticated API request.
*
* @param endpoint API endpoint (e.g., /execute)
* @param method HTTP method
* @param data Request body dictionary (or nil)
* @return Response dictionary
*/
- (NSDictionary*)apiRequest:(NSString*)endpoint method:(NSString*)method data:(NSDictionary*)data;
/**
* Make API request with text/plain body.
*/
- (NSDictionary*)apiRequestText:(NSString*)endpoint method:(NSString*)method body:(NSString*)body;
@end
@implementation UNClient
- (instancetype)init {
self = [super init];
if (self) {
NSString* pk = nil;
NSString* sk = nil;
NSError* error = nil;
if (!UNGetCredentials(&pk, &sk, nil, nil, &error)) {
@throw [NSException exceptionWithName:@"UNAuthenticationError"
reason:[error localizedDescription]
userInfo:nil];
}
_publicKey = pk;
_secretKey = sk;
}
return self;
}
- (instancetype)initWithPublicKey:(NSString*)publicKey secretKey:(NSString*)secretKey {
self = [super init];
if (self) {
_publicKey = publicKey;
_secretKey = secretKey;
}
return self;
}
- (NSDictionary*)apiRequest:(NSString*)endpoint method:(NSString*)method data:(NSDictionary*)data {
NSString* urlString = [UN_API_BASE stringByAppendingString:endpoint];
NSURL* url = [NSURL URLWithString:urlString];
NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:url];
[request setHTTPMethod:method];
[request setTimeoutInterval:UN_DEFAULT_TIMEOUT];
// Prepare body
NSString* bodyString = @"";
if (data) {
NSError* error = nil;
NSData* jsonData = [NSJSONSerialization dataWithJSONObject:data options:0 error:&error];
if (error) {
return @{@"error": [error localizedDescription]};
}
bodyString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
[request setHTTPBody:jsonData];
}
// Generate timestamp and signature
long timestamp = (long)[[NSDate date] timeIntervalSince1970];
NSString* signature = UNComputeSignature(_secretKey, timestamp, method, endpoint, bodyString);
// Set headers
[request setValue:[@"Bearer " stringByAppendingString:_publicKey] forHTTPHeaderField:@"Authorization"];
[request setValue:[NSString stringWithFormat:@"%ld", timestamp] forHTTPHeaderField:@"X-Timestamp"];
[request setValue:signature forHTTPHeaderField:@"X-Signature"];
[request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
NSHTTPURLResponse* response = nil;
NSError* error = nil;
NSData* responseData = [NSURLConnection sendSynchronousRequest:request
returningResponse:&response
error:&error];
if (error) {
return @{@"error": [error localizedDescription]};
}
if ([response statusCode] != 200 && [response statusCode] != 201) {
NSString* errMsg = responseData ? [[NSString alloc] initWithData:responseData encoding:NSUTF8StringEncoding] : @"Unknown error";
return @{@"error": errMsg, @"status_code": @([response statusCode])};
}
NSDictionary* result = [NSJSONSerialization JSONObjectWithData:responseData options:0 error:&error];
if (error) {
return @{@"error": [error localizedDescription]};
}
return result;
}
- (NSDictionary*)apiRequestText:(NSString*)endpoint method:(NSString*)method body:(NSString*)body {
NSString* urlString = [UN_API_BASE stringByAppendingString:endpoint];
NSURL* url = [NSURL URLWithString:urlString];
NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:url];
[request setHTTPMethod:method];
[request setTimeoutInterval:UN_DEFAULT_TIMEOUT];
[request setHTTPBody:[body dataUsingEncoding:NSUTF8StringEncoding]];
long timestamp = (long)[[NSDate date] timeIntervalSince1970];
NSString* signature = UNComputeSignature(_secretKey, timestamp, method, endpoint, body);
[request setValue:[@"Bearer " stringByAppendingString:_publicKey] forHTTPHeaderField:@"Authorization"];
[request setValue:[NSString stringWithFormat:@"%ld", timestamp] forHTTPHeaderField:@"X-Timestamp"];
[request setValue:signature forHTTPHeaderField:@"X-Signature"];
[request setValue:@"text/plain" forHTTPHeaderField:@"Content-Type"];
NSHTTPURLResponse* response = nil;
NSError* error = nil;
NSData* responseData = [NSURLConnection sendSynchronousRequest:request
returningResponse:&response
error:&error];
if (error || ([response statusCode] != 200 && [response statusCode] != 201)) {
return @{@"error": @"Request failed"};
}
NSDictionary* result = [NSJSONSerialization JSONObjectWithData:responseData options:0 error:&error];
return result ?: @{};
}
- (NSDictionary*)execute:(NSString*)language code:(NSString*)code {
return [self execute:language code:code options:nil];
}
- (NSDictionary*)execute:(NSString*)language code:(NSString*)code options:(NSDictionary*)options {
NSMutableDictionary* payload = [NSMutableDictionary dictionaryWithDictionary:@{
@"language": language,
@"code": code,
@"network_mode": options[@"network_mode"] ?: @"zerotrust",
@"ttl": options[@"ttl"] ?: @(UN_DEFAULT_TTL),
@"vcpu": options[@"vcpu"] ?: @1
}];
if (options[@"env"]) payload[@"env"] = options[@"env"];
if (options[@"input_files"]) payload[@"input_files"] = options[@"input_files"];
if ([options[@"return_artifact"] boolValue]) payload[@"return_artifact"] = @YES;
return [self apiRequest:@"/execute" method:@"POST" data:payload];
}
- (NSDictionary*)executeAsync:(NSString*)language code:(NSString*)code options:(NSDictionary*)options {
NSMutableDictionary* payload = [NSMutableDictionary dictionaryWithDictionary:@{
@"language": language,
@"code": code,
@"network_mode": options[@"network_mode"] ?: @"zerotrust",
@"ttl": options[@"ttl"] ?: @(UN_DEFAULT_TTL),
@"vcpu": options[@"vcpu"] ?: @1
}];
if (options[@"env"]) payload[@"env"] = options[@"env"];
if (options[@"input_files"]) payload[@"input_files"] = options[@"input_files"];
if ([options[@"return_artifact"] boolValue]) payload[@"return_artifact"] = @YES;
return [self apiRequest:@"/execute/async" method:@"POST" data:payload];
}
- (NSDictionary*)run:(NSString*)code {
NSString* endpoint = [NSString stringWithFormat:@"/run?ttl=%ld&network_mode=zerotrust", (long)UN_DEFAULT_TTL];
return [self apiRequestText:endpoint method:@"POST" body:code];
}
- (NSDictionary*)runAsync:(NSString*)code {
NSString* endpoint = [NSString stringWithFormat:@"/run/async?ttl=%ld&network_mode=zerotrust", (long)UN_DEFAULT_TTL];
return [self apiRequestText:endpoint method:@"POST" body:code];
}
- (NSDictionary*)getJob:(NSString*)jobId {
NSString* endpoint = [NSString stringWithFormat:@"/jobs/%@", jobId];
return [self apiRequest:endpoint method:@"GET" data:nil];
}
- (NSDictionary*)wait:(NSString*)jobId {
return [self wait:jobId maxPolls:100];
}
- (NSDictionary*)wait:(NSString*)jobId maxPolls:(int)maxPolls {
NSSet* terminalStates = [NSSet setWithArray:@[@"completed", @"failed", @"timeout", @"cancelled"]];
for (int i = 0; i < maxPolls; i++) {
int delayIdx = MIN(i, UN_POLL_DELAYS_COUNT - 1);
usleep(UN_POLL_DELAYS[delayIdx] * 1000); // Convert ms to microseconds
NSDictionary* result = [self getJob:jobId];
NSString* status = result[@"status"];
if ([terminalStates containsObject:status]) {
return result;
}
}
return @{@"error": @"Max polls exceeded", @"job_id": jobId};
}
- (NSDictionary*)cancelJob:(NSString*)jobId {
NSString* endpoint = [NSString stringWithFormat:@"/jobs/%@", jobId];
return [self apiRequest:endpoint method:@"DELETE" data:nil];
}
- (NSArray*)listJobs {
NSDictionary* result = [self apiRequest:@"/jobs" method:@"GET" data:nil];
return result[@"jobs"] ?: @[];
}
- (NSDictionary*)image:(NSString*)prompt {
return [self image:prompt options:nil];
}
- (NSDictionary*)image:(NSString*)prompt options:(NSDictionary*)options {
NSMutableDictionary* payload = [NSMutableDictionary dictionaryWithDictionary:@{
@"prompt": prompt,
@"size": options[@"size"] ?: @"1024x1024",
@"quality": options[@"quality"] ?: @"standard",
@"n": options[@"n"] ?: @1
}];
if (options[@"model"]) payload[@"model"] = options[@"model"];
return [self apiRequest:@"/image" method:@"POST" data:payload];
}
- (NSDictionary*)languages {
// Check cache first
if (UNIsCacheValid()) {
NSDictionary* cached = UNReadLanguagesCache();
if (cached) {
return cached;
}
}
// Fetch from API
NSDictionary* result = [self apiRequest:@"/languages" method:@"GET" data:nil];
// Cache result (only if successful)
if (result && !result[@"error"]) {
UNWriteLanguagesCache(result);
}
return result;
}
@end
// ============================================================================
// Standalone Library Functions
// ============================================================================
/**
* Execute code synchronously (standalone function).
* Uses credentials from environment or config file.
*/
NSDictionary* UNExecute(NSString* language, NSString* code, NSDictionary* options) {
UNClient* client = [[UNClient alloc] init];
return [client execute:language code:code options:options];
}
/**
* Execute code asynchronously (standalone function).
*/
NSDictionary* UNExecuteAsync(NSString* language, NSString* code, NSDictionary* options) {
UNClient* client = [[UNClient alloc] init];
return [client executeAsync:language code:code options:options];
}
/**
* Execute with auto-detect (standalone function).
*/
NSDictionary* UNRun(NSString* code) {
UNClient* client = [[UNClient alloc] init];
return [client run:code];
}
/**
* Execute async with auto-detect (standalone function).
*/
NSDictionary* UNRunAsync(NSString* code) {
UNClient* client = [[UNClient alloc] init];
return [client runAsync:code];
}
/**
* Get job status (standalone function).
*/
NSDictionary* UNGetJob(NSString* jobId) {
UNClient* client = [[UNClient alloc] init];
return [client getJob:jobId];
}
/**
* Wait for job completion (standalone function).
*/
NSDictionary* UNWait(NSString* jobId) {
UNClient* client = [[UNClient alloc] init];
return [client wait:jobId];
}
/**
* Cancel a job (standalone function).
*/
NSDictionary* UNCancelJob(NSString* jobId) {
UNClient* client = [[UNClient alloc] init];
return [client cancelJob:jobId];
}
/**
* List active jobs (standalone function).
*/
NSArray* UNListJobs(void) {
UNClient* client = [[UNClient alloc] init];
return [client listJobs];
}
/**
* Generate image (standalone function).
*/
NSDictionary* UNImage(NSString* prompt, NSDictionary* options) {
UNClient* client = [[UNClient alloc] init];
return [client image:prompt options:options];
}
/**
* Get supported languages (standalone function).
* Results are cached for 1 hour in ~/.unsandbox/languages.json
*/
NSDictionary* UNLanguages(void) {
UNClient* client = [[UNClient alloc] init];
return [client languages];
}
// ============================================================================
// CLI Helper Functions
// ============================================================================
// ============================================================================
// Sudo OTP Challenge Handling
// ============================================================================
/**
* Handle 428 sudo OTP challenge - prompts user for OTP and retries the request.
*
* @param responseData The JSON response containing challenge_id
* @param meth HTTP method (DELETE, POST, etc.)
* @param endpoint API endpoint path
* @param body Request body (or nil for DELETE)
* @param publicKey API public key
* @param secretKey API secret key
* @return YES on success, NO on failure
*/
BOOL handleSudoChallenge(NSDictionary* responseData, NSString* meth, NSString* endpoint, NSString* body, NSString* publicKey, NSString* secretKey) {
NSString* challengeId = responseData[@"challenge_id"];
fprintf(stderr, "%sConfirmation required. Check your email for a one-time code.%s\n",
[YELLOW UTF8String], [RESET UTF8String]);
fprintf(stderr, "Enter OTP: ");
char otpBuffer[64];
if (!fgets(otpBuffer, sizeof(otpBuffer), stdin)) {
fprintf(stderr, "%sError: Failed to read OTP%s\n", [RED UTF8String], [RESET UTF8String]);
return NO;
}
// Strip newline
NSString* otp = [[NSString stringWithUTF8String:otpBuffer] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
if ([otp length] == 0) {
fprintf(stderr, "%sError: Operation cancelled%s\n", [RED UTF8String], [RESET UTF8String]);
return NO;
}
// Build retry request with sudo headers
NSString* urlString = [UN_API_BASE stringByAppendingString:endpoint];
NSURL* url = [NSURL URLWithString:urlString];
NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:url];
[request setHTTPMethod:meth];
[request setTimeoutInterval:UN_DEFAULT_TIMEOUT];
NSString* bodyString = body ?: @"";
if (body && [body length] > 0) {
[request setHTTPBody:[body dataUsingEncoding:NSUTF8StringEncoding]];
}
long timestamp = (long)[[NSDate date] timeIntervalSince1970];
NSString* signature = UNComputeSignature(secretKey, timestamp, meth, endpoint, bodyString);
[request setValue:[@"Bearer " stringByAppendingString:publicKey] forHTTPHeaderField:@"Authorization"];
[request setValue:[NSString stringWithFormat:@"%ld", timestamp] forHTTPHeaderField:@"X-Timestamp"];
[request setValue:signature forHTTPHeaderField:@"X-Signature"];
[request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
[request setValue:otp forHTTPHeaderField:@"X-Sudo-OTP"];
if (challengeId) {
[request setValue:challengeId forHTTPHeaderField:@"X-Sudo-Challenge"];
}
NSHTTPURLResponse* response = nil;
NSError* error = nil;
NSData* responseData2 = [NSURLConnection sendSynchronousRequest:request
returningResponse:&response
error:&error];
if (error || [response statusCode] < 200 || [response statusCode] >= 300) {
fprintf(stderr, "%sError: HTTP %ld%s\n",
[RED UTF8String], (long)[response statusCode], [RESET UTF8String]);
if (responseData2) {
NSString* errMsg = [[NSString alloc] initWithData:responseData2 encoding:NSUTF8StringEncoding];
fprintf(stderr, "%s\n", [errMsg UTF8String]);
}
return NO;
}
fprintf(stderr, "%sOperation completed successfully%s\n", [GREEN UTF8String], [RESET UTF8String]);
return YES;
}
/**
* Make an API request that returns status code and response (for 428 handling).
*/
NSDictionary* apiRequestWithStatusCLI(NSString* endpoint, NSString* method, NSDictionary* data, NSString* publicKey, NSString* secretKey, NSInteger* outStatusCode) {
NSString* urlString = [UN_API_BASE stringByAppendingString:endpoint];
NSURL* url = [NSURL URLWithString:urlString];
NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:url];
[request setHTTPMethod:method];
[request setTimeoutInterval:UN_DEFAULT_TIMEOUT];
NSString* bodyString = @"";
if (data) {
NSError* error = nil;
NSData* jsonData = [NSJSONSerialization dataWithJSONObject:data options:0 error:&error];
if (error) {
*outStatusCode = 500;
return @{@"error": [error localizedDescription]};
}
bodyString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
[request setHTTPBody:jsonData];
}
long timestamp = (long)[[NSDate date] timeIntervalSince1970];
NSString* signature = UNComputeSignature(secretKey, timestamp, method, endpoint, bodyString);
[request setValue:[@"Bearer " stringByAppendingString:publicKey] forHTTPHeaderField:@"Authorization"];
[request setValue:[NSString stringWithFormat:@"%ld", timestamp] forHTTPHeaderField:@"X-Timestamp"];
[request setValue:signature forHTTPHeaderField:@"X-Signature"];
[request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
NSHTTPURLResponse* response = nil;
NSError* error = nil;
NSData* responseData = [NSURLConnection sendSynchronousRequest:request
returningResponse:&response
error:&error];
*outStatusCode = [response statusCode];
if (error) {
return @{@"error": [error localizedDescription]};
}
NSDictionary* result = [NSJSONSerialization JSONObjectWithData:responseData options:0 error:&error];
return result ?: @{};
}
NSDictionary* apiRequestCLI(NSString* endpoint, NSString* method, NSDictionary* data, NSString* publicKey, NSString* secretKey) {
NSString* urlString = [UN_API_BASE stringByAppendingString:endpoint];
NSURL* url = [NSURL URLWithString:urlString];
NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:url];
[request setHTTPMethod:method];
[request setTimeoutInterval:UN_DEFAULT_TIMEOUT];
NSString* bodyString = @"";
if (data) {
NSError* error = nil;
NSData* jsonData = [NSJSONSerialization dataWithJSONObject:data options:0 error:&error];
if (error) {
fprintf(stderr, "%sError creating JSON: %s%s\n",
[RED UTF8String], [[error localizedDescription] UTF8String], [RESET UTF8String]);
exit(1);
}
bodyString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
[request setHTTPBody:jsonData];
}
long timestamp = (long)[[NSDate date] timeIntervalSince1970];
NSString* signature = UNComputeSignature(secretKey, timestamp, method, endpoint, bodyString);
[request setValue:[@"Bearer " stringByAppendingString:publicKey] forHTTPHeaderField:@"Authorization"];
[request setValue:[NSString stringWithFormat:@"%ld", timestamp] forHTTPHeaderField:@"X-Timestamp"];
[request setValue:signature forHTTPHeaderField:@"X-Signature"];
[request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
NSHTTPURLResponse* response = nil;
NSError* error = nil;
NSData* responseData = [NSURLConnection sendSynchronousRequest:request
returningResponse:&response
error:&error];
if (error || ([response statusCode] != 200 && [response statusCode] != 201)) {
fprintf(stderr, "%sError: HTTP %ld%s\n",
[RED UTF8String], (long)[response statusCode], [RESET UTF8String]);
if (responseData) {
NSString* errMsg = [[NSString alloc] initWithData:responseData encoding:NSUTF8StringEncoding];
fprintf(stderr, "%s\n", [errMsg UTF8String]);
UNCheckClockDrift(errMsg);
}
exit(1);
}
NSDictionary* result = [NSJSONSerialization JSONObjectWithData:responseData options:0 error:&error];
if (error) {
fprintf(stderr, "%sError parsing JSON: %s%s\n",
[RED UTF8String], [[error localizedDescription] UTF8String], [RESET UTF8String]);
exit(1);
}
return result;
}
NSDictionary* apiRequestPutTextCLI(NSString* endpoint, NSString* content, NSString* publicKey, NSString* secretKey) {
NSString* urlString = [UN_API_BASE stringByAppendingString:endpoint];
NSURL* url = [NSURL URLWithString:urlString];
NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:url];
[request setHTTPMethod:@"PUT"];
[request setTimeoutInterval:UN_DEFAULT_TIMEOUT];
[request setHTTPBody:[content dataUsingEncoding:NSUTF8StringEncoding]];
long timestamp = (long)[[NSDate date] timeIntervalSince1970];
NSString* signature = UNComputeSignature(secretKey, timestamp, @"PUT", endpoint, content);
[request setValue:[@"Bearer " stringByAppendingString:publicKey] forHTTPHeaderField:@"Authorization"];
[request setValue:[NSString stringWithFormat:@"%ld", timestamp] forHTTPHeaderField:@"X-Timestamp"];
[request setValue:signature forHTTPHeaderField:@"X-Signature"];
[request setValue:@"text/plain" forHTTPHeaderField:@"Content-Type"];
NSHTTPURLResponse* response = nil;
NSError* error = nil;
NSData* responseData = [NSURLConnection sendSynchronousRequest:request
returningResponse:&response
error:&error];
if (error || ([response statusCode] != 200 && [response statusCode] != 201)) {
fprintf(stderr, "%sError: HTTP %ld%s\n",
[RED UTF8String], (long)[response statusCode], [RESET UTF8String]);
if (responseData) {
NSString* errMsg = [[NSString alloc] initWithData:responseData encoding:NSUTF8StringEncoding];
fprintf(stderr, "%s\n", [errMsg UTF8String]);
}
exit(1);
}
NSDictionary* result = [NSJSONSerialization JSONObjectWithData:responseData options:0 error:&error];
return result;
}
NSString* buildEnvContent(NSArray* envVars, NSString* envFile) {
NSMutableArray* lines = [NSMutableArray array];
for (NSString* var in envVars) {
[lines addObject:var];
}
if (envFile && [[NSFileManager defaultManager] fileExistsAtPath:envFile]) {
NSString* fileContent = [NSString stringWithContentsOfFile:envFile encoding:NSUTF8StringEncoding error:nil];
for (NSString* line in [fileContent componentsSeparatedByString:@"\n"]) {
NSString* trimmed = [line stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
if ([trimmed length] == 0 || [trimmed hasPrefix:@"#"]) continue;
[lines addObject:line];
}
}
return [lines componentsJoinedByString:@"\n"];
}
// Service vault functions
void serviceEnvStatus(NSString* serviceId, NSString* publicKey, NSString* secretKey) {
NSString* endpoint = [NSString stringWithFormat:@"/services/%@/env", serviceId];
NSDictionary* result = apiRequestCLI(endpoint, @"GET", nil, publicKey, secretKey);
NSData* jsonData = [NSJSONSerialization dataWithJSONObject:result options:NSJSONWritingPrettyPrinted error:nil];
NSString* jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
printf("%s\n", [jsonString UTF8String]);
}
void serviceEnvSet(NSString* serviceId, NSString* content, NSString* publicKey, NSString* secretKey) {
NSString* endpoint = [NSString stringWithFormat:@"/services/%@/env", serviceId];
NSDictionary* result = apiRequestPutTextCLI(endpoint, content, publicKey, secretKey);
NSData* jsonData = [NSJSONSerialization dataWithJSONObject:result options:NSJSONWritingPrettyPrinted error:nil];
NSString* jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
printf("%s\n", [jsonString UTF8String]);
}
void serviceEnvExport(NSString* serviceId, NSString* publicKey, NSString* secretKey) {
NSString* endpoint = [NSString stringWithFormat:@"/services/%@/env/export", serviceId];
NSDictionary* result = apiRequestCLI(endpoint, @"POST", nil, publicKey, secretKey);
if (result[@"content"]) {
printf("%s", [result[@"content"] UTF8String]);
}
}
void serviceEnvDelete(NSString* serviceId, NSString* publicKey, NSString* secretKey) {
NSString* endpoint = [NSString stringWithFormat:@"/services/%@/env", serviceId];
apiRequestCLI(endpoint, @"DELETE", nil, publicKey, secretKey);
printf("%sVault deleted for: %s%s\n", [GREEN UTF8String], [serviceId UTF8String], [RESET UTF8String]);
}
// ============================================================================
// CLI Commands
// ============================================================================
void cmdExecute(NSArray* args) {
NSString* publicKey, *secretKey;
UNGetApiKeysCLI(&publicKey, &secretKey);
NSString* sourceFile = nil;
NSMutableDictionary* envVars = [NSMutableDictionary dictionary];
NSMutableArray* inputFiles = [NSMutableArray array];
BOOL artifacts = NO;
NSString* outputDir = @".";
NSString* network = nil;
int vcpu = 0;
for (NSUInteger i = 0; i < [args count]; i++) {
NSString* arg = args[i];
if ([arg isEqualToString:@"-e"] && i + 1 < [args count]) {
NSArray* parts = [args[++i] componentsSeparatedByString:@"="];
if ([parts count] >= 2) {
envVars[parts[0]] = [[parts subarrayWithRange:NSMakeRange(1, [parts count] - 1)] componentsJoinedByString:@"="];
}
} else if ([arg isEqualToString:@"-f"] && i + 1 < [args count]) {
[inputFiles addObject:args[++i]];
} else if ([arg isEqualToString:@"-a"]) {
artifacts = YES;
} else if ([arg isEqualToString:@"-o"] && i + 1 < [args count]) {
outputDir = args[++i];
} else if ([arg isEqualToString:@"-n"] && i + 1 < [args count]) {
network = args[++i];
} else if ([arg isEqualToString:@"-v"] && i + 1 < [args count]) {
vcpu = [args[++i] intValue];
} else if (![arg hasPrefix:@"-"]) {
sourceFile = arg;
}
}
if (!sourceFile) {
fprintf(stderr, "Usage: un.m [options] <source_file>\n");
exit(1);
}
NSFileManager* fm = [NSFileManager defaultManager];
if (![fm fileExistsAtPath:sourceFile]) {
fprintf(stderr, "%sError: File not found: %s%s\n",
[RED UTF8String], [sourceFile UTF8String], [RESET UTF8String]);
exit(1);
}
NSError* error = nil;
NSString* code = [NSString stringWithContentsOfFile:sourceFile encoding:NSUTF8StringEncoding error:&error];
if (error) {
fprintf(stderr, "%sError reading file: %s%s\n",
[RED UTF8String], [[error localizedDescription] UTF8String], [RESET UTF8String]);
exit(1);
}
NSString* language = UNDetectLanguage(sourceFile);
if (!language) {
fprintf(stderr, "%sError: Cannot detect language for %s%s\n",
[RED UTF8String], [sourceFile UTF8String], [RESET UTF8String]);
exit(1);
}
NSMutableDictionary* payload = [NSMutableDictionary dictionaryWithDictionary:@{
@"language": language,
@"code": code
}];
if ([envVars count] > 0) {
payload[@"env"] = envVars;
}
if ([inputFiles count] > 0) {
NSMutableArray* files = [NSMutableArray array];
for (NSString* filepath in inputFiles) {
if (![fm fileExistsAtPath:filepath]) {
fprintf(stderr, "%sError: Input file not found: %s%s\n",
[RED UTF8String], [filepath UTF8String], [RESET UTF8String]);
exit(1);
}
NSData* content = [NSData dataWithContentsOfFile:filepath];
NSString* b64Content = [content base64EncodedStringWithOptions:0];
[files addObject:@{
@"filename": [filepath lastPathComponent],
@"content_base64": b64Content
}];
}
payload[@"input_files"] = files;
}
if (artifacts) payload[@"return_artifacts"] = @YES;
if (network) payload[@"network"] = network;
if (vcpu > 0) payload[@"vcpu"] = @(vcpu);
NSDictionary* result = apiRequestCLI(@"/execute", @"POST", payload, publicKey, secretKey);
NSString* stdoutText = result[@"stdout"] ?: @"";
NSString* stderrText = result[@"stderr"] ?: @"";
if ([stdoutText length] > 0) {
printf("%s%s%s", [BLUE UTF8String], [stdoutText UTF8String], [RESET UTF8String]);
}
if ([stderrText length] > 0) {
fprintf(stderr, "%s%s%s", [RED UTF8String], [stderrText UTF8String], [RESET UTF8String]);
}
if (artifacts && result[@"artifacts"]) {
[fm createDirectoryAtPath:outputDir withIntermediateDirectories:YES attributes:nil error:nil];
for (NSDictionary* artifact in result[@"artifacts"]) {
NSString* filename = artifact[@"filename"];
NSString* b64Content = artifact[@"content_base64"];
NSData* content = [[NSData alloc] initWithBase64EncodedString:b64Content options:0];
NSString* path = [outputDir stringByAppendingPathComponent:filename];
[content writeToFile:path atomically:YES];
[fm setAttributes:@{NSFilePosixPermissions: @0755} ofItemAtPath:path error:nil];
fprintf(stderr, "%sSaved: %s%s\n", [GREEN UTF8String], [path UTF8String], [RESET UTF8String]);
}
}
int exitCode = [result[@"exit_code"] intValue];
exit(exitCode);
}
void cmdSession(NSArray* args) {
NSString* publicKey, *secretKey;
UNGetApiKeysCLI(&publicKey, &secretKey);
BOOL listMode = NO;
NSString* killId = nil;
NSString* shell = nil;
NSString* network = nil;
int vcpu = 0;
NSMutableArray* inputFiles = [NSMutableArray array];
for (NSUInteger i = 0; i < [args count]; i++) {
NSString* arg = args[i];
if ([arg isEqualToString:@"--list"]) {
listMode = YES;
} else if ([arg isEqualToString:@"--kill"] && i + 1 < [args count]) {
killId = args[++i];
} else if ([arg isEqualToString:@"--shell"] && i + 1 < [args count]) {
shell = args[++i];
} else if ([arg isEqualToString:@"-f"] && i + 1 < [args count]) {
[inputFiles addObject:args[++i]];
} else if ([arg isEqualToString:@"-n"] && i + 1 < [args count]) {
network = args[++i];
} else if ([arg isEqualToString:@"-v"] && i + 1 < [args count]) {
vcpu = [args[++i] intValue];
}
}
if (listMode) {
NSDictionary* result = apiRequestCLI(@"/sessions", @"GET", nil, publicKey, secretKey);
NSArray* sessions = result[@"sessions"];
if ([sessions count] == 0) {
printf("No active sessions\n");
} else {
printf("%-40s %-10s %-10s %s\n", "ID", "Shell", "Status", "Created");
for (NSDictionary* s in sessions) {
printf("%-40s %-10s %-10s %s\n",
[s[@"id"] UTF8String],
[s[@"shell"] UTF8String],
[s[@"status"] UTF8String],
[s[@"created_at"] UTF8String]);
}
}
return;
}
if (killId) {
NSString* endpoint = [NSString stringWithFormat:@"/sessions/%@", killId];
apiRequestCLI(endpoint, @"DELETE", nil, publicKey, secretKey);
printf("%sSession terminated: %s%s\n", [GREEN UTF8String], [killId UTF8String], [RESET UTF8String]);
return;
}
NSMutableDictionary* payload = [NSMutableDictionary dictionaryWithDictionary:@{
@"shell": shell ?: @"bash"
}];
if (network) payload[@"network"] = network;
if (vcpu > 0) payload[@"vcpu"] = @(vcpu);
if ([inputFiles count] > 0) {
NSFileManager* fm = [NSFileManager defaultManager];
NSMutableArray* files = [NSMutableArray array];
for (NSString* filepath in inputFiles) {
if (![fm fileExistsAtPath:filepath]) {
fprintf(stderr, "%sError: Input file not found: %s%s\n",
[RED UTF8String], [filepath UTF8String], [RESET UTF8String]);
exit(1);
}
NSData* content = [NSData dataWithContentsOfFile:filepath];
NSString* b64Content = [content base64EncodedStringWithOptions:0];
[files addObject:@{
@"filename": [filepath lastPathComponent],
@"content_base64": b64Content
}];
}
payload[@"input_files"] = files;
}
printf("%sCreating session...%s\n", [YELLOW UTF8String], [RESET UTF8String]);
NSDictionary* result = apiRequestCLI(@"/sessions", @"POST", payload, publicKey, secretKey);
printf("%sSession created: %s%s\n", [GREEN UTF8String], [result[@"id"] UTF8String], [RESET UTF8String]);
printf("%s(Interactive sessions require WebSocket - use un2 for full support)%s\n",
[YELLOW UTF8String], [RESET UTF8String]);
}
/**
* Set unfreeze_on_demand flag for a service.
*/
void setServiceUnfreezeOnDemand(NSString* serviceId, BOOL enabled, NSString* publicKey, NSString* secretKey) {
NSString* endpoint = [NSString stringWithFormat:@"/services/%@", serviceId];
NSDictionary* payload = @{@"unfreeze_on_demand": @(enabled)};
apiRequestCLI(endpoint, @"PATCH", payload, publicKey, secretKey);
}
void cmdService(NSArray* args) {
NSString* publicKey, *secretKey;
UNGetApiKeysCLI(&publicKey, &secretKey);
BOOL listMode = NO;
NSString* infoId = nil;
NSString* logsId = nil;
NSString* sleepId = nil;
NSString* wakeId = nil;
NSString* destroyId = nil;
NSString* name = nil;
NSString* ports = nil;
NSString* type = nil;
NSString* bootstrap = nil;
NSString* bootstrapFile = nil;
NSString* network = nil;
int vcpu = 0;
BOOL unfreezeOnDemand = NO;
NSString* setUnfreezeOnDemandId = nil;
BOOL setUnfreezeOnDemandEnabled = NO;
NSMutableArray* inputFiles = [NSMutableArray array];
NSMutableArray* envVars = [NSMutableArray array];
NSString* envFile = nil;
// Check for 'env' subcommand first
if ([args count] >= 1 && [args[0] isEqualToString:@"env"]) {
if ([args count] < 3) {
fprintf(stderr, "Usage: un.m service env <status|set|export|delete> <service_id> [options]\n");
exit(1);
}
NSString* envAction = args[1];
NSString* envTarget = args[2];
for (NSUInteger i = 3; i < [args count]; i++) {
NSString* arg = args[i];
if ([arg isEqualToString:@"-e"] && i + 1 < [args count]) {
[envVars addObject:args[++i]];
} else if ([arg isEqualToString:@"--env-file"] && i + 1 < [args count]) {
envFile = args[++i];
}
}
if ([envAction isEqualToString:@"status"]) {
serviceEnvStatus(envTarget, publicKey, secretKey);
} else if ([envAction isEqualToString:@"set"]) {
NSString* content = buildEnvContent(envVars, envFile);
if ([content length] == 0) {
fprintf(stderr, "%sError: No environment variables to set%s\n", [RED UTF8String], [RESET UTF8String]);
exit(1);
}
serviceEnvSet(envTarget, content, publicKey, secretKey);
} else if ([envAction isEqualToString:@"export"]) {
serviceEnvExport(envTarget, publicKey, secretKey);
} else if ([envAction isEqualToString:@"delete"]) {
serviceEnvDelete(envTarget, publicKey, secretKey);
} else {
fprintf(stderr, "%sError: Unknown env action '%s'. Use status, set, export, or delete%s\n",
[RED UTF8String], [envAction UTF8String], [RESET UTF8String]);
exit(1);
}
return;
}
for (NSUInteger i = 0; i < [args count]; i++) {
NSString* arg = args[i];
if ([arg isEqualToString:@"--list"]) {
listMode = YES;
} else if ([arg isEqualToString:@"--info"] && i + 1 < [args count]) {
infoId = args[++i];
} else if ([arg isEqualToString:@"--logs"] && i + 1 < [args count]) {
logsId = args[++i];
} else if ([arg isEqualToString:@"--freeze"] && i + 1 < [args count]) {
sleepId = args[++i];
} else if ([arg isEqualToString:@"--unfreeze"] && i + 1 < [args count]) {
wakeId = args[++i];
} else if ([arg isEqualToString:@"--destroy"] && i + 1 < [args count]) {
destroyId = args[++i];
} else if ([arg isEqualToString:@"--name"] && i + 1 < [args count]) {
name = args[++i];
} else if ([arg isEqualToString:@"--ports"] && i + 1 < [args count]) {
ports = args[++i];
} else if ([arg isEqualToString:@"--type"] && i + 1 < [args count]) {
type = args[++i];
} else if ([arg isEqualToString:@"--bootstrap"] && i + 1 < [args count]) {
bootstrap = args[++i];
} else if ([arg isEqualToString:@"--bootstrap-file"] && i + 1 < [args count]) {
bootstrapFile = args[++i];
} else if ([arg isEqualToString:@"-f"] && i + 1 < [args count]) {
[inputFiles addObject:args[++i]];
} else if ([arg isEqualToString:@"-e"] && i + 1 < [args count]) {
[envVars addObject:args[++i]];
} else if ([arg isEqualToString:@"--env-file"] && i + 1 < [args count]) {
envFile = args[++i];
} else if ([arg isEqualToString:@"-n"] && i + 1 < [args count]) {
network = args[++i];
} else if ([arg isEqualToString:@"-v"] && i + 1 < [args count]) {
vcpu = [args[++i] intValue];
} else if ([arg isEqualToString:@"--unfreeze-on-demand"]) {
unfreezeOnDemand = YES;
} else if ([arg isEqualToString:@"--set-unfreeze-on-demand"] && i + 2 < [args count]) {
setUnfreezeOnDemandId = args[++i];
NSString* enabledStr = args[++i];
setUnfreezeOnDemandEnabled = [enabledStr isEqualToString:@"true"] || [enabledStr isEqualToString:@"1"];
}
}
if (setUnfreezeOnDemandId) {
setServiceUnfreezeOnDemand(setUnfreezeOnDemandId, setUnfreezeOnDemandEnabled, publicKey, secretKey);
NSString* status = setUnfreezeOnDemandEnabled ? @"enabled" : @"disabled";
printf("%sUnfreeze-on-demand %s for service: %s%s\n",
[GREEN UTF8String], [status UTF8String], [setUnfreezeOnDemandId UTF8String], [RESET UTF8String]);
return;
}
if (listMode) {
NSDictionary* result = apiRequestCLI(@"/services", @"GET", nil, publicKey, secretKey);
NSArray* services = result[@"services"];
if ([services count] == 0) {
printf("No services\n");
} else {
printf("%-20s %-15s %-10s %-15s %s\n", "ID", "Name", "Status", "Ports", "Domains");
for (NSDictionary* s in services) {
NSArray* portArray = s[@"ports"];
NSArray* domainArray = s[@"domains"];
NSString* portStr = [portArray componentsJoinedByString:@","];
NSString* domainStr = [domainArray componentsJoinedByString:@","];
printf("%-20s %-15s %-10s %-15s %s\n",
[s[@"id"] UTF8String],
[s[@"name"] UTF8String],
[s[@"status"] UTF8String],
[portStr UTF8String],
[domainStr UTF8String]);
}
}
return;
}
if (infoId) {
NSString* endpoint = [NSString stringWithFormat:@"/services/%@", infoId];
NSDictionary* result = apiRequestCLI(endpoint, @"GET", nil, publicKey, secretKey);
NSData* jsonData = [NSJSONSerialization dataWithJSONObject:result options:NSJSONWritingPrettyPrinted error:nil];
NSString* jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
printf("%s\n", [jsonString UTF8String]);
return;
}
if (logsId) {
NSString* endpoint = [NSString stringWithFormat:@"/services/%@/logs", logsId];
NSDictionary* result = apiRequestCLI(endpoint, @"GET", nil, publicKey, secretKey);
printf("%s", [result[@"logs"] UTF8String]);
return;
}
if (sleepId) {
NSString* endpoint = [NSString stringWithFormat:@"/services/%@/freeze", sleepId];
apiRequestCLI(endpoint, @"POST", nil, publicKey, secretKey);
printf("%sService frozen: %s%s\n", [GREEN UTF8String], [sleepId UTF8String], [RESET UTF8String]);
return;
}
if (wakeId) {
NSString* endpoint = [NSString stringWithFormat:@"/services/%@/unfreeze", wakeId];
apiRequestCLI(endpoint, @"POST", nil, publicKey, secretKey);
printf("%sService unfreezing: %s%s\n", [GREEN UTF8String], [wakeId UTF8String], [RESET UTF8String]);
return;
}
if (destroyId) {
NSString* endpoint = [NSString stringWithFormat:@"/services/%@", destroyId];
NSInteger statusCode = 0;
NSDictionary* response = apiRequestWithStatusCLI(endpoint, @"DELETE", nil, publicKey, secretKey, &statusCode);
if (statusCode == 428) {
if (handleSudoChallenge(response, @"DELETE", endpoint, nil, publicKey, secretKey)) {
printf("%sService destroyed: %s%s\n", [GREEN UTF8String], [destroyId UTF8String], [RESET UTF8String]);
} else {
exit(1);
}
} else if (statusCode >= 200 && statusCode < 300) {
printf("%sService destroyed: %s%s\n", [GREEN UTF8String], [destroyId UTF8String], [RESET UTF8String]);
} else {
fprintf(stderr, "%sError: HTTP %ld%s\n", [RED UTF8String], (long)statusCode, [RESET UTF8String]);
exit(1);
}
return;
}
if (name) {
NSMutableDictionary* payload = [NSMutableDictionary dictionaryWithDictionary:@{@"name": name}];
if (ports) {
NSArray* portStrings = [ports componentsSeparatedByString:@","];
NSMutableArray* portNumbers = [NSMutableArray array];
for (NSString* p in portStrings) {
[portNumbers addObject:@([p intValue])];
}
payload[@"ports"] = portNumbers;
}
if (type) payload[@"service_type"] = type;
if (bootstrap) payload[@"bootstrap"] = bootstrap;
if (bootstrapFile) {
NSFileManager* fm = [NSFileManager defaultManager];
if ([fm fileExistsAtPath:bootstrapFile]) {
NSString* content = [NSString stringWithContentsOfFile:bootstrapFile encoding:NSUTF8StringEncoding error:nil];
payload[@"bootstrap_content"] = content;
} else {
fprintf(stderr, "%sError: Bootstrap file not found: %s%s\n",
[RED UTF8String], [bootstrapFile UTF8String], [RESET UTF8String]);
exit(1);
}
}
if ([inputFiles count] > 0) {
NSFileManager* fm = [NSFileManager defaultManager];
NSMutableArray* files = [NSMutableArray array];
for (NSString* filepath in inputFiles) {
if (![fm fileExistsAtPath:filepath]) {
fprintf(stderr, "%sError: Input file not found: %s%s\n",
[RED UTF8String], [filepath UTF8String], [RESET UTF8String]);
exit(1);
}
NSData* content = [NSData dataWithContentsOfFile:filepath];
NSString* b64Content = [content base64EncodedStringWithOptions:0];
[files addObject:@{
@"filename": [filepath lastPathComponent],
@"content_base64": b64Content
}];
}
payload[@"input_files"] = files;
}
if (network) payload[@"network"] = network;
if (vcpu > 0) payload[@"vcpu"] = @(vcpu);
if (unfreezeOnDemand) payload[@"unfreeze_on_demand"] = @YES;
NSDictionary* result = apiRequestCLI(@"/services", @"POST", payload, publicKey, secretKey);
printf("%sService created: %s%s\n", [GREEN UTF8String], [result[@"id"] UTF8String], [RESET UTF8String]);
printf("Name: %s\n", [result[@"name"] UTF8String]);
if (result[@"url"]) {
printf("URL: %s\n", [result[@"url"] UTF8String]);
}
NSString* envContent = buildEnvContent(envVars, envFile);
if ([envContent length] > 0 && result[@"id"]) {
printf("%sSetting vault for service...%s\n", [YELLOW UTF8String], [RESET UTF8String]);
serviceEnvSet(result[@"id"], envContent, publicKey, secretKey);
}
return;
}
fprintf(stderr, "%sError: Specify --name to create a service, or use --list, --info, env, etc.%s\n",
[RED UTF8String], [RESET UTF8String]);
exit(1);
}
void cmdKey(NSArray* args) {
NSString* publicKey, *secretKey;
UNGetApiKeysCLI(&publicKey, &secretKey);
printf("%sValid%s\n", [GREEN UTF8String], [RESET UTF8String]);
printf("Public Key: %s\n", [publicKey UTF8String]);
}
void cmdImage(NSArray* args) {
NSString* publicKey, *secretKey;
UNGetApiKeysCLI(&publicKey, &secretKey);
BOOL listMode = NO;
NSString* infoId = nil;
NSString* deleteId = nil;
NSString* lockId = nil;
NSString* unlockId = nil;
NSString* publishId = nil;
NSString* sourceType = nil;
NSString* visibilityId = nil;
NSString* visibilityMode = nil;
NSString* spawnId = nil;
NSString* cloneId = nil;
NSString* name = nil;
NSString* ports = nil;
NSString* grantId = nil;
NSString* revokeId = nil;
NSString* trustedId = nil;
NSString* trustedKey = nil;
NSString* transferId = nil;
NSString* toKey = nil;
for (NSUInteger i = 0; i < [args count]; i++) {
NSString* arg = args[i];
if ([arg isEqualToString:@"--list"] || [arg isEqualToString:@"-l"]) {
listMode = YES;
} else if ([arg isEqualToString:@"--info"] && i + 1 < [args count]) {
infoId = args[++i];
} else if ([arg isEqualToString:@"--delete"] && i + 1 < [args count]) {
deleteId = args[++i];
} else if ([arg isEqualToString:@"--lock"] && i + 1 < [args count]) {
lockId = args[++i];
} else if ([arg isEqualToString:@"--unlock"] && i + 1 < [args count]) {
unlockId = args[++i];
} else if ([arg isEqualToString:@"--publish"] && i + 1 < [args count]) {
publishId = args[++i];
} else if ([arg isEqualToString:@"--source-type"] && i + 1 < [args count]) {
sourceType = args[++i];
} else if ([arg isEqualToString:@"--visibility"] && i + 2 < [args count]) {
visibilityId = args[++i];
visibilityMode = args[++i];
} else if ([arg isEqualToString:@"--spawn"] && i + 1 < [args count]) {
spawnId = args[++i];
} else if ([arg isEqualToString:@"--clone"] && i + 1 < [args count]) {
cloneId = args[++i];
} else if ([arg isEqualToString:@"--name"] && i + 1 < [args count]) {
name = args[++i];
} else if ([arg isEqualToString:@"--ports"] && i + 1 < [args count]) {
ports = args[++i];
} else if ([arg isEqualToString:@"--grant"] && i + 1 < [args count]) {
grantId = args[++i];
} else if ([arg isEqualToString:@"--revoke"] && i + 1 < [args count]) {
revokeId = args[++i];
} else if ([arg isEqualToString:@"--trusted"] && i + 1 < [args count]) {
trustedId = args[++i];
} else if ([arg isEqualToString:@"--trusted-key"] && i + 1 < [args count]) {
trustedKey = args[++i];
} else if ([arg isEqualToString:@"--transfer"] && i + 1 < [args count]) {
transferId = args[++i];
} else if ([arg isEqualToString:@"--to-key"] && i + 1 < [args count]) {
toKey = args[++i];
}
}
if (listMode) {
NSDictionary* result = apiRequestCLI(@"/images", @"GET", nil, publicKey, secretKey);
NSData* jsonData = [NSJSONSerialization dataWithJSONObject:result options:NSJSONWritingPrettyPrinted error:nil];
NSString* jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
printf("%s\n", [jsonString UTF8String]);
return;
}
if (infoId) {
NSString* endpoint = [NSString stringWithFormat:@"/images/%@", infoId];
NSDictionary* result = apiRequestCLI(endpoint, @"GET", nil, publicKey, secretKey);
NSData* jsonData = [NSJSONSerialization dataWithJSONObject:result options:NSJSONWritingPrettyPrinted error:nil];
NSString* jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
printf("%s\n", [jsonString UTF8String]);
return;
}
if (deleteId) {
NSString* endpoint = [NSString stringWithFormat:@"/images/%@", deleteId];
NSInteger statusCode = 0;
NSDictionary* response = apiRequestWithStatusCLI(endpoint, @"DELETE", nil, publicKey, secretKey, &statusCode);
if (statusCode == 428) {
if (handleSudoChallenge(response, @"DELETE", endpoint, nil, publicKey, secretKey)) {
printf("%sImage deleted: %s%s\n", [GREEN UTF8String], [deleteId UTF8String], [RESET UTF8String]);
} else {
exit(1);
}
} else if (statusCode >= 200 && statusCode < 300) {
printf("%sImage deleted: %s%s\n", [GREEN UTF8String], [deleteId UTF8String], [RESET UTF8String]);
} else {
fprintf(stderr, "%sError: HTTP %ld%s\n", [RED UTF8String], (long)statusCode, [RESET UTF8String]);
exit(1);
}
return;
}
if (lockId) {
NSString* endpoint = [NSString stringWithFormat:@"/images/%@/lock", lockId];
apiRequestCLI(endpoint, @"POST", nil, publicKey, secretKey);
printf("%sImage locked: %s%s\n", [GREEN UTF8String], [lockId UTF8String], [RESET UTF8String]);
return;
}
if (unlockId) {
NSString* endpoint = [NSString stringWithFormat:@"/images/%@/unlock", unlockId];
NSInteger statusCode = 0;
NSDictionary* response = apiRequestWithStatusCLI(endpoint, @"POST", @{}, publicKey, secretKey, &statusCode);
if (statusCode == 428) {
if (handleSudoChallenge(response, @"POST", endpoint, @"{}", publicKey, secretKey)) {
printf("%sImage unlocked: %s%s\n", [GREEN UTF8String], [unlockId UTF8String], [RESET UTF8String]);
} else {
exit(1);
}
} else if (statusCode >= 200 && statusCode < 300) {
printf("%sImage unlocked: %s%s\n", [GREEN UTF8String], [unlockId UTF8String], [RESET UTF8String]);
} else {
fprintf(stderr, "%sError: HTTP %ld%s\n", [RED UTF8String], (long)statusCode, [RESET UTF8String]);
exit(1);
}
return;
}
if (publishId) {
if (!sourceType) {
fprintf(stderr, "%sError: --publish requires --source-type (service or snapshot)%s\n",
[RED UTF8String], [RESET UTF8String]);
exit(1);
}
NSMutableDictionary* payload = [NSMutableDictionary dictionaryWithDictionary:@{
@"source_type": sourceType,
@"source_id": publishId
}];
if (name) payload[@"name"] = name;
NSDictionary* result = apiRequestCLI(@"/images/publish", @"POST", payload, publicKey, secretKey);
NSData* jsonData = [NSJSONSerialization dataWithJSONObject:result options:NSJSONWritingPrettyPrinted error:nil];
NSString* jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
printf("%sImage published%s\n", [GREEN UTF8String], [RESET UTF8String]);
printf("%s\n", [jsonString UTF8String]);
return;
}
if (visibilityId && visibilityMode) {
NSString* endpoint = [NSString stringWithFormat:@"/images/%@/visibility", visibilityId];
NSDictionary* payload = @{@"visibility": visibilityMode};
apiRequestCLI(endpoint, @"POST", payload, publicKey, secretKey);
printf("%sImage visibility set to %s: %s%s\n", [GREEN UTF8String], [visibilityMode UTF8String], [visibilityId UTF8String], [RESET UTF8String]);
return;
}
if (spawnId) {
NSString* endpoint = [NSString stringWithFormat:@"/images/%@/spawn", spawnId];
NSMutableDictionary* payload = [NSMutableDictionary dictionary];
if (name) payload[@"name"] = name;
if (ports) {
NSArray* portStrings = [ports componentsSeparatedByString:@","];
NSMutableArray* portNumbers = [NSMutableArray array];
for (NSString* p in portStrings) {
[portNumbers addObject:@([p intValue])];
}
payload[@"ports"] = portNumbers;
}
NSDictionary* result = apiRequestCLI(endpoint, @"POST", payload, publicKey, secretKey);
NSData* jsonData = [NSJSONSerialization dataWithJSONObject:result options:NSJSONWritingPrettyPrinted error:nil];
NSString* jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
printf("%sService spawned from image%s\n", [GREEN UTF8String], [RESET UTF8String]);
printf("%s\n", [jsonString UTF8String]);
return;
}
if (cloneId) {
NSString* endpoint = [NSString stringWithFormat:@"/images/%@/clone", cloneId];
NSMutableDictionary* payload = [NSMutableDictionary dictionary];
if (name) payload[@"name"] = name;
NSDictionary* result = apiRequestCLI(endpoint, @"POST", payload, publicKey, secretKey);
NSData* jsonData = [NSJSONSerialization dataWithJSONObject:result options:NSJSONWritingPrettyPrinted error:nil];
NSString* jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
printf("%sImage cloned%s\n", [GREEN UTF8String], [RESET UTF8String]);
printf("%s\n", [jsonString UTF8String]);
return;
}
if (grantId) {
if (!trustedKey) {
fprintf(stderr, "%sError: --grant requires --trusted-key%s\n", [RED UTF8String], [RESET UTF8String]);
exit(1);
}
NSString* endpoint = [NSString stringWithFormat:@"/images/%@/grant", grantId];
NSDictionary* payload = @{@"trusted_api_key": trustedKey};
apiRequestCLI(endpoint, @"POST", payload, publicKey, secretKey);
printf("%sAccess granted to %s%s\n", [GREEN UTF8String], [trustedKey UTF8String], [RESET UTF8String]);
return;
}
if (revokeId) {
if (!trustedKey) {
fprintf(stderr, "%sError: --revoke requires --trusted-key%s\n", [RED UTF8String], [RESET UTF8String]);
exit(1);
}
NSString* endpoint = [NSString stringWithFormat:@"/images/%@/revoke", revokeId];
NSDictionary* payload = @{@"trusted_api_key": trustedKey};
apiRequestCLI(endpoint, @"POST", payload, publicKey, secretKey);
printf("%sAccess revoked from %s%s\n", [GREEN UTF8String], [trustedKey UTF8String], [RESET UTF8String]);
return;
}
if (trustedId) {
NSString* endpoint = [NSString stringWithFormat:@"/images/%@/trusted", trustedId];
NSDictionary* result = apiRequestCLI(endpoint, @"GET", nil, publicKey, secretKey);
NSData* jsonData = [NSJSONSerialization dataWithJSONObject:result options:NSJSONWritingPrettyPrinted error:nil];
NSString* jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
printf("%s\n", [jsonString UTF8String]);
return;
}
if (transferId) {
if (!toKey) {
fprintf(stderr, "%sError: --transfer requires --to-key%s\n", [RED UTF8String], [RESET UTF8String]);
exit(1);
}
NSString* endpoint = [NSString stringWithFormat:@"/images/%@/transfer", transferId];
NSDictionary* payload = @{@"to_api_key": toKey};
NSInteger statusCode = 0;
NSDictionary* response = apiRequestWithStatusCLI(endpoint, @"POST", payload, publicKey, secretKey, &statusCode);
if (statusCode == 428) {
NSData* bodyData = [NSJSONSerialization dataWithJSONObject:payload options:0 error:nil];
NSString* bodyStr = [[NSString alloc] initWithData:bodyData encoding:NSUTF8StringEncoding];
if (handleSudoChallenge(response, @"POST", endpoint, bodyStr, publicKey, secretKey)) {
printf("%sImage transferred to %s%s\n", [GREEN UTF8String], [toKey UTF8String], [RESET UTF8String]);
} else {
exit(1);
}
} else if (statusCode >= 200 && statusCode < 300) {
printf("%sImage transferred to %s%s\n", [GREEN UTF8String], [toKey UTF8String], [RESET UTF8String]);
} else {
fprintf(stderr, "%sError: HTTP %ld%s\n", [RED UTF8String], (long)statusCode, [RESET UTF8String]);
exit(1);
}
return;
}
fprintf(stderr, "%sError: Use --list, --info, --delete, --lock, --unlock, --publish, --visibility, --spawn, --clone, --grant, --revoke, --trusted, or --transfer%s\n",
[RED UTF8String], [RESET UTF8String]);
exit(1);
}
// ============================================================================
// Snapshot Command
// ============================================================================
void cmdSnapshot(NSArray* args) {
NSString* publicKey, *secretKey;
UNGetApiKeysCLI(&publicKey, &secretKey);
BOOL listMode = NO;
NSString* infoId = nil;
NSString* sessionId = nil;
NSString* serviceId = nil;
NSString* restoreId = nil;
NSString* deleteId = nil;
NSString* lockId = nil;
NSString* unlockId = nil;
NSString* cloneId = nil;
NSString* cloneType = nil;
NSString* name = nil;
NSString* ports = nil;
BOOL hot = NO;
for (NSUInteger i = 0; i < [args count]; i++) {
NSString* arg = args[i];
if ([arg isEqualToString:@"--list"] || [arg isEqualToString:@"-l"]) {
listMode = YES;
} else if ([arg isEqualToString:@"--info"] && i + 1 < [args count]) {
infoId = args[++i];
} else if ([arg isEqualToString:@"--session"] && i + 1 < [args count]) {
sessionId = args[++i];
} else if ([arg isEqualToString:@"--service"] && i + 1 < [args count]) {
serviceId = args[++i];
} else if ([arg isEqualToString:@"--restore"] && i + 1 < [args count]) {
restoreId = args[++i];
} else if ([arg isEqualToString:@"--delete"] && i + 1 < [args count]) {
deleteId = args[++i];
} else if ([arg isEqualToString:@"--lock"] && i + 1 < [args count]) {
lockId = args[++i];
} else if ([arg isEqualToString:@"--unlock"] && i + 1 < [args count]) {
unlockId = args[++i];
} else if ([arg isEqualToString:@"--clone"] && i + 1 < [args count]) {
cloneId = args[++i];
} else if ([arg isEqualToString:@"--clone-type"] && i + 1 < [args count]) {
cloneType = args[++i];
} else if ([arg isEqualToString:@"--name"] && i + 1 < [args count]) {
name = args[++i];
} else if ([arg isEqualToString:@"--ports"] && i + 1 < [args count]) {
ports = args[++i];
} else if ([arg isEqualToString:@"--hot"]) {
hot = YES;
}
}
if (listMode) {
NSDictionary* result = apiRequestCLI(@"/snapshots", @"GET", nil, publicKey, secretKey);
NSData* jsonData = [NSJSONSerialization dataWithJSONObject:result options:NSJSONWritingPrettyPrinted error:nil];
NSString* jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
printf("%s\n", [jsonString UTF8String]);
return;
}
if (infoId) {
NSString* endpoint = [NSString stringWithFormat:@"/snapshots/%@", infoId];
NSDictionary* result = apiRequestCLI(endpoint, @"GET", nil, publicKey, secretKey);
NSData* jsonData = [NSJSONSerialization dataWithJSONObject:result options:NSJSONWritingPrettyPrinted error:nil];
NSString* jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
printf("%s\n", [jsonString UTF8String]);
return;
}
if (sessionId) {
NSString* endpoint = [NSString stringWithFormat:@"/sessions/%@/snapshot", sessionId];
NSMutableDictionary* payload = [NSMutableDictionary dictionary];
if (name) payload[@"name"] = name;
if (hot) payload[@"hot"] = @YES;
NSDictionary* result = apiRequestCLI(endpoint, @"POST", payload, publicKey, secretKey);
NSData* jsonData = [NSJSONSerialization dataWithJSONObject:result options:NSJSONWritingPrettyPrinted error:nil];
NSString* jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
printf("%sSnapshot created%s\n", [GREEN UTF8String], [RESET UTF8String]);
printf("%s\n", [jsonString UTF8String]);
return;
}
if (serviceId) {
NSString* endpoint = [NSString stringWithFormat:@"/services/%@/snapshot", serviceId];
NSMutableDictionary* payload = [NSMutableDictionary dictionary];
if (name) payload[@"name"] = name;
if (hot) payload[@"hot"] = @YES;
NSDictionary* result = apiRequestCLI(endpoint, @"POST", payload, publicKey, secretKey);
NSData* jsonData = [NSJSONSerialization dataWithJSONObject:result options:NSJSONWritingPrettyPrinted error:nil];
NSString* jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
printf("%sSnapshot created%s\n", [GREEN UTF8String], [RESET UTF8String]);
printf("%s\n", [jsonString UTF8String]);
return;
}
if (restoreId) {
NSString* endpoint = [NSString stringWithFormat:@"/snapshots/%@/restore", restoreId];
NSDictionary* result = apiRequestCLI(endpoint, @"POST", @{}, publicKey, secretKey);
NSData* jsonData = [NSJSONSerialization dataWithJSONObject:result options:NSJSONWritingPrettyPrinted error:nil];
NSString* jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
printf("%sSnapshot restored%s\n", [GREEN UTF8String], [RESET UTF8String]);
printf("%s\n", [jsonString UTF8String]);
return;
}
if (deleteId) {
NSString* endpoint = [NSString stringWithFormat:@"/snapshots/%@", deleteId];
NSInteger statusCode = 0;
NSDictionary* response = apiRequestWithStatusCLI(endpoint, @"DELETE", nil, publicKey, secretKey, &statusCode);
if (statusCode == 428) {
if (handleSudoChallenge(response, @"DELETE", endpoint, nil, publicKey, secretKey)) {
printf("%sSnapshot deleted: %s%s\n", [GREEN UTF8String], [deleteId UTF8String], [RESET UTF8String]);
} else {
exit(1);
}
} else if (statusCode >= 200 && statusCode < 300) {
printf("%sSnapshot deleted: %s%s\n", [GREEN UTF8String], [deleteId UTF8String], [RESET UTF8String]);
} else {
fprintf(stderr, "%sError: HTTP %ld%s\n", [RED UTF8String], (long)statusCode, [RESET UTF8String]);
exit(1);
}
return;
}
if (lockId) {
NSString* endpoint = [NSString stringWithFormat:@"/snapshots/%@/lock", lockId];
apiRequestCLI(endpoint, @"POST", nil, publicKey, secretKey);
printf("%sSnapshot locked: %s%s\n", [GREEN UTF8String], [lockId UTF8String], [RESET UTF8String]);
return;
}
if (unlockId) {
NSString* endpoint = [NSString stringWithFormat:@"/snapshots/%@/unlock", unlockId];
NSInteger statusCode = 0;
NSDictionary* response = apiRequestWithStatusCLI(endpoint, @"POST", @{}, publicKey, secretKey, &statusCode);
if (statusCode == 428) {
if (handleSudoChallenge(response, @"POST", endpoint, @"{}", publicKey, secretKey)) {
printf("%sSnapshot unlocked: %s%s\n", [GREEN UTF8String], [unlockId UTF8String], [RESET UTF8String]);
} else {
exit(1);
}
} else if (statusCode >= 200 && statusCode < 300) {
printf("%sSnapshot unlocked: %s%s\n", [GREEN UTF8String], [unlockId UTF8String], [RESET UTF8String]);
} else {
fprintf(stderr, "%sError: HTTP %ld%s\n", [RED UTF8String], (long)statusCode, [RESET UTF8String]);
exit(1);
}
return;
}
if (cloneId) {
NSString* endpoint = [NSString stringWithFormat:@"/snapshots/%@/clone", cloneId];
NSMutableDictionary* payload = [NSMutableDictionary dictionary];
payload[@"clone_type"] = cloneType ?: @"session";
if (name) payload[@"name"] = name;
if (ports) {
NSArray* portStrings = [ports componentsSeparatedByString:@","];
NSMutableArray* portNumbers = [NSMutableArray array];
for (NSString* p in portStrings) {
[portNumbers addObject:@([p intValue])];
}
payload[@"ports"] = portNumbers;
}
NSDictionary* result = apiRequestCLI(endpoint, @"POST", payload, publicKey, secretKey);
NSData* jsonData = [NSJSONSerialization dataWithJSONObject:result options:NSJSONWritingPrettyPrinted error:nil];
NSString* jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
printf("%sSnapshot cloned%s\n", [GREEN UTF8String], [RESET UTF8String]);
printf("%s\n", [jsonString UTF8String]);
return;
}
fprintf(stderr, "%sError: Use --list, --info, --session, --service, --restore, --delete, --lock, --unlock, or --clone%s\n",
[RED UTF8String], [RESET UTF8String]);
exit(1);
}
// ============================================================================
// Logs Command
// ============================================================================
void cmdLogs(NSArray* args) {
NSString* publicKey, *secretKey;
UNGetApiKeysCLI(&publicKey, &secretKey);
NSString* source = @"all";
NSInteger lines = 100;
NSString* since = @"1h";
NSString* grepPattern = nil;
BOOL follow = NO;
for (NSUInteger i = 0; i < [args count]; i++) {
NSString* arg = args[i];
if (([arg isEqualToString:@"--source"] || [arg isEqualToString:@"-s"]) && i + 1 < [args count]) {
source = args[++i];
} else if (([arg isEqualToString:@"--lines"] || [arg isEqualToString:@"-n"]) && i + 1 < [args count]) {
lines = [args[++i] integerValue];
} else if ([arg isEqualToString:@"--since"] && i + 1 < [args count]) {
since = args[++i];
} else if (([arg isEqualToString:@"--grep"] || [arg isEqualToString:@"-g"]) && i + 1 < [args count]) {
grepPattern = args[++i];
} else if ([arg isEqualToString:@"--follow"] || [arg isEqualToString:@"-f"]) {
follow = YES;
}
}
if (follow) {
// Streaming logs via SSE
NSMutableString* endpoint = [NSMutableString stringWithFormat:@"/paas/logs/stream?source=%@", source];
if (grepPattern) {
NSString* encoded = [grepPattern stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]];
[endpoint appendFormat:@"&grep=%@", encoded];
}
NSString* urlStr = [NSString stringWithFormat:@"%@%@", UN_PORTAL_BASE, endpoint];
NSURL* url = [NSURL URLWithString:urlStr];
NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:url];
request.HTTPMethod = @"GET";
[request setValue:@"text/event-stream" forHTTPHeaderField:@"Accept"];
// Add HMAC authentication headers
if (secretKey && [secretKey length] > 0) {
NSString* timestamp = [NSString stringWithFormat:@"%ld", (long)[[NSDate date] timeIntervalSince1970]];
NSString* message = [NSString stringWithFormat:@"%@:GET:%@:", timestamp, endpoint];
NSString* signature = hmacSign(secretKey, message);
[request setValue:[NSString stringWithFormat:@"Bearer %@", publicKey] forHTTPHeaderField:@"Authorization"];
[request setValue:timestamp forHTTPHeaderField:@"X-Timestamp"];
[request setValue:signature forHTTPHeaderField:@"X-Signature"];
} else {
[request setValue:[NSString stringWithFormat:@"Bearer %@", publicKey] forHTTPHeaderField:@"Authorization"];
}
printf("Streaming logs (Ctrl+C to stop)...\n");
// Use synchronous download for SSE - will block
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
__block NSData* responseData = nil;
NSURLSessionDataTask* task = [[NSURLSession sharedSession] dataTaskWithRequest:request
completionHandler:^(NSData* data, NSURLResponse* response, NSError* error) {
responseData = data;
dispatch_semaphore_signal(semaphore);
}];
[task resume];
// For SSE, we need to handle streaming differently - just show we attempted
// Real SSE would need a custom NSURLSession delegate
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
if (responseData) {
NSString* output = [[NSString alloc] initWithData:responseData encoding:NSUTF8StringEncoding];
printf("%s\n", [output UTF8String]);
}
return;
}
// Fetch logs
NSMutableString* endpoint = [NSMutableString stringWithFormat:@"/paas/logs?source=%@&lines=%ld&since=%@",
source, (long)lines, since];
if (grepPattern) {
NSString* encoded = [grepPattern stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]];
[endpoint appendFormat:@"&grep=%@", encoded];
}
// Call Portal API for logs
NSString* urlStr = [NSString stringWithFormat:@"%@%@", UN_PORTAL_BASE, endpoint];
NSURL* url = [NSURL URLWithString:urlStr];
NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:url];
request.HTTPMethod = @"GET";
[request setValue:@"application/json" forHTTPHeaderField:@"Accept"];
// Add HMAC authentication headers
if (secretKey && [secretKey length] > 0) {
NSString* timestamp = [NSString stringWithFormat:@"%ld", (long)[[NSDate date] timeIntervalSince1970]];
NSString* message = [NSString stringWithFormat:@"%@:GET:%@:", timestamp, endpoint];
NSString* signature = hmacSign(secretKey, message);
[request setValue:[NSString stringWithFormat:@"Bearer %@", publicKey] forHTTPHeaderField:@"Authorization"];
[request setValue:timestamp forHTTPHeaderField:@"X-Timestamp"];
[request setValue:signature forHTTPHeaderField:@"X-Signature"];
} else {
[request setValue:[NSString stringWithFormat:@"Bearer %@", publicKey] forHTTPHeaderField:@"Authorization"];
}
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
__block NSDictionary* responseData = nil;
__block NSInteger statusCode = 0;
NSURLSessionDataTask* task = [[NSURLSession sharedSession] dataTaskWithRequest:request
completionHandler:^(NSData* data, NSURLResponse* response, NSError* error) {
NSHTTPURLResponse* httpResponse = (NSHTTPURLResponse*)response;
statusCode = httpResponse.statusCode;
if (data) {
responseData = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
}
dispatch_semaphore_signal(semaphore);
}];
[task resume];
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
if (statusCode == 200 && responseData) {
NSArray* logLines = responseData[@"logs"];
if (logLines && [logLines isKindOfClass:[NSArray class]]) {
for (NSDictionary* entry in logLines) {
NSString* src = entry[@"source"] ?: @"unknown";
NSString* msg = entry[@"line"] ?: @"";
printf("[%s] %s\n", [src UTF8String], [msg UTF8String]);
}
} else {
NSData* jsonData = [NSJSONSerialization dataWithJSONObject:responseData options:NSJSONWritingPrettyPrinted error:nil];
NSString* jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
printf("%s\n", [jsonString UTF8String]);
}
} else {
fprintf(stderr, "%sError: HTTP %ld%s\n", [RED UTF8String], (long)statusCode, [RESET UTF8String]);
exit(1);
}
}
// ============================================================================
// Health Command
// ============================================================================
void cmdHealth(void) {
NSURL* url = [NSURL URLWithString:[NSString stringWithFormat:@"%@/health", UN_API_BASE]];
NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:url];
request.HTTPMethod = @"GET";
request.timeoutInterval = 10;
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
__block NSDictionary* responseData = nil;
__block NSInteger statusCode = 0;
NSURLSessionDataTask* task = [[NSURLSession sharedSession] dataTaskWithRequest:request
completionHandler:^(NSData* data, NSURLResponse* response, NSError* error) {
NSHTTPURLResponse* httpResponse = (NSHTTPURLResponse*)response;
statusCode = httpResponse.statusCode;
if (data) {
responseData = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
}
dispatch_semaphore_signal(semaphore);
}];
[task resume];
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
if (statusCode == 200) {
printf("%sAPI is healthy%s\n", [GREEN UTF8String], [RESET UTF8String]);
} else {
printf("%sAPI may be unhealthy%s\n", [RED UTF8String], [RESET UTF8String]);
}
if (responseData) {
NSData* jsonData = [NSJSONSerialization dataWithJSONObject:responseData options:NSJSONWritingPrettyPrinted error:nil];
NSString* jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
printf("%s\n", [jsonString UTF8String]);
}
}
// ============================================================================
// Version Command
// ============================================================================
void cmdVersion(void) {
printf("un.m version 1.0.0\n");
printf("API: %s\n", [UN_API_BASE UTF8String]);
printf("Portal: %s\n", [UN_PORTAL_BASE UTF8String]);
}
void cmdLanguages(BOOL jsonOutput) {
NSString* publicKey, *secretKey;
UNGetApiKeysCLI(&publicKey, &secretKey);
NSDictionary* result = apiRequestCLI(@"/languages", @"GET", nil, publicKey, secretKey);
NSArray* languages = result[@"languages"];
if (jsonOutput) {
// Output as JSON array
NSError* error = nil;
NSData* jsonData = [NSJSONSerialization dataWithJSONObject:languages options:0 error:&error];
if (!error && jsonData) {
NSString* jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
printf("%s\n", [jsonString UTF8String]);
}
} else {
// Output one language per line
for (NSString* lang in languages) {
printf("%s\n", [lang UTF8String]);
}
}
}
void showHelp(void) {
printf("unsandbox - Execute code in secure sandboxes\n\n");
printf("Usage:\n");
printf(" un.m [options] <source_file>\n");
printf(" un.m session [options]\n");
printf(" un.m service [options]\n");
printf(" un.m image [options]\n");
printf(" un.m languages [--json]\n");
printf(" un.m key [options]\n\n");
printf("Execute options:\n");
printf(" -e KEY=VALUE Environment variable (multiple allowed)\n");
printf(" -f FILE Input file (multiple allowed)\n");
printf(" -a Return artifacts\n");
printf(" -o DIR Output directory for artifacts\n");
printf(" -n MODE Network mode (zerotrust|semitrusted)\n");
printf(" -v N vCPU count (1-8)\n\n");
printf("Session options:\n");
printf(" --list List active sessions\n");
printf(" --kill ID Terminate session\n");
printf(" --shell NAME Shell/REPL (default: bash)\n\n");
printf("Service options:\n");
printf(" --list List services\n");
printf(" --info ID Get service details\n");
printf(" --logs ID Get service logs\n");
printf(" --freeze ID Freeze service\n");
printf(" --unfreeze ID Unfreeze service\n");
printf(" --destroy ID Destroy service\n");
printf(" --name NAME Create service with name\n");
printf(" --ports PORTS Comma-separated ports\n");
printf(" --bootstrap CMD Bootstrap command\n\n");
printf("Image options:\n");
printf(" --list, -l List all images\n");
printf(" --info ID Get image details\n");
printf(" --delete ID Delete an image\n");
printf(" --lock ID Lock image to prevent deletion\n");
printf(" --unlock ID Unlock image\n");
printf(" --publish ID Publish image from service/snapshot\n");
printf(" --source-type TYPE Source type: service or snapshot\n");
printf(" --visibility ID MODE Set visibility: private, unlisted, or public\n");
printf(" --spawn ID Spawn new service from image\n");
printf(" --clone ID Clone an image\n");
printf(" --name NAME Name for spawned service or cloned image\n");
printf(" --ports PORTS Ports for spawned service\n\n");
printf("Languages options:\n");
printf(" --json Output as JSON array\n\n");
printf("Library Usage:\n");
printf(" #import \"un.m\"\n");
printf(" UNClient *client = [[UNClient alloc] init];\n");
printf(" NSDictionary *result = [client execute:@\"python\" code:@\"print('Hello')\"];\n");
}
// ============================================================================
// Main Entry Point
// ============================================================================
#ifndef UN_LIBRARY_ONLY
int main(int argc, const char* argv[]) {
@autoreleasepool {
if (argc < 2) {
showHelp();
return 1;
}
NSMutableArray* args = [NSMutableArray array];
for (int i = 1; i < argc; i++) {
[args addObject:[NSString stringWithUTF8String:argv[i]]];
}
// Pre-scan for --account N before subcommand dispatch
for (NSUInteger i = 0; i < [args count]; i++) {
if ([args[i] isEqualToString:@"--account"] && i + 1 < [args count]) {
g_accountIndex = [args[i + 1] integerValue];
break;
}
}
NSString* firstArg = args[0];
if ([firstArg isEqualToString:@"--help"] || [firstArg isEqualToString:@"-h"]) {
showHelp();
return 0;
}
if ([firstArg isEqualToString:@"session"]) {
cmdSession([args subarrayWithRange:NSMakeRange(1, [args count] - 1)]);
} else if ([firstArg isEqualToString:@"service"]) {
cmdService([args subarrayWithRange:NSMakeRange(1, [args count] - 1)]);
} else if ([firstArg isEqualToString:@"image"]) {
cmdImage([args subarrayWithRange:NSMakeRange(1, [args count] - 1)]);
} else if ([firstArg isEqualToString:@"languages"]) {
BOOL jsonOutput = NO;
for (NSUInteger i = 1; i < [args count]; i++) {
if ([args[i] isEqualToString:@"--json"]) {
jsonOutput = YES;
}
}
cmdLanguages(jsonOutput);
} else if ([firstArg isEqualToString:@"key"]) {
cmdKey([args subarrayWithRange:NSMakeRange(1, [args count] - 1)]);
} else if ([firstArg isEqualToString:@"snapshot"]) {
cmdSnapshot([args subarrayWithRange:NSMakeRange(1, [args count] - 1)]);
} else if ([firstArg isEqualToString:@"logs"]) {
cmdLogs([args subarrayWithRange:NSMakeRange(1, [args count] - 1)]);
} else if ([firstArg isEqualToString:@"health"]) {
cmdHealth();
} else if ([firstArg isEqualToString:@"version"] || [firstArg isEqualToString:@"--version"]) {
cmdVersion();
} else {
cmdExecute(args);
}
}
return 0;
}
#endif
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