un-inception/un.m
russell@unturf.com f503df9ce1 Refactor C, C++, Rust, Zig SDKs: add library exports + HMAC auth + caching
- Rust SDK: Complete refactoring with public library functions (execute, execute_async, wait, get_job, cancel_job, list_jobs, languages, detect_language)
- C SDK: Added library function documentation with 4-tier authentication (args, env vars, ~/.unsandbox/accounts.csv, ./accounts.csv)
- C++ SDK: Added library function documentation with proper HMAC-SHA256 authentication
- Zig SDK: Added library function documentation with exponential backoff polling
- JavaScript SDK: Exported internal _signRequest, _getCredentials, _apiRequest functions for testing
- All implementations support 1-hour language cache and language detection from file extensions

All SDKs follow the same API pattern from un.py reference implementation:
- execute(language, code, opts) - synchronous execution
- execute_async(language, code, opts) - async submission
- wait(job_id, opts) - polling with exponential backoff [300, 450, 700, 900, 650, 1600, 2000]ms
- languages(cache_ttl) - cached language list
- detect_language(filename) - extension-based detection
2026-01-15 15:35:39 -05:00

1567 lines
60 KiB
Objective-C

// 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. Environment variables: UNSANDBOX_PUBLIC_KEY + UNSANDBOX_SECRET_KEY
// 3. Config file: ~/.unsandbox/accounts.csv (public_key,secret_key per line)
#!/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
// ============================================================================
/**
* Get API credentials from environment or config file.
* Priority: 1. Arguments, 2. Environment vars, 3. ~/.unsandbox/accounts.csv
*
* @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: 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 3: Config file ~/.unsandbox/accounts.csv
NSString* home = NSHomeDirectory();
NSString* accountsPath = [home stringByAppendingPathComponent:@".unsandbox/accounts.csv"];
NSFileManager* fm = [NSFileManager defaultManager];
if ([fm fileExistsAtPath:accountsPath]) {
NSString* content = [NSString stringWithContentsOfFile:accountsPath encoding:NSUTF8StringEncoding error:nil];
if (content) {
NSArray* lines = [content componentsSeparatedByString:@"\n"];
for (NSString* line in lines) {
NSString* trimmed = [line stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
if ([trimmed length] == 0 || [trimmed hasPrefix:@"#"]) continue;
NSArray* parts = [trimmed componentsSeparatedByString:@","];
if ([parts count] >= 2) {
NSString* pk = [parts[0] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
NSString* sk = [parts[1] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
if ([pk hasPrefix:@"unsb-pk-"] && [sk hasPrefix:@"unsb-sk-"]) {
*publicKey = pk;
*secretKey = sk;
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
// ============================================================================
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]);
}
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;
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];
}
}
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];
apiRequestCLI(endpoint, @"DELETE", nil, publicKey, secretKey);
printf("%sService destroyed: %s%s\n", [GREEN UTF8String], [destroyId UTF8String], [RESET UTF8String]);
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);
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 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 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("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]]];
}
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:@"key"]) {
cmdKey([args subarrayWithRange:NSMakeRange(1, [args count] - 1)]);
} else {
cmdExecute(args);
}
}
return 0;
}
#endif