// 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 #import // ============================================================================ // 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 // ============================================================================ // ============================================================================ // 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] \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 [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] \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]]]; } 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