Console Playground

CLI

Fast command-line client for code execution and interactive sessions. 42+ languages, 30+ shells/REPLs.

Official OpenAPI Swagger Docs ↗

Quick Start — C

# Download + setup
curl -O https://git.unturf.com/engineering/unturf/un-inception/-/raw/main/clients/c/src/un.c -O https://git.unturf.com/engineering/unturf/un-inception/-/raw/main/clients/c/src/un.h
export UNSANDBOX_PUBLIC_KEY="unsb-pk-xxxx-xxxx-xxxx-xxxx"
export UNSANDBOX_SECRET_KEY="unsb-sk-xxxxx-xxxxx-xxxxx-xxxxx"
# Compile first
gcc -o un un.c -lcurl -lwebsockets -lcrypto

# Run code
./un script.c

Downloads

Install Guide →
Static Binary
Linux x86_64 (5.3MB)
un
C SDK
un.c (413.5 KB)

Features

  • 42+ languages - Python, JS, Go, Rust, C++, Java...
  • Sessions - 30+ shells/REPLs, tmux persistence
  • Files - Upload files, collect artifacts
  • Services - Persistent containers with domains
  • Snapshots - Point-in-time backups
  • Images - Publish, share, transfer

Integration Quickstart ⚡

Add unsandbox superpowers to your existing C app:

1
Download
curl -O https://git.unturf.com/engineering/unturf/un-inception/-/raw/main/clients/c/src/un.c \
     -O https://git.unturf.com/engineering/unturf/un-inception/-/raw/main/clients/c/src/un.h
2
Set API Keys
# Option A: Environment variables
export UNSANDBOX_PUBLIC_KEY="unsb-pk-xxxx-xxxx-xxxx-xxxx"
export UNSANDBOX_SECRET_KEY="unsb-sk-xxxxx-xxxxx-xxxxx-xxxxx"

# Option B: Config file (persistent)
mkdir -p ~/.unsandbox
echo "unsb-pk-xxxx-xxxx-xxxx-xxxx,unsb-sk-xxxxx-xxxxx-xxxxx-xxxxx" > ~/.unsandbox/accounts.csv
3
Hello World
// main.c - Your app using unsandbox SDK
#include <stdio.h>
#include "un.h"

int main() {
    unsandbox_result_t *r = unsandbox_execute("c", "#include <stdio.h>\nint main() { printf(\"Hello from C running on unsandbox!\\n\"); return 0; }", NULL, NULL);
    if (r && r->success) printf("%s", r->stdout_str);
    unsandbox_free_result(r);
    return 0;
}

// Compile: gcc -o myapp main.c un.c -lcurl -lwebsockets -lcrypto
Demo cooldown: s
stdout:

                      
JSON Response:

                      
4
Compile & Run
gcc -o myapp main.c un.c -lcurl -lwebsockets -lcrypto && ./myapp
Source Code 📄 (11571 lines)
MD5: 19784fea302e9fbb0d50a9c7e2af0c8f SHA256: 2222e65a358188d24494f81d6cbf376479716f0a603f2c5ce07df03eabe0fb25
/*
 * un - unsandbox.com CLI
 *
 * Authentication priority (highest to lowest, per POSIX convention):
 *   1. CLI flags: -p (public key) + -k (secret key)
 *   2. Environment variables: UNSANDBOX_PUBLIC_KEY + UNSANDBOX_SECRET_KEY
 *   3. Config file: ~/.unsandbox/accounts.csv (format: public_key,secret_key per line)
 *      - Use --account N to select account by index (0-based, default: 0)
 *      - Or set UNSANDBOX_ACCOUNT=N environment variable
 *
 * Request authentication:
 *   Authorization: Bearer <public_key>                        <- identifies account
 *   X-Timestamp: <unix_seconds>                               <- replay prevention
 *   X-Signature: HMAC-SHA256(secret_key, ts:method:path:body) <- proves secret + body integrity
 *
 * The secret key is NEVER transmitted. Server decrypts stored secret to verify HMAC.
 * Timestamp must be within ±5 minutes of server time (prevents replay attacks).
 * Body is included in signature to prevent tampering (empty string for GET/DELETE).
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <stdarg.h>
#include <time.h>
#include <limits.h>
#include <curl/curl.h>
#include <sys/stat.h>
#include <unistd.h>
#include <termios.h>
#include <sys/ioctl.h>
#include <signal.h>
#include <poll.h>
#include <libwebsockets.h>
#include <pwd.h>

#ifdef UNSANDBOX_LIBRARY
#include "un.h"
#endif

#define API_URL "https://api.unsandbox.com/execute"
#define API_BASE "https://api.unsandbox.com"
#define PORTAL_BASE "https://unsandbox.com"
#define MAX_FILE_SIZE (100 * 1024 * 1024) // 100MB max single file
#define MAX_INPUT_FILES 1000
#define MAX_TOTAL_INPUT_SIZE (4096L * 1024 * 1024) // 4GB total across all input files
#define LARGE_UPLOAD_WARN_SIZE (1024L * 1024 * 1024) // Warn if total > 1GB
#define MAX_ENV_VARS 256  // LXC limit is typically higher
#define MAX_ENV_CONTENT_SIZE (64 * 1024)  // 64KB max env vault size
#define LANGUAGES_CACHE_TTL 3600  // 1 hour cache for languages list

// ============================================================================
// SHA-256 Implementation (for HMAC-SHA256)
// ============================================================================

static const uint32_t sha256_k[64] = {
    0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
    0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
    0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
    0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
    0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
    0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
    0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
    0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2
};

#define SHA256_ROTR(x, n) (((x) >> (n)) | ((x) << (32 - (n))))
#define SHA256_CH(x, y, z) (((x) & (y)) ^ (~(x) & (z)))
#define SHA256_MAJ(x, y, z) (((x) & (y)) ^ ((x) & (z)) ^ ((y) & (z)))
#define SHA256_EP0(x) (SHA256_ROTR(x, 2) ^ SHA256_ROTR(x, 13) ^ SHA256_ROTR(x, 22))
#define SHA256_EP1(x) (SHA256_ROTR(x, 6) ^ SHA256_ROTR(x, 11) ^ SHA256_ROTR(x, 25))
#define SHA256_SIG0(x) (SHA256_ROTR(x, 7) ^ SHA256_ROTR(x, 18) ^ ((x) >> 3))
#define SHA256_SIG1(x) (SHA256_ROTR(x, 17) ^ SHA256_ROTR(x, 19) ^ ((x) >> 10))

typedef struct {
    uint32_t state[8];
    uint64_t count;
    unsigned char buffer[64];
} UN_SHA256_CTX;

static void sha256_init(UN_SHA256_CTX *ctx) {
    ctx->state[0] = 0x6a09e667;
    ctx->state[1] = 0xbb67ae85;
    ctx->state[2] = 0x3c6ef372;
    ctx->state[3] = 0xa54ff53a;
    ctx->state[4] = 0x510e527f;
    ctx->state[5] = 0x9b05688c;
    ctx->state[6] = 0x1f83d9ab;
    ctx->state[7] = 0x5be0cd19;
    ctx->count = 0;
}

static void sha256_transform(UN_SHA256_CTX *ctx, const unsigned char *data) {
    uint32_t a, b, c, d, e, f, g, h, t1, t2, w[64];
    int i;

    for (i = 0; i < 16; i++) {
        w[i] = ((uint32_t)data[i * 4] << 24) | ((uint32_t)data[i * 4 + 1] << 16) |
               ((uint32_t)data[i * 4 + 2] << 8) | ((uint32_t)data[i * 4 + 3]);
    }
    for (i = 16; i < 64; i++) {
        w[i] = SHA256_SIG1(w[i - 2]) + w[i - 7] + SHA256_SIG0(w[i - 15]) + w[i - 16];
    }

    a = ctx->state[0]; b = ctx->state[1]; c = ctx->state[2]; d = ctx->state[3];
    e = ctx->state[4]; f = ctx->state[5]; g = ctx->state[6]; h = ctx->state[7];

    for (i = 0; i < 64; i++) {
        t1 = h + SHA256_EP1(e) + SHA256_CH(e, f, g) + sha256_k[i] + w[i];
        t2 = SHA256_EP0(a) + SHA256_MAJ(a, b, c);
        h = g; g = f; f = e; e = d + t1;
        d = c; c = b; b = a; a = t1 + t2;
    }

    ctx->state[0] += a; ctx->state[1] += b; ctx->state[2] += c; ctx->state[3] += d;
    ctx->state[4] += e; ctx->state[5] += f; ctx->state[6] += g; ctx->state[7] += h;
}

static void sha256_update(UN_SHA256_CTX *ctx, const unsigned char *data, size_t len) {
    size_t i, index, part_len;
    index = (size_t)(ctx->count & 0x3F);
    ctx->count += len;
    part_len = 64 - index;
    if (len >= part_len) {
        memcpy(&ctx->buffer[index], data, part_len);
        sha256_transform(ctx, ctx->buffer);
        for (i = part_len; i + 63 < len; i += 64)
            sha256_transform(ctx, &data[i]);
        index = 0;
    } else {
        i = 0;
    }
    memcpy(&ctx->buffer[index], &data[i], len - i);
}

static void sha256_final(UN_SHA256_CTX *ctx, unsigned char hash[32]) {
    unsigned char pad[64];
    unsigned char count_bits[8];
    size_t index, pad_len;
    uint64_t bits = ctx->count * 8;
    int i;

    for (i = 0; i < 8; i++) {
        count_bits[i] = (unsigned char)(bits >> (56 - i * 8));
    }

    index = (size_t)(ctx->count & 0x3F);
    pad_len = (index < 56) ? (56 - index) : (120 - index);
    memset(pad, 0, pad_len);
    pad[0] = 0x80;
    sha256_update(ctx, pad, pad_len);
    sha256_update(ctx, count_bits, 8);

    for (i = 0; i < 8; i++) {
        hash[i * 4] = (unsigned char)(ctx->state[i] >> 24);
        hash[i * 4 + 1] = (unsigned char)(ctx->state[i] >> 16);
        hash[i * 4 + 2] = (unsigned char)(ctx->state[i] >> 8);
        hash[i * 4 + 3] = (unsigned char)(ctx->state[i]);
    }
}

// Compute raw SHA-256 hash (32 bytes)
static void sha256_raw(const unsigned char *data, size_t len, unsigned char hash[32]) {
    UN_SHA256_CTX ctx;
    sha256_init(&ctx);
    sha256_update(&ctx, data, len);
    sha256_final(&ctx, hash);
}

// ============================================================================
// HMAC-SHA256 Implementation
// ============================================================================

#define HMAC_SHA256_BLOCK_SIZE 64
#define HMAC_SHA256_HASH_SIZE 32

// Compute HMAC-SHA256 and return as lowercase hex string (64 chars + null)
static char* hmac_sha256_hex(const char *key, size_t key_len, const char *data, size_t data_len) {
    unsigned char k_ipad[HMAC_SHA256_BLOCK_SIZE];
    unsigned char k_opad[HMAC_SHA256_BLOCK_SIZE];
    unsigned char tk[HMAC_SHA256_HASH_SIZE];
    unsigned char inner_hash[HMAC_SHA256_HASH_SIZE];
    unsigned char final_hash[HMAC_SHA256_HASH_SIZE];
    size_t i;

    // If key is longer than block size, hash it first
    if (key_len > HMAC_SHA256_BLOCK_SIZE) {
        sha256_raw((const unsigned char *)key, key_len, tk);
        key = (const char *)tk;
        key_len = HMAC_SHA256_HASH_SIZE;
    }

    // XOR key with ipad and opad values
    memset(k_ipad, 0x36, HMAC_SHA256_BLOCK_SIZE);
    memset(k_opad, 0x5c, HMAC_SHA256_BLOCK_SIZE);
    for (i = 0; i < key_len; i++) {
        k_ipad[i] ^= (unsigned char)key[i];
        k_opad[i] ^= (unsigned char)key[i];
    }

    // Inner hash: SHA256(k_ipad || data)
    UN_SHA256_CTX ctx;
    sha256_init(&ctx);
    sha256_update(&ctx, k_ipad, HMAC_SHA256_BLOCK_SIZE);
    sha256_update(&ctx, (const unsigned char *)data, data_len);
    sha256_final(&ctx, inner_hash);

    // Outer hash: SHA256(k_opad || inner_hash)
    sha256_init(&ctx);
    sha256_update(&ctx, k_opad, HMAC_SHA256_BLOCK_SIZE);
    sha256_update(&ctx, inner_hash, HMAC_SHA256_HASH_SIZE);
    sha256_final(&ctx, final_hash);

    // Convert to hex
    char *hex = malloc(65);
    if (!hex) return NULL;
    for (i = 0; i < HMAC_SHA256_HASH_SIZE; i++) {
        sprintf(hex + i * 2, "%02x", final_hash[i]);
    }
    hex[64] = '\0';
    return hex;
}

// Sign a request: HMAC-SHA256(secret_key, timestamp:method:path:body)
// Returns signature as hex string (caller must free)
// body can be NULL for bodyless requests (GET, DELETE)
static char* sign_request(const char *secret_key, long timestamp, const char *method, const char *path, const char *body) {
    // Build message: "timestamp:method:path:body"
    // Body is included raw to prevent tampering (empty string if NULL)
    char ts_str[32];
    snprintf(ts_str, sizeof(ts_str), "%ld", timestamp);

    const char *body_str = body ? body : "";
    size_t msg_len = strlen(ts_str) + 1 + strlen(method) + 1 + strlen(path) + 1 + strlen(body_str);
    char *message = malloc(msg_len + 1);
    if (!message) return NULL;

    snprintf(message, msg_len + 1, "%s:%s:%s:%s", ts_str, method, path, body_str);

    char *signature = hmac_sha256_hex(secret_key, strlen(secret_key), message, strlen(message));
    free(message);
    return signature;
}

// ============================================================================
// Account Credentials Management (~/.unsandbox/accounts.csv)
// ============================================================================

typedef struct {
    char *public_key;   // unsb-pk-xxxx-xxxx-xxxx-xxxx - used as bearer token to identify account
    char *secret_key;   // unsb-sk-xxxxx-xxxxx-xxxxx-xxxxx - used only for HMAC signing, never transmitted
} UnsandboxCredentials;

// Get path to ~/.unsandbox/accounts.csv
static char* get_accounts_csv_path(void) {
    const char *home = getenv("HOME");
    if (!home) {
        struct passwd *pw = getpwuid(getuid());
        if (pw) home = pw->pw_dir;
    }
    if (!home) return NULL;

    char *path = malloc(strlen(home) + 32);
    if (!path) return NULL;
    sprintf(path, "%s/.unsandbox/accounts.csv", home);
    return path;
}

// Ensure ~/.unsandbox directory exists
__attribute__((unused))
static void ensure_unsandbox_dir(void) {
    const char *home = getenv("HOME");
    if (!home) {
        struct passwd *pw = getpwuid(getuid());
        if (pw) home = pw->pw_dir;
    }
    if (!home) return;

    char dir[512];
    snprintf(dir, sizeof(dir), "%s/.unsandbox", home);
    mkdir(dir, 0700);
}

// Load account from ~/.unsandbox/accounts.csv by index (0-based)
// Format: public_key,secret_key (one per line)
// index -1 means use first valid account
static UnsandboxCredentials* load_credentials_from_csv(int account_index) {
    char *path = get_accounts_csv_path();
    if (!path) return NULL;

    FILE *f = fopen(path, "r");
    free(path);
    if (!f) return NULL;

    char line[1024];
    UnsandboxCredentials *creds = NULL;
    int current_index = 0;

    while (fgets(line, sizeof(line), f)) {
        // Skip empty lines and comments
        if (line[0] == '\n' || line[0] == '#') continue;

        // Remove newline
        size_t len = strlen(line);
        if (len > 0 && line[len - 1] == '\n') line[len - 1] = '\0';

        // Parse CSV: public_key,secret_key[,comment]
        char *comma = strchr(line, ',');
        if (!comma) continue;

        *comma = '\0';
        char *pk = line;
        char *sk = comma + 1;

        // Strip optional 3rd field (comment) after second comma
        char *comma2 = strchr(sk, ',');
        if (comma2) *comma2 = '\0';

        // Validate key prefixes
        if (strncmp(pk, "unsb-pk-", 8) != 0) continue;
        if (strncmp(sk, "unsb-sk-", 8) != 0) continue;

        // Check if this is the account we want
        if (account_index >= 0 && current_index != account_index) {
            current_index++;
            continue;
        }

        creds = malloc(sizeof(UnsandboxCredentials));
        if (!creds) break;

        creds->public_key = strdup(pk);
        creds->secret_key = strdup(sk);

        if (!creds->public_key || !creds->secret_key) {
            free(creds->public_key);
            free(creds->secret_key);
            free(creds);
            creds = NULL;
        }
        break;
    }

    fclose(f);
    return creds;
}

// Count total accounts in CSV
__attribute__((unused))
static int count_accounts_in_csv(void) {
    char *path = get_accounts_csv_path();
    if (!path) return 0;

    FILE *f = fopen(path, "r");
    free(path);
    if (!f) return 0;

    char line[1024];
    int count = 0;

    while (fgets(line, sizeof(line), f)) {
        if (line[0] == '\n' || line[0] == '#') continue;
        size_t len = strlen(line);
        if (len > 0 && line[len - 1] == '\n') line[len - 1] = '\0';
        char *comma = strchr(line, ',');
        if (!comma) continue;
        *comma = '\0';
        if (strncmp(line, "unsb-pk-", 8) == 0 && strncmp(comma + 1, "unsb-sk-", 8) == 0) {
            count++;
        }
    }

    fclose(f);
    return count;
}

// Get credentials using POSIX priority: CLI flags > env vars > CSV file
// cli_pk/cli_sk: from -p/-k flags (can be NULL)
// account_index: from --account flag (-1 means use env var or default to 0)
static UnsandboxCredentials* get_credentials(const char *cli_pk, const char *cli_sk, int account_index) {
    // Priority 1: CLI flags (-p and -k) - highest priority per POSIX convention
    if (cli_pk && cli_sk && strlen(cli_pk) > 0 && strlen(cli_sk) > 0) {
        UnsandboxCredentials *creds = malloc(sizeof(UnsandboxCredentials));
        if (!creds) return NULL;

        creds->public_key = strdup(cli_pk);
        creds->secret_key = strdup(cli_sk);

        if (!creds->public_key || !creds->secret_key) {
            free(creds->public_key);
            free(creds->secret_key);
            free(creds);
            return NULL;
        }
        return creds;
    }

    // Priority 2: Environment variables (keys)
    const char *env_pk = getenv("UNSANDBOX_PUBLIC_KEY");
    const char *env_sk = getenv("UNSANDBOX_SECRET_KEY");

    if (env_pk && env_sk && strlen(env_pk) > 0 && strlen(env_sk) > 0) {
        UnsandboxCredentials *creds = malloc(sizeof(UnsandboxCredentials));
        if (!creds) return NULL;

        creds->public_key = strdup(env_pk);
        creds->secret_key = strdup(env_sk);

        if (!creds->public_key || !creds->secret_key) {
            free(creds->public_key);
            free(creds->secret_key);
            free(creds);
            return NULL;
        }
        return creds;
    }

    // Priority 3: Config file (~/.unsandbox/accounts.csv)
    // Use account_index from --account flag, or UNSANDBOX_ACCOUNT env var, or default to 0
    int csv_index = account_index;
    if (csv_index < 0) {
        const char *env_account = getenv("UNSANDBOX_ACCOUNT");
        if (env_account && strlen(env_account) > 0) {
            csv_index = atoi(env_account);
        } else {
            csv_index = 0;
        }
    }
    return load_credentials_from_csv(csv_index);
}

static void free_credentials(UnsandboxCredentials *creds) {
    if (!creds) return;
    free(creds->public_key);
    free(creds->secret_key);
    free(creds);
}

// ============================================================================
// End Credentials Management
// ============================================================================

// ============================================================================
// HMAC Auth Headers Helper
// ============================================================================

// Add HMAC authentication headers to a curl_slist
// Returns a new slist with auth headers appended (caller must free with curl_slist_free_all)
// method: "GET", "POST", etc.
// path: e.g., "/execute", "/services", "/sessions"
//
// Adds these headers:
//   Authorization: Bearer <public_key>                                 (identifies account)
//   X-Timestamp: <unix_timestamp>                                      (replay prevention)
//   X-Signature: <HMAC-SHA256(secret_key, timestamp:method:path:body)> (proves secret + body integrity)
// body can be NULL for bodyless requests (GET, DELETE)
static struct curl_slist* add_hmac_auth_headers(struct curl_slist *headers,
                                                  const UnsandboxCredentials *creds,
                                                  const char *method,
                                                  const char *path,
                                                  const char *body) {
    if (!creds || !creds->public_key || !creds->secret_key) return headers;

    long timestamp = (long)time(NULL);
    char *signature = sign_request(creds->secret_key, timestamp, method, path, body);
    if (!signature) return headers;

    char auth_header[256];
    char ts_header[64];
    char sig_header[128];

    // Public key identifies the account (server looks up by key)
    snprintf(auth_header, sizeof(auth_header), "Authorization: Bearer %s", creds->public_key);
    // Timestamp prevents replay attacks (server checks ±5 minutes)
    snprintf(ts_header, sizeof(ts_header), "X-Timestamp: %ld", timestamp);
    // Signature proves possession of secret key and body integrity
    snprintf(sig_header, sizeof(sig_header), "X-Signature: %s", signature);

    headers = curl_slist_append(headers, auth_header);
    headers = curl_slist_append(headers, ts_header);
    headers = curl_slist_append(headers, sig_header);

    free(signature);
    return headers;
}

// ============================================================================
// End HMAC Auth Headers Helper
// ============================================================================

// Polling delays (milliseconds) - matches opencompletion.com cadence
// Cumulative: 300ms, 750ms, 1450ms, 2350ms, 3000ms, 4600ms, 6600ms+
static const int POLL_DELAYS[] = {300, 450, 700, 900, 650, 1600, 2000};
#define POLL_DELAYS_COUNT 7
#define POLL_MAX_CONSECUTIVE_ERRORS 30
#define POLL_ERROR_BACKOFF_MS 2000

// Response buffer structure
struct ResponseBuffer {
    char *data;
    size_t size;
};

// Input file structure
struct InputFile {
    char *filename;
    char *content_base64;
};

// Environment variable structure
struct EnvVar {
    char *key;
    char *value;
};

// Write callback for libcurl
static size_t write_callback(void *contents, size_t size, size_t nmemb, void *userp) {
    size_t realsize = size * nmemb;
    struct ResponseBuffer *mem = (struct ResponseBuffer *)userp;

    char *ptr = realloc(mem->data, mem->size + realsize + 1);
    if (!ptr) {
        fprintf(stderr, "Error: out of memory\n");
        return 0;
    }

    mem->data = ptr;
    memcpy(&(mem->data[mem->size]), contents, realsize);
    mem->size += realsize;
    mem->data[mem->size] = 0;

    return realsize;
}

// Base64 encoding table
static const char base64_table[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";

// Base64 encode
char* base64_encode(const unsigned char *data, size_t input_length, size_t *output_length) {
    *output_length = 4 * ((input_length + 2) / 3);
    char *encoded = malloc(*output_length + 1);
    if (!encoded) return NULL;

    size_t i, j;
    for (i = 0, j = 0; i < input_length;) {
        uint32_t octet_a = i < input_length ? data[i++] : 0;
        uint32_t octet_b = i < input_length ? data[i++] : 0;
        uint32_t octet_c = i < input_length ? data[i++] : 0;
        uint32_t triple = (octet_a << 16) + (octet_b << 8) + octet_c;

        encoded[j++] = base64_table[(triple >> 18) & 0x3F];
        encoded[j++] = base64_table[(triple >> 12) & 0x3F];
        encoded[j++] = base64_table[(triple >> 6) & 0x3F];
        encoded[j++] = base64_table[triple & 0x3F];
    }

    // Add padding
    int mod = input_length % 3;
    if (mod > 0) {
        encoded[*output_length - 1] = '=';
        if (mod == 1) encoded[*output_length - 2] = '=';
    }

    encoded[*output_length] = '\0';
    return encoded;
}

// Base64 decode
unsigned char* base64_decode(const char *data, size_t input_length, size_t *output_length) {
    if (input_length % 4 != 0) return NULL;

    *output_length = input_length / 4 * 3;
    if (data[input_length - 1] == '=') (*output_length)--;
    if (data[input_length - 2] == '=') (*output_length)--;

    unsigned char *decoded = malloc(*output_length + 1);
    if (!decoded) return NULL;

    int decoding_table[256];
    for (int i = 0; i < 256; i++) decoding_table[i] = -1;
    for (int i = 0; i < 64; i++) decoding_table[(unsigned char)base64_table[i]] = i;

    size_t i, j;
    for (i = 0, j = 0; i < input_length;) {
        uint32_t sextet_a = data[i] == '=' ? 0 : decoding_table[(unsigned char)data[i]]; i++;
        uint32_t sextet_b = data[i] == '=' ? 0 : decoding_table[(unsigned char)data[i]]; i++;
        uint32_t sextet_c = data[i] == '=' ? 0 : decoding_table[(unsigned char)data[i]]; i++;
        uint32_t sextet_d = data[i] == '=' ? 0 : decoding_table[(unsigned char)data[i]]; i++;

        uint32_t triple = (sextet_a << 18) + (sextet_b << 12) + (sextet_c << 6) + sextet_d;

        if (j < *output_length) decoded[j++] = (triple >> 16) & 0xFF;
        if (j < *output_length) decoded[j++] = (triple >> 8) & 0xFF;
        if (j < *output_length) decoded[j++] = triple & 0xFF;
    }

    decoded[*output_length] = '\0';
    return decoded;
}

// Detect language from shebang line
const char* detect_language_from_shebang(const char *code) {
    if (code[0] != '#' || code[1] != '!') return NULL;

    if (strstr(code, "/python") || strstr(code, "/python3") || strstr(code, "/python2")) return "python";
    if (strstr(code, "/node") || strstr(code, "/nodejs")) return "javascript";
    if (strstr(code, "/ruby")) return "ruby";
    if (strstr(code, "/perl")) return "perl";
    if (strstr(code, "/php")) return "php";
    if (strstr(code, "/bash") || strstr(code, "/sh")) return "bash";
    if (strstr(code, "/lua")) return "lua";
    if (strstr(code, "/tclsh") || strstr(code, "/wish")) return "tcl";
    if (strstr(code, "/raku") || strstr(code, "/perl6")) return "raku";
    if (strstr(code, "/julia")) return "julia";
    if (strstr(code, "/Rscript")) return "r";
    if (strstr(code, "/groovy")) return "groovy";
    if (strstr(code, "/scala")) return "scala";
    if (strstr(code, "/swift")) return "swift";
    if (strstr(code, "/racket")) return "racket";
    if (strstr(code, "/scheme") || strstr(code, "/guile")) return "scheme";
    if (strstr(code, "/clisp") || strstr(code, "/sbcl")) return "commonlisp";
    if (strstr(code, "/ocaml")) return "ocaml";
    if (strstr(code, "/elixir")) return "elixir";

    return NULL;
}

// Detect language from file extension
const char* detect_language_from_extension(const char *filename) {
    const char *ext = strrchr(filename, '.');
    if (!ext) return NULL;
    ext++;

    if (strcmp(ext, "py") == 0) return "python";
    if (strcmp(ext, "js") == 0) return "javascript";
    if (strcmp(ext, "ts") == 0) return "typescript";
    if (strcmp(ext, "rb") == 0) return "ruby";
    if (strcmp(ext, "php") == 0) return "php";
    if (strcmp(ext, "pl") == 0) return "perl";
    if (strcmp(ext, "sh") == 0) return "bash";
    if (strcmp(ext, "r") == 0 || strcmp(ext, "R") == 0) return "r";
    if (strcmp(ext, "lua") == 0) return "lua";
    if (strcmp(ext, "go") == 0) return "go";
    if (strcmp(ext, "rs") == 0) return "rust";
    if (strcmp(ext, "c") == 0) return "c";
    if (strcmp(ext, "cpp") == 0 || strcmp(ext, "cc") == 0 || strcmp(ext, "cxx") == 0) return "cpp";
    if (strcmp(ext, "java") == 0) return "java";
    if (strcmp(ext, "kt") == 0) return "kotlin";
    if (strcmp(ext, "m") == 0) return "objc";
    if (strcmp(ext, "cs") == 0) return "csharp";
    if (strcmp(ext, "fs") == 0) return "fsharp";
    if (strcmp(ext, "hs") == 0) return "haskell";
    if (strcmp(ext, "ml") == 0) return "ocaml";
    if (strcmp(ext, "clj") == 0) return "clojure";
    if (strcmp(ext, "scm") == 0 || strcmp(ext, "ss") == 0) return "scheme";
    if (strcmp(ext, "erl") == 0) return "erlang";
    if (strcmp(ext, "ex") == 0 || strcmp(ext, "exs") == 0) return "elixir";
    if (strcmp(ext, "jl") == 0) return "julia";
    if (strcmp(ext, "d") == 0) return "d";
    if (strcmp(ext, "nim") == 0) return "nim";
    if (strcmp(ext, "zig") == 0) return "zig";
    if (strcmp(ext, "v") == 0) return "v";
    if (strcmp(ext, "cr") == 0) return "crystal";
    if (strcmp(ext, "dart") == 0) return "dart";
    if (strcmp(ext, "groovy") == 0) return "groovy";
    if (strcmp(ext, "f90") == 0 || strcmp(ext, "f95") == 0) return "fortran";
    if (strcmp(ext, "lisp") == 0 || strcmp(ext, "lsp") == 0) return "commonlisp";
    if (strcmp(ext, "cob") == 0) return "cobol";
    if (strcmp(ext, "tcl") == 0) return "tcl";
    if (strcmp(ext, "raku") == 0) return "raku";
    if (strcmp(ext, "pro") == 0 || strcmp(ext, "p") == 0) return "prolog";
    if (strcmp(ext, "4th") == 0 || strcmp(ext, "forth") == 0 || strcmp(ext, "fth") == 0) return "forth";

    return NULL;
}

// Read file contents
char* read_file(const char *filename, size_t *size) {
    FILE *f = fopen(filename, "rb");
    if (!f) {
        fprintf(stderr, "Error: cannot open file '%s'\n", filename);
        return NULL;
    }

    fseek(f, 0, SEEK_END);
    long fsize = ftell(f);
    fseek(f, 0, SEEK_SET);

    if (fsize > MAX_FILE_SIZE) {
        fprintf(stderr, "Error: file too large (max %d bytes)\n", MAX_FILE_SIZE);
        fclose(f);
        return NULL;
    }

    char *content = malloc(fsize + 1);
    if (!content) {
        fprintf(stderr, "Error: out of memory\n");
        fclose(f);
        return NULL;
    }

    size_t read_size = fread(content, 1, fsize, f);
    content[read_size] = 0;
    *size = read_size;

    fclose(f);
    return content;
}

// Escape JSON string
char* escape_json_string(const char *str) {
    size_t len = strlen(str);
    char *escaped = malloc(len * 6 + 1); // Worst case: \uXXXX for each char
    if (!escaped) return NULL;

    char *out = escaped;
    for (size_t i = 0; i < len; i++) {
        switch (str[i]) {
            case '"':  *out++ = '\\'; *out++ = '"'; break;
            case '\\': *out++ = '\\'; *out++ = '\\'; break;
            case '\b': *out++ = '\\'; *out++ = 'b'; break;
            case '\f': *out++ = '\\'; *out++ = 'f'; break;
            case '\n': *out++ = '\\'; *out++ = 'n'; break;
            case '\r': *out++ = '\\'; *out++ = 'r'; break;
            case '\t': *out++ = '\\'; *out++ = 't'; break;
            default:
                if ((unsigned char)str[i] < 32) {
                    sprintf(out, "\\u%04x", (unsigned char)str[i]);
                    out += 6;
                } else {
                    *out++ = str[i];
                }
                break;
        }
    }
    *out = 0;
    return escaped;
}

// Extract JSON string value (simple parser)
char* extract_json_string(const char *json, const char *key) {
    char search[256];
    snprintf(search, sizeof(search), "\"%s\":\"", key);
    const char *start = strstr(json, search);
    if (!start) return NULL;

    start += strlen(search);
    const char *end = start;

    while (*end && !(*end == '"' && *(end - 1) != '\\')) {
        end++;
    }

    size_t len = end - start;
    char *result = malloc(len + 1);
    if (!result) return NULL;

    // Unescape while copying
    char *out = result;
    for (const char *p = start; p < end; p++) {
        if (*p == '\\' && p + 1 < end) {
            p++;
            switch (*p) {
                case 'n': *out++ = '\n'; break;
                case 't': *out++ = '\t'; break;
                case 'r': *out++ = '\r'; break;
                case '\\': *out++ = '\\'; break;
                case '"': *out++ = '"'; break;
                default: *out++ = *p; break;
            }
        } else {
            *out++ = *p;
        }
    }
    *out = 0;
    return result;
}

// Extract JSON number value (returns -1 if not found or null)
long long extract_json_number(const char *json, const char *key) {
    char search[256];
    snprintf(search, sizeof(search), "\"%s\":", key);
    const char *start = strstr(json, search);
    if (!start) return -1;

    start += strlen(search);
    // Skip whitespace
    while (*start == ' ' || *start == '\t') start++;

    // Check for null
    if (strncmp(start, "null", 4) == 0) return -1;

    return atoll(start);
}

// ============================================================================
// JSON Array Helpers (for Library API)
// ============================================================================

// Count objects in a JSON array (for malloc sizing)
// Returns number of top-level objects in array, or 0 if not found/empty
__attribute__((unused))
static int count_json_array_objects(const char *json, const char *array_key) {
    char search[256];
    snprintf(search, sizeof(search), "\"%s\":[", array_key);
    const char *start = strstr(json, search);
    if (!start) return 0;

    start += strlen(search);
    int count = 0, depth = 0;
    for (const char *p = start; *p && !(*p == ']' && depth == 0); p++) {
        if (*p == '{') {
            if (depth == 0) count++;
            depth++;
        } else if (*p == '}') {
            depth--;
        } else if (*p == '"') {
            // Skip string contents (may contain { } characters)
            p++;
            while (*p && !(*p == '"' && *(p-1) != '\\')) p++;
        }
    }
    return count;
}

// Skip past current JSON object, return pointer to position after closing }
// If pos doesn't point to '{', returns pos unchanged
__attribute__((unused))
static const char* skip_json_object(const char *pos) {
    if (!pos || *pos != '{') return pos;
    int depth = 1;
    pos++;
    while (*pos && depth > 0) {
        if (*pos == '{') {
            depth++;
        } else if (*pos == '}') {
            depth--;
        } else if (*pos == '"') {
            // Skip string contents
            pos++;
            while (*pos && !(*pos == '"' && *(pos-1) != '\\')) pos++;
        }
        pos++;
    }
    return pos;
}

// ============================================================================
// Thread-Local Error Storage (for Library API)
// ============================================================================

static __thread char unsandbox_error_buffer[512] = {0};

__attribute__((unused))
static void set_last_error(const char *fmt, ...) {
    va_list args;
    va_start(args, fmt);
    vsnprintf(unsandbox_error_buffer, sizeof(unsandbox_error_buffer), fmt, args);
    va_end(args);
}

// Format bytes to human readable (e.g., 1234567 -> "1.2M")
void format_bytes(long long bytes, char *buf, size_t bufsize) {
    if (bytes < 0) {
        snprintf(buf, bufsize, "-");
    } else if (bytes < 1024) {
        snprintf(buf, bufsize, "%lldB", bytes);
    } else if (bytes < 1024 * 1024) {
        snprintf(buf, bufsize, "%.1fK", bytes / 1024.0);
    } else if (bytes < 1024LL * 1024 * 1024) {
        snprintf(buf, bufsize, "%.1fM", bytes / (1024.0 * 1024));
    } else {
        snprintf(buf, bufsize, "%.1fG", bytes / (1024.0 * 1024 * 1024));
    }
}

// Get basename without extension
char* get_basename_no_ext(const char *path) {
    const char *base = strrchr(path, '/');
    base = base ? base + 1 : path;

    char *result = strdup(base);
    char *dot = strrchr(result, '.');
    if (dot) *dot = '\0';
    return result;
}

// Parse and handle response
void parse_and_print_response(const char *json_response, int save_artifacts, const char *artifact_dir, const char *source_file) {
    // Print stdout in blue
    char *out = extract_json_string(json_response, "stdout");
    if (out && strlen(out) > 0) {
        printf("\033[34m%s\033[0m", out);
        free(out);
    }

    // Print stderr in red
    char *err = extract_json_string(json_response, "stderr");
    if (err && strlen(err) > 0) {
        fprintf(stderr, "\033[31m%s\033[0m", err);
        free(err);
    }

    // Print API error in bold red
    char *error = extract_json_string(json_response, "error");
    if (error && strlen(error) > 0) {
        fprintf(stderr, "\033[1;31mError: %s\033[0m\n", error);
        free(error);
    }

    // Handle artifacts - API returns "artifacts":[{"filename":"...", "content_base64":"..."}]
    if (save_artifacts) {
        const char *artifacts_start = strstr(json_response, "\"artifacts\":[");
        if (artifacts_start) {
            const char *pos = artifacts_start + 13; // Skip "artifacts":[

            // Process each artifact in array
            while ((pos = strchr(pos, '{')) != NULL) {
                char *artifact_data = extract_json_string(pos, "content_base64");
                char *artifact_filename = extract_json_string(pos, "filename");

                if (artifact_data) {
                    size_t decoded_len;
                    unsigned char *decoded = base64_decode(artifact_data, strlen(artifact_data), &decoded_len);

                    if (decoded) {
                        char output_path[512];
                        const char *fname = artifact_filename ? artifact_filename : "a.out";

                        // For executables, use source filename instead of API filename
                        char *source_basename = NULL;
                        int is_elf = decoded_len >= 4 && decoded[0] == 0x7F &&
                                     decoded[1] == 'E' && decoded[2] == 'L' && decoded[3] == 'F';
                        int is_pe = decoded_len >= 2 && decoded[0] == 'M' && decoded[1] == 'Z';
                        int is_macho = decoded_len >= 4 &&
                                       ((decoded[0] == 0xCF && decoded[1] == 0xFA) ||
                                        (decoded[0] == 0xFE && decoded[1] == 0xED));

                        if ((is_elf || is_pe || is_macho) && source_file) {
                            source_basename = get_basename_no_ext(source_file);
                            fname = source_basename;
                        }

                        if (artifact_dir) {
                            snprintf(output_path, sizeof(output_path), "%s/%s", artifact_dir, fname);
                        } else {
                            snprintf(output_path, sizeof(output_path), "%s", fname);
                        }

                        FILE *f = fopen(output_path, "wb");
                        if (f) {
                            fwrite(decoded, 1, decoded_len, f);
                            fclose(f);

                            // Check magic bytes to determine if executable
                            int is_executable = 0;
                            int is_data = 0;
                            if (decoded_len >= 4) {
                                // Executable formats
                                if (decoded[0] == 0x7F && decoded[1] == 'E' &&
                                    decoded[2] == 'L' && decoded[3] == 'F') {
                                    is_executable = 1;  // ELF
                                } else if (decoded[0] == 'M' && decoded[1] == 'Z') {
                                    is_executable = 1;  // PE/Windows
                                } else if ((decoded[0] == 0xCF && decoded[1] == 0xFA) ||
                                           (decoded[0] == 0xFE && decoded[1] == 0xED)) {
                                    is_executable = 1;  // Mach-O
                                } else if (decoded[0] == '#' && decoded[1] == '!') {
                                    is_executable = 1;  // Shebang script
                                }
                                // Data formats - don't make executable
                                else if (decoded[0] == 0x89 && decoded[1] == 'P' &&
                                         decoded[2] == 'N' && decoded[3] == 'G') {
                                    is_data = 1;  // PNG
                                } else if (decoded[0] == 0xFF && decoded[1] == 0xD8) {
                                    is_data = 1;  // JPEG
                                } else if (decoded[0] == 'G' && decoded[1] == 'I' &&
                                           decoded[2] == 'F' && decoded[3] == '8') {
                                    is_data = 1;  // GIF
                                } else if (decoded[0] == '%' && decoded[1] == 'P' &&
                                           decoded[2] == 'D' && decoded[3] == 'F') {
                                    is_data = 1;  // PDF
                                } else if (decoded[0] == 'P' && decoded[1] == 'K' &&
                                           decoded[2] == 0x03 && decoded[3] == 0x04) {
                                    is_data = 1;  // ZIP
                                } else if (decoded[0] == 0x1F && decoded[1] == 0x8B) {
                                    is_data = 1;  // GZIP
                                } else if (decoded[0] == '{' || decoded[0] == '[') {
                                    is_data = 1;  // JSON
                                } else if (decoded[0] == '<') {
                                    is_data = 1;  // XML/HTML
                                }
                            }

                            // Fall back to extension if magic bytes inconclusive
                            if (!is_executable && !is_data) {
                                const char *ext = strrchr(fname, '.');
                                if (ext) {
                                    is_data = (
                                        // Images
                                        strcmp(ext, ".png") == 0 || strcmp(ext, ".jpg") == 0 ||
                                        strcmp(ext, ".jpeg") == 0 || strcmp(ext, ".gif") == 0 ||
                                        strcmp(ext, ".svg") == 0 || strcmp(ext, ".webp") == 0 ||
                                        strcmp(ext, ".bmp") == 0 || strcmp(ext, ".ico") == 0 ||
                                        strcmp(ext, ".tiff") == 0 || strcmp(ext, ".tif") == 0 ||
                                        strcmp(ext, ".psd") == 0 || strcmp(ext, ".ai") == 0 ||
                                        strcmp(ext, ".eps") == 0 || strcmp(ext, ".raw") == 0 ||
                                        // Documents
                                        strcmp(ext, ".pdf") == 0 || strcmp(ext, ".doc") == 0 ||
                                        strcmp(ext, ".docx") == 0 || strcmp(ext, ".xls") == 0 ||
                                        strcmp(ext, ".xlsx") == 0 || strcmp(ext, ".ppt") == 0 ||
                                        strcmp(ext, ".pptx") == 0 || strcmp(ext, ".odt") == 0 ||
                                        strcmp(ext, ".ods") == 0 || strcmp(ext, ".odp") == 0 ||
                                        strcmp(ext, ".rtf") == 0 || strcmp(ext, ".tex") == 0 ||
                                        // Text/Config
                                        strcmp(ext, ".txt") == 0 || strcmp(ext, ".md") == 0 ||
                                        strcmp(ext, ".rst") == 0 || strcmp(ext, ".log") == 0 ||
                                        strcmp(ext, ".json") == 0 || strcmp(ext, ".xml") == 0 ||
                                        strcmp(ext, ".yaml") == 0 || strcmp(ext, ".yml") == 0 ||
                                        strcmp(ext, ".toml") == 0 || strcmp(ext, ".ini") == 0 ||
                                        strcmp(ext, ".conf") == 0 || strcmp(ext, ".cfg") == 0 ||
                                        strcmp(ext, ".config") == 0 || strcmp(ext, ".env") == 0 ||
                                        strcmp(ext, ".properties") == 0 || strcmp(ext, ".plist") == 0 ||
                                        // Data
                                        strcmp(ext, ".csv") == 0 || strcmp(ext, ".tsv") == 0 ||
                                        strcmp(ext, ".sql") == 0 || strcmp(ext, ".db") == 0 ||
                                        strcmp(ext, ".sqlite") == 0 || strcmp(ext, ".parquet") == 0 ||
                                        strcmp(ext, ".avro") == 0 || strcmp(ext, ".npy") == 0 ||
                                        strcmp(ext, ".npz") == 0 || strcmp(ext, ".pkl") == 0 ||
                                        strcmp(ext, ".pickle") == 0 || strcmp(ext, ".h5") == 0 ||
                                        strcmp(ext, ".hdf5") == 0 ||
                                        // Web
                                        strcmp(ext, ".html") == 0 || strcmp(ext, ".htm") == 0 ||
                                        strcmp(ext, ".css") == 0 || strcmp(ext, ".scss") == 0 ||
                                        strcmp(ext, ".sass") == 0 || strcmp(ext, ".less") == 0 ||
                                        strcmp(ext, ".woff") == 0 || strcmp(ext, ".woff2") == 0 ||
                                        strcmp(ext, ".ttf") == 0 || strcmp(ext, ".otf") == 0 ||
                                        strcmp(ext, ".eot") == 0 ||
                                        // Archives
                                        strcmp(ext, ".zip") == 0 || strcmp(ext, ".tar") == 0 ||
                                        strcmp(ext, ".gz") == 0 || strcmp(ext, ".tgz") == 0 ||
                                        strcmp(ext, ".bz2") == 0 || strcmp(ext, ".xz") == 0 ||
                                        strcmp(ext, ".7z") == 0 || strcmp(ext, ".rar") == 0 ||
                                        strcmp(ext, ".zst") == 0 ||
                                        // Audio
                                        strcmp(ext, ".mp3") == 0 || strcmp(ext, ".wav") == 0 ||
                                        strcmp(ext, ".flac") == 0 || strcmp(ext, ".aac") == 0 ||
                                        strcmp(ext, ".ogg") == 0 || strcmp(ext, ".m4a") == 0 ||
                                        strcmp(ext, ".wma") == 0 || strcmp(ext, ".aiff") == 0 ||
                                        // Video
                                        strcmp(ext, ".mp4") == 0 || strcmp(ext, ".mkv") == 0 ||
                                        strcmp(ext, ".avi") == 0 || strcmp(ext, ".mov") == 0 ||
                                        strcmp(ext, ".wmv") == 0 || strcmp(ext, ".flv") == 0 ||
                                        strcmp(ext, ".webm") == 0 || strcmp(ext, ".m4v") == 0 ||
                                        // 3D/CAD
                                        strcmp(ext, ".obj") == 0 || strcmp(ext, ".stl") == 0 ||
                                        strcmp(ext, ".fbx") == 0 || strcmp(ext, ".gltf") == 0 ||
                                        strcmp(ext, ".glb") == 0 ||
                                        // Misc
                                        strcmp(ext, ".lock") == 0 || strcmp(ext, ".sum") == 0 ||
                                        strcmp(ext, ".map") == 0 || strcmp(ext, ".wasm") == 0
                                    );
                                }
                            }

                            if (is_executable || !is_data) {
                                chmod(output_path, 0755);
                            }
                            fprintf(stderr, "\033[32mArtifact saved: %s (%zu bytes)\033[0m\n", output_path, decoded_len);
                        }
                        if (source_basename) free(source_basename);
                        free(decoded);
                    }
                    free(artifact_data);
                }
                if (artifact_filename) free(artifact_filename);

                // Move to next object
                pos++;
                const char *next_obj = strchr(pos, '{');
                const char *end_arr = strchr(pos, ']');
                if (!next_obj || (end_arr && end_arr < next_obj)) break;
                pos = next_obj;
            }
        }
    }
}

// Get basename from path
const char* get_basename(const char *path) {
    const char *base = strrchr(path, '/');
    return base ? base + 1 : path;
}

// Poll job status with exponential backoff and transient error resilience
// Returns the final response JSON (caller must free), or NULL on error
static char* poll_job_status(const UnsandboxCredentials *creds, const char *job_id) {
    CURL *curl = curl_easy_init();
    if (!curl) return NULL;

    char url[512];
    char path[256];
    snprintf(path, sizeof(path), "/jobs/%s", job_id);
    snprintf(url, sizeof(url), "%s%s", API_BASE, path);

    int poll_count = 0;
    int consecutive_errors = 0;
    int saw_server_errors = 0;
    char *final_response = NULL;

    while (1) {
        // Sleep before polling — use backoff schedule for normal polls,
        // fixed backoff during error recovery
        if (consecutive_errors > 0) {
            usleep(POLL_ERROR_BACKOFF_MS * 1000);
        } else {
            int delay_idx = poll_count < POLL_DELAYS_COUNT ? poll_count : POLL_DELAYS_COUNT - 1;
            usleep(POLL_DELAYS[delay_idx] * 1000);
        }
        poll_count++;

        struct ResponseBuffer response = {0};
        response.data = malloc(1);
        response.size = 0;

        // Regenerate auth headers each poll to keep timestamp fresh
        struct curl_slist *headers = NULL;
        headers = add_hmac_auth_headers(headers, creds, "GET", path, NULL);

        curl_easy_setopt(curl, CURLOPT_URL, url);
        curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
        curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L);
        curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
        curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);

        CURLcode res = curl_easy_perform(curl);
        curl_slist_free_all(headers);

        if (res != CURLE_OK) {
            consecutive_errors++;
            if (consecutive_errors >= POLL_MAX_CONSECUTIVE_ERRORS) {
                fprintf(stderr, "Error: Lost connection to API after %d retries\n", consecutive_errors);
                fprintf(stderr, "Job ID: %s — check later with: un jobs --get %s\n", job_id, job_id);
                free(response.data);
                break;
            }
            fprintf(stderr, "Connection error, retrying... (%d/%d)\n", consecutive_errors, POLL_MAX_CONSECUTIVE_ERRORS);
            free(response.data);
            continue;
        }

        long http_code = 0;
        curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);

        if (http_code == 404) {
            consecutive_errors++;
            if (saw_server_errors) {
                // API restarted (502/503 then 404) — job state lost
                fprintf(stderr, "Error: API restarted — job result lost (in-memory job state cleared)\n");
                fprintf(stderr, "The command may have completed on the container.\n");
                fprintf(stderr, "Job ID: %s\n", job_id);
                free(response.data);
                break;
            }
            if (consecutive_errors > 5) {
                fprintf(stderr, "Error: job %s not found\n", job_id);
                free(response.data);
                break;
            }
            // Job might not be registered yet (brief race window)
            free(response.data);
            continue;
        }

        if (http_code >= 500) {
            consecutive_errors++;
            saw_server_errors = 1;
            if (consecutive_errors >= POLL_MAX_CONSECUTIVE_ERRORS) {
                fprintf(stderr, "Error: Server errors after %d retries\n", consecutive_errors);
                fprintf(stderr, "Job ID: %s — check later with: un jobs --get %s\n", job_id, job_id);
                free(response.data);
                break;
            }
            fprintf(stderr, "Server error %ld, retrying... (%d/%d)\n", http_code, consecutive_errors, POLL_MAX_CONSECUTIVE_ERRORS);
            free(response.data);
            continue;
        }

        if (http_code != 200) {
            // 4xx (non-404) — not transient, bail immediately
            fprintf(stderr, "Error: HTTP %ld while polling job\n", http_code);
            free(response.data);
            break;
        }

        // Success — reset error counter
        consecutive_errors = 0;

        // Check status field
        char *status = extract_json_string(response.data, "status");
        if (!status) {
            // No status field - might be final result format
            final_response = response.data;
            break;
        }

        int is_terminal = (strcmp(status, "completed") == 0 ||
                          strcmp(status, "failed") == 0 ||
                          strcmp(status, "timeout") == 0 ||
                          strcmp(status, "cancelled") == 0);
        free(status);

        if (is_terminal) {
            final_response = response.data;
            break;
        }

        // Still running - continue polling
        free(response.data);
    }

    curl_easy_cleanup(curl);
    return final_response;
}

// ============================================================================
// Interactive Shell Support
// ============================================================================

static struct termios orig_termios;
static int shell_running = 0;
static struct lws *shell_wsi = NULL;

// Terminal size
static void get_terminal_size(int *cols, int *rows) {
    struct winsize ws;
    if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == 0) {
        *cols = ws.ws_col;
        *rows = ws.ws_row;
    } else {
        *cols = 80;
        *rows = 24;
    }
}

// Raw terminal mode
static void enable_raw_mode(void) {
    tcgetattr(STDIN_FILENO, &orig_termios);
    struct termios raw = orig_termios;
    raw.c_lflag &= ~(ECHO | ICANON | ISIG | IEXTEN);
    raw.c_iflag &= ~(IXON | ICRNL | BRKINT | INPCK | ISTRIP);
    raw.c_oflag &= ~(OPOST);
    raw.c_cflag |= (CS8);
    raw.c_cc[VMIN] = 0;
    raw.c_cc[VTIME] = 1;
    tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw);
}

static void disable_raw_mode(void) {
    tcsetattr(STDIN_FILENO, TCSAFLUSH, &orig_termios);
}

// Signal handler for terminal resize
static void handle_sigwinch(int sig) {
    (void)sig;
    // Flag set - resize sent in main loop
}

// Signal handler for interrupt
static void handle_sigint(int sig) {
    (void)sig;
    shell_running = 0;
}

// WebSocket shell state
struct shell_state {
    char *session_id;
    int connected;
    unsigned char *send_buf;
    size_t send_len;
    int need_resize;
    int detached;  // 1 if session was detached (can reconnect), 0 if ended
    int need_initial_enter;  // 1 to send Enter after connect (tmux repaint fix)
};

// WebSocket callback for shell
static int shell_ws_callback(struct lws *wsi, enum lws_callback_reasons reason,
                              void *user, void *in, size_t len) {
    struct shell_state *state = (struct shell_state *)user;

    switch (reason) {
    case LWS_CALLBACK_CLIENT_ESTABLISHED:
        state->connected = 1;
        state->need_resize = 1;  // Send initial resize
        lws_callback_on_writable(wsi);
        break;

    case LWS_CALLBACK_CLIENT_RECEIVE:
        if (in && len > 0) {
            // Check if it's a binary frame (raw stdout) or text frame (JSON control)
            if (lws_frame_is_binary(wsi)) {
                // Binary frame = raw shell output, write directly
                if (write(STDOUT_FILENO, in, len) < 0) { /* ignore */ }
            } else {
                // Text frame = JSON control message (exit, error, detached, etc.)
                char *json = (char *)in;
                if (strstr(json, "\"type\":\"exit\"")) {
                    shell_running = 0;
                    state->detached = 0;  // Session ended
                } else if (strstr(json, "\"type\":\"detached\"")) {
                    shell_running = 0;
                    state->detached = 1;  // Detached, can reconnect
                }
            }
        }
        break;

    case LWS_CALLBACK_CLIENT_WRITEABLE:
        if (!state->connected) break;

        // Send resize if needed
        if (state->need_resize) {
            int cols, rows;
            get_terminal_size(&cols, &rows);
            char msg[128];
            int mlen = snprintf(msg, sizeof(msg),
                "{\"type\":\"resize\",\"cols\":%d,\"rows\":%d}", cols, rows);
            unsigned char buf[LWS_PRE + 128];
            memcpy(&buf[LWS_PRE], msg, mlen);
            lws_write(wsi, &buf[LWS_PRE], mlen, LWS_WRITE_TEXT);
            state->need_resize = 0;
            lws_callback_on_writable(wsi);
            break;
        }

        // Send initial Enter to force tmux repaint (fixes blank screen on connect)
        if (state->need_initial_enter) {
            unsigned char buf[LWS_PRE + 1];
            buf[LWS_PRE] = '\n';
            lws_write(wsi, &buf[LWS_PRE], 1, LWS_WRITE_BINARY);
            state->need_initial_enter = 0;
            break;
        }

        // Send stdin data as binary frame (fast path, no JSON encoding)
        if (state->send_buf && state->send_len > 0) {
            unsigned char buf[LWS_PRE + 256];
            memcpy(&buf[LWS_PRE], state->send_buf, state->send_len);
            lws_write(wsi, &buf[LWS_PRE], state->send_len, LWS_WRITE_BINARY);

            free(state->send_buf);
            state->send_buf = NULL;
            state->send_len = 0;
        }
        break;

    case LWS_CALLBACK_CLIENT_CONNECTION_ERROR:
        fprintf(stderr, "\r\nConnection error: %s\r\n", in ? (char *)in : "unknown");
        shell_running = 0;
        break;

    case LWS_CALLBACK_CLIENT_CLOSED:
        shell_running = 0;
        break;

    default:
        break;
    }
    return 0;
}

static const struct lws_protocols shell_protocols[] = {
    {"unsandbox-shell", shell_ws_callback, sizeof(struct shell_state), 4096, 0, NULL, 0},
    {NULL, NULL, 0, 0, 0, NULL, 0}
};

// Session info returned from create_session
struct SessionInfo {
    char *session_id;
    char *container_name;
};

// Find session ID by container name (for reconnect by container name)
static char* find_session_by_container(const UnsandboxCredentials *creds, const char *container_name) {
    CURL *curl = curl_easy_init();
    if (!curl) return NULL;

    struct ResponseBuffer response = {0};
    response.data = malloc(1);
    response.size = 0;

    char url[256];
    snprintf(url, sizeof(url), "%s/sessions", API_BASE);

    struct curl_slist *headers = NULL;
    headers = add_hmac_auth_headers(headers, creds, "GET", "/sessions", NULL);

    curl_easy_setopt(curl, CURLOPT_URL, url);
    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);

    CURLcode res = curl_easy_perform(curl);
    curl_slist_free_all(headers);
    curl_easy_cleanup(curl);

    if (res != CURLE_OK || !response.data) {
        free(response.data);
        return NULL;
    }

    // Search for matching container name in sessions array
    const char *sessions_start = strstr(response.data, "\"sessions\":[");
    if (sessions_start) {
        const char *pos = sessions_start + 12;

        while ((pos = strchr(pos, '{')) != NULL) {
            char *container = extract_json_string(pos, "container_name");
            char *session_id = extract_json_string(pos, "id");

            if (container && strcmp(container, container_name) == 0) {
                free(container);
                free(response.data);
                return session_id;  // Found it!
            }

            if (container) free(container);
            if (session_id) free(session_id);

            pos++;
            const char *next_obj = strchr(pos, '{');
            const char *end_arr = strchr(pos, ']');
            if (!next_obj || (end_arr && end_arr < next_obj)) break;
            pos = next_obj;
        }
    }

    free(response.data);
    return NULL;
}

// Forward declaration for sudo challenge handler (defined after destroy_service)
static int handle_sudo_challenge(const char *response_data,
                                  const UnsandboxCredentials *creds,
                                  const char *method, const char *url,
                                  const char *path, const char *body);

// Kill a session by ID or container name
static int kill_session(const UnsandboxCredentials *creds, const char *session_id_or_container) {
    char *session_id = NULL;

    // Check if it looks like a container name or session ID
    // Container names: unsb-vm-*, exec-*, sandbox-* (legacy)
    if (strncmp(session_id_or_container, "unsb-vm-", 8) == 0 ||
        strncmp(session_id_or_container, "exec-", 5) == 0 ||
        strncmp(session_id_or_container, "sandbox-", 8) == 0) {
        // It's a container name - look up the session ID
        fprintf(stderr, "Looking up session for %s...", session_id_or_container);
        fflush(stderr);
        session_id = find_session_by_container(creds, session_id_or_container);
        if (!session_id) {
            fprintf(stderr, " not found\nError: No active session for container '%s'\n", session_id_or_container);
            return 1;
        }
        fprintf(stderr, " found\n");
    } else {
        session_id = strdup(session_id_or_container);
    }

    fprintf(stderr, "Terminating session %s...", session_id);
    fflush(stderr);

    CURL *curl = curl_easy_init();
    if (!curl) {
        free(session_id);
        return 1;
    }

    struct ResponseBuffer response = {0};
    response.data = malloc(1);
    response.size = 0;

    char url[512];
    char path[256];
    snprintf(url, sizeof(url), "%s/sessions/%s", API_BASE, session_id);
    snprintf(path, sizeof(path), "/sessions/%s", session_id);

    struct curl_slist *headers = NULL;
    headers = add_hmac_auth_headers(headers, creds, "DELETE", path, NULL);

    curl_easy_setopt(curl, CURLOPT_URL, url);
    curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "DELETE");
    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);

    CURLcode res = curl_easy_perform(curl);

    long http_code = 0;
    curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);

    curl_slist_free_all(headers);
    curl_easy_cleanup(curl);

    if (res != CURLE_OK) {
        fprintf(stderr, " failed\nError: %s\n", curl_easy_strerror(res));
        free(response.data);
        free(session_id);
        return 1;
    }

    if (http_code == 200) {
        fprintf(stderr, " done\n");
        fprintf(stderr, "\033[32mSession terminated successfully\033[0m\n");
    } else if (http_code == 404) {
        fprintf(stderr, " not found\nError: Session not found or already terminated\n");
        free(response.data);
        free(session_id);
        return 1;
    } else if (http_code == 428) {
        fprintf(stderr, "\n");
        int result = handle_sudo_challenge(response.data, creds, "DELETE", url, path, NULL);
        free(response.data);
        free(session_id);
        return result;
    } else {
        fprintf(stderr, " failed\nError: HTTP %ld\n", http_code);
        if (response.data) fprintf(stderr, "%s\n", response.data);
        free(response.data);
        free(session_id);
        return 1;
    }

    free(response.data);
    free(session_id);
    return 0;
}

// Freeze a session
static int freeze_session(const UnsandboxCredentials *creds, const char *session_id) {
    CURL *curl = curl_easy_init();
    if (!curl) return 1;

    struct ResponseBuffer response = {0};
    response.data = malloc(1);
    response.size = 0;

    char url[512];
    char path[256];
    snprintf(url, sizeof(url), "%s/sessions/%s/freeze", API_BASE, session_id);
    snprintf(path, sizeof(path), "/sessions/%s/freeze", session_id);

    struct curl_slist *headers = NULL;
    headers = curl_slist_append(headers, "Content-Type: application/json");
    headers = add_hmac_auth_headers(headers, creds, "POST", path, "{}");

    curl_easy_setopt(curl, CURLOPT_URL, url);
    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
    curl_easy_setopt(curl, CURLOPT_POSTFIELDS, "{}");
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);

    CURLcode res = curl_easy_perform(curl);

    long http_code = 0;
    curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);

    curl_slist_free_all(headers);
    curl_easy_cleanup(curl);

    if (res != CURLE_OK) {
        fprintf(stderr, "Error: %s\n", curl_easy_strerror(res));
        free(response.data);
        return 1;
    }

    if (http_code == 404) {
        fprintf(stderr, "Error: Session not found\n");
        free(response.data);
        return 1;
    }

    if (http_code == 400) {
        // Parse error message from response
        fprintf(stderr, "Error: %s\n", response.data);
        free(response.data);
        return 1;
    }

    if (http_code != 200) {
        fprintf(stderr, "Error: HTTP %ld\n", http_code);
        if (response.data) fprintf(stderr, "%s\n", response.data);
        free(response.data);
        return 1;
    }

    fprintf(stderr, "\033[32mSession frozen successfully\033[0m\n");
    free(response.data);
    return 0;
}

// Unfreeze a session
static int unfreeze_session(const UnsandboxCredentials *creds, const char *session_id) {
    CURL *curl = curl_easy_init();
    if (!curl) return 1;

    struct ResponseBuffer response = {0};
    response.data = malloc(1);
    response.size = 0;

    char url[512];
    char path[256];
    snprintf(url, sizeof(url), "%s/sessions/%s/unfreeze", API_BASE, session_id);
    snprintf(path, sizeof(path), "/sessions/%s/unfreeze", session_id);

    struct curl_slist *headers = NULL;
    headers = curl_slist_append(headers, "Content-Type: application/json");
    headers = add_hmac_auth_headers(headers, creds, "POST", path, "{}");

    curl_easy_setopt(curl, CURLOPT_URL, url);
    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
    curl_easy_setopt(curl, CURLOPT_POSTFIELDS, "{}");
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);

    CURLcode res = curl_easy_perform(curl);

    long http_code = 0;
    curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);

    curl_slist_free_all(headers);
    curl_easy_cleanup(curl);

    if (res != CURLE_OK) {
        fprintf(stderr, "Error: %s\n", curl_easy_strerror(res));
        free(response.data);
        return 1;
    }

    if (http_code == 404) {
        fprintf(stderr, "Error: Session not found\n");
        free(response.data);
        return 1;
    }

    if (http_code == 429) {
        // Concurrency limit reached
        fprintf(stderr, "Error: Concurrency limit reached - cannot unfreeze session\n");
        free(response.data);
        return 1;
    }

    if (http_code != 200) {
        fprintf(stderr, "Error: HTTP %ld\n", http_code);
        if (response.data) fprintf(stderr, "%s\n", response.data);
        free(response.data);
        return 1;
    }

    printf("\033[32mSession woken successfully\033[0m\n");
    free(response.data);
    return 0;
}

// Boost a session's resources (increase vCPU, memory is derived: vcpu * 2048MB)
static int boost_session(const UnsandboxCredentials *creds, const char *session_id, int vcpu) {
    CURL *curl = curl_easy_init();
    if (!curl) return 1;

    struct ResponseBuffer response = {0};
    response.data = malloc(1);
    response.size = 0;

    char url[512];
    char path[256];
    snprintf(url, sizeof(url), "%s/sessions/%s/boost", API_BASE, session_id);
    snprintf(path, sizeof(path), "/sessions/%s/boost", session_id);

    char post_data[256];
    snprintf(post_data, sizeof(post_data), "{\"vcpu\":%d}", vcpu);

    struct curl_slist *headers = NULL;
    headers = curl_slist_append(headers, "Content-Type: application/json");
    headers = add_hmac_auth_headers(headers, creds, "POST", path, post_data);

    curl_easy_setopt(curl, CURLOPT_URL, url);
    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
    curl_easy_setopt(curl, CURLOPT_POSTFIELDS, post_data);
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);

    CURLcode res = curl_easy_perform(curl);

    long http_code = 0;
    curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);

    curl_slist_free_all(headers);
    curl_easy_cleanup(curl);

    if (res != CURLE_OK) {
        fprintf(stderr, "Error: %s\n", curl_easy_strerror(res));
        free(response.data);
        return 1;
    }

    if (http_code == 404) {
        fprintf(stderr, "Error: Session not found\n");
        free(response.data);
        return 1;
    }

    if (http_code == 429) {
        fprintf(stderr, "Error: Not enough concurrency slots to boost (boost consumes additional slots)\n");
        free(response.data);
        return 1;
    }

    if (http_code != 200) {
        fprintf(stderr, "Error: HTTP %ld\n", http_code);
        if (response.data) fprintf(stderr, "%s\n", response.data);
        free(response.data);
        return 1;
    }

    int memory_mb = vcpu * 2048;
    printf("\033[32mSession boosted to %d vCPU, %d MB RAM\033[0m\n", vcpu, memory_mb);
    free(response.data);
    return 0;
}

// Remove boost from a session (return to base resources)
static int unboost_session(const UnsandboxCredentials *creds, const char *session_id) {
    CURL *curl = curl_easy_init();
    if (!curl) return 1;

    struct ResponseBuffer response = {0};
    response.data = malloc(1);
    response.size = 0;

    char url[512];
    char path[256];
    snprintf(url, sizeof(url), "%s/sessions/%s/unboost", API_BASE, session_id);
    snprintf(path, sizeof(path), "/sessions/%s/unboost", session_id);

    struct curl_slist *headers = NULL;
    headers = curl_slist_append(headers, "Content-Type: application/json");
    headers = add_hmac_auth_headers(headers, creds, "POST", path, "{}");

    curl_easy_setopt(curl, CURLOPT_URL, url);
    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
    curl_easy_setopt(curl, CURLOPT_POSTFIELDS, "{}");
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);

    CURLcode res = curl_easy_perform(curl);

    long http_code = 0;
    curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);

    curl_slist_free_all(headers);
    curl_easy_cleanup(curl);

    if (res != CURLE_OK) {
        fprintf(stderr, "Error: %s\n", curl_easy_strerror(res));
        free(response.data);
        return 1;
    }

    if (http_code == 404) {
        fprintf(stderr, "Error: Session not found\n");
        free(response.data);
        return 1;
    }

    if (http_code != 200) {
        fprintf(stderr, "Error: HTTP %ld\n", http_code);
        if (response.data) fprintf(stderr, "%s\n", response.data);
        free(response.data);
        return 1;
    }

    printf("\033[32mSession unboosted, returning to base resources\033[0m\n");
    free(response.data);
    return 0;
}

// List active sessions
static int list_sessions(const UnsandboxCredentials *creds) {
    CURL *curl = curl_easy_init();
    if (!curl) return 1;

    struct ResponseBuffer response = {0};
    response.data = malloc(1);
    response.size = 0;

    char url[256];
    snprintf(url, sizeof(url), "%s/sessions", API_BASE);

    struct curl_slist *headers = NULL;
    headers = add_hmac_auth_headers(headers, creds, "GET", "/sessions", NULL);

    curl_easy_setopt(curl, CURLOPT_URL, url);
    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);

    CURLcode res = curl_easy_perform(curl);

    long http_code = 0;
    curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);

    curl_slist_free_all(headers);
    curl_easy_cleanup(curl);

    if (res != CURLE_OK) {
        fprintf(stderr, "Error: %s\n", curl_easy_strerror(res));
        free(response.data);
        return 1;
    }

    if (http_code != 200) {
        fprintf(stderr, "Error: HTTP %ld\n", http_code);
        if (response.data) fprintf(stderr, "%s\n", response.data);
        free(response.data);
        return 1;
    }

    // Parse and display sessions
    // Format: {"sessions":[...], "count": N}
    if (!response.data || !strstr(response.data, "\"sessions\"")) {
        fprintf(stderr, "Error: Invalid response\n");
        free(response.data);
        return 1;
    }

    // Extract count
    const char *count_str = strstr(response.data, "\"count\":");
    int count = 0;
    if (count_str) {
        count = atoi(count_str + 8);
    }

    if (count == 0) {
        printf("No active sessions\n");
        free(response.data);
        return 0;
    }

    printf("Active sessions: %d\n\n", count);
    printf("%-40s %-20s %-10s %-8s %-10s\n", "SESSION ID", "CONTAINER", "SHELL", "TTL", "STATUS");
    printf("%-40s %-20s %-10s %-8s %-10s\n", "----------------------------------------",
           "--------------------", "----------", "--------", "----------");

    // Parse sessions array - simple parser for [{...}, {...}]
    const char *sessions_start = strstr(response.data, "\"sessions\":[");
    if (sessions_start) {
        const char *pos = sessions_start + 12;

        while ((pos = strchr(pos, '{')) != NULL) {
            char *session_id = extract_json_string(pos, "id");
            char *container = extract_json_string(pos, "container_name");
            char *shell = extract_json_string(pos, "shell");
            char *status = extract_json_string(pos, "status");

            // Extract remaining_ttl (numeric)
            int remaining_ttl = 0;
            const char *ttl_str = strstr(pos, "\"remaining_ttl\":");
            if (ttl_str) {
                remaining_ttl = atoi(ttl_str + 16);
            }

            // Format TTL as human-readable
            char ttl_fmt[16];
            if (remaining_ttl >= 3600) {
                snprintf(ttl_fmt, sizeof(ttl_fmt), "%dh%dm", remaining_ttl / 3600, (remaining_ttl % 3600) / 60);
            } else if (remaining_ttl >= 60) {
                snprintf(ttl_fmt, sizeof(ttl_fmt), "%dm%ds", remaining_ttl / 60, remaining_ttl % 60);
            } else {
                snprintf(ttl_fmt, sizeof(ttl_fmt), "%ds", remaining_ttl);
            }

            printf("%-40s %-20s %-10s %-8s %-10s\n",
                   session_id ? session_id : "-",
                   container ? container : "-",
                   shell ? shell : "bash",
                   ttl_fmt,
                   status ? status : "-");

            if (session_id) free(session_id);
            if (container) free(container);
            if (shell) free(shell);
            if (status) free(status);

            // Move to next object
            pos++;
            const char *next_obj = strchr(pos, '{');
            const char *end_arr = strchr(pos, ']');
            if (!next_obj || (end_arr && end_arr < next_obj)) break;
            pos = next_obj;
        }
    }

    free(response.data);
    return 0;
}

// Get path to languages cache file (~/.unsandbox/languages.json)
static char* get_languages_cache_path(void) {
    const char *home = getenv("HOME");
    if (!home) {
        struct passwd *pw = getpwuid(getuid());
        if (pw) home = pw->pw_dir;
    }
    if (!home) return NULL;

    char *path = malloc(strlen(home) + 32);
    if (!path) return NULL;
    sprintf(path, "%s/.unsandbox/languages.json", home);
    return path;
}

// Load languages from cache if valid (< 1 hour old)
// Returns JSON string with languages array, or NULL if cache invalid/missing
static char* load_languages_cache(void) {
    char *cache_path = get_languages_cache_path();
    if (!cache_path) return NULL;

    struct stat st;
    if (stat(cache_path, &st) != 0) {
        free(cache_path);
        return NULL;
    }

    // Check if cache is fresh (< 1 hour old)
    time_t now = time(NULL);
    if (now - st.st_mtime >= LANGUAGES_CACHE_TTL) {
        free(cache_path);
        return NULL;
    }

    FILE *f = fopen(cache_path, "r");
    free(cache_path);
    if (!f) return NULL;

    fseek(f, 0, SEEK_END);
    long size = ftell(f);
    fseek(f, 0, SEEK_SET);

    if (size <= 0 || size > 1024 * 1024) {  // Sanity check: max 1MB
        fclose(f);
        return NULL;
    }

    char *data = malloc(size + 1);
    if (!data) {
        fclose(f);
        return NULL;
    }

    size_t read = fread(data, 1, size, f);
    fclose(f);
    data[read] = '\0';

    // Verify it contains languages array
    if (!strstr(data, "\"languages\"")) {
        free(data);
        return NULL;
    }

    return data;
}

// Save languages to cache
static void save_languages_cache(const char *json_response) {
    if (!json_response) return;

    char *cache_path = get_languages_cache_path();
    if (!cache_path) return;

    // Ensure ~/.unsandbox directory exists
    char *dir = strdup(cache_path);
    if (dir) {
        char *last_slash = strrchr(dir, '/');
        if (last_slash) {
            *last_slash = '\0';
            mkdir(dir, 0700);
        }
        free(dir);
    }

    FILE *f = fopen(cache_path, "w");
    free(cache_path);
    if (!f) return;

    // Extract just the languages array if present, wrap in standard format
    const char *langs_start = strstr(json_response, "\"languages\":[");
    if (langs_start) {
        const char *arr_start = strchr(langs_start, '[');
        const char *arr_end = strchr(arr_start, ']');
        if (arr_start && arr_end) {
            fprintf(f, "{\"languages\":%.*s,\"timestamp\":%ld}",
                    (int)(arr_end - arr_start + 1), arr_start, (long)time(NULL));
        }
    } else {
        // Try to find raw array
        const char *arr_start = strchr(json_response, '[');
        const char *arr_end = arr_start ? strchr(arr_start, ']') : NULL;
        if (arr_start && arr_end) {
            fprintf(f, "{\"languages\":%.*s,\"timestamp\":%ld}",
                    (int)(arr_end - arr_start + 1), arr_start, (long)time(NULL));
        }
    }

    fclose(f);
}

// List supported languages (CLI helper)
static int list_languages_cli(const UnsandboxCredentials *creds, int json_output) {
    // Try cache first
    char *cached = load_languages_cache();
    if (cached) {
        const char *langs_start = strstr(cached, "\"languages\":[");
        if (!langs_start) langs_start = strchr(cached, '[');

        if (langs_start) {
            const char *arr_start = strchr(langs_start, '[');
            if (arr_start) {
                if (json_output) {
                    const char *arr_end = strchr(arr_start, ']');
                    if (arr_end) {
                        printf("%.*s\n", (int)(arr_end - arr_start + 1), arr_start);
                    }
                } else {
                    const char *p = arr_start + 1;
                    while (*p && *p != ']') {
                        if (*p == '"') {
                            p++;
                            const char *end = strchr(p, '"');
                            if (end) {
                                printf("%.*s\n", (int)(end - p), p);
                                p = end + 1;
                            }
                        } else {
                            p++;
                        }
                    }
                }
                free(cached);
                return 0;
            }
        }
        free(cached);
    }

    // Cache miss or invalid - fetch from API
    CURL *curl = curl_easy_init();
    if (!curl) return 1;

    struct ResponseBuffer response = {0};
    response.data = malloc(1);
    response.size = 0;

    char url[256];
    snprintf(url, sizeof(url), "%s/languages", API_BASE);

    struct curl_slist *headers = NULL;
    headers = add_hmac_auth_headers(headers, creds, "GET", "/languages", NULL);

    curl_easy_setopt(curl, CURLOPT_URL, url);
    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
    curl_easy_setopt(curl, CURLOPT_USERAGENT, "un-cli/2.0");

    CURLcode res = curl_easy_perform(curl);
    long http_code = 0;
    curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);

    curl_slist_free_all(headers);
    curl_easy_cleanup(curl);

    if (res != CURLE_OK || http_code != 200) {
        fprintf(stderr, "Error: Failed to fetch languages (HTTP %ld)\n", http_code);
        free(response.data);
        return 1;
    }

    // Find the languages array in JSON: "languages":["lang1","lang2",...] or just ["lang1",...]
    const char *langs_start = strstr(response.data, "\"languages\":[");
    if (!langs_start) {
        langs_start = strchr(response.data, '[');
    }

    if (!langs_start) {
        fprintf(stderr, "Error: Invalid response format\n");
        free(response.data);
        return 1;
    }

    const char *arr_start = strchr(langs_start, '[');
    if (!arr_start) {
        fprintf(stderr, "Error: Invalid response format\n");
        free(response.data);
        return 1;
    }

    if (json_output) {
        // Output as JSON array - find matching ]
        const char *arr_end = strchr(arr_start, ']');
        if (arr_end) {
            int len = arr_end - arr_start + 1;
            printf("%.*s\n", len, arr_start);
        }
    } else {
        // Output one language per line
        const char *p = arr_start + 1;
        while (*p && *p != ']') {
            if (*p == '"') {
                p++;
                const char *end = strchr(p, '"');
                if (end) {
                    printf("%.*s\n", (int)(end - p), p);
                    p = end + 1;
                }
            } else {
                p++;
            }
        }
    }

    // Save to cache for future use
    save_languages_cache(response.data);

    free(response.data);
    return 0;
}

// Create a session via HTTP API
// multiplexer: NULL for no multiplexer (default), "screen", or "tmux"
// input_files: array of files to include (written to /tmp/ in container)
// input_file_count: number of input files
static struct SessionInfo create_session(const UnsandboxCredentials *creds, const char *network_mode, int audit, const char *shell, const char *multiplexer, int vcpu, struct InputFile *input_files, int input_file_count) {
    struct SessionInfo info = {NULL, NULL};
    CURL *curl = curl_easy_init();
    if (!curl) return info;

    struct ResponseBuffer response = {0};
    response.data = malloc(1);
    response.size = 0;

    char url[256];
    if (audit) {
        snprintf(url, sizeof(url), "%s/sessions?audit=1", API_BASE);
    } else {
        snprintf(url, sizeof(url), "%s/sessions", API_BASE);
    }

    // Calculate required payload size
    size_t payload_size = 512;  // Base size for JSON structure
    for (int i = 0; i < input_file_count; i++) {
        payload_size += strlen(input_files[i].content_base64) + 256;
    }

    // Build payload with optional shell and multiplexer
    char *payload = malloc(payload_size);
    if (!payload) {
        curl_easy_cleanup(curl);
        free(response.data);
        return info;
    }
    char *p = payload;
    p += sprintf(p, "{\"network_mode\":\"%s\",\"ttl\":3600",
                 network_mode ? network_mode : "zerotrust");
    if (shell && strlen(shell) > 0) {
        p += sprintf(p, ",\"shell\":\"%s\"", shell);
    }
    if (multiplexer && strlen(multiplexer) > 0) {
        p += sprintf(p, ",\"multiplexer\":\"%s\"", multiplexer);
    }
    if (vcpu > 1) {
        p += sprintf(p, ",\"vcpu\":%d", vcpu);
    }
    // Add input files
    if (input_file_count > 0) {
        p += sprintf(p, ",\"input_files\":[");
        for (int i = 0; i < input_file_count; i++) {
            if (i > 0) *p++ = ',';
            char *esc_filename = escape_json_string(input_files[i].filename);
            p += sprintf(p, "{\"filename\":\"%s\",\"content\":\"%s\"}",
                        esc_filename, input_files[i].content_base64);
            free(esc_filename);
        }
        p += sprintf(p, "]");
    }
    p += sprintf(p, "}");

    struct curl_slist *headers = NULL;
    headers = curl_slist_append(headers, "Content-Type: application/json");
    headers = add_hmac_auth_headers(headers, creds, "POST", "/sessions", payload);

    curl_easy_setopt(curl, CURLOPT_URL, url);
    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
    curl_easy_setopt(curl, CURLOPT_POSTFIELDS, payload);
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);

    CURLcode res = curl_easy_perform(curl);

    // Get HTTP status code before cleanup
    long http_code = 0;
    curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);

    curl_slist_free_all(headers);
    curl_easy_cleanup(curl);

    if (res != CURLE_OK) {
        fprintf(stderr, " curl error: %s\n", curl_easy_strerror(res));
        free(response.data);
        return info;
    }

    // Extract session_id and container_name from response
    info.session_id = extract_json_string(response.data, "session_id");
    info.container_name = extract_json_string(response.data, "container_name");
    if (!info.session_id && response.data) {
        // Parse error response for user-friendly message
        char *error = extract_json_string(response.data, "error");
        char *message = extract_json_string(response.data, "message");

        if (http_code == 429 && error) {
            if (strcmp(error, "concurrency_limit_reached") == 0) {
                // Parse active/limit for helpful context
                const char *active_str = strstr(response.data, "\"active_executions\":");
                const char *limit_str = strstr(response.data, "\"concurrency_limit\":");
                int active = active_str ? atoi(active_str + 20) : 0;
                int limit = limit_str ? atoi(limit_str + 20) : 1;

                fprintf(stderr, "\n\n");
                fprintf(stderr, "  \033[1;33mSession limit reached\033[0m\n\n");
                fprintf(stderr, "  You have %d of %d concurrent session%s in use.\n",
                        active, limit, limit == 1 ? "" : "s");
                fprintf(stderr, "\n");
                fprintf(stderr, "  To continue, either:\n");
                fprintf(stderr, "    1. Wait for a running session to finish\n");
                fprintf(stderr, "    2. Run '\033[36mun session --list\033[0m' to see active sessions\n");
                fprintf(stderr, "    3. Run '\033[36mun session --attach <id>\033[0m' to reconnect\n");
                fprintf(stderr, "\n");
            } else if (strcmp(error, "rate_limit_exceeded") == 0) {
                fprintf(stderr, "\n\n");
                fprintf(stderr, "  \033[1;33mRate limit exceeded\033[0m\n\n");
                if (message) {
                    fprintf(stderr, "  %s\n", message);
                } else {
                    fprintf(stderr, "  Too many requests. Please wait a moment and try again.\n");
                }
                fprintf(stderr, "\n");
            } else {
                fprintf(stderr, "\n  \033[1;31mError:\033[0m %s\n", message ? message : error);
            }
        } else if (http_code == 401) {
            fprintf(stderr, "\n\n");
            fprintf(stderr, "  \033[1;31mAuthentication failed\033[0m\n\n");
            // Check if it's a timestamp issue
            if (message && (strstr(message, "timestamp") || strstr(message, "Timestamp"))) {
                fprintf(stderr, "  Request timestamp expired (must be within 5 minutes of server time).\n\n");
                fprintf(stderr, "  \033[1;33mYour computer's clock may have drifted.\033[0m\n");
                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");
            } else {
                fprintf(stderr, "  Your API key is invalid or expired.\n");
                fprintf(stderr, "  Set UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY to valid keys.\n");
            }
            fprintf(stderr, "\n");
        } else if (error || message) {
            fprintf(stderr, "\n  \033[1;31mError:\033[0m %s\n", message ? message : error);
        } else {
            fprintf(stderr, " HTTP %ld: %s\n", http_code, response.data);
        }

        if (error) free(error);
        if (message) free(message);
    }
    free(payload);
    free(response.data);
    return info;
}

// Terminate a session and optionally save artifacts
// save_artifacts: 1 to save, 0 to discard
// artifact_dir: directory to save artifacts (NULL for current dir)
// container_name: used to postfix artifact filenames (e.g., bash_history-sandbox-abc123)
static void terminate_session(const UnsandboxCredentials *creds, const char *session_id, int save_artifacts, const char *artifact_dir, int audit_history, const char *container_name) {
    CURL *curl = curl_easy_init();
    if (!curl) return;

    struct ResponseBuffer response = {0};
    response.data = malloc(1);
    response.size = 0;

    char url[512];
    char path[256];
    snprintf(path, sizeof(path), "/sessions/%s", session_id);
    if (audit_history) {
        // Request audit - server will copy bash history to /tmp/artifacts before termination
        snprintf(url, sizeof(url), "%s%s?audit=1", API_BASE, path);
    } else {
        snprintf(url, sizeof(url), "%s%s", API_BASE, path);
    }

    struct curl_slist *headers = NULL;
    headers = add_hmac_auth_headers(headers, creds, "DELETE", path, NULL);

    curl_easy_setopt(curl, CURLOPT_URL, url);
    curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "DELETE");
    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);

    CURLcode res = curl_easy_perform(curl);
    curl_slist_free_all(headers);
    curl_easy_cleanup(curl);

    if (res == CURLE_OK && response.data && save_artifacts) {
        // Parse artifacts from response with container name for postfixing filenames
        const char *artifacts_start = strstr(response.data, "\"artifacts\":[");
        if (artifacts_start) {
            const char *pos = artifacts_start + 13; // Skip "artifacts":[

            // Process each artifact in array
            while ((pos = strchr(pos, '{')) != NULL) {
                char *artifact_data = extract_json_string(pos, "content_base64");
                char *artifact_filename = extract_json_string(pos, "filename");

                if (artifact_data) {
                    size_t decoded_len;
                    unsigned char *decoded = base64_decode(artifact_data, strlen(artifact_data), &decoded_len);

                    if (decoded) {
                        char output_path[512];
                        const char *fname = artifact_filename ? artifact_filename : "artifact";

                        // Postfix filename with container name if available
                        // e.g., bash_history -> bash_history-sandbox-abc123
                        char postfixed_name[256];
                        if (container_name) {
                            // Find extension if any
                            const char *ext = strrchr(fname, '.');
                            if (ext) {
                                // Has extension: file.ext -> file-container.ext
                                size_t base_len = ext - fname;
                                snprintf(postfixed_name, sizeof(postfixed_name), "%.*s-%s%s",
                                         (int)base_len, fname, container_name, ext);
                            } else {
                                // No extension: file -> file-container
                                snprintf(postfixed_name, sizeof(postfixed_name), "%s-%s", fname, container_name);
                            }
                            fname = postfixed_name;
                        }

                        if (artifact_dir) {
                            snprintf(output_path, sizeof(output_path), "%s/%s", artifact_dir, fname);
                        } else {
                            snprintf(output_path, sizeof(output_path), "%s", fname);
                        }

                        FILE *f = fopen(output_path, "wb");
                        if (f) {
                            fwrite(decoded, 1, decoded_len, f);
                            fclose(f);
                            fprintf(stderr, "\033[32mArtifact saved: %s (%zu bytes)\033[0m\n", output_path, decoded_len);
                        }
                        free(decoded);
                    }
                    free(artifact_data);
                }
                if (artifact_filename) free(artifact_filename);

                // Move to next object
                pos++;
                const char *next_obj = strchr(pos, '{');
                const char *end_arr = strchr(pos, ']');
                if (!next_obj || (end_arr && end_arr < next_obj)) break;
                pos = next_obj;
            }
        }
    }

    free(response.data);
}

// Reconnect to existing session (shared WebSocket setup with shell_command)
static int reconnect_session(const UnsandboxCredentials *creds, const char *session_id_or_container, int save_artifacts, const char *artifact_dir, int audit_history);

// Main shell command
// multiplexer: NULL for no multiplexer (default), "screen", or "tmux"
// input_files: array of files to include in session (written to /tmp/ in container)
// input_file_count: number of input files
static int shell_command(const UnsandboxCredentials *creds, const char *network_mode, int save_artifacts, const char *artifact_dir, int audit_history, const char *shell, const char *multiplexer, int vcpu, struct InputFile *input_files, int input_file_count) {
    // Disable stdout buffering for real-time output
    setvbuf(stdout, NULL, _IONBF, 0);

    // Create session (pass audit flag to enable script recording on server)
    fprintf(stderr, "Connecting to unsandbox...");
    fflush(stderr);
    struct SessionInfo session = create_session(creds, network_mode, audit_history, shell, multiplexer, vcpu, input_files, input_file_count);
    if (!session.session_id) {
        // Detailed error already printed by create_session
        return 1;
    }
    char *session_id = session.session_id;
    char *container_name = session.container_name;
    fprintf(stderr, " done\n");

    // Set up signal handlers
    signal(SIGWINCH, handle_sigwinch);
    signal(SIGINT, handle_sigint);

    // Enable raw terminal mode
    enable_raw_mode();
    atexit(disable_raw_mode);

    // Disable libwebsockets logging (too noisy)
    lws_set_log_level(0, NULL);

    // Create WebSocket context
    struct lws_context_creation_info info;
    memset(&info, 0, sizeof(info));
    info.port = CONTEXT_PORT_NO_LISTEN;
    info.protocols = shell_protocols;
    info.options = LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT;

    struct lws_context *context = lws_create_context(&info);
    if (!context) {
        fprintf(stderr, "\r\nError: Failed to create WebSocket context\r\n");
        free(session_id);
        return 1;
    }

    // Connect to WebSocket
    struct lws_client_connect_info ccinfo;
    memset(&ccinfo, 0, sizeof(ccinfo));
    ccinfo.context = context;
    ccinfo.address = "api.unsandbox.com";
    ccinfo.port = 443;

    char path[256];
    snprintf(path, sizeof(path), "/sessions/%s/shell", session_id);
    ccinfo.path = path;

    ccinfo.host = ccinfo.address;
    ccinfo.origin = ccinfo.address;
    ccinfo.protocol = shell_protocols[0].name;
    // LCCSCF_IP_LOW_LATENCY sets TCP_NODELAY to disable Nagle's algorithm
    ccinfo.ssl_connection = LCCSCF_USE_SSL | LCCSCF_IP_LOW_LATENCY;

    shell_wsi = lws_client_connect_via_info(&ccinfo);
    if (!shell_wsi) {
        fprintf(stderr, "\r\nError: Failed to connect to WebSocket\r\n");
        lws_context_destroy(context);
        free(session_id);
        return 1;
    }

    // Get user data from wsi
    struct shell_state *state = (struct shell_state *)lws_wsi_user(shell_wsi);
    state->session_id = session_id;
    // Send initial Enter to force tmux/screen repaint (fixes blank screen on connect)
    if (multiplexer && strlen(multiplexer) > 0) {
        state->need_initial_enter = 1;
    }

    shell_running = 1;

    // Main event loop - poll ONLY on stdin, service websocket separately
    while (shell_running) {
        // Poll stdin with very short timeout (1ms)
        struct pollfd pfd;
        pfd.fd = STDIN_FILENO;
        pfd.events = POLLIN;

        if (poll(&pfd, 1, 1) > 0 && (pfd.revents & POLLIN)) {
            char buf[256];
            ssize_t n = read(STDIN_FILENO, buf, sizeof(buf));
            if (n > 0 && state->connected) {
                state->send_buf = malloc(n);
                memcpy(state->send_buf, buf, n);
                state->send_len = n;
                lws_callback_on_writable(shell_wsi);
                // Wake up lws_service if it's sleeping
                lws_cancel_service(context);
            }
        }

        // Service websocket - use NEGATIVE timeout for true non-blocking (lws 3.2+)
        lws_service(context, -1);
    }

    // Cleanup
    disable_raw_mode();
    int was_detached = state->detached;
    lws_context_destroy(context);

    if (was_detached) {
        fprintf(stderr, "\r\n\033[32mSession detached.\033[0m Reconnect with: un session --attach %s\r\n", container_name);
        // Don't terminate - session is still running in multiplexer
    } else {
        fprintf(stderr, "\r\nSession ended.\r\n");
        // Terminate session and collect artifacts (postfix with container name)
        terminate_session(creds, session_id, save_artifacts, artifact_dir, audit_history, container_name);
    }

    // Hint for replaying audit logs (only when session ended, not detached)
    if (audit_history && !was_detached) {
        fprintf(stderr, "\033[33mTip: Replay session with: zcat session.log*.gz | less -R\033[0m\n");
    }
    free(session_id);
    if (container_name) free(container_name);

    return 0;
}

// Reconnect to an existing session by ID or container name
static int reconnect_session(const UnsandboxCredentials *creds, const char *session_id_or_container, int save_artifacts, const char *artifact_dir, int audit_history) {
    // Disable stdout buffering for real-time output
    setvbuf(stdout, NULL, _IONBF, 0);

    char *session_id = NULL;
    char *container_name = NULL;

    // Check if it looks like a container name or session ID
    // Container names: unsb-vm-*, exec-*, sandbox-* (legacy)
    if (strncmp(session_id_or_container, "unsb-vm-", 8) == 0 ||
        strncmp(session_id_or_container, "exec-", 5) == 0 ||
        strncmp(session_id_or_container, "sandbox-", 8) == 0) {
        // It's a container name - look up the session ID
        fprintf(stderr, "Looking up session for %s...", session_id_or_container);
        fflush(stderr);
        session_id = find_session_by_container(creds, session_id_or_container);
        if (!session_id) {
            fprintf(stderr, " not found\nError: No active session for container '%s'\n", session_id_or_container);
            return 1;
        }
        container_name = strdup(session_id_or_container);
        fprintf(stderr, " found\n");
    } else {
        // Assume it's a session ID
        session_id = strdup(session_id_or_container);
    }

    fprintf(stderr, "Reconnecting to session %s...", session_id);
    fflush(stderr);

    // Set up signal handlers
    signal(SIGWINCH, handle_sigwinch);
    signal(SIGINT, handle_sigint);

    // Enable raw terminal mode
    enable_raw_mode();
    atexit(disable_raw_mode);

    // Disable libwebsockets logging
    lws_set_log_level(0, NULL);

    // Create WebSocket context
    struct lws_context_creation_info info;
    memset(&info, 0, sizeof(info));
    info.port = CONTEXT_PORT_NO_LISTEN;
    info.protocols = shell_protocols;
    info.options = LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT;

    struct lws_context *context = lws_create_context(&info);
    if (!context) {
        fprintf(stderr, " failed\nError: Failed to create WebSocket context\n");
        free(session_id);
        if (container_name) free(container_name);
        return 1;
    }

    // Connect to WebSocket
    struct lws_client_connect_info ccinfo;
    memset(&ccinfo, 0, sizeof(ccinfo));
    ccinfo.context = context;
    ccinfo.address = "api.unsandbox.com";
    ccinfo.port = 443;

    char path[256];
    snprintf(path, sizeof(path), "/sessions/%s/shell", session_id);
    ccinfo.path = path;

    ccinfo.host = ccinfo.address;
    ccinfo.origin = ccinfo.address;
    ccinfo.protocol = shell_protocols[0].name;
    ccinfo.ssl_connection = LCCSCF_USE_SSL | LCCSCF_IP_LOW_LATENCY;

    shell_wsi = lws_client_connect_via_info(&ccinfo);
    if (!shell_wsi) {
        fprintf(stderr, " failed\nError: Failed to connect (session may have expired)\n");
        lws_context_destroy(context);
        free(session_id);
        if (container_name) free(container_name);
        return 1;
    }

    fprintf(stderr, " done\n");

    // Get user data from wsi
    struct shell_state *state = (struct shell_state *)lws_wsi_user(shell_wsi);
    state->session_id = session_id;
    // Always send initial Enter on reconnect (fixes tmux/screen blank screen)
    state->need_initial_enter = 1;

    shell_running = 1;

    // Main event loop
    while (shell_running) {
        struct pollfd pfd;
        pfd.fd = STDIN_FILENO;
        pfd.events = POLLIN;

        if (poll(&pfd, 1, 1) > 0 && (pfd.revents & POLLIN)) {
            char buf[256];
            ssize_t n = read(STDIN_FILENO, buf, sizeof(buf));
            if (n > 0 && state->connected) {
                state->send_buf = malloc(n);
                memcpy(state->send_buf, buf, n);
                state->send_len = n;
                lws_callback_on_writable(shell_wsi);
                lws_cancel_service(context);
            }
        }

        lws_service(context, -1);
    }

    // Cleanup
    disable_raw_mode();
    lws_context_destroy(context);

    fprintf(stderr, "\r\nSession ended.\r\n");

    // Note: We don't terminate the session on reconnect disconnect
    // The session stays alive for future reconnects
    // Only collect artifacts if explicitly requested
    if (save_artifacts || audit_history) {
        terminate_session(creds, session_id, save_artifacts, artifact_dir, audit_history, container_name);
        if (audit_history) {
            fprintf(stderr, "\033[33mTip: Replay session with: zcat session.log*.gz | less -R\033[0m\n");
        }
    } else {
        fprintf(stderr, "\033[33mSession still active. Reconnect with: un session --attach %s\033[0m\n",
                container_name ? container_name : session_id);
    }

    free(session_id);
    if (container_name) free(container_name);

    return 0;
}

// ============================================================================
// End Interactive Shell Support
// ============================================================================

// ============================================================================
// Service Management Support
// ============================================================================

// Get bootstrap logs for a service
// mode: 0 = tail (last 9000 lines), 1 = all logs
static char* get_service_logs(const UnsandboxCredentials *creds, const char *service_id, int all_logs) {
    CURL *curl = curl_easy_init();
    if (!curl) return NULL;

    struct ResponseBuffer response = {0};
    response.data = malloc(1);
    response.size = 0;

    char url[512];
    char path[256];
    if (all_logs) {
        snprintf(url, sizeof(url), "%s/services/%s/logs?all=true", API_BASE, service_id);
        snprintf(path, sizeof(path), "/services/%s/logs?all=true", service_id);
    } else {
        snprintf(url, sizeof(url), "%s/services/%s/logs", API_BASE, service_id);
        snprintf(path, sizeof(path), "/services/%s/logs", service_id);
    }

    struct curl_slist *headers = NULL;
    headers = add_hmac_auth_headers(headers, creds, "GET", path, NULL);

    curl_easy_setopt(curl, CURLOPT_URL, url);
    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);

    CURLcode res = curl_easy_perform(curl);

    curl_slist_free_all(headers);
    curl_easy_cleanup(curl);

    if (res != CURLE_OK) {
        free(response.data);
        return NULL;
    }

    // Extract log from response
    char *log = extract_json_string(response.data, "log");
    free(response.data);
    return log;
}

// ============================================================================
// PaaS Logs (fetch and stream production logs from portal)
// ============================================================================

// Log cursor: ~/.unsandbox/log_cursor stores last fetch time as Unix timestamp.
// Used to fetch only new logs since last call (ops semaphore).

static const char *get_log_cursor_path(void) {
    static char path[512];
    const char *home = getenv("HOME");
    if (!home) return NULL;
    snprintf(path, sizeof(path), "%s/.unsandbox/paas_log_cursor", home);
    return path;
}

// Read cursor: returns seconds-ago string like "42s", or NULL if no cursor
static char *read_log_cursor(void) {
    const char *path = get_log_cursor_path();
    if (!path) return NULL;

    FILE *f = fopen(path, "r");
    if (!f) return NULL;

    char buf[64];
    if (!fgets(buf, sizeof(buf), f)) { fclose(f); return NULL; }
    fclose(f);

    long ts = atol(buf);
    if (ts <= 0) return NULL;

    long ago = (long)time(NULL) - ts;
    if (ago < 5) ago = 5;  // minimum 5s to avoid empty results

    static char since_str[32];
    snprintf(since_str, sizeof(since_str), "%lds", ago);
    return since_str;
}

// Write cursor: store current Unix timestamp
static void write_log_cursor(void) {
    const char *path = get_log_cursor_path();
    if (!path) return;

    // Ensure ~/.unsandbox/ exists
    char dir[512];
    const char *home = getenv("HOME");
    if (!home) return;
    snprintf(dir, sizeof(dir), "%s/.unsandbox", home);
    mkdir(dir, 0700);  // ignore error if exists

    FILE *f = fopen(path, "w");
    if (!f) return;
    fprintf(f, "%ld\n", (long)time(NULL));
    fclose(f);
}

// Volatile flag for SIGINT handling during SSE streaming (forward declaration)
static volatile int stream_interrupted = 0;
// Track last received log time for reconnect deduplication
static volatile time_t stream_last_recv_time = 0;

// Streaming write callback — prints SSE data lines to stdout as they arrive
static size_t stream_sse_callback(void *contents, size_t size, size_t nmemb, void *userp) {
    size_t realsize = size * nmemb;
    // Buffer for partial SSE events across calls
    static char sse_buf[65536];
    static size_t sse_buf_len = 0;
    (void)userp;

    // Append new data to buffer
    size_t space = sizeof(sse_buf) - sse_buf_len - 1;
    size_t copy = realsize < space ? realsize : space;
    memcpy(sse_buf + sse_buf_len, contents, copy);
    sse_buf_len += copy;
    sse_buf[sse_buf_len] = 0;

    // Process complete SSE events (delimited by \n\n)
    char *pos = sse_buf;
    char *event_end;
    while ((event_end = strstr(pos, "\n\n")) != NULL) {
        *event_end = 0;
        // Parse each line in the event
        char *line = pos;
        while (*line) {
            char *nl = strchr(line, '\n');
            if (nl) *nl = 0;

            if (strncmp(line, "data: ", 6) == 0) {
                const char *payload = line + 6;
                if (strcmp(payload, ":keepalive") != 0) {
                    // Try to extract source + line from JSON
                    // Format: {"source":"api","line":"..."}
                    char *source = extract_json_string(payload, "source");
                    char *logline = extract_json_string(payload, "line");
                    if (source && logline) {
                        printf("\033[36m%-12s\033[0m %s\n", source, logline);
                        fflush(stdout);
                        stream_last_recv_time = time(NULL);
                    } else if (logline) {
                        printf("%s\n", logline);
                        fflush(stdout);
                        stream_last_recv_time = time(NULL);
                    } else {
                        // Plain text payload
                        printf("%s\n", payload);
                        fflush(stdout);
                        stream_last_recv_time = time(NULL);
                    }
                    free(source);
                    free(logline);
                }
            }

            if (nl) line = nl + 1;
            else break;
        }
        pos = event_end + 2;
    }

    // Move remaining partial data to front of buffer
    if (pos > sse_buf) {
        sse_buf_len = strlen(pos);
        memmove(sse_buf, pos, sse_buf_len + 1);
    }

    if (stream_interrupted) return 0;
    return realsize;
}

// Fetch PaaS logs (batch) — returns 0 on success
static int fetch_paas_logs(const UnsandboxCredentials *creds,
                           const char *source, int lines,
                           const char *since, const char *grep,
                           int json_output, const char *level) {
    CURL *curl = curl_easy_init();
    if (!curl) return 1;

    struct ResponseBuffer response = {0};
    response.data = malloc(1);
    response.size = 0;

    // Build path: /logs/{source}
    char path[256];
    if (strcmp(source, "all") == 0)
        snprintf(path, sizeof(path), "/logs/all");
    else
        snprintf(path, sizeof(path), "/logs/%s", source);

    // Build full URL with query params
    char url[1024];
    int off = snprintf(url, sizeof(url), "%s%s?lines=%d", PORTAL_BASE, path, lines);
    if (since && since[0])
        off += snprintf(url + off, sizeof(url) - off, "&since=%s", since);
    if (grep && grep[0])
        off += snprintf(url + off, sizeof(url) - off, "&grep=%s", grep);
    if (level && level[0])
        off += snprintf(url + off, sizeof(url) - off, "&level=%s", level);

    // HMAC signs path only (no query string) — matches server conn.request_path
    struct curl_slist *headers = NULL;
    headers = add_hmac_auth_headers(headers, creds, "GET", path, NULL);

    curl_easy_setopt(curl, CURLOPT_URL, url);
    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
    curl_easy_setopt(curl, CURLOPT_USERAGENT, "un-cli/2.0");

    CURLcode res = curl_easy_perform(curl);
    long http_code = 0;
    curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);

    curl_slist_free_all(headers);
    curl_easy_cleanup(curl);

    if (res != CURLE_OK) {
        fprintf(stderr, "Error: Network error fetching logs: %s\n", curl_easy_strerror(res));
        free(response.data);
        return 1;
    }

    if (http_code != 200) {
        char *error = extract_json_string(response.data, "error");
        fprintf(stderr, "Error: HTTP %ld", http_code);
        if (error) { fprintf(stderr, " — %s", error); free(error); }
        fprintf(stderr, "\n");
        free(response.data);
        return 1;
    }

    // Update cursor on success
    write_log_cursor();

    if (json_output) {
        // Raw JSON output
        printf("%s\n", response.data);
        free(response.data);
        return 0;
    }

    // Check if this is an "all" response (has "sources" key)
    int is_all = (strstr(response.data, "\"sources\"") != NULL);

    if (is_all) {
        // Parse merged lines: "lines":[{"source":"api","line":"..."},...]
        const char *lines_start = strstr(response.data, "\"lines\":[");
        if (!lines_start) {
            fprintf(stderr, "Error: Invalid response format\n");
            free(response.data);
            return 1;
        }
        const char *p = strchr(lines_start, '[');
        if (!p) { free(response.data); return 1; }
        p++; // skip '['

        // Walk through array of objects
        while (*p) {
            // Find next object
            const char *obj_start = strchr(p, '{');
            if (!obj_start) break;

            // Find matching close brace (simple — no nested objects in log entries)
            const char *obj_end = strchr(obj_start, '}');
            if (!obj_end) break;

            // Extract source and line from this object
            size_t obj_len = obj_end - obj_start + 1;
            char *obj = malloc(obj_len + 1);
            memcpy(obj, obj_start, obj_len);
            obj[obj_len] = 0;

            char *src = extract_json_string(obj, "source");
            char *line = extract_json_string(obj, "line");

            if (src && line) {
                printf("\033[36m%-12s\033[0m %s\n", src, line);
            } else if (line) {
                printf("%s\n", line);
            }

            free(src);
            free(line);
            free(obj);
            p = obj_end + 1;
        }
    } else {
        // Single source: "lines":["line1","line2",...]
        const char *lines_start = strstr(response.data, "\"lines\":[");
        if (!lines_start) {
            fprintf(stderr, "Error: Invalid response format\n");
            free(response.data);
            return 1;
        }
        const char *arr_start = strchr(lines_start, '[');
        if (!arr_start) { free(response.data); return 1; }

        // Parse string array
        const char *p = arr_start + 1;
        while (*p && *p != ']') {
            if (*p == '"') {
                p++;
                // Find end of string (handle escaped quotes)
                const char *end = p;
                while (*end && !(*end == '"' && *(end - 1) != '\\')) end++;
                printf("%.*s\n", (int)(end - p), p);
                p = end + 1;
            } else {
                p++;
            }
        }
    }

    free(response.data);
    return 0;
}

static void stream_sigint_handler(int sig) {
    (void)sig;
    stream_interrupted = 1;
}

// Stream PaaS logs (SSE) — blocks until Ctrl+C, auto-reconnects on drop
static int stream_paas_logs(const UnsandboxCredentials *creds,
                            const char *source, const char *grep,
                            const char *level) {
    // Build path: /logs/{source}/stream
    char path[256];
    if (strcmp(source, "all") == 0)
        snprintf(path, sizeof(path), "/logs/all/stream");
    else
        snprintf(path, sizeof(path), "/logs/%s/stream", source);

    // Build URL with optional query params
    char url[1024];
    int off = snprintf(url, sizeof(url), "%s%s", PORTAL_BASE, path);
    char sep = '?';
    if (grep && grep[0]) {
        off += snprintf(url + off, sizeof(url) - off, "%cgrep=%s", sep, grep);
        sep = '&';
    }
    if (level && level[0]) {
        off += snprintf(url + off, sizeof(url) - off, "%clevel=%s", sep, level);
        sep = '&';
    }
    (void)sep;

    // Install SIGINT handler for clean shutdown
    stream_interrupted = 0;
    struct sigaction sa, old_sa;
    memset(&sa, 0, sizeof(sa));
    sa.sa_handler = stream_sigint_handler;
    sigaction(SIGINT, &sa, &old_sa);

    fprintf(stderr, "Streaming %s logs... (Ctrl+C to stop)\n", source);

    int reconnect_delay = 2;  // Start at 2s, cap at 30s
    int is_reconnect = 0;
    stream_last_recv_time = 0;

    while (!stream_interrupted) {
        CURL *curl = curl_easy_init();
        if (!curl) { sigaction(SIGINT, &old_sa, NULL); return 1; }

        // Fresh HMAC headers each connection (timestamps expire)
        struct curl_slist *headers = NULL;
        headers = add_hmac_auth_headers(headers, creds, "GET", path, NULL);

        // On reconnect, add since= to pick up where we left off
        char reconnect_url[2048];
        if (is_reconnect && stream_last_recv_time > 0) {
            int gap = (int)(time(NULL) - stream_last_recv_time) + 5;  // +5s overlap buffer
            if (gap < 10) gap = 10;
            snprintf(reconnect_url, sizeof(reconnect_url), "%s%csince=%ds",
                     url, strchr(url, '?') ? '&' : '?', gap);
        } else {
            snprintf(reconnect_url, sizeof(reconnect_url), "%s", url);
        }

        curl_easy_setopt(curl, CURLOPT_URL, reconnect_url);
        curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
        curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, stream_sse_callback);
        curl_easy_setopt(curl, CURLOPT_WRITEDATA, NULL);
        curl_easy_setopt(curl, CURLOPT_USERAGENT, "un-cli/2.0");
        // Force HTTP/1.1 — SSE streaming breaks with HTTP/2 framing
        curl_easy_setopt(curl, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
        // No timeout — stream until interrupted
        curl_easy_setopt(curl, CURLOPT_TIMEOUT, 0L);
        // Low-speed limit to detect dead connections (10 bytes in 120s accounts for keepalives)
        curl_easy_setopt(curl, CURLOPT_LOW_SPEED_LIMIT, 1L);
        curl_easy_setopt(curl, CURLOPT_LOW_SPEED_TIME, 120L);

        time_t connect_time = time(NULL);
        CURLcode res = curl_easy_perform(curl);

        long http_code = 0;
        curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);

        curl_slist_free_all(headers);
        curl_easy_cleanup(curl);

        // Reset backoff if we were connected for >30s (healthy connection)
        if (time(NULL) - connect_time > 30) reconnect_delay = 2;

        if (stream_interrupted) {
            break;
        }

        // Fatal errors — don't reconnect
        if (http_code == 401 || http_code == 403) {
            fprintf(stderr, "Error: HTTP %ld (auth failed)\n", http_code);
            sigaction(SIGINT, &old_sa, NULL);
            return 1;
        }

        // Recoverable: partial file, connection reset, low-speed timeout
        if (res == CURLE_PARTIAL_FILE || res == CURLE_RECV_ERROR ||
            res == CURLE_OPERATION_TIMEDOUT || res == CURLE_GOT_NOTHING ||
            (res == CURLE_OK && http_code == 200)) {
            // Silent reconnect — don't pollute output
            sleep(reconnect_delay);
            if (reconnect_delay < 30) reconnect_delay = reconnect_delay * 2;
            if (reconnect_delay > 30) reconnect_delay = 30;
            is_reconnect = 1;
            continue;
        }

        // Transient HTTP errors (502, 503, 504) — silent reconnect
        if (http_code >= 500) {
            sleep(reconnect_delay);
            if (reconnect_delay < 30) reconnect_delay = reconnect_delay * 2;
            if (reconnect_delay > 30) reconnect_delay = 30;
            is_reconnect = 1;
            continue;
        }

        // Non-recoverable curl error
        if (res != CURLE_OK && res != CURLE_WRITE_ERROR) {
            fprintf(stderr, "Error: Stream error: %s\n", curl_easy_strerror(res));
            sigaction(SIGINT, &old_sa, NULL);
            return 1;
        }

        // CURLE_WRITE_ERROR from our callback returning 0 (shouldn't happen without interrupt)
        break;
    }

    sigaction(SIGINT, &old_sa, NULL);
    write_log_cursor();
    fprintf(stderr, "\nStream stopped.\n");
    return 0;
}

// Create a service via HTTP API
// bootstrap_content: if provided, sent as bootstrap_content (file contents)
// bootstrap: if bootstrap_content is NULL and this starts with http, sent as bootstrap URL
// service_type: optional type for SRV-enabled services (minecraft, mumble, teamspeak, etc.)
// input_files: array of files to include (written to /tmp/ in container)
// input_file_count: number of input files
// golden_image: optional LXD image alias to use instead of default (for testing)
static char* create_service(const UnsandboxCredentials *creds, const char *name, const char *ports, const char *domains, const char *bootstrap, const char *bootstrap_content, const char *network_mode, int vcpu, const char *service_type, struct InputFile *input_files, int input_file_count, const char *golden_image, int unfreeze_on_demand) {
    CURL *curl = curl_easy_init();
    if (!curl) return NULL;

    struct ResponseBuffer response = {0};
    response.data = malloc(1);
    response.size = 0;

    char url[256];
    snprintf(url, sizeof(url), "%s/services", API_BASE);

    // Calculate required payload size (bootstrap_content can be large)
    size_t payload_size = 1024;  // Base size for JSON structure
    if (bootstrap_content) {
        payload_size += strlen(bootstrap_content) * 2 + 100;  // Escaped content + field name
    } else if (bootstrap) {
        payload_size += strlen(bootstrap) * 2 + 100;
    }
    if (domains) {
        payload_size += strlen(domains) * 2 + 100;  // Escaped domains + JSON overhead
    }
    // Add space for input files
    for (int i = 0; i < input_file_count; i++) {
        payload_size += strlen(input_files[i].content_base64) + 256;
    }

    // Build payload
    char *payload = malloc(payload_size);
    if (!payload) {
        curl_easy_cleanup(curl);
        return NULL;
    }
    char *p = payload;
    p += sprintf(p, "{");

    if (name && strlen(name) > 0) {
        char *esc_name = escape_json_string(name);
        p += sprintf(p, "\"name\":\"%s\"", esc_name);
        free(esc_name);
    }

    if (ports && strlen(ports) > 0) {
        if (p > payload + 1) p += sprintf(p, ",");
        p += sprintf(p, "\"ports\":[");
        // Parse comma-separated ports
        char *ports_copy = strdup(ports);
        char *port_token = strtok(ports_copy, ",");
        int first = 1;
        while (port_token) {
            if (!first) p += sprintf(p, ",");
            p += sprintf(p, "%d", atoi(port_token));
            first = 0;
            port_token = strtok(NULL, ",");
        }
        free(ports_copy);
        p += sprintf(p, "]");
    }

    // Add custom_domains as JSON array of strings
    if (domains && strlen(domains) > 0) {
        if (p > payload + 1) p += sprintf(p, ",");
        p += sprintf(p, "\"custom_domains\":[");
        // Parse comma-separated domains
        char *domains_copy = strdup(domains);
        char *domain_token = strtok(domains_copy, ",");
        int first = 1;
        while (domain_token) {
            if (!first) p += sprintf(p, ",");
            // Trim whitespace from domain
            while (*domain_token == ' ') domain_token++;
            char *end = domain_token + strlen(domain_token) - 1;
            while (end > domain_token && *end == ' ') *end-- = '\0';
            char *esc_domain = escape_json_string(domain_token);
            p += sprintf(p, "\"%s\"", esc_domain);
            free(esc_domain);
            first = 0;
            domain_token = strtok(NULL, ",");
        }
        free(domains_copy);
        p += sprintf(p, "]");
    }

    // Prefer bootstrap_content (file contents), fall back to bootstrap (URL/command)
    if (bootstrap_content && strlen(bootstrap_content) > 0) {
        if (p > payload + 1) p += sprintf(p, ",");
        char *esc_content = escape_json_string(bootstrap_content);
        p += sprintf(p, "\"bootstrap_content\":\"%s\"", esc_content);
        free(esc_content);
    } else if (bootstrap && strlen(bootstrap) > 0) {
        if (p > payload + 1) p += sprintf(p, ",");
        char *esc_bootstrap = escape_json_string(bootstrap);
        p += sprintf(p, "\"bootstrap\":\"%s\"", esc_bootstrap);
        free(esc_bootstrap);
    }

    if (network_mode && strlen(network_mode) > 0) {
        if (p > payload + 1) p += sprintf(p, ",");
        p += sprintf(p, "\"network_mode\":\"%s\"", network_mode);
    }

    if (vcpu > 1) {
        if (p > payload + 1) p += sprintf(p, ",");
        p += sprintf(p, "\"vcpu\":%d", vcpu);
    }

    // Service type for SRV-enabled services (minecraft, mumble, etc.)
    if (service_type && strlen(service_type) > 0) {
        if (p > payload + 1) p += sprintf(p, ",");
        char *esc_type = escape_json_string(service_type);
        p += sprintf(p, "\"service_type\":\"%s\"", esc_type);
        free(esc_type);
    }

    // Golden image override (for testing with different base images)
    if (golden_image && strlen(golden_image) > 0) {
        if (p > payload + 1) p += sprintf(p, ",");
        char *esc_image = escape_json_string(golden_image);
        p += sprintf(p, "\"golden_image\":\"%s\"", esc_image);
        free(esc_image);
    }

    // Add input files
    if (input_file_count > 0) {
        if (p > payload + 1) p += sprintf(p, ",");
        p += sprintf(p, "\"input_files\":[");
        for (int i = 0; i < input_file_count; i++) {
            if (i > 0) *p++ = ',';
            char *esc_filename = escape_json_string(input_files[i].filename);
            p += sprintf(p, "{\"filename\":\"%s\",\"content\":\"%s\"}",
                        esc_filename, input_files[i].content_base64);
            free(esc_filename);
        }
        p += sprintf(p, "]");
    }

    // Unfreeze on demand (auto-wake on HTTP request)
    if (unfreeze_on_demand) {
        if (p > payload + 1) p += sprintf(p, ",");
        p += sprintf(p, "\"unfreeze_on_demand\":true");
    }

    p += sprintf(p, "}");

    struct curl_slist *headers = NULL;
    headers = curl_slist_append(headers, "Content-Type: application/json");
    headers = add_hmac_auth_headers(headers, creds, "POST", "/services", payload);

    curl_easy_setopt(curl, CURLOPT_URL, url);
    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
    curl_easy_setopt(curl, CURLOPT_POSTFIELDS, payload);
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);

    CURLcode res = curl_easy_perform(curl);

    long http_code = 0;
    curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);

    curl_slist_free_all(headers);
    curl_easy_cleanup(curl);

    free(payload);  // Done with payload after curl_easy_perform

    if (res != CURLE_OK) {
        fprintf(stderr, "Error: %s\n", curl_easy_strerror(res));
        free(response.data);
        return NULL;
    }

    if (http_code != 200 && http_code != 201) {
        fprintf(stderr, "Error: HTTP %ld\n", http_code);
        if (response.data) fprintf(stderr, "%s\n", response.data);
        free(response.data);
        return NULL;
    }

    // Extract service ID from response (try both "service_id" and "id" for compatibility)
    char *service_id = extract_json_string(response.data, "service_id");
    if (!service_id) {
        service_id = extract_json_string(response.data, "id");
    }
    free(response.data);
    return service_id;
}

// List all services
static int list_services(const UnsandboxCredentials *creds) {
    CURL *curl = curl_easy_init();
    if (!curl) return 1;

    struct ResponseBuffer response = {0};
    response.data = malloc(1);
    response.size = 0;

    char url[256];
    snprintf(url, sizeof(url), "%s/services", API_BASE);

    struct curl_slist *headers = NULL;
    headers = add_hmac_auth_headers(headers, creds, "GET", "/services", NULL);

    curl_easy_setopt(curl, CURLOPT_URL, url);
    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);

    CURLcode res = curl_easy_perform(curl);

    long http_code = 0;
    curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);

    curl_slist_free_all(headers);
    curl_easy_cleanup(curl);

    if (res != CURLE_OK) {
        fprintf(stderr, "Error: %s\n", curl_easy_strerror(res));
        free(response.data);
        return 1;
    }

    if (http_code != 200) {
        fprintf(stderr, "Error: HTTP %ld\n", http_code);
        if (response.data) fprintf(stderr, "%s\n", response.data);
        free(response.data);
        return 1;
    }

    // Parse and display services
    if (!response.data || !strstr(response.data, "\"services\"")) {
        fprintf(stderr, "Error: Invalid response\n");
        free(response.data);
        return 1;
    }

    // Extract count
    const char *count_str = strstr(response.data, "\"count\":");
    int count = 0;
    if (count_str) {
        count = atoi(count_str + 8);
    }

    if (count == 0) {
        printf("No services found\n");
        free(response.data);
        return 0;
    }

    printf("Services: %d\n\n", count);
    printf("%-40s %-20s %-10s %-8s %-15s\n", "SERVICE ID", "NAME", "STATUS", "DISK", "PORTS");
    printf("%-40s %-20s %-10s %-8s %-15s\n", "----------------------------------------",
           "--------------------", "----------", "--------", "---------------");

    // Parse services array
    const char *services_start = strstr(response.data, "\"services\":[");
    if (services_start) {
        const char *pos = services_start + 12;

        while ((pos = strchr(pos, '{')) != NULL) {
            char *service_id = extract_json_string(pos, "id");
            char *name = extract_json_string(pos, "name");
            char *status = extract_json_string(pos, "state");

            // Extract disk_used (bytes as number)
            long long disk_used = extract_json_number(pos, "disk_used");
            char disk_str[16];
            format_bytes(disk_used, disk_str, sizeof(disk_str));

            // Extract ports array
            char ports_str[128] = "-";
            const char *ports_start = strstr(pos, "\"ports\":[");
            if (ports_start) {
                const char *ports_end = strchr(ports_start + 9, ']');
                if (ports_end) {
                    size_t len = ports_end - (ports_start + 9);
                    if (len < sizeof(ports_str) - 1) {
                        strncpy(ports_str, ports_start + 9, len);
                        ports_str[len] = '\0';
                    }
                }
            }

            printf("%-40s %-20s %-10s %-8s %-15s\n",
                   service_id ? service_id : "-",
                   name ? name : "-",
                   status ? status : "-",
                   disk_str,
                   ports_str);

            if (service_id) free(service_id);
            if (name) free(name);
            if (status) free(status);

            // Move to next object by skipping past current object's closing }
            // Need to properly match braces to handle nested objects like port_mappings
            int brace_depth = 1;
            pos++;  // move past opening {
            while (*pos && brace_depth > 0) {
                if (*pos == '{') brace_depth++;
                else if (*pos == '}') brace_depth--;
                else if (*pos == '"') {
                    // Skip strings (may contain { or })
                    pos++;
                    while (*pos && !(*pos == '"' && *(pos-1) != '\\')) pos++;
                }
                pos++;
            }
            // pos now points just past the closing } of current object
            const char *next_obj = strchr(pos, '{');
            const char *end_arr = strchr(pos, ']');
            if (!next_obj || (end_arr && end_arr < next_obj)) break;
            pos = next_obj;
        }
    }

    free(response.data);
    return 0;
}

// Get service info
static int get_service_info(const UnsandboxCredentials *creds, const char *service_id) {
    CURL *curl = curl_easy_init();
    if (!curl) return 1;

    struct ResponseBuffer response = {0};
    response.data = malloc(1);
    response.size = 0;

    char url[512];
    snprintf(url, sizeof(url), "%s/services/%s", API_BASE, service_id);

    char path[256];
    snprintf(path, sizeof(path), "/services/%s", service_id);

    struct curl_slist *headers = NULL;
    headers = add_hmac_auth_headers(headers, creds, "GET", path, NULL);

    curl_easy_setopt(curl, CURLOPT_URL, url);
    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);

    CURLcode res = curl_easy_perform(curl);

    long http_code = 0;
    curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);

    curl_slist_free_all(headers);
    curl_easy_cleanup(curl);

    if (res != CURLE_OK) {
        fprintf(stderr, "Error: %s\n", curl_easy_strerror(res));
        free(response.data);
        return 1;
    }

    if (http_code == 404) {
        fprintf(stderr, "Error: Service not found\n");
        free(response.data);
        return 1;
    }

    if (http_code != 200) {
        fprintf(stderr, "Error: HTTP %ld\n", http_code);
        if (response.data) fprintf(stderr, "%s\n", response.data);
        free(response.data);
        return 1;
    }

    // Extract and display service details
    char *id = extract_json_string(response.data, "id");
    char *name = extract_json_string(response.data, "name");
    char *status = extract_json_string(response.data, "status");
    char *network_mode = extract_json_string(response.data, "network_mode");
    int locked = (int)extract_json_number(response.data, "locked");
    int unfreeze_on_demand = (int)extract_json_number(response.data, "unfreeze_on_demand");

    printf("Service Information:\n");
    printf("  ID:              %s\n", id ? id : "-");
    printf("  Name:            %s\n", name ? name : "-");
    printf("  Status:          %s\n", status ? status : "-");
    printf("  Network Mode:    %s\n", network_mode ? network_mode : "-");
    printf("  Locked:          %s\n", locked ? "yes" : "no");
    printf("  Auto-Unfreeze:   %s\n", unfreeze_on_demand ? "yes" : "no");

    // Extract ports array
    const char *ports_start = strstr(response.data, "\"ports\":[");
    if (ports_start) {
        printf("  Ports:           ");
        const char *pos = ports_start + 9;
        const char *ports_end = strchr(pos, ']');
        if (ports_end) {
            char ports_buf[256];
            size_t len = ports_end - pos;
            if (len < sizeof(ports_buf)) {
                strncpy(ports_buf, pos, len);
                ports_buf[len] = '\0';
                printf("%s\n", ports_buf);
            }
        }
    }

    if (id) free(id);
    if (name) free(name);
    if (status) free(status);
    if (network_mode) free(network_mode);
    free(response.data);
    return 0;
}

// Freeze a service
static int freeze_service(const UnsandboxCredentials *creds, const char *service_id) {
    CURL *curl = curl_easy_init();
    if (!curl) return 1;

    struct ResponseBuffer response = {0};
    response.data = malloc(1);
    response.size = 0;

    char url[512];
    snprintf(url, sizeof(url), "%s/services/%s/freeze", API_BASE, service_id);

    char path[256];
    snprintf(path, sizeof(path), "/services/%s/freeze", service_id);

    struct curl_slist *headers = NULL;
    headers = curl_slist_append(headers, "Content-Type: application/json");
    headers = add_hmac_auth_headers(headers, creds, "POST", path, "{}");

    curl_easy_setopt(curl, CURLOPT_URL, url);
    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
    curl_easy_setopt(curl, CURLOPT_POSTFIELDS, "{}");
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);

    CURLcode res = curl_easy_perform(curl);

    long http_code = 0;
    curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);

    curl_slist_free_all(headers);
    curl_easy_cleanup(curl);

    if (res != CURLE_OK) {
        fprintf(stderr, "Error: %s\n", curl_easy_strerror(res));
        free(response.data);
        return 1;
    }

    if (http_code == 404) {
        fprintf(stderr, "Error: Service not found\n");
        free(response.data);
        return 1;
    }

    if (http_code != 200) {
        fprintf(stderr, "Error: HTTP %ld\n", http_code);
        if (response.data) fprintf(stderr, "%s\n", response.data);
        free(response.data);
        return 1;
    }

    printf("\033[32mService frozen successfully\033[0m\n");
    free(response.data);
    return 0;
}

// Unfreeze a service
static int unfreeze_service(const UnsandboxCredentials *creds, const char *service_id) {
    CURL *curl = curl_easy_init();
    if (!curl) return 1;

    struct ResponseBuffer response = {0};
    response.data = malloc(1);
    response.size = 0;

    char url[512];
    snprintf(url, sizeof(url), "%s/services/%s/unfreeze", API_BASE, service_id);

    char path[256];
    snprintf(path, sizeof(path), "/services/%s/unfreeze", service_id);

    struct curl_slist *headers = NULL;
    headers = curl_slist_append(headers, "Content-Type: application/json");
    headers = add_hmac_auth_headers(headers, creds, "POST", path, "{}");

    curl_easy_setopt(curl, CURLOPT_URL, url);
    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
    curl_easy_setopt(curl, CURLOPT_POSTFIELDS, "{}");
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);

    CURLcode res = curl_easy_perform(curl);

    long http_code = 0;
    curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);

    curl_slist_free_all(headers);
    curl_easy_cleanup(curl);

    if (res != CURLE_OK) {
        fprintf(stderr, "Error: %s\n", curl_easy_strerror(res));
        free(response.data);
        return 1;
    }

    if (http_code == 404) {
        fprintf(stderr, "Error: Service not found\n");
        free(response.data);
        return 1;
    }

    if (http_code != 200) {
        fprintf(stderr, "Error: HTTP %ld\n", http_code);
        if (response.data) fprintf(stderr, "%s\n", response.data);
        free(response.data);
        return 1;
    }

    printf("\033[32mService unfrozen successfully\033[0m\n");
    free(response.data);
    return 0;
}

// Handle 428 sudo OTP challenge - prompts user for OTP and retries the request
static int handle_sudo_challenge(const char *response_data,
                                  const UnsandboxCredentials *creds,
                                  const char *method, const char *url,
                                  const char *path, const char *body) {
    // Extract challenge_id from response
    char *challenge_id = extract_json_string(response_data, "challenge_id");

    fprintf(stderr, "\033[33mConfirmation required. Check your email for a one-time code.\033[0m\n");
    fprintf(stderr, "Enter OTP: ");

    char otp[32];
    if (!fgets(otp, sizeof(otp), stdin)) {
        fprintf(stderr, "Error: Failed to read OTP\n");
        if (challenge_id) free(challenge_id);
        return 1;
    }
    // Strip newline
    size_t otp_len = strlen(otp);
    if (otp_len > 0 && otp[otp_len - 1] == '\n') otp[otp_len - 1] = '\0';
    otp_len = strlen(otp);
    if (otp_len > 0 && otp[otp_len - 1] == '\r') otp[otp_len - 1] = '\0';

    if (strlen(otp) == 0) {
        fprintf(stderr, "Error: Operation cancelled\n");
        if (challenge_id) free(challenge_id);
        return 1;
    }

    // Retry the request with sudo headers
    CURL *curl = curl_easy_init();
    if (!curl) {
        if (challenge_id) free(challenge_id);
        return 1;
    }

    struct ResponseBuffer retry_response = {0};
    retry_response.data = malloc(1);
    retry_response.size = 0;

    struct curl_slist *headers = NULL;
    headers = add_hmac_auth_headers(headers, creds, method, path, body);

    // Add sudo headers
    char otp_header[64];
    snprintf(otp_header, sizeof(otp_header), "X-Sudo-OTP: %s", otp);
    headers = curl_slist_append(headers, otp_header);

    if (challenge_id) {
        char challenge_header[256];
        snprintf(challenge_header, sizeof(challenge_header), "X-Sudo-Challenge: %s", challenge_id);
        headers = curl_slist_append(headers, challenge_header);
        free(challenge_id);
    }

    if (body) {
        headers = curl_slist_append(headers, "Content-Type: application/json");
    }

    curl_easy_setopt(curl, CURLOPT_URL, url);
    if (strcmp(method, "DELETE") == 0) {
        curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "DELETE");
    } else if (strcmp(method, "POST") == 0) {
        curl_easy_setopt(curl, CURLOPT_POST, 1L);
        curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body ? body : "");
    }
    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &retry_response);

    CURLcode res = curl_easy_perform(curl);

    long http_code = 0;
    curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);

    curl_slist_free_all(headers);
    curl_easy_cleanup(curl);

    if (res != CURLE_OK) {
        fprintf(stderr, "Error: %s\n", curl_easy_strerror(res));
        free(retry_response.data);
        return 1;
    }

    if (http_code >= 200 && http_code < 300) {
        printf("\033[32mOperation completed successfully\033[0m\n");
        free(retry_response.data);
        return 0;
    }

    fprintf(stderr, "Error: HTTP %ld\n", http_code);
    if (retry_response.data) {
        char *error = extract_json_string(retry_response.data, "error");
        if (error) {
            fprintf(stderr, "%s\n", error);
            free(error);
        } else {
            fprintf(stderr, "%s\n", retry_response.data);
        }
    }
    free(retry_response.data);
    return 1;
}

// Destroy a service
static int destroy_service(const UnsandboxCredentials *creds, const char *service_id) {
    CURL *curl = curl_easy_init();
    if (!curl) return 1;

    struct ResponseBuffer response = {0};
    response.data = malloc(1);
    response.size = 0;

    char url[512];
    snprintf(url, sizeof(url), "%s/services/%s", API_BASE, service_id);

    char path[256];
    snprintf(path, sizeof(path), "/services/%s", service_id);

    struct curl_slist *headers = NULL;
    headers = add_hmac_auth_headers(headers, creds, "DELETE", path, NULL);

    curl_easy_setopt(curl, CURLOPT_URL, url);
    curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "DELETE");
    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);

    CURLcode res = curl_easy_perform(curl);

    long http_code = 0;
    curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);

    curl_slist_free_all(headers);
    curl_easy_cleanup(curl);

    if (res != CURLE_OK) {
        fprintf(stderr, "Error: %s\n", curl_easy_strerror(res));
        free(response.data);
        return 1;
    }

    if (http_code == 404) {
        fprintf(stderr, "Error: Service not found\n");
        free(response.data);
        return 1;
    }

    if (http_code == 428) {
        int result = handle_sudo_challenge(response.data, creds, "DELETE", url, path, NULL);
        free(response.data);
        return result;
    }

    if (http_code != 200) {
        fprintf(stderr, "Error: HTTP %ld\n", http_code);
        if (response.data) fprintf(stderr, "%s\n", response.data);
        free(response.data);
        return 1;
    }

    printf("\033[32mService destroyed successfully\033[0m\n");
    free(response.data);
    return 0;
}

// Lock a service to prevent deletion
static int lock_service(const UnsandboxCredentials *creds, const char *service_id) {
    CURL *curl = curl_easy_init();
    if (!curl) return 1;

    struct ResponseBuffer response = {0};
    response.data = malloc(1);
    response.size = 0;

    char url[512];
    snprintf(url, sizeof(url), "%s/services/%s/lock", API_BASE, service_id);

    char path[256];
    snprintf(path, sizeof(path), "/services/%s/lock", service_id);

    const char *body = "{}";
    struct curl_slist *headers = NULL;
    headers = add_hmac_auth_headers(headers, creds, "POST", path, body);
    headers = curl_slist_append(headers, "Content-Type: application/json");

    curl_easy_setopt(curl, CURLOPT_URL, url);
    curl_easy_setopt(curl, CURLOPT_POST, 1L);
    curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body);
    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);

    CURLcode res = curl_easy_perform(curl);

    long http_code = 0;
    curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);

    curl_slist_free_all(headers);
    curl_easy_cleanup(curl);

    if (res != CURLE_OK) {
        fprintf(stderr, "Error: %s\n", curl_easy_strerror(res));
        free(response.data);
        return 1;
    }

    if (http_code == 404) {
        fprintf(stderr, "Error: Service not found\n");
        free(response.data);
        return 1;
    }

    if (http_code != 200) {
        fprintf(stderr, "Error: HTTP %ld\n", http_code);
        if (response.data) fprintf(stderr, "%s\n", response.data);
        free(response.data);
        return 1;
    }

    printf("\033[32mService locked successfully\033[0m\n");
    free(response.data);
    return 0;
}

// Unlock a service to allow deletion
static int unlock_service(const UnsandboxCredentials *creds, const char *service_id) {
    CURL *curl = curl_easy_init();
    if (!curl) return 1;

    struct ResponseBuffer response = {0};
    response.data = malloc(1);
    response.size = 0;

    char url[512];
    snprintf(url, sizeof(url), "%s/services/%s/unlock", API_BASE, service_id);

    char path[256];
    snprintf(path, sizeof(path), "/services/%s/unlock", service_id);

    const char *body = "{}";
    struct curl_slist *headers = NULL;
    headers = add_hmac_auth_headers(headers, creds, "POST", path, body);
    headers = curl_slist_append(headers, "Content-Type: application/json");

    curl_easy_setopt(curl, CURLOPT_URL, url);
    curl_easy_setopt(curl, CURLOPT_POST, 1L);
    curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body);
    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);

    CURLcode res = curl_easy_perform(curl);

    long http_code = 0;
    curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);

    curl_slist_free_all(headers);
    curl_easy_cleanup(curl);

    if (res != CURLE_OK) {
        fprintf(stderr, "Error: %s\n", curl_easy_strerror(res));
        free(response.data);
        return 1;
    }

    if (http_code == 404) {
        fprintf(stderr, "Error: Service not found\n");
        free(response.data);
        return 1;
    }

    if (http_code == 428) {
        int result = handle_sudo_challenge(response.data, creds, "POST", url, path, body);
        free(response.data);
        return result;
    }

    if (http_code != 200) {
        fprintf(stderr, "Error: HTTP %ld\n", http_code);
        if (response.data) fprintf(stderr, "%s\n", response.data);
        free(response.data);
        return 1;
    }

    printf("\033[32mService unlocked successfully\033[0m\n");
    free(response.data);
    return 0;
}

// Update custom domains for a service
// action: "custom_domains" (full replace), "add", or "remove"
// domains: comma-separated domain list
static int update_service_domains(const UnsandboxCredentials *creds, const char *service_id, const char *action, const char *domains) {
    CURL *curl = curl_easy_init();
    if (!curl) return 1;

    struct ResponseBuffer response = {0};
    response.data = malloc(1);
    response.size = 0;

    char url[512];
    snprintf(url, sizeof(url), "%s/services/%s/domains", API_BASE, service_id);

    char path[256];
    snprintf(path, sizeof(path), "/services/%s/domains", service_id);

    // Build JSON payload: {"action": ["domain1", "domain2"]}
    size_t payload_size = strlen(domains) * 2 + 256;
    char *payload = malloc(payload_size);
    if (!payload) {
        curl_easy_cleanup(curl);
        return 1;
    }
    char *p = payload;
    p += sprintf(p, "{\"%s\":[", action);

    char *domains_copy = strdup(domains);
    char *token = strtok(domains_copy, ",");
    int first = 1;
    while (token) {
        while (*token == ' ') token++;
        char *end = token + strlen(token) - 1;
        while (end > token && *end == ' ') *end-- = '\0';
        if (!first) p += sprintf(p, ",");
        char *esc = escape_json_string(token);
        p += sprintf(p, "\"%s\"", esc);
        free(esc);
        first = 0;
        token = strtok(NULL, ",");
    }
    free(domains_copy);
    p += sprintf(p, "]}");

    struct curl_slist *headers = NULL;
    headers = add_hmac_auth_headers(headers, creds, "PUT", path, payload);
    headers = curl_slist_append(headers, "Content-Type: application/json");

    curl_easy_setopt(curl, CURLOPT_URL, url);
    curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "PUT");
    curl_easy_setopt(curl, CURLOPT_POSTFIELDS, payload);
    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);

    CURLcode res = curl_easy_perform(curl);

    long http_code = 0;
    curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);

    curl_slist_free_all(headers);
    curl_easy_cleanup(curl);
    free(payload);

    if (res != CURLE_OK) {
        fprintf(stderr, "Error: %s\n", curl_easy_strerror(res));
        free(response.data);
        return 1;
    }

    if (http_code == 404) {
        fprintf(stderr, "Error: Service not found\n");
        free(response.data);
        return 1;
    }

    if (http_code == 403) {
        fprintf(stderr, "Error: Not authorized to modify this service\n");
        free(response.data);
        return 1;
    }

    if (http_code != 200) {
        fprintf(stderr, "Error: HTTP %ld\n", http_code);
        if (response.data) fprintf(stderr, "%s\n", response.data);
        free(response.data);
        return 1;
    }

    printf("\033[32mCustom domains updated successfully\033[0m\n");
    // Print resulting domains from response
    if (response.data) {
        // Quick parse for custom_domains array
        char *cd = strstr(response.data, "\"custom_domains\"");
        if (cd) {
            char *start = strchr(cd, '[');
            char *end = start ? strchr(start, ']') : NULL;
            if (start && end) {
                char tmp = *(end + 1);
                *(end + 1) = '\0';
                printf("Domains: %s\n", start);
                *(end + 1) = tmp;
            }
        }
    }
    free(response.data);
    return 0;
}

// Set unfreeze_on_demand for a service
static int set_unfreeze_on_demand(const UnsandboxCredentials *creds, const char *service_id, int enabled) {
    CURL *curl = curl_easy_init();
    if (!curl) return 1;

    struct ResponseBuffer response = {0};
    response.data = malloc(1);
    response.size = 0;

    char url[512];
    snprintf(url, sizeof(url), "%s/services/%s", API_BASE, service_id);

    char path[256];
    snprintf(path, sizeof(path), "/services/%s", service_id);

    char body[128];
    snprintf(body, sizeof(body), "{\"unfreeze_on_demand\":%s}", enabled ? "true" : "false");

    struct curl_slist *headers = NULL;
    headers = add_hmac_auth_headers(headers, creds, "PATCH", path, body);
    headers = curl_slist_append(headers, "Content-Type: application/json");

    curl_easy_setopt(curl, CURLOPT_URL, url);
    curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "PATCH");
    curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body);
    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);

    CURLcode res = curl_easy_perform(curl);

    long http_code = 0;
    curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);

    curl_slist_free_all(headers);
    curl_easy_cleanup(curl);

    if (res != CURLE_OK) {
        fprintf(stderr, "Error: %s\n", curl_easy_strerror(res));
        free(response.data);
        return 1;
    }

    if (http_code == 404) {
        fprintf(stderr, "Error: Service not found\n");
        free(response.data);
        return 1;
    }

    if (http_code != 200) {
        fprintf(stderr, "Error: HTTP %ld\n", http_code);
        if (response.data) fprintf(stderr, "%s\n", response.data);
        free(response.data);
        return 1;
    }

    printf("\033[32mAuto-unfreeze %s\033[0m\n", enabled ? "enabled" : "disabled");
    free(response.data);
    return 0;
}

// Set show_freeze_page for a service (controls whether frozen services show payment page or JSON error)
static int set_show_freeze_page(const UnsandboxCredentials *creds, const char *service_id, int enabled) {
    CURL *curl = curl_easy_init();
    if (!curl) return 1;

    struct ResponseBuffer response = {0};
    response.data = malloc(1);
    response.size = 0;

    char url[512];
    snprintf(url, sizeof(url), "%s/services/%s", API_BASE, service_id);

    char path[256];
    snprintf(path, sizeof(path), "/services/%s", service_id);

    char body[128];
    snprintf(body, sizeof(body), "{\"show_freeze_page\":%s}", enabled ? "true" : "false");

    struct curl_slist *headers = NULL;
    headers = add_hmac_auth_headers(headers, creds, "PATCH", path, body);
    headers = curl_slist_append(headers, "Content-Type: application/json");

    curl_easy_setopt(curl, CURLOPT_URL, url);
    curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "PATCH");
    curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body);
    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);

    CURLcode res = curl_easy_perform(curl);

    long http_code = 0;
    curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);

    curl_slist_free_all(headers);
    curl_easy_cleanup(curl);

    if (res != CURLE_OK) {
        fprintf(stderr, "Error: %s\n", curl_easy_strerror(res));
        free(response.data);
        return 1;
    }

    if (http_code == 404) {
        fprintf(stderr, "Error: Service not found\n");
        free(response.data);
        return 1;
    }

    if (http_code != 200) {
        fprintf(stderr, "Error: HTTP %ld\n", http_code);
        if (response.data) fprintf(stderr, "%s\n", response.data);
        free(response.data);
        return 1;
    }

    printf("\033[32mFreeze page %s\033[0m\n", enabled ? "enabled" : "disabled");
    free(response.data);
    return 0;
}

// Resize a service (change vCPU/memory live)
static int resize_service(const UnsandboxCredentials *creds, const char *service_id, int vcpu) {
    CURL *curl = curl_easy_init();
    if (!curl) return 1;

    struct ResponseBuffer response = {0};
    response.data = malloc(1);
    response.size = 0;

    char url[512];
    snprintf(url, sizeof(url), "%s/services/%s", API_BASE, service_id);

    char path[256];
    snprintf(path, sizeof(path), "/services/%s", service_id);

    char body[64];
    snprintf(body, sizeof(body), "{\"vcpu\":%d}", vcpu);

    struct curl_slist *headers = NULL;
    headers = add_hmac_auth_headers(headers, creds, "PATCH", path, body);
    headers = curl_slist_append(headers, "Content-Type: application/json");

    curl_easy_setopt(curl, CURLOPT_URL, url);
    curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "PATCH");
    curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body);
    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);

    CURLcode res = curl_easy_perform(curl);

    long http_code = 0;
    curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);

    curl_slist_free_all(headers);
    curl_easy_cleanup(curl);

    if (res != CURLE_OK) {
        fprintf(stderr, "Error: %s\n", curl_easy_strerror(res));
        free(response.data);
        return 1;
    }

    if (http_code == 404) {
        fprintf(stderr, "Error: Service not found\n");
        free(response.data);
        return 1;
    }

    if (http_code == 429) {
        fprintf(stderr, "Error: Cannot resize - would exceed tier concurrency limit\n");
        if (response.data) fprintf(stderr, "%s\n", response.data);
        free(response.data);
        return 1;
    }

    if (http_code == 400) {
        fprintf(stderr, "Error: Invalid vcpu value (must be 1-8)\n");
        if (response.data) fprintf(stderr, "%s\n", response.data);
        free(response.data);
        return 1;
    }

    if (http_code != 200) {
        fprintf(stderr, "Error: HTTP %ld\n", http_code);
        if (response.data) fprintf(stderr, "%s\n", response.data);
        free(response.data);
        return 1;
    }

    // Parse response to show details
    printf("\033[32mService resized to %d vCPU, %dGB RAM\033[0m\n", vcpu, vcpu * 2);
    if (response.data) {
        printf("Details: %s\n", response.data);
    }
    free(response.data);
    return 0;
}

// ============================================================================
// Environment Secrets Vault Functions
// ============================================================================

// Get environment vault status for a service
// Returns: 0 on success, 1 on error
static int service_env_status(const UnsandboxCredentials *creds, const char *service_id) {
    CURL *curl = curl_easy_init();
    if (!curl) return 1;

    struct ResponseBuffer response = {0};
    response.data = malloc(1);
    response.size = 0;

    char url[512];
    snprintf(url, sizeof(url), "%s/services/%s/env", API_BASE, service_id);

    char path[256];
    snprintf(path, sizeof(path), "/services/%s/env", service_id);

    struct curl_slist *headers = NULL;
    headers = add_hmac_auth_headers(headers, creds, "GET", path, NULL);

    curl_easy_setopt(curl, CURLOPT_URL, url);
    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);

    CURLcode res = curl_easy_perform(curl);

    long http_code = 0;
    curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);

    curl_slist_free_all(headers);
    curl_easy_cleanup(curl);

    if (res != CURLE_OK) {
        fprintf(stderr, "Error: %s\n", curl_easy_strerror(res));
        free(response.data);
        return 1;
    }

    if (http_code == 404) {
        fprintf(stderr, "Error: Service not found\n");
        free(response.data);
        return 1;
    }

    if (http_code != 200) {
        fprintf(stderr, "Error: HTTP %ld\n", http_code);
        if (response.data) fprintf(stderr, "%s\n", response.data);
        free(response.data);
        return 1;
    }

    // Parse response: {"has_vault": true, "updated_at": 123456, "count": 3}
    // Check for has_vault field
    const char *has_vault_str = strstr(response.data, "\"has_vault\":");
    int has_vault = 0;
    if (has_vault_str) {
        has_vault_str += 12;  // Skip "has_vault":
        while (*has_vault_str == ' ') has_vault_str++;
        has_vault = (strncmp(has_vault_str, "true", 4) == 0);
    }

    if (!has_vault) {
        printf("Vault exists: no\n");
        printf("Variable count: 0\n");
    } else {
        printf("Vault exists: yes\n");

        // Extract count
        long long count = extract_json_number(response.data, "count");
        if (count >= 0) {
            printf("Variable count: %lld\n", count);
        }

        // Extract updated_at
        long long updated_at = extract_json_number(response.data, "updated_at");
        if (updated_at > 0) {
            time_t ts = (time_t)updated_at;
            struct tm *tm_info = localtime(&ts);
            char time_buf[64];
            strftime(time_buf, sizeof(time_buf), "%Y-%m-%d %H:%M:%S", tm_info);
            printf("Last updated: %s\n", time_buf);
        }
    }

    free(response.data);
    return 0;
}

// Set environment vault for a service (PUT /services/:id/env)
// env_content: .env format string (KEY=VALUE\nKEY2=VALUE2\n...)
// Returns: 0 on success, 1 on error
static int service_env_set(const UnsandboxCredentials *creds, const char *service_id, const char *env_content) {
    if (!env_content || strlen(env_content) == 0) {
        fprintf(stderr, "Error: No environment content provided\n");
        return 1;
    }

    if (strlen(env_content) > MAX_ENV_CONTENT_SIZE) {
        fprintf(stderr, "Error: Environment content too large (max %d bytes)\n", MAX_ENV_CONTENT_SIZE);
        return 1;
    }

    CURL *curl = curl_easy_init();
    if (!curl) return 1;

    struct ResponseBuffer response = {0};
    response.data = malloc(1);
    response.size = 0;

    char url[512];
    snprintf(url, sizeof(url), "%s/services/%s/env", API_BASE, service_id);

    char path[256];
    snprintf(path, sizeof(path), "/services/%s/env", service_id);

    struct curl_slist *headers = NULL;
    headers = curl_slist_append(headers, "Content-Type: text/plain");
    headers = add_hmac_auth_headers(headers, creds, "PUT", path, env_content);

    curl_easy_setopt(curl, CURLOPT_URL, url);
    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
    curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "PUT");
    curl_easy_setopt(curl, CURLOPT_POSTFIELDS, env_content);
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);

    CURLcode res = curl_easy_perform(curl);

    long http_code = 0;
    curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);

    curl_slist_free_all(headers);
    curl_easy_cleanup(curl);

    if (res != CURLE_OK) {
        fprintf(stderr, "Error: %s\n", curl_easy_strerror(res));
        free(response.data);
        return 1;
    }

    if (http_code == 404) {
        fprintf(stderr, "Error: Service not found\n");
        free(response.data);
        return 1;
    }

    if (http_code != 200) {
        fprintf(stderr, "Error: HTTP %ld\n", http_code);
        if (response.data) fprintf(stderr, "%s\n", response.data);
        free(response.data);
        return 1;
    }

    // Extract count from response
    long long count = extract_json_number(response.data, "count");
    if (count >= 0) {
        printf("\033[32mEnvironment vault updated: %lld variable%s\033[0m\n",
               count, count == 1 ? "" : "s");
    } else {
        printf("\033[32mEnvironment vault updated\033[0m\n");
    }

    // Print note about taking effect
    char *message = extract_json_string(response.data, "message");
    if (message) {
        printf("%s\n", message);
        free(message);
    }

    free(response.data);
    return 0;
}

// Export environment vault for a service (POST /services/:id/env/export)
// HMAC auth proves ownership - returns .env format string
// Returns: 0 on success, 1 on error
static int service_env_export(const UnsandboxCredentials *creds, const char *service_id) {
    // HMAC auth proves ownership - no additional confirmation needed
    CURL *curl = curl_easy_init();
    if (!curl) return 1;

    struct ResponseBuffer response = {0};
    response.data = malloc(1);
    response.size = 0;

    char url[512];
    snprintf(url, sizeof(url), "%s/services/%s/env/export", API_BASE, service_id);

    char path[256];
    snprintf(path, sizeof(path), "/services/%s/env/export", service_id);

    struct curl_slist *headers = NULL;
    headers = curl_slist_append(headers, "Content-Type: application/json");
    headers = add_hmac_auth_headers(headers, creds, "POST", path, "");

    curl_easy_setopt(curl, CURLOPT_URL, url);
    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
    curl_easy_setopt(curl, CURLOPT_POST, 1L);
    curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, 0L);
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);

    CURLcode res = curl_easy_perform(curl);

    long http_code = 0;
    curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);

    curl_slist_free_all(headers);
    curl_easy_cleanup(curl);

    if (res != CURLE_OK) {
        fprintf(stderr, "Error: %s\n", curl_easy_strerror(res));
        free(response.data);
        return 1;
    }

    if (http_code == 404) {
        fprintf(stderr, "Error: Service not found or no vault exists\n");
        free(response.data);
        return 1;
    }

    if (http_code == 401 || http_code == 403) {
        fprintf(stderr, "Error: Not authorized\n");
        free(response.data);
        return 1;
    }

    if (http_code != 200) {
        fprintf(stderr, "Error: HTTP %ld\n", http_code);
        if (response.data) fprintf(stderr, "%s\n", response.data);
        free(response.data);
        return 1;
    }

    // Extract env content from response
    char *env_content = extract_json_string(response.data, "env");
    if (env_content) {
        printf("%s", env_content);
        // Ensure trailing newline
        if (strlen(env_content) > 0 && env_content[strlen(env_content) - 1] != '\n') {
            printf("\n");
        }
        free(env_content);
    }

    free(response.data);
    return 0;
}

// Delete environment vault for a service (DELETE /services/:id/env)
// Returns: 0 on success, 1 on error
static int service_env_delete(const UnsandboxCredentials *creds, const char *service_id) {
    CURL *curl = curl_easy_init();
    if (!curl) return 1;

    struct ResponseBuffer response = {0};
    response.data = malloc(1);
    response.size = 0;

    char url[512];
    snprintf(url, sizeof(url), "%s/services/%s/env", API_BASE, service_id);

    char path[256];
    snprintf(path, sizeof(path), "/services/%s/env", service_id);

    struct curl_slist *headers = NULL;
    headers = add_hmac_auth_headers(headers, creds, "DELETE", path, NULL);

    curl_easy_setopt(curl, CURLOPT_URL, url);
    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
    curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "DELETE");
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);

    CURLcode res = curl_easy_perform(curl);

    long http_code = 0;
    curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);

    curl_slist_free_all(headers);
    curl_easy_cleanup(curl);

    if (res != CURLE_OK) {
        fprintf(stderr, "Error: %s\n", curl_easy_strerror(res));
        free(response.data);
        return 1;
    }

    if (http_code == 404) {
        fprintf(stderr, "Error: Service not found or no vault exists\n");
        free(response.data);
        return 1;
    }

    if (http_code != 200) {
        fprintf(stderr, "Error: HTTP %ld\n", http_code);
        if (response.data) fprintf(stderr, "%s\n", response.data);
        free(response.data);
        return 1;
    }

    printf("\033[32mEnvironment vault deleted\033[0m\n");

    // Print note about taking effect
    char *message = extract_json_string(response.data, "message");
    if (message) {
        printf("%s\n", message);
        free(message);
    }

    free(response.data);
    return 0;
}

// Read .env file contents
// Returns: allocated string with file contents, or NULL on error
static char* read_env_file(const char *filename) {
    FILE *f = fopen(filename, "r");
    if (!f) {
        fprintf(stderr, "Error: Cannot open env file '%s'\n", filename);
        return NULL;
    }

    fseek(f, 0, SEEK_END);
    long fsize = ftell(f);
    fseek(f, 0, SEEK_SET);

    if (fsize > MAX_ENV_CONTENT_SIZE) {
        fprintf(stderr, "Error: Env file too large (max %d bytes)\n", MAX_ENV_CONTENT_SIZE);
        fclose(f);
        return NULL;
    }

    char *content = malloc(fsize + 1);
    if (!content) {
        fprintf(stderr, "Error: Out of memory\n");
        fclose(f);
        return NULL;
    }

    size_t read_size = fread(content, 1, fsize, f);
    content[read_size] = '\0';
    fclose(f);

    return content;
}

// Read env content from stdin until EOF
// Returns: allocated string with content, or NULL on error
static char* read_env_stdin(void) {
    size_t capacity = 4096;
    size_t size = 0;
    char *content = malloc(capacity);
    if (!content) return NULL;

    char buf[1024];
    while (fgets(buf, sizeof(buf), stdin)) {
        size_t len = strlen(buf);
        if (size + len + 1 > capacity) {
            capacity *= 2;
            if (capacity > MAX_ENV_CONTENT_SIZE) {
                fprintf(stderr, "Error: Input too large (max %d bytes)\n", MAX_ENV_CONTENT_SIZE);
                free(content);
                return NULL;
            }
            char *new_content = realloc(content, capacity);
            if (!new_content) {
                free(content);
                return NULL;
            }
            content = new_content;
        }
        memcpy(content + size, buf, len);
        size += len;
    }
    content[size] = '\0';
    return content;
}

// ============================================================================
// End Environment Secrets Vault Functions
// ============================================================================

// Redeploy a service (re-run bootstrap script)
// Bootstrap scripts should be idempotent for proper upgrade behavior
static int redeploy_service(const UnsandboxCredentials *creds, const char *service_id, const char *bootstrap) {
    CURL *curl = curl_easy_init();
    if (!curl) return 1;

    struct ResponseBuffer response = {0};
    response.data = malloc(1);
    response.size = 0;

    char url[512];
    snprintf(url, sizeof(url), "%s/services/%s/redeploy", API_BASE, service_id);

    char path[256];
    snprintf(path, sizeof(path), "/services/%s/redeploy", service_id);

    // Check if bootstrap is a file or URL
    char *bootstrap_content = NULL;
    const char *bootstrap_url = NULL;

    if (bootstrap && strlen(bootstrap) > 0) {
        if (strncmp(bootstrap, "http://", 7) == 0 ||
            strncmp(bootstrap, "https://", 8) == 0) {
            bootstrap_url = bootstrap;
        } else {
            // Try to read as file
            struct stat st;
            if (stat(bootstrap, &st) == 0 && S_ISREG(st.st_mode)) {
                size_t fsize;
                bootstrap_content = read_file(bootstrap, &fsize);
                if (!bootstrap_content) {
                    fprintf(stderr, "Error reading bootstrap file: %s\n", bootstrap);
                    curl_easy_cleanup(curl);
                    free(response.data);
                    return 1;
                }
                printf("Read bootstrap script (%zu bytes) from %s\n", fsize, bootstrap);
            } else {
                // Treat as inline command
                bootstrap_url = bootstrap;
            }
        }
    }

    // Calculate required payload size
    size_t payload_size = 256;  // Base size
    if (bootstrap_content) {
        payload_size += strlen(bootstrap_content) * 2 + 100;
    } else if (bootstrap_url) {
        payload_size += strlen(bootstrap_url) * 2 + 100;
    }

    // Build JSON payload manually (matching create_service pattern)
    char *payload = malloc(payload_size);
    if (!payload) {
        if (bootstrap_content) free(bootstrap_content);
        curl_easy_cleanup(curl);
        free(response.data);
        return 1;
    }
    char *p = payload;
    p += sprintf(p, "{");

    if (bootstrap_content) {
        char *esc_content = escape_json_string(bootstrap_content);
        p += sprintf(p, "\"bootstrap_content\":\"%s\"", esc_content);
        free(esc_content);
        free(bootstrap_content);
    } else if (bootstrap_url) {
        char *esc_url = escape_json_string(bootstrap_url);
        p += sprintf(p, "\"bootstrap\":\"%s\"", esc_url);
        free(esc_url);
    }

    p += sprintf(p, "}");

    struct curl_slist *headers = NULL;
    headers = curl_slist_append(headers, "Content-Type: application/json");
    headers = add_hmac_auth_headers(headers, creds, "POST", path, payload);

    curl_easy_setopt(curl, CURLOPT_URL, url);
    curl_easy_setopt(curl, CURLOPT_POSTFIELDS, payload);
    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);

    printf("Redeploying service '%s'...\n", service_id);
    CURLcode res = curl_easy_perform(curl);

    long http_code = 0;
    curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);

    curl_slist_free_all(headers);
    curl_easy_cleanup(curl);
    free(payload);

    if (res != CURLE_OK) {
        fprintf(stderr, "Error: %s\n", curl_easy_strerror(res));
        free(response.data);
        return 1;
    }

    if (http_code == 404) {
        fprintf(stderr, "Error: Service not found\n");
        free(response.data);
        return 1;
    }

    if (http_code == 400) {
        fprintf(stderr, "Error: No bootstrap script provided. Use --bootstrap option.\n");
        if (response.data) fprintf(stderr, "%s\n", response.data);
        free(response.data);
        return 1;
    }

    if (http_code != 200) {
        fprintf(stderr, "Error: HTTP %ld\n", http_code);
        if (response.data) fprintf(stderr, "%s\n", response.data);
        free(response.data);
        return 1;
    }

    printf("\033[32mRedeploy initiated successfully\033[0m\n");
    printf("Note: Bootstrap scripts should be idempotent for proper upgrade behavior.\n");
    printf("Use 'un service --logs %s' to check progress.\n", service_id);
    free(response.data);
    return 0;
}

// Execute a command in a running service container
// Uses async job polling for long-running commands
// Optional input_files: files to upload before executing (written to /tmp/input/)
static int execute_service(const UnsandboxCredentials *creds, const char *service_id, const char *command, int timeout_ms, struct InputFile *input_files, int input_file_count) {
    CURL *curl = curl_easy_init();
    if (!curl) return 1;

    struct ResponseBuffer response = {0};
    response.data = malloc(1);
    response.size = 0;

    char url[512];
    snprintf(url, sizeof(url), "%s/services/%s/execute", API_BASE, service_id);

    char path[256];
    snprintf(path, sizeof(path), "/services/%s/execute", service_id);

    // Build JSON payload
    char *esc_command = escape_json_string(command);
    if (!esc_command) {
        curl_easy_cleanup(curl);
        free(response.data);
        return 1;
    }

    // Calculate payload size (base + files)
    size_t payload_size = 8192;
    for (int i = 0; i < input_file_count; i++) {
        payload_size += strlen(input_files[i].content_base64) + 256;
    }

    char *payload = malloc(payload_size);
    if (!payload) {
        free(esc_command);
        curl_easy_cleanup(curl);
        free(response.data);
        return 1;
    }

    char *p = payload;
    p += sprintf(p, "{\"command\":\"%s\",\"timeout\":%d", esc_command, timeout_ms);
    free(esc_command);

    // Add input files if provided
    if (input_file_count > 0) {
        p += sprintf(p, ",\"input_files\":[");
        for (int i = 0; i < input_file_count; i++) {
            if (i > 0) *p++ = ',';
            char *esc_filename = escape_json_string(input_files[i].filename);
            p += sprintf(p, "{\"filename\":\"%s\",\"content\":\"%s\"}",
                        esc_filename, input_files[i].content_base64);
            free(esc_filename);
        }
        p += sprintf(p, "]");
    }
    p += sprintf(p, "}");

    struct curl_slist *headers = NULL;
    headers = curl_slist_append(headers, "Content-Type: application/json");
    headers = add_hmac_auth_headers(headers, creds, "POST", path, payload);

    curl_easy_setopt(curl, CURLOPT_URL, url);
    curl_easy_setopt(curl, CURLOPT_POSTFIELDS, payload);
    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);

    CURLcode res = curl_easy_perform(curl);

    long http_code = 0;
    curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);

    curl_slist_free_all(headers);
    curl_easy_cleanup(curl);
    free(payload);

    if (res != CURLE_OK) {
        fprintf(stderr, "Error: %s\n", curl_easy_strerror(res));
        free(response.data);
        return 1;
    }

    if (http_code == 404) {
        fprintf(stderr, "Error: Service not found\n");
        free(response.data);
        return 1;
    }

    if (http_code == 409) {
        fprintf(stderr, "Error: Service is not running. Unfreeze it first with --unfreeze\n");
        free(response.data);
        return 1;
    }

    if (http_code != 202) {
        fprintf(stderr, "Error: HTTP %ld\n", http_code);
        if (response.data) fprintf(stderr, "%s\n", response.data);
        free(response.data);
        return 1;
    }

    // Extract job_id from response
    char *job_id = extract_json_string(response.data, "job_id");
    free(response.data);

    if (!job_id) {
        fprintf(stderr, "Error: No job_id in response\n");
        return 1;
    }

    fprintf(stderr, "job %s\n", job_id);

    // Poll for job completion using shared resilient poller
    char *final_response = poll_job_status(creds, job_id);

    if (!final_response) {
        free(job_id);
        return 1;
    }

    // Check terminal status
    char *status = extract_json_string(final_response, "status");
    int ret = 0;

    if (status && (strcmp(status, "failed") == 0 || strcmp(status, "cancelled") == 0)) {
        char *error = extract_json_string(final_response, "error");
        fprintf(stderr, "Error: Job %s: %s\n", status, error ? error : "unknown");
        if (error) free(error);
        ret = 1;
    } else {
        parse_and_print_response(final_response, 0, NULL, NULL);
    }

    if (status) free(status);
    free(final_response);
    free(job_id);
    return ret;
}

// Execute a command in a service and capture output (returns malloc'd string or NULL)
static char* execute_service_capture(const UnsandboxCredentials *creds, const char *service_id, const char *command, int timeout_ms) {
    CURL *curl = curl_easy_init();
    if (!curl) return NULL;

    struct ResponseBuffer response = {0};
    response.data = malloc(1);
    response.size = 0;

    char path[256];
    char url[512];
    snprintf(path, sizeof(path), "/services/%s/execute", service_id);
    snprintf(url, sizeof(url), "%s%s", API_BASE, path);

    char *esc_command = escape_json_string(command);
    if (!esc_command) {
        curl_easy_cleanup(curl);
        free(response.data);
        return NULL;
    }

    char payload[8192];
    snprintf(payload, sizeof(payload), "{\"command\":\"%s\",\"timeout\":%d}", esc_command, timeout_ms);
    free(esc_command);

    struct curl_slist *headers = NULL;
    headers = curl_slist_append(headers, "Content-Type: application/json");
    headers = add_hmac_auth_headers(headers, creds, "POST", path, payload);

    curl_easy_setopt(curl, CURLOPT_URL, url);
    curl_easy_setopt(curl, CURLOPT_POSTFIELDS, payload);
    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);

    CURLcode res = curl_easy_perform(curl);

    long http_code = 0;
    curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);

    curl_slist_free_all(headers);
    curl_easy_cleanup(curl);

    if (res != CURLE_OK || http_code != 202) {
        if (http_code == 409) {
            fprintf(stderr, "\033[34mError: Instance is not running\n\033[0m");
        }
        free(response.data);
        return NULL;
    }

    char *job_id = extract_json_string(response.data, "job_id");
    free(response.data);

    if (!job_id) return NULL;

    char job_path[256];
    char job_url[512];
    snprintf(job_path, sizeof(job_path), "/jobs/%s", job_id);
    snprintf(job_url, sizeof(job_url), "%s%s", API_BASE, job_path);

    int poll_count = 0;
    int max_polls = (timeout_ms / 1000) + 10;

    while (poll_count < max_polls) {
        usleep(500000);
        poll_count++;

        curl = curl_easy_init();
        if (!curl) {
            free(job_id);
            return NULL;
        }

        struct ResponseBuffer job_response = {0};
        job_response.data = malloc(1);
        job_response.size = 0;

        // Regenerate auth headers each poll to keep timestamp fresh
        headers = NULL;
        headers = add_hmac_auth_headers(headers, creds, "GET", job_path, NULL);

        curl_easy_setopt(curl, CURLOPT_URL, job_url);
        curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
        curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
        curl_easy_setopt(curl, CURLOPT_WRITEDATA, &job_response);

        res = curl_easy_perform(curl);
        curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);

        curl_slist_free_all(headers);
        curl_easy_cleanup(curl);

        if (res != CURLE_OK || http_code != 200) {
            free(job_response.data);
            continue;
        }

        char *status = extract_json_string(job_response.data, "status");
        if (status && strcmp(status, "completed") == 0) {
            // Extract stdout from result
            char *output = extract_json_string(job_response.data, "stdout");
            free(status);
            free(job_response.data);
            free(job_id);
            return output;  // Caller must free
        }

        if (status && (strcmp(status, "failed") == 0 || strcmp(status, "cancelled") == 0)) {
            free(status);
            free(job_response.data);
            free(job_id);
            return NULL;
        }

        if (status) free(status);
        free(job_response.data);
    }

    free(job_id);
    return NULL;
}

// ============================================================================
// Snapshot Management Support
// ============================================================================

// List all snapshots
static int list_snapshots(const UnsandboxCredentials *creds) {
    CURL *curl = curl_easy_init();
    if (!curl) return 1;

    struct ResponseBuffer response = {0};
    response.data = malloc(1);
    response.size = 0;

    char url[256];
    snprintf(url, sizeof(url), "%s/snapshots", API_BASE);

    struct curl_slist *headers = NULL;
    headers = add_hmac_auth_headers(headers, creds, "GET", "/snapshots", NULL);

    curl_easy_setopt(curl, CURLOPT_URL, url);
    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);

    CURLcode res = curl_easy_perform(curl);

    long http_code = 0;
    curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);

    curl_slist_free_all(headers);
    curl_easy_cleanup(curl);

    if (res != CURLE_OK) {
        fprintf(stderr, "Error: %s\n", curl_easy_strerror(res));
        free(response.data);
        return 1;
    }

    if (http_code == 403) {
        fprintf(stderr, "Error: Snapshots not available for free tier\n");
        free(response.data);
        return 1;
    }

    if (http_code != 200) {
        fprintf(stderr, "Error: HTTP %ld\n", http_code);
        if (response.data) fprintf(stderr, "%s\n", response.data);
        free(response.data);
        return 1;
    }

    // Parse and display snapshots
    if (!response.data || !strstr(response.data, "\"snapshots\"")) {
        fprintf(stderr, "Error: Invalid response\n");
        free(response.data);
        return 1;
    }

    // Extract count
    const char *count_str = strstr(response.data, "\"count\":");
    int count = 0;
    if (count_str) {
        count = atoi(count_str + 8);
    }

    if (count == 0) {
        printf("No snapshots found\n");
        free(response.data);
        return 0;
    }

    printf("Snapshots: %d\n\n", count);
    printf("%-40s %-20s %-12s %-30s %-8s\n", "SNAPSHOT ID", "NAME", "SOURCE TYPE", "SOURCE ID", "SIZE");
    printf("%-40s %-20s %-12s %-30s %-8s\n", "----------------------------------------",
           "--------------------", "------------", "------------------------------", "--------");

    // Parse snapshots array
    const char *snapshots_start = strstr(response.data, "\"snapshots\":[");
    if (snapshots_start) {
        const char *pos = snapshots_start + 13;

        while ((pos = strchr(pos, '{')) != NULL) {
            char *snapshot_id = extract_json_string(pos, "id");
            char *name = extract_json_string(pos, "name");
            char *source_type = extract_json_string(pos, "source_type");
            char *source_id = extract_json_string(pos, "source_id");
            long long size_bytes = extract_json_number(pos, "size_bytes");

            char size_str[16];
            format_bytes(size_bytes, size_str, sizeof(size_str));

            printf("%-40s %-20s %-12s %-30s %-8s\n",
                   snapshot_id ? snapshot_id : "-",
                   name ? name : "-",
                   source_type ? source_type : "-",
                   source_id ? source_id : "-",
                   size_str);

            if (snapshot_id) free(snapshot_id);
            if (name) free(name);
            if (source_type) free(source_type);
            if (source_id) free(source_id);

            // Move to next object
            int brace_depth = 1;
            pos++;
            while (*pos && brace_depth > 0) {
                if (*pos == '{') brace_depth++;
                else if (*pos == '}') brace_depth--;
                else if (*pos == '"') {
                    pos++;
                    while (*pos && !(*pos == '"' && *(pos-1) != '\\')) pos++;
                }
                pos++;
            }
            const char *next_obj = strchr(pos, '{');
            const char *end_arr = strchr(pos, ']');
            if (!next_obj || (end_arr && end_arr < next_obj)) break;
            pos = next_obj;
        }
    }

    free(response.data);
    return 0;
}

// Get snapshot info
static int get_snapshot_info(const UnsandboxCredentials *creds, const char *snapshot_id) {
    CURL *curl = curl_easy_init();
    if (!curl) return 1;

    struct ResponseBuffer response = {0};
    response.data = malloc(1);
    response.size = 0;

    char url[512];
    snprintf(url, sizeof(url), "%s/snapshots/%s", API_BASE, snapshot_id);

    char path[256];
    snprintf(path, sizeof(path), "/snapshots/%s", snapshot_id);

    struct curl_slist *headers = NULL;
    headers = add_hmac_auth_headers(headers, creds, "GET", path, NULL);

    curl_easy_setopt(curl, CURLOPT_URL, url);
    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);

    CURLcode res = curl_easy_perform(curl);

    long http_code = 0;
    curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);

    curl_slist_free_all(headers);
    curl_easy_cleanup(curl);

    if (res != CURLE_OK) {
        fprintf(stderr, "Error: %s\n", curl_easy_strerror(res));
        free(response.data);
        return 1;
    }

    if (http_code == 404) {
        fprintf(stderr, "Error: Snapshot not found\n");
        free(response.data);
        return 1;
    }

    if (http_code != 200) {
        fprintf(stderr, "Error: HTTP %ld\n", http_code);
        if (response.data) fprintf(stderr, "%s\n", response.data);
        free(response.data);
        return 1;
    }

    // Parse and display snapshot info
    char *id = extract_json_string(response.data, "id");
    char *name = extract_json_string(response.data, "name");
    char *source_type = extract_json_string(response.data, "source_type");
    char *source_id = extract_json_string(response.data, "source_id");
    char *container_name = extract_json_string(response.data, "container_name");
    char *status = extract_json_string(response.data, "status");
    long long size_bytes = extract_json_number(response.data, "size_bytes");
    long long created_at = extract_json_number(response.data, "created_at");

    char size_str[16];
    format_bytes(size_bytes, size_str, sizeof(size_str));

    printf("\033[1mSnapshot Details\033[0m\n\n");
    printf("%-20s %s\n", "Snapshot ID:", id ? id : "-");
    printf("%-20s %s\n", "Name:", name ? name : "-");
    printf("%-20s %s\n", "Source Type:", source_type ? source_type : "-");
    printf("%-20s %s\n", "Source ID:", source_id ? source_id : "-");
    printf("%-20s %s\n", "Container:", container_name ? container_name : "-");
    printf("%-20s %s\n", "Size:", size_str);
    printf("%-20s %s\n", "Status:", status ? status : "-");

    if (created_at > 0) {
        time_t t = (time_t)created_at;
        struct tm *tm_info = localtime(&t);
        char time_buf[64];
        strftime(time_buf, sizeof(time_buf), "%Y-%m-%d %H:%M:%S", tm_info);
        printf("%-20s %s\n", "Created:", time_buf);
    }

    if (id) free(id);
    if (name) free(name);
    if (source_type) free(source_type);
    if (source_id) free(source_id);
    if (container_name) free(container_name);
    if (status) free(status);

    free(response.data);
    return 0;
}

// Delete a snapshot
static int delete_snapshot(const UnsandboxCredentials *creds, const char *snapshot_id) {
    CURL *curl = curl_easy_init();
    if (!curl) return 1;

    struct ResponseBuffer response = {0};
    response.data = malloc(1);
    response.size = 0;

    char url[512];
    snprintf(url, sizeof(url), "%s/snapshots/%s", API_BASE, snapshot_id);

    char path[256];
    snprintf(path, sizeof(path), "/snapshots/%s", snapshot_id);

    struct curl_slist *headers = NULL;
    headers = add_hmac_auth_headers(headers, creds, "DELETE", path, NULL);

    curl_easy_setopt(curl, CURLOPT_URL, url);
    curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "DELETE");
    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);

    CURLcode res = curl_easy_perform(curl);

    long http_code = 0;
    curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);

    curl_slist_free_all(headers);
    curl_easy_cleanup(curl);

    if (res != CURLE_OK) {
        fprintf(stderr, "Error: %s\n", curl_easy_strerror(res));
        free(response.data);
        return 1;
    }

    if (http_code == 404) {
        fprintf(stderr, "Error: Snapshot not found\n");
        free(response.data);
        return 1;
    }

    if (http_code == 428) {
        int result = handle_sudo_challenge(response.data, creds, "DELETE", url, path, NULL);
        free(response.data);
        return result;
    }

    if (http_code != 200) {
        fprintf(stderr, "Error: HTTP %ld\n", http_code);
        if (response.data) fprintf(stderr, "%s\n", response.data);
        free(response.data);
        return 1;
    }

    printf("\033[32mSnapshot deleted successfully\033[0m\n");
    free(response.data);
    return 0;
}

// Lock a snapshot to prevent deletion
static int lock_snapshot(const UnsandboxCredentials *creds, const char *snapshot_id) {
    CURL *curl = curl_easy_init();
    if (!curl) return 1;

    struct ResponseBuffer response = {0};
    response.data = malloc(1);
    response.size = 0;

    char url[512];
    snprintf(url, sizeof(url), "%s/snapshots/%s/lock", API_BASE, snapshot_id);

    char path[256];
    snprintf(path, sizeof(path), "/snapshots/%s/lock", snapshot_id);

    const char *body = "{}";
    struct curl_slist *headers = NULL;
    headers = add_hmac_auth_headers(headers, creds, "POST", path, body);
    headers = curl_slist_append(headers, "Content-Type: application/json");

    curl_easy_setopt(curl, CURLOPT_URL, url);
    curl_easy_setopt(curl, CURLOPT_POST, 1L);
    curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body);
    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);

    CURLcode res = curl_easy_perform(curl);

    long http_code = 0;
    curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);

    curl_slist_free_all(headers);
    curl_easy_cleanup(curl);

    if (res != CURLE_OK) {
        fprintf(stderr, "Error: %s\n", curl_easy_strerror(res));
        free(response.data);
        return 1;
    }

    if (http_code == 404) {
        fprintf(stderr, "Error: Snapshot not found\n");
        free(response.data);
        return 1;
    }

    if (http_code != 200) {
        fprintf(stderr, "Error: HTTP %ld\n", http_code);
        if (response.data) fprintf(stderr, "%s\n", response.data);
        free(response.data);
        return 1;
    }

    printf("\033[32mSnapshot locked successfully\033[0m\n");
    free(response.data);
    return 0;
}

// Unlock a snapshot to allow deletion
static int unlock_snapshot(const UnsandboxCredentials *creds, const char *snapshot_id) {
    CURL *curl = curl_easy_init();
    if (!curl) return 1;

    struct ResponseBuffer response = {0};
    response.data = malloc(1);
    response.size = 0;

    char url[512];
    snprintf(url, sizeof(url), "%s/snapshots/%s/unlock", API_BASE, snapshot_id);

    char path[256];
    snprintf(path, sizeof(path), "/snapshots/%s/unlock", snapshot_id);

    const char *body = "{}";
    struct curl_slist *headers = NULL;
    headers = add_hmac_auth_headers(headers, creds, "POST", path, body);
    headers = curl_slist_append(headers, "Content-Type: application/json");

    curl_easy_setopt(curl, CURLOPT_URL, url);
    curl_easy_setopt(curl, CURLOPT_POST, 1L);
    curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body);
    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);

    CURLcode res = curl_easy_perform(curl);

    long http_code = 0;
    curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);

    curl_slist_free_all(headers);
    curl_easy_cleanup(curl);

    if (res != CURLE_OK) {
        fprintf(stderr, "Error: %s\n", curl_easy_strerror(res));
        free(response.data);
        return 1;
    }

    if (http_code == 404) {
        fprintf(stderr, "Error: Snapshot not found\n");
        free(response.data);
        return 1;
    }

    if (http_code == 428) {
        int result = handle_sudo_challenge(response.data, creds, "POST", url, path, body);
        free(response.data);
        return result;
    }

    if (http_code != 200) {
        fprintf(stderr, "Error: HTTP %ld\n", http_code);
        if (response.data) fprintf(stderr, "%s\n", response.data);
        free(response.data);
        return 1;
    }

    printf("\033[32mSnapshot unlocked successfully\033[0m\n");
    free(response.data);
    return 0;
}

// Create snapshot of a session
static int create_session_snapshot(const UnsandboxCredentials *creds, const char *session_id, const char *name, int hot) {
    CURL *curl = curl_easy_init();
    if (!curl) return 1;

    struct ResponseBuffer response = {0};
    response.data = malloc(1);
    response.size = 0;

    char url[512];
    snprintf(url, sizeof(url), "%s/sessions/%s/snapshot", API_BASE, session_id);

    char path[256];
    snprintf(path, sizeof(path), "/sessions/%s/snapshot", session_id);

    char payload[1024];
    if (name && strlen(name) > 0) {
        char *esc_name = escape_json_string(name);
        snprintf(payload, sizeof(payload), "{\"name\":\"%s\",\"hot\":%s}", esc_name, hot ? "true" : "false");
        free(esc_name);
    } else {
        snprintf(payload, sizeof(payload), "{\"hot\":%s}", hot ? "true" : "false");
    }

    struct curl_slist *headers = NULL;
    headers = curl_slist_append(headers, "Content-Type: application/json");
    headers = add_hmac_auth_headers(headers, creds, "POST", path, payload);

    curl_easy_setopt(curl, CURLOPT_URL, url);
    curl_easy_setopt(curl, CURLOPT_POSTFIELDS, payload);
    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);

    fprintf(stderr, "Creating snapshot of session %s...", session_id);
    fflush(stderr);

    CURLcode res = curl_easy_perform(curl);

    long http_code = 0;
    curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);

    curl_slist_free_all(headers);
    curl_easy_cleanup(curl);

    if (res != CURLE_OK) {
        fprintf(stderr, " failed\n");
        fprintf(stderr, "Error: %s\n", curl_easy_strerror(res));
        free(response.data);
        return 1;
    }

    if (http_code == 403) {
        fprintf(stderr, " failed\n");
        fprintf(stderr, "Error: Snapshots not available for free tier\n");
        free(response.data);
        return 1;
    }

    if (http_code == 404) {
        fprintf(stderr, " failed\n");
        fprintf(stderr, "Error: Session not found\n");
        free(response.data);
        return 1;
    }

    if (http_code != 200 && http_code != 201) {
        fprintf(stderr, " failed\n");
        fprintf(stderr, "Error: HTTP %ld\n", http_code);
        if (response.data) fprintf(stderr, "%s\n", response.data);
        free(response.data);
        return 1;
    }

    fprintf(stderr, " done\n");

    char *snapshot_id = extract_json_string(response.data, "id");
    if (snapshot_id) {
        printf("\033[32mSnapshot created successfully\033[0m\n");
        printf("Snapshot ID: %s\n", snapshot_id);
        free(snapshot_id);
    }

    free(response.data);
    return 0;
}

// Create snapshot of a service
static int create_service_snapshot(const UnsandboxCredentials *creds, const char *service_id, const char *name, int hot) {
    CURL *curl = curl_easy_init();
    if (!curl) return 1;

    struct ResponseBuffer response = {0};
    response.data = malloc(1);
    response.size = 0;

    char url[512];
    snprintf(url, sizeof(url), "%s/services/%s/snapshot", API_BASE, service_id);

    char path[256];
    snprintf(path, sizeof(path), "/services/%s/snapshot", service_id);

    char payload[1024];
    if (name && strlen(name) > 0) {
        char *esc_name = escape_json_string(name);
        snprintf(payload, sizeof(payload), "{\"name\":\"%s\",\"hot\":%s}", esc_name, hot ? "true" : "false");
        free(esc_name);
    } else {
        snprintf(payload, sizeof(payload), "{\"hot\":%s}", hot ? "true" : "false");
    }

    struct curl_slist *headers = NULL;
    headers = curl_slist_append(headers, "Content-Type: application/json");
    headers = add_hmac_auth_headers(headers, creds, "POST", path, payload);

    curl_easy_setopt(curl, CURLOPT_URL, url);
    curl_easy_setopt(curl, CURLOPT_POSTFIELDS, payload);
    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);

    fprintf(stderr, "Creating snapshot of service %s...", service_id);
    fflush(stderr);

    CURLcode res = curl_easy_perform(curl);

    long http_code = 0;
    curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);

    curl_slist_free_all(headers);
    curl_easy_cleanup(curl);

    if (res != CURLE_OK) {
        fprintf(stderr, " failed\n");
        fprintf(stderr, "Error: %s\n", curl_easy_strerror(res));
        free(response.data);
        return 1;
    }

    if (http_code == 403) {
        fprintf(stderr, " failed\n");
        fprintf(stderr, "Error: Snapshots not available for free tier\n");
        free(response.data);
        return 1;
    }

    if (http_code == 404) {
        fprintf(stderr, " failed\n");
        fprintf(stderr, "Error: Service not found\n");
        free(response.data);
        return 1;
    }

    if (http_code != 200 && http_code != 201) {
        fprintf(stderr, " failed\n");
        fprintf(stderr, "Error: HTTP %ld\n", http_code);
        if (response.data) fprintf(stderr, "%s\n", response.data);
        free(response.data);
        return 1;
    }

    fprintf(stderr, " done\n");

    char *snapshot_id = extract_json_string(response.data, "id");
    if (snapshot_id) {
        printf("\033[32mSnapshot created successfully\033[0m\n");
        printf("Snapshot ID: %s\n", snapshot_id);
        free(snapshot_id);
    }

    free(response.data);
    return 0;
}

// Restore session from snapshot
static int restore_from_snapshot(const UnsandboxCredentials *creds, const char *snapshot_id, const char *type) {
    CURL *curl = curl_easy_init();
    if (!curl) return 1;

    struct ResponseBuffer response = {0};
    response.data = malloc(1);
    response.size = 0;

    char url[512];
    snprintf(url, sizeof(url), "%s/snapshots/%s/restore", API_BASE, snapshot_id);

    char path[256];
    snprintf(path, sizeof(path), "/snapshots/%s/restore", snapshot_id);

    struct curl_slist *headers = NULL;
    headers = curl_slist_append(headers, "Content-Type: application/json");
    headers = add_hmac_auth_headers(headers, creds, "POST", path, "");

    curl_easy_setopt(curl, CURLOPT_URL, url);
    curl_easy_setopt(curl, CURLOPT_POST, 1L);
    curl_easy_setopt(curl, CURLOPT_POSTFIELDS, "");
    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);

    fprintf(stderr, "Restoring %s from snapshot %s...", type, snapshot_id);
    fflush(stderr);

    CURLcode res = curl_easy_perform(curl);

    long http_code = 0;
    curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);

    curl_slist_free_all(headers);
    curl_easy_cleanup(curl);

    if (res != CURLE_OK) {
        fprintf(stderr, " failed\n");
        fprintf(stderr, "Error: %s\n", curl_easy_strerror(res));
        free(response.data);
        return 1;
    }

    if (http_code == 404) {
        fprintf(stderr, " failed\n");
        fprintf(stderr, "Error: Snapshot not found\n");
        free(response.data);
        return 1;
    }

    if (http_code != 200) {
        fprintf(stderr, " failed\n");
        fprintf(stderr, "Error: HTTP %ld\n", http_code);
        if (response.data) fprintf(stderr, "%s\n", response.data);
        free(response.data);
        return 1;
    }

    fprintf(stderr, " done\n");
    printf("\033[32m%s restored from snapshot\033[0m\n", type);

    free(response.data);
    return 0;
}

// Clone from snapshot to create new session or service
static int clone_snapshot(const UnsandboxCredentials *creds, const char *snapshot_id, const char *clone_type,
                         const char *name, const char *shell, const char *ports) {
    CURL *curl = curl_easy_init();
    if (!curl) return 1;

    struct ResponseBuffer response = {0};
    response.data = malloc(1);
    response.size = 0;

    char url[512];
    snprintf(url, sizeof(url), "%s/snapshots/%s/clone", API_BASE, snapshot_id);

    char path[256];
    snprintf(path, sizeof(path), "/snapshots/%s/clone", snapshot_id);

    // Build payload
    char payload[2048];
    char *p = payload;
    p += sprintf(p, "{\"type\":\"%s\"", clone_type);

    if (name && strlen(name) > 0) {
        char *esc = escape_json_string(name);
        p += sprintf(p, ",\"name\":\"%s\"", esc);
        free(esc);
    }

    if (shell && strlen(shell) > 0) {
        char *esc = escape_json_string(shell);
        p += sprintf(p, ",\"shell\":\"%s\"", esc);
        free(esc);
    }

    if (ports && strlen(ports) > 0) {
        // Parse comma-separated ports into array
        p += sprintf(p, ",\"ports\":[");
        char *ports_copy = strdup(ports);
        char *tok = strtok(ports_copy, ",");
        int first = 1;
        while (tok) {
            if (!first) p += sprintf(p, ",");
            p += sprintf(p, "%d", atoi(tok));
            first = 0;
            tok = strtok(NULL, ",");
        }
        free(ports_copy);
        p += sprintf(p, "]");
    }

    p += sprintf(p, "}");

    struct curl_slist *headers = NULL;
    headers = curl_slist_append(headers, "Content-Type: application/json");
    headers = add_hmac_auth_headers(headers, creds, "POST", path, payload);

    curl_easy_setopt(curl, CURLOPT_URL, url);
    curl_easy_setopt(curl, CURLOPT_POSTFIELDS, payload);
    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);

    fprintf(stderr, "Cloning snapshot %s to create new %s...", snapshot_id, clone_type);
    fflush(stderr);

    CURLcode res = curl_easy_perform(curl);

    long http_code = 0;
    curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);

    curl_slist_free_all(headers);
    curl_easy_cleanup(curl);

    if (res != CURLE_OK) {
        fprintf(stderr, " failed\n");
        fprintf(stderr, "Error: %s\n", curl_easy_strerror(res));
        free(response.data);
        return 1;
    }

    if (http_code == 403) {
        fprintf(stderr, " failed\n");
        fprintf(stderr, "Error: Snapshots not available for free tier\n");
        free(response.data);
        return 1;
    }

    if (http_code == 404) {
        fprintf(stderr, " failed\n");
        fprintf(stderr, "Error: Snapshot not found\n");
        free(response.data);
        return 1;
    }

    if (http_code != 200 && http_code != 201) {
        fprintf(stderr, " failed\n");
        fprintf(stderr, "Error: HTTP %ld\n", http_code);
        if (response.data) fprintf(stderr, "%s\n", response.data);
        free(response.data);
        return 1;
    }

    fprintf(stderr, " done\n");

    if (strcmp(clone_type, "session") == 0) {
        char *session_id = extract_json_string(response.data, "session_id");
        if (session_id) {
            printf("\033[32mSession created from snapshot\033[0m\n");
            printf("Session ID: %s\n", session_id);
            free(session_id);
        }
    } else {
        char *service_id = extract_json_string(response.data, "service_id");
        if (service_id) {
            printf("\033[32mService created from snapshot\033[0m\n");
            printf("Service ID: %s\n", service_id);
            free(service_id);
        }
    }

    free(response.data);
    return 0;
}

// ============================================================================
// End Snapshot Management Support
// ============================================================================

// ============================================================================
// End Service Management Support
// ============================================================================

// ============================================================================
// LXD Container Images API
// ============================================================================

// Publish an image from a service or snapshot
static char* image_publish(const UnsandboxCredentials *creds, const char *source_type,
                           const char *source_id, const char *name, const char *description) {
    if (!creds || !creds->public_key || !creds->secret_key || !source_type || !source_id) {
        return NULL;
    }

    CURL *curl = curl_easy_init();
    if (!curl) return NULL;

    struct ResponseBuffer response = {0};
    response.data = malloc(1);
    response.size = 0;

    char url[256];
    snprintf(url, sizeof(url), "%s/images", API_BASE);

    // Build JSON body
    char body[2048];
    char *p = body;
    p += sprintf(p, "{\"source_type\":\"%s\",\"source_id\":\"%s\"", source_type, source_id);
    if (name && strlen(name) > 0) {
        char *esc = escape_json_string(name);
        p += sprintf(p, ",\"name\":\"%s\"", esc);
        free(esc);
    }
    if (description && strlen(description) > 0) {
        char *esc = escape_json_string(description);
        p += sprintf(p, ",\"description\":\"%s\"", esc);
        free(esc);
    }
    p += sprintf(p, "}");

    struct curl_slist *headers = NULL;
    headers = curl_slist_append(headers, "Content-Type: application/json");
    headers = add_hmac_auth_headers(headers, creds, "POST", "/images", body);

    curl_easy_setopt(curl, CURLOPT_URL, url);
    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
    curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body);
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);

    CURLcode res = curl_easy_perform(curl);
    curl_slist_free_all(headers);
    curl_easy_cleanup(curl);

    if (res != CURLE_OK) {
        free(response.data);
        return NULL;
    }

    return response.data;
}

// List images (filter_type can be NULL, "owned", "shared", or "public")
static char* list_images(const UnsandboxCredentials *creds, const char *filter_type) {
    if (!creds || !creds->public_key || !creds->secret_key) {
        return NULL;
    }

    CURL *curl = curl_easy_init();
    if (!curl) return NULL;

    struct ResponseBuffer response = {0};
    response.data = malloc(1);
    response.size = 0;

    char url[256];
    char path[128];
    if (filter_type && strlen(filter_type) > 0) {
        snprintf(url, sizeof(url), "%s/images/%s", API_BASE, filter_type);
        snprintf(path, sizeof(path), "/images/%s", filter_type);
    } else {
        snprintf(url, sizeof(url), "%s/images", API_BASE);
        snprintf(path, sizeof(path), "/images");
    }

    struct curl_slist *headers = NULL;
    headers = add_hmac_auth_headers(headers, creds, "GET", path, NULL);

    curl_easy_setopt(curl, CURLOPT_URL, url);
    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);

    CURLcode res = curl_easy_perform(curl);
    curl_slist_free_all(headers);
    curl_easy_cleanup(curl);

    if (res != CURLE_OK) {
        free(response.data);
        return NULL;
    }

    return response.data;
}

// Get image details
static char* get_image(const UnsandboxCredentials *creds, const char *image_id) {
    if (!creds || !creds->public_key || !creds->secret_key || !image_id) {
        return NULL;
    }

    CURL *curl = curl_easy_init();
    if (!curl) return NULL;

    struct ResponseBuffer response = {0};
    response.data = malloc(1);
    response.size = 0;

    char url[256];
    char path[128];
    snprintf(url, sizeof(url), "%s/images/%s", API_BASE, image_id);
    snprintf(path, sizeof(path), "/images/%s", image_id);

    struct curl_slist *headers = NULL;
    headers = add_hmac_auth_headers(headers, creds, "GET", path, NULL);

    curl_easy_setopt(curl, CURLOPT_URL, url);
    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);

    CURLcode res = curl_easy_perform(curl);
    curl_slist_free_all(headers);
    curl_easy_cleanup(curl);

    if (res != CURLE_OK) {
        free(response.data);
        return NULL;
    }

    return response.data;
}

// Delete an image
static int delete_image(const UnsandboxCredentials *creds, const char *image_id) {
    if (!creds || !creds->public_key || !creds->secret_key || !image_id) {
        return 1;
    }

    CURL *curl = curl_easy_init();
    if (!curl) return 1;

    struct ResponseBuffer response = {0};
    response.data = malloc(1);
    response.size = 0;

    char url[256];
    char path[128];
    snprintf(url, sizeof(url), "%s/images/%s", API_BASE, image_id);
    snprintf(path, sizeof(path), "/images/%s", image_id);

    struct curl_slist *headers = NULL;
    headers = add_hmac_auth_headers(headers, creds, "DELETE", path, NULL);

    curl_easy_setopt(curl, CURLOPT_URL, url);
    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
    curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "DELETE");
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);

    CURLcode res = curl_easy_perform(curl);

    long http_code = 0;
    curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);

    curl_slist_free_all(headers);
    curl_easy_cleanup(curl);

    if (res != CURLE_OK) {
        fprintf(stderr, "Error: %s\n", curl_easy_strerror(res));
        free(response.data);
        return 1;
    }

    if (http_code == 428) {
        int result = handle_sudo_challenge(response.data, creds, "DELETE", url, path, NULL);
        free(response.data);
        return result;
    }

    if (http_code != 200) {
        fprintf(stderr, "Error: HTTP %ld\n", http_code);
        if (response.data) fprintf(stderr, "%s\n", response.data);
        free(response.data);
        return 1;
    }

    free(response.data);
    return 0;
}

// Lock an image
static int lock_image(const UnsandboxCredentials *creds, const char *image_id) {
    if (!creds || !creds->public_key || !creds->secret_key || !image_id) {
        return 1;
    }

    CURL *curl = curl_easy_init();
    if (!curl) return 1;

    struct ResponseBuffer response = {0};
    response.data = malloc(1);
    response.size = 0;

    char url[256];
    char path[128];
    snprintf(url, sizeof(url), "%s/images/%s/lock", API_BASE, image_id);
    snprintf(path, sizeof(path), "/images/%s/lock", image_id);

    struct curl_slist *headers = NULL;
    headers = curl_slist_append(headers, "Content-Type: application/json");
    headers = add_hmac_auth_headers(headers, creds, "POST", path, "{}");

    curl_easy_setopt(curl, CURLOPT_URL, url);
    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
    curl_easy_setopt(curl, CURLOPT_POSTFIELDS, "{}");
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);

    CURLcode res = curl_easy_perform(curl);
    curl_slist_free_all(headers);
    curl_easy_cleanup(curl);

    free(response.data);
    return (res == CURLE_OK) ? 0 : 1;
}

// Unlock an image
static int unlock_image(const UnsandboxCredentials *creds, const char *image_id) {
    if (!creds || !creds->public_key || !creds->secret_key || !image_id) {
        return 1;
    }

    CURL *curl = curl_easy_init();
    if (!curl) return 1;

    struct ResponseBuffer response = {0};
    response.data = malloc(1);
    response.size = 0;

    char url[256];
    char path[128];
    snprintf(url, sizeof(url), "%s/images/%s/unlock", API_BASE, image_id);
    snprintf(path, sizeof(path), "/images/%s/unlock", image_id);

    struct curl_slist *headers = NULL;
    headers = curl_slist_append(headers, "Content-Type: application/json");
    headers = add_hmac_auth_headers(headers, creds, "POST", path, "{}");

    curl_easy_setopt(curl, CURLOPT_URL, url);
    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
    curl_easy_setopt(curl, CURLOPT_POSTFIELDS, "{}");
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);

    CURLcode res = curl_easy_perform(curl);

    long http_code = 0;
    curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);

    curl_slist_free_all(headers);
    curl_easy_cleanup(curl);

    if (res != CURLE_OK) {
        fprintf(stderr, "Error: %s\n", curl_easy_strerror(res));
        free(response.data);
        return 1;
    }

    if (http_code == 428) {
        int result = handle_sudo_challenge(response.data, creds, "POST", url, path, "{}");
        free(response.data);
        return result;
    }

    if (http_code != 200) {
        fprintf(stderr, "Error: HTTP %ld\n", http_code);
        if (response.data) fprintf(stderr, "%s\n", response.data);
        free(response.data);
        return 1;
    }

    free(response.data);
    return 0;
}

// Set image visibility (private, unlisted, public)
static int set_image_visibility(const UnsandboxCredentials *creds, const char *image_id, const char *visibility) {
    if (!creds || !creds->public_key || !creds->secret_key || !image_id || !visibility) {
        return 1;
    }

    CURL *curl = curl_easy_init();
    if (!curl) return 1;

    struct ResponseBuffer response = {0};
    response.data = malloc(1);
    response.size = 0;

    char url[256];
    char path[128];
    snprintf(url, sizeof(url), "%s/images/%s/visibility", API_BASE, image_id);
    snprintf(path, sizeof(path), "/images/%s/visibility", image_id);

    char body[128];
    snprintf(body, sizeof(body), "{\"visibility\":\"%s\"}", visibility);

    struct curl_slist *headers = NULL;
    headers = curl_slist_append(headers, "Content-Type: application/json");
    headers = add_hmac_auth_headers(headers, creds, "POST", path, body);

    curl_easy_setopt(curl, CURLOPT_URL, url);
    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
    curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body);
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);

    CURLcode res = curl_easy_perform(curl);
    curl_slist_free_all(headers);
    curl_easy_cleanup(curl);

    free(response.data);
    return (res == CURLE_OK) ? 0 : 1;
}

// Grant image access to another API key
__attribute__((unused))
static int grant_image_access(const UnsandboxCredentials *creds, const char *image_id, const char *trusted_api_key) {
    if (!creds || !creds->public_key || !creds->secret_key || !image_id || !trusted_api_key) {
        return 1;
    }

    CURL *curl = curl_easy_init();
    if (!curl) return 1;

    struct ResponseBuffer response = {0};
    response.data = malloc(1);
    response.size = 0;

    char url[256];
    char path[128];
    snprintf(url, sizeof(url), "%s/images/%s/grant", API_BASE, image_id);
    snprintf(path, sizeof(path), "/images/%s/grant", image_id);

    char body[256];
    snprintf(body, sizeof(body), "{\"trusted_api_key\":\"%s\"}", trusted_api_key);

    struct curl_slist *headers = NULL;
    headers = curl_slist_append(headers, "Content-Type: application/json");
    headers = add_hmac_auth_headers(headers, creds, "POST", path, body);

    curl_easy_setopt(curl, CURLOPT_URL, url);
    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
    curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body);
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);

    CURLcode res = curl_easy_perform(curl);
    curl_slist_free_all(headers);
    curl_easy_cleanup(curl);

    free(response.data);
    return (res == CURLE_OK) ? 0 : 1;
}

// Revoke image access from another API key
__attribute__((unused))
static int revoke_image_access(const UnsandboxCredentials *creds, const char *image_id, const char *trusted_api_key) {
    if (!creds || !creds->public_key || !creds->secret_key || !image_id || !trusted_api_key) {
        return 1;
    }

    CURL *curl = curl_easy_init();
    if (!curl) return 1;

    struct ResponseBuffer response = {0};
    response.data = malloc(1);
    response.size = 0;

    char url[256];
    char path[128];
    snprintf(url, sizeof(url), "%s/images/%s/revoke", API_BASE, image_id);
    snprintf(path, sizeof(path), "/images/%s/revoke", image_id);

    char body[256];
    snprintf(body, sizeof(body), "{\"trusted_api_key\":\"%s\"}", trusted_api_key);

    struct curl_slist *headers = NULL;
    headers = curl_slist_append(headers, "Content-Type: application/json");
    headers = add_hmac_auth_headers(headers, creds, "POST", path, body);

    curl_easy_setopt(curl, CURLOPT_URL, url);
    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
    curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body);
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);

    CURLcode res = curl_easy_perform(curl);
    curl_slist_free_all(headers);
    curl_easy_cleanup(curl);

    free(response.data);
    return (res == CURLE_OK) ? 0 : 1;
}

// List API keys with access to an image
__attribute__((unused))
static char* list_image_trusted(const UnsandboxCredentials *creds, const char *image_id) {
    if (!creds || !creds->public_key || !creds->secret_key || !image_id) {
        return NULL;
    }

    CURL *curl = curl_easy_init();
    if (!curl) return NULL;

    struct ResponseBuffer response = {0};
    response.data = malloc(1);
    response.size = 0;

    char url[256];
    char path[128];
    snprintf(url, sizeof(url), "%s/images/%s/trusted", API_BASE, image_id);
    snprintf(path, sizeof(path), "/images/%s/trusted", image_id);

    struct curl_slist *headers = NULL;
    headers = add_hmac_auth_headers(headers, creds, "GET", path, NULL);

    curl_easy_setopt(curl, CURLOPT_URL, url);
    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);

    CURLcode res = curl_easy_perform(curl);
    curl_slist_free_all(headers);
    curl_easy_cleanup(curl);

    if (res != CURLE_OK) {
        free(response.data);
        return NULL;
    }

    return response.data;
}

// Transfer image ownership to another API key
__attribute__((unused))
static int transfer_image(const UnsandboxCredentials *creds, const char *image_id, const char *to_api_key) {
    if (!creds || !creds->public_key || !creds->secret_key || !image_id || !to_api_key) {
        return 1;
    }

    CURL *curl = curl_easy_init();
    if (!curl) return 1;

    struct ResponseBuffer response = {0};
    response.data = malloc(1);
    response.size = 0;

    char url[256];
    char path[128];
    snprintf(url, sizeof(url), "%s/images/%s/transfer", API_BASE, image_id);
    snprintf(path, sizeof(path), "/images/%s/transfer", image_id);

    char body[256];
    snprintf(body, sizeof(body), "{\"to_api_key\":\"%s\"}", to_api_key);

    struct curl_slist *headers = NULL;
    headers = curl_slist_append(headers, "Content-Type: application/json");
    headers = add_hmac_auth_headers(headers, creds, "POST", path, body);

    curl_easy_setopt(curl, CURLOPT_URL, url);
    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
    curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body);
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);

    CURLcode res = curl_easy_perform(curl);
    curl_slist_free_all(headers);
    curl_easy_cleanup(curl);

    free(response.data);
    return (res == CURLE_OK) ? 0 : 1;
}

// Spawn a new service from an image
static char* spawn_from_image(const UnsandboxCredentials *creds, const char *image_id,
                              const char *name, const char *ports, const char *bootstrap,
                              const char *network_mode) {
    if (!creds || !creds->public_key || !creds->secret_key || !image_id) {
        return NULL;
    }

    CURL *curl = curl_easy_init();
    if (!curl) return NULL;

    struct ResponseBuffer response = {0};
    response.data = malloc(1);
    response.size = 0;

    char url[256];
    char path[128];
    snprintf(url, sizeof(url), "%s/images/%s/spawn", API_BASE, image_id);
    snprintf(path, sizeof(path), "/images/%s/spawn", image_id);

    // Build JSON body
    char body[4096];
    char *p = body;
    p += sprintf(p, "{");
    int need_comma = 0;

    if (name && strlen(name) > 0) {
        char *esc = escape_json_string(name);
        p += sprintf(p, "\"name\":\"%s\"", esc);
        free(esc);
        need_comma = 1;
    }
    if (ports && strlen(ports) > 0) {
        if (need_comma) p += sprintf(p, ",");
        p += sprintf(p, "\"ports\":\"%s\"", ports);
        need_comma = 1;
    }
    if (bootstrap && strlen(bootstrap) > 0) {
        if (need_comma) p += sprintf(p, ",");
        char *esc = escape_json_string(bootstrap);
        p += sprintf(p, "\"bootstrap\":\"%s\"", esc);
        free(esc);
        need_comma = 1;
    }
    if (network_mode && strlen(network_mode) > 0) {
        if (need_comma) p += sprintf(p, ",");
        p += sprintf(p, "\"network_mode\":\"%s\"", network_mode);
    }
    p += sprintf(p, "}");

    struct curl_slist *headers = NULL;
    headers = curl_slist_append(headers, "Content-Type: application/json");
    headers = add_hmac_auth_headers(headers, creds, "POST", path, body);

    curl_easy_setopt(curl, CURLOPT_URL, url);
    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
    curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body);
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);

    CURLcode res = curl_easy_perform(curl);
    curl_slist_free_all(headers);
    curl_easy_cleanup(curl);

    if (res != CURLE_OK) {
        free(response.data);
        return NULL;
    }

    return response.data;
}

// Clone an image to create a copy owned by the current user
static char* clone_image(const UnsandboxCredentials *creds, const char *image_id,
                         const char *name, const char *description) {
    if (!creds || !creds->public_key || !creds->secret_key || !image_id) {
        return NULL;
    }

    CURL *curl = curl_easy_init();
    if (!curl) return NULL;

    struct ResponseBuffer response = {0};
    response.data = malloc(1);
    response.size = 0;

    char url[256];
    char path[128];
    snprintf(url, sizeof(url), "%s/images/%s/clone", API_BASE, image_id);
    snprintf(path, sizeof(path), "/images/%s/clone", image_id);

    // Build JSON body
    char body[2048];
    char *p = body;
    p += sprintf(p, "{");
    int need_comma = 0;

    if (name && strlen(name) > 0) {
        char *esc = escape_json_string(name);
        p += sprintf(p, "\"name\":\"%s\"", esc);
        free(esc);
        need_comma = 1;
    }
    if (description && strlen(description) > 0) {
        if (need_comma) p += sprintf(p, ",");
        char *esc = escape_json_string(description);
        p += sprintf(p, "\"description\":\"%s\"", esc);
        free(esc);
    }
    p += sprintf(p, "}");

    struct curl_slist *headers = NULL;
    headers = curl_slist_append(headers, "Content-Type: application/json");
    headers = add_hmac_auth_headers(headers, creds, "POST", path, body);

    curl_easy_setopt(curl, CURLOPT_URL, url);
    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
    curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body);
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);

    CURLcode res = curl_easy_perform(curl);
    curl_slist_free_all(headers);
    curl_easy_cleanup(curl);

    if (res != CURLE_OK) {
        free(response.data);
        return NULL;
    }

    return response.data;
}

// ============================================================================
// End LXD Container Images API
// ============================================================================

// ============================================================================
// Key Validation Support
// ============================================================================

static int validate_api_key(const UnsandboxCredentials *creds) {
    if (!creds || !creds->public_key || !creds->secret_key) {
        fprintf(stderr, "Error: Both public and secret keys required for validation\n");
        return 1;
    }

    CURL *curl = curl_easy_init();
    if (!curl) return 1;

    struct ResponseBuffer response = {0};
    response.data = malloc(1);
    response.size = 0;

    char url[256];
    snprintf(url, sizeof(url), "%s/keys/validate", API_BASE);

    // Use HMAC authentication with empty body
    struct curl_slist *headers = NULL;
    headers = curl_slist_append(headers, "Content-Type: application/json");
    headers = add_hmac_auth_headers(headers, creds, "POST", "/keys/validate", "");

    curl_easy_setopt(curl, CURLOPT_URL, url);
    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
    curl_easy_setopt(curl, CURLOPT_POST, 1L);
    curl_easy_setopt(curl, CURLOPT_POSTFIELDS, "");
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);

    CURLcode res = curl_easy_perform(curl);

    long http_code = 0;
    curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);

    curl_slist_free_all(headers);
    curl_easy_cleanup(curl);

    if (res != CURLE_OK) {
        fprintf(stderr, "Error: %s\n", curl_easy_strerror(res));
        free(response.data);
        return 1;
    }

    if (http_code != 200) {
        // Parse error response
        if (response.data) {
            char *error = extract_json_string(response.data, "error");
            char *reason = extract_json_string(response.data, "reason");
            if (error) {
                printf("\033[31mInvalid\033[0m: %s\n", error);
                free(error);
            } else if (reason) {
                if (strcmp(reason, "invalid_key") == 0) {
                    printf("\033[31mInvalid\033[0m: key not found\n");
                } else if (strcmp(reason, "expired") == 0) {
                    printf("\033[31mExpired\033[0m\n\n");

                    // Show key details if available
                    char *public_key = extract_json_string(response.data, "public_key");
                    long long tier = extract_json_number(response.data, "tier");
                    char *expired_at = extract_json_string(response.data, "expired_at_datetime");
                    char *expired_ago = extract_json_string(response.data, "expired_ago");
                    char *renew_url = extract_json_string(response.data, "renew_url");

                    if (public_key) {
                        printf("%-20s %s\n", "Public Key:", public_key);
                        free(public_key);
                    }
                    if (tier >= 0) {
                        printf("%-20s %lld\n", "Tier:", tier);
                    }
                    if (expired_at) {
                        printf("%-20s %s", "Expired:", expired_at);
                        if (expired_ago) {
                            printf(" (%s)", expired_ago);
                        }
                        printf("\n");
                        free(expired_at);
                    }
                    if (expired_ago) free(expired_ago);

                    printf("\n\033[33mTo renew:\033[0m Visit %s\n",
                           renew_url ? renew_url : "https://unsandbox.com/pricing");
                    if (renew_url) free(renew_url);
                } else if (strcmp(reason, "suspended") == 0) {
                    printf("\033[31mSuspended\033[0m: key has been suspended\n");
                } else {
                    printf("\033[31mInvalid\033[0m: %s\n", reason);
                }
                free(reason);
            } else {
                printf("\033[31mInvalid\033[0m: HTTP %ld\n", http_code);
            }
        }
        free(response.data);
        return 1;
    }

    // Check for valid:false in 200 response
    const char *valid_check = strstr(response.data, "\"valid\":false");
    if (valid_check) {
        char *reason = extract_json_string(response.data, "reason");
        if (reason) {
            if (strcmp(reason, "invalid_key") == 0) {
                printf("\033[31mInvalid\033[0m: key not found\n");
            } else if (strcmp(reason, "expired") == 0) {
                printf("\033[31mExpired\033[0m\n\n");

                // Show key details if available
                char *public_key = extract_json_string(response.data, "public_key");
                long long tier = extract_json_number(response.data, "tier");
                char *expired_at = extract_json_string(response.data, "expired_at_datetime");
                char *expired_ago = extract_json_string(response.data, "expired_ago");
                char *renew_url = extract_json_string(response.data, "renew_url");

                if (public_key) {
                    printf("%-20s %s\n", "Public Key:", public_key);
                    free(public_key);
                }
                if (tier >= 0) {
                    printf("%-20s %lld\n", "Tier:", tier);
                }
                if (expired_at) {
                    printf("%-20s %s", "Expired:", expired_at);
                    if (expired_ago) {
                        printf(" (%s)", expired_ago);
                    }
                    printf("\n");
                    free(expired_at);
                }
                if (expired_ago) free(expired_ago);

                printf("\n\033[33mTo renew:\033[0m Visit %s\n",
                       renew_url ? renew_url : "https://unsandbox.com/pricing");
                if (renew_url) free(renew_url);
            } else if (strcmp(reason, "suspended") == 0) {
                printf("\033[31mSuspended\033[0m: key has been suspended\n");
            } else {
                printf("\033[31mInvalid\033[0m: %s\n", reason);
            }
            free(reason);
        } else {
            printf("\033[31mInvalid key\033[0m\n");
        }
        free(response.data);
        return 1;
    }

    // Parse valid response
    if (!response.data) {
        fprintf(stderr, "Error: Empty response\n");
        return 1;
    }

    // Check if valid
    const char *valid_str = strstr(response.data, "\"valid\":");
    int valid = 0;
    if (valid_str) {
        valid = (strstr(valid_str, "true") == valid_str + 8);
    }

    if (!valid) {
        printf("\033[31mInvalid key\033[0m\n");
        free(response.data);
        return 1;
    }

    // Extract fields
    long long tier = extract_json_number(response.data, "tier");
    char *status = extract_json_string(response.data, "status");
    char *valid_through = extract_json_string(response.data, "valid_through_datetime");
    char *valid_for = extract_json_string(response.data, "valid_for_human");
    char *public_key = extract_json_string(response.data, "public_key");
    long long rate_per_minute = extract_json_number(response.data, "rate_per_minute");
    long long burst = extract_json_number(response.data, "burst");
    long long concurrency = extract_json_number(response.data, "concurrency");

    // Display key info
    printf("\033[32mValid\033[0m\n\n");
    printf("%-20s %s\n", "Public Key:", public_key ? public_key : "N/A");
    printf("%-20s %lld\n", "Tier:", tier);
    printf("%-20s %s\n", "Status:", status ? status : "N/A");
    printf("%-20s %s\n", "Expires:", valid_through ? valid_through : "N/A");
    printf("%-20s %s\n", "Time Remaining:", valid_for ? valid_for : "N/A");
    printf("%-20s %lld/min\n", "Rate Limit:", rate_per_minute);
    printf("%-20s %lld\n", "Burst:", burst);
    printf("%-20s %lld\n", "Concurrency:", concurrency);

    if (status) free(status);
    if (valid_through) free(valid_through);
    if (valid_for) free(valid_for);
    if (public_key) free(public_key);
    free(response.data);

    return 0;
}

// ============================================================================
// End Key Validation Support
// ============================================================================

void print_usage(const char *prog) {
    fprintf(stderr, "Usage: %s [options] <source_file>\n", prog);
    fprintf(stderr, "       %s session [options]\n", prog);
    fprintf(stderr, "       %s service [options]\n", prog);
    fprintf(stderr, "       %s snapshot [options]\n", prog);
    fprintf(stderr, "       %s image [options]\n", prog);
    fprintf(stderr, "       %s languages [--json]\n", prog);
    fprintf(stderr, "       %s jobs [options]\n", prog);
    fprintf(stderr, "       %s paas <command> [options]\n", prog);
    fprintf(stderr, "       %s key\n\n", prog);
    fprintf(stderr, "Commands:\n");
    fprintf(stderr, "  (default)        Execute source file in sandbox\n");
    fprintf(stderr, "  session          Open interactive shell/REPL session\n");
    fprintf(stderr, "  service          Manage persistent services\n");
    fprintf(stderr, "  snapshot         Manage container snapshots\n");
    fprintf(stderr, "  image            Manage images (publish, spawn, clone)\n");
    fprintf(stderr, "  languages        List available languages (--json for JSON output)\n");
    fprintf(stderr, "  jobs             List, inspect, or cancel async jobs\n");
    fprintf(stderr, "  paas             PaaS platform management (logs, etc.)\n");
    fprintf(stderr, "  key              Check API key validity and expiration\n");
    fprintf(stderr, "\nOptions:\n");
    fprintf(stderr, "  -s, --shell LANG Specify language (default: bash if arg is not a file)\n");
    fprintf(stderr, "  -e KEY=VALUE     Set environment variable (can use multiple times)\n");
    fprintf(stderr, "  -f FILE          Add input file to /tmp/ (can use multiple times)\n");
    fprintf(stderr, "  -F FILE          Add input file with path preserved (can use multiple times)\n");
    fprintf(stderr, "  -a               Return and save artifacts (compiled binaries)\n");
    fprintf(stderr, "  -o DIR           Output directory for artifacts (default: current dir)\n");
    fprintf(stderr, "  -p KEY           Public key (or set UNSANDBOX_PUBLIC_KEY env var)\n");
    fprintf(stderr, "  -k KEY           Secret key (or set UNSANDBOX_SECRET_KEY env var)\n");
    fprintf(stderr, "  -n MODE          Network mode: zerotrust (default) or semitrusted\n");
    fprintf(stderr, "  -v N, --vcpu N   vCPU count 1-8, each vCPU gets 2GB RAM. Default: 1\n");
    fprintf(stderr, "  -y               Skip confirmation for large uploads (>1GB)\n");
    fprintf(stderr, "  -h               Show this help\n");
    fprintf(stderr, "\nSession options:\n");
    fprintf(stderr, "  -s, --shell SHELL  Shell/REPL to use (default: bash)\n");
    fprintf(stderr, "  -l, --list         List active sessions\n");
    fprintf(stderr, "  --attach ID        Reconnect to existing session (ID or container name)\n");
    fprintf(stderr, "  --kill ID          Terminate a session (ID or container name)\n");
    fprintf(stderr, "  --audit            Record session for auditing\n");
    fprintf(stderr, "  --tmux             Enable session persistence with tmux (allows reconnect)\n");
    fprintf(stderr, "  --screen           Enable session persistence with screen (allows reconnect)\n");
    fprintf(stderr, "  --snapshot ID      Create snapshot of session (paid tiers only)\n");
    fprintf(stderr, "  --restore SNAPSHOT Restore session from snapshot\n");
    fprintf(stderr, "  --snapshot-name N  Name for the snapshot\n");
    fprintf(stderr, "  --hot              Take snapshot without freezing (live snapshot)\n");
    fprintf(stderr, "\nService options:\n");
    fprintf(stderr, "  --name NAME        Service name (creates new service)\n");
    fprintf(stderr, "  --ports PORTS      Comma-separated ports (e.g., 80,443)\n");
    fprintf(stderr, "  --domains DOMAINS  Comma-separated custom domains (e.g., example.com,www.example.com)\n");
    fprintf(stderr, "  --type TYPE        Service type for SRV records (minecraft, mumble, teamspeak, source, tcp, udp)\n");
    fprintf(stderr, "  --golden-image IMG Use custom LXD image alias (for testing, e.g., jammy-golden-22.04)\n");
    fprintf(stderr, "  --bootstrap CMD    Bootstrap command or URI to run on startup\n");
    fprintf(stderr, "  --bootstrap-file FILE  Upload local file as bootstrap script content\n");
    fprintf(stderr, "  --unfreeze-on-demand   Enable auto-unfreeze when HTTP request arrives\n");
    fprintf(stderr, "  -e, --env KEY=VAL  Set environment variable (can repeat, stored encrypted)\n");
    fprintf(stderr, "  --env-file FILE    Load env vars from .env file (stored encrypted)\n");
    fprintf(stderr, "  -f FILE            Upload file to /tmp/ (can use multiple times)\n");
    fprintf(stderr, "  -F FILE            Upload file with path preserved (can use multiple times)\n");
    fprintf(stderr, "  -l, --list         List all services\n");
    fprintf(stderr, "  --info ID          Get service details\n");
    fprintf(stderr, "  --tail ID          Get last 9000 lines of bootstrap logs\n");
    fprintf(stderr, "  --logs ID          Get all bootstrap logs\n");
    fprintf(stderr, "  --download-logs ID FILE  Download all logs to file\n");
    fprintf(stderr, "  --freeze ID        Freeze a service\n");
    fprintf(stderr, "  --unfreeze ID      Unfreeze a service\n");
    fprintf(stderr, "  --destroy ID       Destroy a service\n");
    fprintf(stderr, "  --lock ID          Lock a service to prevent deletion\n");
    fprintf(stderr, "  --unlock ID        Unlock a service to allow deletion\n");
    fprintf(stderr, "  --auto-unfreeze ID     Enable auto-unfreeze on HTTP request\n");
    fprintf(stderr, "  --no-auto-unfreeze ID  Disable auto-unfreeze on HTTP request\n");
    fprintf(stderr, "  --show-freeze-page ID  Enable freeze page (show payment page when frozen)\n");
    fprintf(stderr, "  --no-show-freeze-page ID  Disable freeze page (return JSON error when frozen)\n");
    fprintf(stderr, "  --resize ID        Resize service vCPU/memory (requires --vcpu)\n");
    fprintf(stderr, "  --redeploy ID      Re-run bootstrap script (optional: --bootstrap or --bootstrap-file)\n");
    fprintf(stderr, "  --execute ID CMD   Run a command in a running service (use -f to upload files first)\n");
    fprintf(stderr, "  -t, --timeout SEC  Timeout for --execute in seconds (default: 30, 0=unlimited)\n");
    fprintf(stderr, "  --dump-bootstrap ID [FILE]  Dump bootstrap script (for migrations)\n");
    fprintf(stderr, "  --snapshot ID      Create snapshot of service (paid tiers only)\n");
    fprintf(stderr, "  --restore SNAPSHOT Restore service from snapshot\n");
    fprintf(stderr, "  --snapshot-name N  Name for the snapshot\n");
    fprintf(stderr, "  --hot              Take snapshot without freezing (live snapshot)\n");
    fprintf(stderr, "\nService environment vault:\n");
    fprintf(stderr, "  env status ID      Show vault status (exists, count, updated)\n");
    fprintf(stderr, "  env set ID         Set vault from --env-file FILE or stdin\n");
    fprintf(stderr, "  env export ID      Export vault contents to stdout\n");
    fprintf(stderr, "  env delete ID      Delete vault\n");
    fprintf(stderr, "\nSnapshot options:\n");
    fprintf(stderr, "  -l, --list         List all snapshots\n");
    fprintf(stderr, "  --info ID          Get snapshot details\n");
    fprintf(stderr, "  --delete ID        Delete a snapshot\n");
    fprintf(stderr, "  --lock ID          Lock a snapshot to prevent deletion\n");
    fprintf(stderr, "  --unlock ID        Unlock a snapshot to allow deletion\n");
    fprintf(stderr, "  --clone ID         Clone snapshot to new session/service\n");
    fprintf(stderr, "  --type TYPE        Clone type: session or service (for --clone)\n");
    fprintf(stderr, "  --name NAME        Name for cloned service (for --clone)\n");
    fprintf(stderr, "  --shell SHELL      Shell for cloned session (for --clone)\n");
    fprintf(stderr, "  --ports PORTS      Ports for cloned service (for --clone)\n");
    fprintf(stderr, "\nImage options:\n");
    fprintf(stderr, "  -l, --list         List all images\n");
    fprintf(stderr, "  --info ID          Get image details\n");
    fprintf(stderr, "  --delete ID        Delete an image\n");
    fprintf(stderr, "  --lock ID          Lock an image to prevent deletion\n");
    fprintf(stderr, "  --unlock ID        Unlock an image to allow deletion\n");
    fprintf(stderr, "  --publish ID       Publish image from service/snapshot (requires --source-type)\n");
    fprintf(stderr, "  --source-type TYPE Source type: service or snapshot (for --publish)\n");
    fprintf(stderr, "  --visibility ID V  Set visibility: private, unlisted, or public\n");
    fprintf(stderr, "  --spawn ID         Spawn new service from image\n");
    fprintf(stderr, "  --clone ID         Clone an image\n");
    fprintf(stderr, "  --name NAME        Name for spawned service or cloned image\n");
    fprintf(stderr, "  --ports PORTS      Ports for spawned service\n");
    fprintf(stderr, "\nAvailable shells/REPLs:\n");
    fprintf(stderr, "  Shells: bash, dash, sh, zsh, fish, ksh, tcsh, csh, elvish, xonsh, ash\n");
    fprintf(stderr, "  REPLs:  python3, bpython, ipython, node, ruby, irb, lua, php, perl\n");
    fprintf(stderr, "          guile, ghci, erl, iex, sbcl, clisp, r, julia, clojure\n");
    fprintf(stderr, "\nSession behavior:\n");
    fprintf(stderr, "  Default:    Session terminates immediately on disconnect (clean exit)\n");
    fprintf(stderr, "  --tmux:     Session persists on disconnect, reconnect with --attach\n");
    fprintf(stderr, "  --screen:   Session persists on disconnect, reconnect with --attach\n");
    fprintf(stderr, "\nExamples:\n");
    fprintf(stderr, "  %s script.py                       # execute Python script\n", prog);
    fprintf(stderr, "  %s -s bash 'echo hello'            # execute inline command\n", prog);
    fprintf(stderr, "  %s -e DEBUG=1 script.py            # with environment variable\n", prog);
    fprintf(stderr, "  %s -f data.csv process.py          # with input file\n", prog);
    fprintf(stderr, "  %s -a -o ./bin main.c              # save compiled artifacts\n", prog);
    fprintf(stderr, "  %s session                         # interactive bash (terminates on disconnect)\n", prog);
    fprintf(stderr, "  %s session --tmux                  # bash with tmux (can reconnect)\n", prog);
    fprintf(stderr, "  %s session --screen                # bash with screen (can reconnect)\n", prog);
    fprintf(stderr, "  %s session --list                  # list active sessions\n", prog);
    fprintf(stderr, "  %s session --kill sandbox-abc      # terminate a session\n", prog);
    fprintf(stderr, "  %s session --freeze sandbox-abc    # freeze session (requires --tmux/--screen)\n", prog);
    fprintf(stderr, "  %s session --unfreeze sandbox-abc  # unfreeze a frozen session\n", prog);
    fprintf(stderr, "  %s session --boost sandbox-abc     # boost to 2 vCPU, 4GB RAM\n", prog);
    fprintf(stderr, "  %s session --boost sandbox-abc --boost-vcpu 4  # 4 vCPU, 8GB RAM\n", prog);
    fprintf(stderr, "  %s session --unboost sandbox-abc   # return to base resources\n", prog);
    fprintf(stderr, "  %s session --attach sandbox-abc    # reconnect by container name\n", prog);
    fprintf(stderr, "  %s session --shell python3         # Python REPL\n", prog);
    fprintf(stderr, "  %s session --shell node --tmux     # Node.js REPL with reconnect\n", prog);
    fprintf(stderr, "  %s session -n semitrusted          # session with network access\n", prog);
    fprintf(stderr, "  %s session --audit -o ./logs       # record session for auditing\n", prog);
    fprintf(stderr, "  %s session -f data.csv             # session with input file in /tmp/\n", prog);
    fprintf(stderr, "  %s service --name web --ports 80,443 --bootstrap \"python3 -m http.server 80\"\n", prog);
    fprintf(stderr, "  %s service --name app --ports 8000 --bootstrap-file ./setup.sh\n", prog);
    fprintf(stderr, "  %s service --name app -f app.tar.gz --bootstrap-file ./setup.sh  # deploy tarball\n", prog);
    fprintf(stderr, "  %s service --name blog --ports 8000 --domains blog.example.com,www.example.com\n", prog);
    fprintf(stderr, "  %s service --list                  # list all services\n", prog);
    fprintf(stderr, "  %s service --info abc123           # get service details\n", prog);
    fprintf(stderr, "  %s service --logs abc123           # get bootstrap logs\n", prog);
    fprintf(stderr, "  %s service --freeze abc123         # freeze a service\n", prog);
    fprintf(stderr, "  %s service --unfreeze abc123       # unfreeze a service\n", prog);
    fprintf(stderr, "  %s service --resize abc123 --vcpu 4  # scale to 4 vCPU, 8GB RAM\n", prog);
    fprintf(stderr, "  %s service --destroy abc123        # destroy a service\n", prog);
    fprintf(stderr, "  %s service --redeploy abc123 --bootstrap-file ./script.sh\n", prog);
    fprintf(stderr, "  %s service --redeploy abc123       # uses stored encrypted bootstrap\n", prog);
    fprintf(stderr, "  %s service --execute maldoror 'journalctl -u myapp -n 50'\n", prog);
    fprintf(stderr, "  %s service -f data.txt --execute myapp 'cat /tmp/input/data.txt'  # upload then run\n", prog);
    fprintf(stderr, "  %s service --dump-bootstrap maldoror  # print bootstrap to stdout\n", prog);
    fprintf(stderr, "  %s service --dump-bootstrap maldoror backup.sh  # save to file\n", prog);
    fprintf(stderr, "  %s service --name app -e API_KEY=secret -e DEBUG=1  # with env vars\n", prog);
    fprintf(stderr, "  %s service --name app --env-file .env  # with env file\n", prog);
    fprintf(stderr, "  %s service env status myapp        # check vault status\n", prog);
    fprintf(stderr, "  %s service env set myapp -e KEY=val -e SECRET=xxx  # set from flags\n", prog);
    fprintf(stderr, "  %s service env set myapp --env-file .env  # set vault from file\n", prog);
    fprintf(stderr, "  %s service env set myapp < .env    # set vault from stdin\n", prog);
    fprintf(stderr, "  %s service env export myapp        # export vault contents\n", prog);
    fprintf(stderr, "  %s service env delete myapp        # delete vault\n", prog);
    fprintf(stderr, "  %s service --snapshot abc123       # create snapshot of service\n", prog);
    fprintf(stderr, "  %s service --restore unsb-snapshot-xxxx  # restore service\n", prog);
    fprintf(stderr, "  %s session --snapshot abc123       # create snapshot of session\n", prog);
    fprintf(stderr, "  %s session --restore unsb-snapshot-xxxx  # restore session\n", prog);
    fprintf(stderr, "  %s snapshot --list                 # list all snapshots\n", prog);
    fprintf(stderr, "  %s snapshot --info unsb-snapshot-xxxx  # get snapshot details\n", prog);
    fprintf(stderr, "  %s snapshot --delete unsb-snapshot-xxxx  # delete a snapshot\n", prog);
    fprintf(stderr, "  %s snapshot --clone unsb-snapshot-xxxx --type service --name myapp\n", prog);
    fprintf(stderr, "  %s jobs                                  # list all jobs\n", prog);
    fprintf(stderr, "  %s jobs --get JOB_ID                    # get job status and result\n", prog);
    fprintf(stderr, "  %s jobs --cancel JOB_ID                 # cancel a running job\n", prog);
    fprintf(stderr, "  %s paas logs                             # last 100 lines from all sources\n", prog);
    fprintf(stderr, "  %s paas logs --api -n 500               # last 500 API log lines\n", prog);
    fprintf(stderr, "  %s paas logs --portal --grep error       # portal logs matching 'error'\n", prog);
    fprintf(stderr, "  %s paas logs --pool cammy --since 1h     # cammy pool logs from last hour\n", prog);
    fprintf(stderr, "  %s paas logs -l warning                  # warnings and above\n", prog);
    fprintf(stderr, "  %s paas logs --follow                    # follow all logs in real-time\n", prog);
    fprintf(stderr, "  %s paas logs --follow -l warning         # follow warnings+ in real-time\n", prog);
    fprintf(stderr, "  %s key                             # check API key validity\n", prog);
    fprintf(stderr, "  %s key --extend                    # open portal to extend key\n", prog);
    fprintf(stderr, "\nAuthentication:\n");
    fprintf(stderr, "  Credentials are loaded in order of priority:\n");
    fprintf(stderr, "  1. -p and -k flags (public and secret key)\n");
    fprintf(stderr, "  2. UNSANDBOX_PUBLIC_KEY + UNSANDBOX_SECRET_KEY env vars\n");
    fprintf(stderr, "  3. ~/.unsandbox/accounts.csv (format: public_key,secret_key per line)\n");
    fprintf(stderr, "     Use --account N to select account by index (0-based, default: 0)\n");
    fprintf(stderr, "     Or set UNSANDBOX_ACCOUNT=N env var\n");
}

/* ============================================================================
 * LIBRARY API IMPLEMENTATION
 * These functions implement the un.h public API for library use.
 * ============================================================================ */

const char *unsandbox_version(void) {
    return "4.3.4";
}

const char *unsandbox_detect_language(const char *filename) {
    if (!filename) return NULL;
    return detect_language_from_extension(filename);
}

char *unsandbox_hmac_sign(const char *secret_key, const char *message) {
    if (!secret_key || !message) return NULL;
    return hmac_sha256_hex(secret_key, strlen(secret_key), message, strlen(message));
}

int unsandbox_health_check(void) {
    CURL *curl = curl_easy_init();
    if (!curl) return -1;

    curl_easy_setopt(curl, CURLOPT_URL, API_BASE "/health");
    curl_easy_setopt(curl, CURLOPT_TIMEOUT, 5L);
    curl_easy_setopt(curl, CURLOPT_NOBODY, 1L);

    CURLcode res = curl_easy_perform(curl);
    long http_code = 0;
    curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);
    curl_easy_cleanup(curl);

    if (res != CURLE_OK) return -1;
    return (http_code == 200) ? 1 : 0;
}

#ifdef UNSANDBOX_LIBRARY
/* ============================================================================
 * Library API - Only compiled when UNSANDBOX_LIBRARY is defined
 * ============================================================================ */

/* Memory management */
void unsandbox_free_result(unsandbox_result_t *result) {
    if (!result) return;
    free(result->stdout_str);
    free(result->stderr_str);
    free(result->language);
    free(result->error_message);
    free(result);
}

void unsandbox_free_job(unsandbox_job_t *job) {
    if (!job) return;
    free(job->id);
    free(job->language);
    free(job->status);
    free(job->error_message);
    free(job);
}

void unsandbox_free_job_list(unsandbox_job_list_t *jobs) {
    if (!jobs) return;
    for (size_t i = 0; i < jobs->count; i++) {
        free(jobs->jobs[i].id);
        free(jobs->jobs[i].language);
        free(jobs->jobs[i].status);
        free(jobs->jobs[i].error_message);
    }
    free(jobs->jobs);
    free(jobs);
}

void unsandbox_free_languages(unsandbox_languages_t *langs) {
    if (!langs) return;
    for (size_t i = 0; i < langs->count; i++) {
        free(langs->languages[i]);
    }
    free(langs->languages);
    free(langs);
}

void unsandbox_free_session(unsandbox_session_t *session) {
    if (!session) return;
    free(session->id);
    free(session->container_name);
    free(session->status);
    free(session->network_mode);
    free(session);
}

void unsandbox_free_session_list(unsandbox_session_list_t *sessions) {
    if (!sessions) return;
    for (size_t i = 0; i < sessions->count; i++) {
        free(sessions->sessions[i].id);
        free(sessions->sessions[i].container_name);
        free(sessions->sessions[i].status);
        free(sessions->sessions[i].network_mode);
    }
    free(sessions->sessions);
    free(sessions);
}

void unsandbox_free_service(unsandbox_service_t *service) {
    if (!service) return;
    free(service->id);
    free(service->name);
    free(service->status);
    free(service->container_name);
    free(service->network_mode);
    free(service->ports);
    free(service->domains);
    free(service);
}

void unsandbox_free_service_list(unsandbox_service_list_t *services) {
    if (!services) return;
    for (size_t i = 0; i < services->count; i++) {
        free(services->services[i].id);
        free(services->services[i].name);
        free(services->services[i].status);
        free(services->services[i].container_name);
        free(services->services[i].network_mode);
        free(services->services[i].ports);
        free(services->services[i].domains);
    }
    free(services->services);
    free(services);
}

void unsandbox_free_snapshot(unsandbox_snapshot_t *snapshot) {
    if (!snapshot) return;
    free(snapshot->id);
    free(snapshot->name);
    free(snapshot->type);
    free(snapshot->source_id);
    free(snapshot);
}

void unsandbox_free_snapshot_list(unsandbox_snapshot_list_t *snapshots) {
    if (!snapshots) return;
    for (size_t i = 0; i < snapshots->count; i++) {
        free(snapshots->snapshots[i].id);
        free(snapshots->snapshots[i].name);
        free(snapshots->snapshots[i].type);
        free(snapshots->snapshots[i].source_id);
    }
    free(snapshots->snapshots);
    free(snapshots);
}

void unsandbox_free_key_info(unsandbox_key_info_t *info) {
    if (!info) return;
    free(info->tier);
    free(info->error_message);
    free(info);
}

/* ============================================================================
 * Session Functions Implementation
 * ============================================================================ */

int unsandbox_session_destroy(const char *session_id,
                              const char *public_key, const char *secret_key) {
    UnsandboxCredentials *creds = get_credentials(public_key, secret_key, -1);
    if (!creds) return -1;
    int result = kill_session(creds, session_id);
    free_credentials(creds);
    return result;
}

int unsandbox_session_freeze(const char *session_id,
                             const char *public_key, const char *secret_key) {
    UnsandboxCredentials *creds = get_credentials(public_key, secret_key, -1);
    if (!creds) return -1;
    int result = freeze_session(creds, session_id);
    free_credentials(creds);
    return result;
}

int unsandbox_session_unfreeze(const char *session_id,
                               const char *public_key, const char *secret_key) {
    UnsandboxCredentials *creds = get_credentials(public_key, secret_key, -1);
    if (!creds) return -1;
    int result = unfreeze_session(creds, session_id);
    free_credentials(creds);
    return result;
}

int unsandbox_session_boost(const char *session_id, int vcpu,
                            const char *public_key, const char *secret_key) {
    UnsandboxCredentials *creds = get_credentials(public_key, secret_key, -1);
    if (!creds) return -1;
    int result = boost_session(creds, session_id, vcpu);
    free_credentials(creds);
    return result;
}

int unsandbox_session_unboost(const char *session_id,
                              const char *public_key, const char *secret_key) {
    UnsandboxCredentials *creds = get_credentials(public_key, secret_key, -1);
    if (!creds) return -1;
    int result = unboost_session(creds, session_id);
    free_credentials(creds);
    return result;
}

/* ============================================================================
 * Service Functions Implementation
 * ============================================================================ */

int unsandbox_service_destroy(const char *service_id,
                              const char *public_key, const char *secret_key) {
    UnsandboxCredentials *creds = get_credentials(public_key, secret_key, -1);
    if (!creds) return -1;
    int result = destroy_service(creds, service_id);
    free_credentials(creds);
    return result;
}

int unsandbox_service_freeze(const char *service_id,
                             const char *public_key, const char *secret_key) {
    UnsandboxCredentials *creds = get_credentials(public_key, secret_key, -1);
    if (!creds) return -1;
    int result = freeze_service(creds, service_id);
    free_credentials(creds);
    return result;
}

int unsandbox_service_unfreeze(const char *service_id,
                               const char *public_key, const char *secret_key) {
    UnsandboxCredentials *creds = get_credentials(public_key, secret_key, -1);
    if (!creds) return -1;
    int result = unfreeze_service(creds, service_id);
    free_credentials(creds);
    return result;
}

int unsandbox_service_lock(const char *service_id,
                           const char *public_key, const char *secret_key) {
    UnsandboxCredentials *creds = get_credentials(public_key, secret_key, -1);
    if (!creds) return -1;
    int result = lock_service(creds, service_id);
    free_credentials(creds);
    return result;
}

int unsandbox_service_unlock(const char *service_id,
                             const char *public_key, const char *secret_key) {
    UnsandboxCredentials *creds = get_credentials(public_key, secret_key, -1);
    if (!creds) return -1;
    int result = unlock_service(creds, service_id);
    free_credentials(creds);
    return result;
}

int unsandbox_service_set_unfreeze_on_demand(const char *service_id, int enabled,
                                             const char *public_key, const char *secret_key) {
    UnsandboxCredentials *creds = get_credentials(public_key, secret_key, -1);
    if (!creds) return -1;
    int result = set_unfreeze_on_demand(creds, service_id, enabled);
    free_credentials(creds);
    return result;
}

int unsandbox_service_redeploy(const char *service_id, const char *bootstrap,
                               const char *public_key, const char *secret_key) {
    UnsandboxCredentials *creds = get_credentials(public_key, secret_key, -1);
    if (!creds) return -1;
    int result = redeploy_service(creds, service_id, bootstrap);
    free_credentials(creds);
    return result;
}

char *unsandbox_service_logs(const char *service_id, int all_logs,
                             const char *public_key, const char *secret_key) {
    UnsandboxCredentials *creds = get_credentials(public_key, secret_key, -1);
    if (!creds) return NULL;
    char *result = get_service_logs(creds, service_id, all_logs);
    free_credentials(creds);
    return result;
}

int unsandbox_service_env_set(const char *service_id, const char *env_content,
                              const char *public_key, const char *secret_key) {
    UnsandboxCredentials *creds = get_credentials(public_key, secret_key, -1);
    if (!creds) return -1;
    int result = service_env_set(creds, service_id, env_content);
    free_credentials(creds);
    return result;
}

int unsandbox_service_env_delete(const char *service_id,
                                 const char *public_key, const char *secret_key) {
    UnsandboxCredentials *creds = get_credentials(public_key, secret_key, -1);
    if (!creds) return -1;
    int result = service_env_delete(creds, service_id);
    free_credentials(creds);
    return result;
}

int unsandbox_service_resize(const char *service_id, int vcpu,
                             const char *public_key, const char *secret_key) {
    UnsandboxCredentials *creds = get_credentials(public_key, secret_key, -1);
    if (!creds) return -1;
    int result = resize_service(creds, service_id, vcpu);
    free_credentials(creds);
    return result;
}

/* ============================================================================
 * Snapshot Functions Implementation
 * ============================================================================ */

int unsandbox_snapshot_delete(const char *snapshot_id,
                              const char *public_key, const char *secret_key) {
    UnsandboxCredentials *creds = get_credentials(public_key, secret_key, -1);
    if (!creds) return -1;
    int result = delete_snapshot(creds, snapshot_id);
    free_credentials(creds);
    return result;
}

int unsandbox_snapshot_lock(const char *snapshot_id,
                            const char *public_key, const char *secret_key) {
    UnsandboxCredentials *creds = get_credentials(public_key, secret_key, -1);
    if (!creds) return -1;
    int result = lock_snapshot(creds, snapshot_id);
    free_credentials(creds);
    return result;
}

int unsandbox_snapshot_unlock(const char *snapshot_id,
                              const char *public_key, const char *secret_key) {
    UnsandboxCredentials *creds = get_credentials(public_key, secret_key, -1);
    if (!creds) return -1;
    int result = unlock_snapshot(creds, snapshot_id);
    free_credentials(creds);
    return result;
}

int unsandbox_snapshot_restore(const char *snapshot_id,
                               const char *public_key, const char *secret_key) {
    UnsandboxCredentials *creds = get_credentials(public_key, secret_key, -1);
    if (!creds) return -1;
    int result = restore_from_snapshot(creds, snapshot_id, NULL);
    free_credentials(creds);
    return result;
}

/* ============================================================================
 * Key Validation Implementation
 * ============================================================================ */

unsandbox_key_info_t *unsandbox_validate_keys(const char *public_key, const char *secret_key) {
    UnsandboxCredentials *creds = get_credentials(public_key, secret_key, -1);
    if (!creds) return NULL;

    unsandbox_key_info_t *info = calloc(1, sizeof(unsandbox_key_info_t));
    if (!info) {
        free_credentials(creds);
        return NULL;
    }

    int result = validate_api_key(creds);
    info->valid = (result == 0) ? 1 : 0;

    free_credentials(creds);
    return info;
}

/* ============================================================================
 * PaaS Logs Library Implementation
 * ============================================================================ */

char *unsandbox_logs_fetch(
    const char *source,
    int lines,
    const char *since,
    const char *grep,
    const char *public_key, const char *secret_key) {

    UnsandboxCredentials *creds = get_credentials(public_key, secret_key, -1);
    if (!creds) { set_last_error("No credentials"); return NULL; }

    CURL *curl = curl_easy_init();
    if (!curl) { free_credentials(creds); set_last_error("curl init failed"); return NULL; }

    struct ResponseBuffer response = {0};
    response.data = malloc(1);
    response.size = 0;

    char path[256];
    if (!source || strcmp(source, "all") == 0)
        snprintf(path, sizeof(path), "/logs/all");
    else
        snprintf(path, sizeof(path), "/logs/%s", source);

    char url[1024];
    int off = snprintf(url, sizeof(url), "%s%s?lines=%d", PORTAL_BASE, path, lines > 0 ? lines : 100);
    if (since && since[0])
        off += snprintf(url + off, sizeof(url) - off, "&since=%s", since);
    if (grep && grep[0])
        off += snprintf(url + off, sizeof(url) - off, "&grep=%s", grep);

    struct curl_slist *headers = NULL;
    headers = add_hmac_auth_headers(headers, creds, "GET", path, NULL);

    curl_easy_setopt(curl, CURLOPT_URL, url);
    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);

    CURLcode res = curl_easy_perform(curl);
    long http_code = 0;
    curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);

    curl_slist_free_all(headers);
    curl_easy_cleanup(curl);
    free_credentials(creds);

    if (res != CURLE_OK || http_code != 200) {
        char errbuf[256];
        snprintf(errbuf, sizeof(errbuf), "HTTP %ld: %s", http_code,
                 res != CURLE_OK ? curl_easy_strerror(res) : "request failed");
        set_last_error(errbuf);
        free(response.data);
        return NULL;
    }

    return response.data; // caller frees
}

// Streaming callback context for library API
struct StreamCallbackCtx {
    unsandbox_log_callback_t callback;
    void *userdata;
};

static size_t stream_lib_sse_callback(void *contents, size_t size, size_t nmemb, void *userp) {
    size_t realsize = size * nmemb;
    struct StreamCallbackCtx *ctx = (struct StreamCallbackCtx *)userp;
    static char buf[65536];
    static size_t buf_len = 0;

    size_t space = sizeof(buf) - buf_len - 1;
    size_t copy = realsize < space ? realsize : space;
    memcpy(buf + buf_len, contents, copy);
    buf_len += copy;
    buf[buf_len] = 0;

    char *pos = buf;
    char *event_end;
    while ((event_end = strstr(pos, "\n\n")) != NULL) {
        *event_end = 0;
        char *line = pos;
        while (*line) {
            char *nl = strchr(line, '\n');
            if (nl) *nl = 0;
            if (strncmp(line, "data: ", 6) == 0) {
                const char *payload = line + 6;
                if (strcmp(payload, ":keepalive") != 0 && ctx->callback) {
                    char *source = extract_json_string(payload, "source");
                    char *logline = extract_json_string(payload, "line");
                    ctx->callback(source, logline ? logline : payload, ctx->userdata);
                    free(source);
                    free(logline);
                }
            }
            if (nl) line = nl + 1;
            else break;
        }
        pos = event_end + 2;
    }

    if (pos > buf) {
        buf_len = strlen(pos);
        memmove(buf, pos, buf_len + 1);
    }

    if (stream_interrupted) return 0; // abort
    return realsize;
}

int unsandbox_logs_stream(
    const char *source,
    const char *grep,
    unsandbox_log_callback_t callback,
    void *userdata,
    const char *public_key, const char *secret_key) {

    UnsandboxCredentials *creds = get_credentials(public_key, secret_key, -1);
    if (!creds) { set_last_error("No credentials"); return 1; }

    CURL *curl = curl_easy_init();
    if (!curl) { free_credentials(creds); set_last_error("curl init failed"); return 1; }

    char path[256];
    if (!source || strcmp(source, "all") == 0)
        snprintf(path, sizeof(path), "/logs/all/stream");
    else
        snprintf(path, sizeof(path), "/logs/%s/stream", source);

    char url[1024];
    if (grep && grep[0])
        snprintf(url, sizeof(url), "%s%s?grep=%s", PORTAL_BASE, path, grep);
    else
        snprintf(url, sizeof(url), "%s%s", PORTAL_BASE, path);

    struct curl_slist *headers = NULL;
    headers = add_hmac_auth_headers(headers, creds, "GET", path, NULL);

    struct StreamCallbackCtx ctx = { callback, userdata };

    curl_easy_setopt(curl, CURLOPT_URL, url);
    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, stream_lib_sse_callback);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &ctx);
    curl_easy_setopt(curl, CURLOPT_TIMEOUT, 0L);
    curl_easy_setopt(curl, CURLOPT_LOW_SPEED_LIMIT, 1L);
    curl_easy_setopt(curl, CURLOPT_LOW_SPEED_TIME, 120L);

    stream_interrupted = 0;
    CURLcode res = curl_easy_perform(curl);

    curl_slist_free_all(headers);
    curl_easy_cleanup(curl);
    free_credentials(creds);

    if (res != CURLE_OK && res != CURLE_WRITE_ERROR) {
        set_last_error(curl_easy_strerror(res));
        return 1;
    }

    return 0;
}

/* ============================================================================
 * Stub Implementations (TODO: Full implementation)
 * These functions are declared in un.h but need HTTP/JSON handling
 * ============================================================================ */

/* Execution - full library implementations */

// Helper to parse execute response into result struct
static unsandbox_result_t *parse_execute_response(const char *json) {
    if (!json) return NULL;

    unsandbox_result_t *result = calloc(1, sizeof(unsandbox_result_t));
    if (!result) {
        set_last_error("Out of memory");
        return NULL;
    }

    result->stdout_str = extract_json_string(json, "stdout");
    result->stderr_str = extract_json_string(json, "stderr");
    result->error_message = extract_json_string(json, "error");
    result->language = extract_json_string(json, "language");
    result->exit_code = (int)extract_json_number(json, "exit_code");
    long long exec_time_ms = extract_json_number(json, "execution_time");
    result->execution_time = exec_time_ms >= 0 ? exec_time_ms / 1000.0 : 0.0;
    result->success = (result->exit_code == 0 && !result->error_message);

    return result;
}

unsandbox_result_t *unsandbox_execute(
    const char *language, const char *code,
    const char *public_key, const char *secret_key) {

    if (!language || !code) {
        set_last_error("Language and code are required");
        return NULL;
    }

    UnsandboxCredentials *creds = get_credentials(public_key, secret_key, -1);
    if (!creds) {
        set_last_error("No credentials available");
        return NULL;
    }

    // Escape code for JSON
    char *escaped_code = escape_json_string(code);
    if (!escaped_code) {
        set_last_error("Failed to escape code");
        free_credentials(creds);
        return NULL;
    }

    // Build JSON payload
    size_t payload_size = strlen(escaped_code) + strlen(language) + 64;
    char *json_payload = malloc(payload_size);
    if (!json_payload) {
        set_last_error("Out of memory");
        free(escaped_code);
        free_credentials(creds);
        return NULL;
    }
    snprintf(json_payload, payload_size, "{\"language\":\"%s\",\"code\":\"%s\"}", language, escaped_code);
    free(escaped_code);

    // Initialize curl
    CURL *curl = curl_easy_init();
    if (!curl) {
        set_last_error("Failed to initialize curl");
        free(json_payload);
        free_credentials(creds);
        return NULL;
    }

    struct ResponseBuffer response = {0};
    response.data = malloc(1);
    response.size = 0;

    struct curl_slist *headers = NULL;
    headers = curl_slist_append(headers, "Content-Type: application/json");
    headers = add_hmac_auth_headers(headers, creds, "POST", "/execute", json_payload);

    curl_easy_setopt(curl, CURLOPT_URL, API_URL);
    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
    curl_easy_setopt(curl, CURLOPT_POSTFIELDS, json_payload);
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
    curl_easy_setopt(curl, CURLOPT_USERAGENT, "un-cli/2.0");
    curl_easy_setopt(curl, CURLOPT_TIMEOUT, 120L);

    CURLcode res = curl_easy_perform(curl);
    long http_code = 0;
    curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);

    curl_slist_free_all(headers);
    curl_easy_cleanup(curl);
    free(json_payload);

    if (res != CURLE_OK) {
        set_last_error("Request failed: %s", curl_easy_strerror(res));
        free(response.data);
        free_credentials(creds);
        return NULL;
    }

    if (http_code != 200) {
        set_last_error("HTTP %ld: %s", http_code, response.data ? response.data : "Unknown error");
        free(response.data);
        free_credentials(creds);
        return NULL;
    }

    // Check if we need to poll for job completion
    char *job_id = extract_json_string(response.data, "job_id");
    char *status = extract_json_string(response.data, "status");

    unsandbox_result_t *result = NULL;
    if (job_id && status && (strcmp(status, "pending") == 0 || strcmp(status, "running") == 0)) {
        // Need to poll
        char *final_response = poll_job_status(creds, job_id);
        result = parse_execute_response(final_response);
        free(final_response);
    } else {
        result = parse_execute_response(response.data);
    }

    free(job_id);
    free(status);
    free(response.data);
    free_credentials(creds);
    return result;
}

char *unsandbox_execute_async(
    const char *language, const char *code,
    const char *public_key, const char *secret_key) {

    if (!language || !code) {
        set_last_error("Language and code are required");
        return NULL;
    }

    UnsandboxCredentials *creds = get_credentials(public_key, secret_key, -1);
    if (!creds) {
        set_last_error("No credentials available");
        return NULL;
    }

    char *escaped_code = escape_json_string(code);
    if (!escaped_code) {
        set_last_error("Failed to escape code");
        free_credentials(creds);
        return NULL;
    }

    size_t payload_size = strlen(escaped_code) + strlen(language) + 64;
    char *json_payload = malloc(payload_size);
    if (!json_payload) {
        set_last_error("Out of memory");
        free(escaped_code);
        free_credentials(creds);
        return NULL;
    }
    snprintf(json_payload, payload_size, "{\"language\":\"%s\",\"code\":\"%s\"}", language, escaped_code);
    free(escaped_code);

    CURL *curl = curl_easy_init();
    if (!curl) {
        set_last_error("Failed to initialize curl");
        free(json_payload);
        free_credentials(creds);
        return NULL;
    }

    struct ResponseBuffer response = {0};
    response.data = malloc(1);
    response.size = 0;

    struct curl_slist *headers = NULL;
    headers = curl_slist_append(headers, "Content-Type: application/json");
    headers = add_hmac_auth_headers(headers, creds, "POST", "/execute", json_payload);

    curl_easy_setopt(curl, CURLOPT_URL, API_URL);
    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
    curl_easy_setopt(curl, CURLOPT_POSTFIELDS, json_payload);
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
    curl_easy_setopt(curl, CURLOPT_USERAGENT, "un-cli/2.0");
    curl_easy_setopt(curl, CURLOPT_TIMEOUT, 30L);

    CURLcode res = curl_easy_perform(curl);
    long http_code = 0;
    curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);

    curl_slist_free_all(headers);
    curl_easy_cleanup(curl);
    free(json_payload);
    free_credentials(creds);

    if (res != CURLE_OK) {
        set_last_error("Request failed: %s", curl_easy_strerror(res));
        free(response.data);
        return NULL;
    }

    if (http_code != 200 && http_code != 202) {
        set_last_error("HTTP %ld: %s", http_code, response.data ? response.data : "Unknown error");
        free(response.data);
        return NULL;
    }

    char *job_id = extract_json_string(response.data, "job_id");
    free(response.data);

    if (!job_id) {
        set_last_error("No job_id in response");
    }
    return job_id;
}

unsandbox_result_t *unsandbox_wait_job(
    const char *job_id,
    const char *public_key, const char *secret_key) {

    if (!job_id) {
        set_last_error("Job ID is required");
        return NULL;
    }

    UnsandboxCredentials *creds = get_credentials(public_key, secret_key, -1);
    if (!creds) {
        set_last_error("No credentials available");
        return NULL;
    }

    char *final_response = poll_job_status(creds, job_id);
    free_credentials(creds);

    if (!final_response) {
        set_last_error("Failed to poll job status");
        return NULL;
    }

    unsandbox_result_t *result = parse_execute_response(final_response);
    free(final_response);
    return result;
}

unsandbox_job_t *unsandbox_get_job(
    const char *job_id,
    const char *public_key, const char *secret_key) {

    if (!job_id) {
        set_last_error("Job ID is required");
        return NULL;
    }

    UnsandboxCredentials *creds = get_credentials(public_key, secret_key, -1);
    if (!creds) {
        set_last_error("No credentials available");
        return NULL;
    }

    char url[256];
    snprintf(url, sizeof(url), "%s/jobs/%s", API_BASE, job_id);

    CURL *curl = curl_easy_init();
    if (!curl) {
        set_last_error("Failed to initialize curl");
        free_credentials(creds);
        return NULL;
    }

    struct ResponseBuffer response = {0};
    response.data = malloc(1);
    response.size = 0;

    char path[128];
    snprintf(path, sizeof(path), "/jobs/%s", job_id);

    struct curl_slist *headers = NULL;
    headers = add_hmac_auth_headers(headers, creds, "GET", path, NULL);

    curl_easy_setopt(curl, CURLOPT_URL, url);
    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
    curl_easy_setopt(curl, CURLOPT_USERAGENT, "un-cli/2.0");

    CURLcode res = curl_easy_perform(curl);
    long http_code = 0;
    curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);

    curl_slist_free_all(headers);
    curl_easy_cleanup(curl);
    free_credentials(creds);

    if (res != CURLE_OK || http_code != 200) {
        set_last_error("Failed to get job: HTTP %ld", http_code);
        free(response.data);
        return NULL;
    }

    unsandbox_job_t *job = calloc(1, sizeof(unsandbox_job_t));
    if (!job) {
        set_last_error("Out of memory");
        free(response.data);
        return NULL;
    }

    job->id = extract_json_string(response.data, "job_id");
    if (!job->id) job->id = strdup(job_id);
    job->language = extract_json_string(response.data, "language");
    job->status = extract_json_string(response.data, "status");
    job->created_at = extract_json_number(response.data, "created_at");
    job->completed_at = extract_json_number(response.data, "completed_at");
    job->error_message = extract_json_string(response.data, "error");

    free(response.data);
    return job;
}

int unsandbox_cancel_job(
    const char *job_id,
    const char *public_key, const char *secret_key) {

    if (!job_id) {
        set_last_error("Job ID is required");
        return -1;
    }

    UnsandboxCredentials *creds = get_credentials(public_key, secret_key, -1);
    if (!creds) {
        set_last_error("No credentials available");
        return -1;
    }

    char url[256];
    snprintf(url, sizeof(url), "%s/jobs/%s", API_BASE, job_id);

    CURL *curl = curl_easy_init();
    if (!curl) {
        set_last_error("Failed to initialize curl");
        free_credentials(creds);
        return -1;
    }

    char path[128];
    snprintf(path, sizeof(path), "/jobs/%s", job_id);

    struct curl_slist *headers = NULL;
    headers = add_hmac_auth_headers(headers, creds, "DELETE", path, NULL);

    curl_easy_setopt(curl, CURLOPT_URL, url);
    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
    curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "DELETE");
    curl_easy_setopt(curl, CURLOPT_USERAGENT, "un-cli/2.0");

    CURLcode res = curl_easy_perform(curl);
    long http_code = 0;
    curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);

    curl_slist_free_all(headers);
    curl_easy_cleanup(curl);
    free_credentials(creds);

    if (res != CURLE_OK) {
        set_last_error("Request failed: %s", curl_easy_strerror(res));
        return -1;
    }

    return (http_code == 200 || http_code == 204) ? 0 : -1;
}

unsandbox_job_list_t *unsandbox_list_jobs(
    const char *public_key, const char *secret_key) {

    UnsandboxCredentials *creds = get_credentials(public_key, secret_key, -1);
    if (!creds) {
        set_last_error("No credentials available");
        return NULL;
    }

    char url[256];
    snprintf(url, sizeof(url), "%s/jobs", API_BASE);

    CURL *curl = curl_easy_init();
    if (!curl) {
        set_last_error("Failed to initialize curl");
        free_credentials(creds);
        return NULL;
    }

    struct ResponseBuffer response = {0};
    response.data = malloc(1);
    response.size = 0;

    struct curl_slist *headers = NULL;
    headers = add_hmac_auth_headers(headers, creds, "GET", "/jobs", NULL);

    curl_easy_setopt(curl, CURLOPT_URL, url);
    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
    curl_easy_setopt(curl, CURLOPT_USERAGENT, "un-cli/2.0");

    CURLcode res = curl_easy_perform(curl);
    long http_code = 0;
    curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);

    curl_slist_free_all(headers);
    curl_easy_cleanup(curl);
    free_credentials(creds);

    if (res != CURLE_OK || http_code != 200) {
        set_last_error("Failed to list jobs: HTTP %ld", http_code);
        free(response.data);
        return NULL;
    }

    int count = count_json_array_objects(response.data, "jobs");
    unsandbox_job_list_t *list = calloc(1, sizeof(unsandbox_job_list_t));
    if (!list) {
        set_last_error("Out of memory");
        free(response.data);
        return NULL;
    }

    if (count > 0) {
        list->jobs = calloc(count, sizeof(unsandbox_job_t));
        if (!list->jobs) {
            set_last_error("Out of memory");
            free(list);
            free(response.data);
            return NULL;
        }
        list->count = count;

        const char *jobs_start = strstr(response.data, "\"jobs\":[");
        if (jobs_start) {
            const char *pos = jobs_start + 8;
            for (size_t i = 0; i < list->count && pos; i++) {
                pos = strchr(pos, '{');
                if (!pos) break;

                list->jobs[i].id = extract_json_string(pos, "job_id");
                list->jobs[i].language = extract_json_string(pos, "language");
                list->jobs[i].status = extract_json_string(pos, "status");
                list->jobs[i].created_at = extract_json_number(pos, "created_at");
                list->jobs[i].completed_at = extract_json_number(pos, "completed_at");
                list->jobs[i].error_message = extract_json_string(pos, "error");

                pos = skip_json_object(pos);
            }
        }
    }

    free(response.data);
    return list;
}

unsandbox_languages_t *unsandbox_get_languages(
    const char *public_key, const char *secret_key) {

    UnsandboxCredentials *creds = get_credentials(public_key, secret_key, -1);
    if (!creds) {
        set_last_error("No credentials available");
        return NULL;
    }

    char url[256];
    snprintf(url, sizeof(url), "%s/languages", API_BASE);

    CURL *curl = curl_easy_init();
    if (!curl) {
        set_last_error("Failed to initialize curl");
        free_credentials(creds);
        return NULL;
    }

    struct ResponseBuffer response = {0};
    response.data = malloc(1);
    response.size = 0;

    struct curl_slist *headers = NULL;
    headers = add_hmac_auth_headers(headers, creds, "GET", "/languages", NULL);

    curl_easy_setopt(curl, CURLOPT_URL, url);
    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
    curl_easy_setopt(curl, CURLOPT_USERAGENT, "un-cli/2.0");

    CURLcode res = curl_easy_perform(curl);
    long http_code = 0;
    curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);

    curl_slist_free_all(headers);
    curl_easy_cleanup(curl);
    free_credentials(creds);

    if (res != CURLE_OK || http_code != 200) {
        set_last_error("Failed to get languages: HTTP %ld", http_code);
        free(response.data);
        return NULL;
    }

    // Count languages - look for "languages":["lang1","lang2",...]
    const char *langs_start = strstr(response.data, "\"languages\":[");
    if (!langs_start) {
        // Try alternative format: just an array
        langs_start = strchr(response.data, '[');
    }

    int count = 0;
    if (langs_start) {
        const char *p = strchr(langs_start, '[');
        if (p) {
            p++;
            while (*p && *p != ']') {
                if (*p == '"') {
                    count++;
                    p++;
                    while (*p && *p != '"') p++;
                }
                if (*p) p++;
            }
        }
    }

    unsandbox_languages_t *langs = calloc(1, sizeof(unsandbox_languages_t));
    if (!langs) {
        set_last_error("Out of memory");
        free(response.data);
        return NULL;
    }

    if (count > 0) {
        langs->languages = calloc(count, sizeof(char *));
        if (!langs->languages) {
            set_last_error("Out of memory");
            free(langs);
            free(response.data);
            return NULL;
        }
        langs->count = count;

        // Parse again to extract strings
        const char *p = strchr(langs_start, '[');
        if (p) {
            p++;
            size_t i = 0;
            while (*p && *p != ']' && i < langs->count) {
                if (*p == '"') {
                    p++;
                    const char *end = strchr(p, '"');
                    if (end) {
                        size_t len = end - p;
                        langs->languages[i] = malloc(len + 1);
                        if (langs->languages[i]) {
                            memcpy(langs->languages[i], p, len);
                            langs->languages[i][len] = '\0';
                            i++;
                        }
                        p = end;
                    }
                }
                if (*p) p++;
            }
        }
    }

    free(response.data);
    return langs;
}

/* Session - full library implementations */

unsandbox_session_list_t *unsandbox_session_list(
    const char *public_key, const char *secret_key) {

    UnsandboxCredentials *creds = get_credentials(public_key, secret_key, -1);
    if (!creds) {
        set_last_error("No credentials available");
        return NULL;
    }

    char url[256];
    snprintf(url, sizeof(url), "%s/sessions", API_BASE);

    CURL *curl = curl_easy_init();
    if (!curl) {
        set_last_error("Failed to initialize curl");
        free_credentials(creds);
        return NULL;
    }

    struct ResponseBuffer response = {0};
    response.data = malloc(1);
    response.size = 0;

    struct curl_slist *headers = NULL;
    headers = add_hmac_auth_headers(headers, creds, "GET", "/sessions", NULL);

    curl_easy_setopt(curl, CURLOPT_URL, url);
    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
    curl_easy_setopt(curl, CURLOPT_USERAGENT, "un-cli/2.0");

    CURLcode res = curl_easy_perform(curl);
    long http_code = 0;
    curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);

    curl_slist_free_all(headers);
    curl_easy_cleanup(curl);
    free_credentials(creds);

    if (res != CURLE_OK || http_code != 200) {
        set_last_error("Failed to list sessions: HTTP %ld", http_code);
        free(response.data);
        return NULL;
    }

    int count = count_json_array_objects(response.data, "sessions");
    unsandbox_session_list_t *list = calloc(1, sizeof(unsandbox_session_list_t));
    if (!list) {
        set_last_error("Out of memory");
        free(response.data);
        return NULL;
    }

    if (count > 0) {
        list->sessions = calloc(count, sizeof(unsandbox_session_t));
        if (!list->sessions) {
            set_last_error("Out of memory");
            free(list);
            free(response.data);
            return NULL;
        }
        list->count = count;

        const char *sessions_start = strstr(response.data, "\"sessions\":[");
        if (sessions_start) {
            const char *pos = sessions_start + 12;
            for (size_t i = 0; i < list->count && pos; i++) {
                pos = strchr(pos, '{');
                if (!pos) break;

                list->sessions[i].id = extract_json_string(pos, "id");
                list->sessions[i].container_name = extract_json_string(pos, "container_name");
                list->sessions[i].status = extract_json_string(pos, "status");
                list->sessions[i].network_mode = extract_json_string(pos, "network_mode");
                list->sessions[i].vcpu = (int)extract_json_number(pos, "vcpu");
                list->sessions[i].created_at = extract_json_number(pos, "created_at");
                list->sessions[i].last_activity = extract_json_number(pos, "last_activity");

                pos = skip_json_object(pos);
            }
        }
    }

    free(response.data);
    return list;
}

unsandbox_session_t *unsandbox_session_get(
    const char *session_id,
    const char *public_key, const char *secret_key) {

    if (!session_id) {
        set_last_error("Session ID is required");
        return NULL;
    }

    UnsandboxCredentials *creds = get_credentials(public_key, secret_key, -1);
    if (!creds) {
        set_last_error("No credentials available");
        return NULL;
    }

    char url[256];
    snprintf(url, sizeof(url), "%s/sessions/%s", API_BASE, session_id);

    CURL *curl = curl_easy_init();
    if (!curl) {
        set_last_error("Failed to initialize curl");
        free_credentials(creds);
        return NULL;
    }

    struct ResponseBuffer response = {0};
    response.data = malloc(1);
    response.size = 0;

    char path[128];
    snprintf(path, sizeof(path), "/sessions/%s", session_id);

    struct curl_slist *headers = NULL;
    headers = add_hmac_auth_headers(headers, creds, "GET", path, NULL);

    curl_easy_setopt(curl, CURLOPT_URL, url);
    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
    curl_easy_setopt(curl, CURLOPT_USERAGENT, "un-cli/2.0");

    CURLcode res = curl_easy_perform(curl);
    long http_code = 0;
    curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);

    curl_slist_free_all(headers);
    curl_easy_cleanup(curl);
    free_credentials(creds);

    if (res != CURLE_OK || http_code != 200) {
        set_last_error("Failed to get session: HTTP %ld", http_code);
        free(response.data);
        return NULL;
    }

    unsandbox_session_t *session = calloc(1, sizeof(unsandbox_session_t));
    if (!session) {
        set_last_error("Out of memory");
        free(response.data);
        return NULL;
    }

    session->id = extract_json_string(response.data, "session_id");
    if (!session->id) session->id = extract_json_string(response.data, "id");
    session->container_name = extract_json_string(response.data, "container_name");
    session->status = extract_json_string(response.data, "status");
    session->network_mode = extract_json_string(response.data, "network_mode");
    session->vcpu = (int)extract_json_number(response.data, "vcpu");
    session->created_at = extract_json_number(response.data, "created_at");
    session->last_activity = extract_json_number(response.data, "last_activity");

    free(response.data);
    return session;
}

unsandbox_session_t *unsandbox_session_create(
    const char *network_mode, const char *shell,
    const char *public_key, const char *secret_key) {

    UnsandboxCredentials *creds = get_credentials(public_key, secret_key, -1);
    if (!creds) {
        set_last_error("No credentials available");
        return NULL;
    }

    // Build payload
    char payload[512];
    char *p = payload;
    p += sprintf(p, "{");
    int has_field = 0;
    if (network_mode && strlen(network_mode) > 0) {
        p += sprintf(p, "\"network_mode\":\"%s\"", network_mode);
        has_field = 1;
    }
    if (shell && strlen(shell) > 0) {
        if (has_field) p += sprintf(p, ",");
        p += sprintf(p, "\"shell\":\"%s\"", shell);
    }
    p += sprintf(p, "}");

    char url[256];
    snprintf(url, sizeof(url), "%s/sessions", API_BASE);

    CURL *curl = curl_easy_init();
    if (!curl) {
        set_last_error("Failed to initialize curl");
        free_credentials(creds);
        return NULL;
    }

    struct ResponseBuffer response = {0};
    response.data = malloc(1);
    response.size = 0;

    struct curl_slist *headers = NULL;
    headers = curl_slist_append(headers, "Content-Type: application/json");
    headers = add_hmac_auth_headers(headers, creds, "POST", "/sessions", payload);

    curl_easy_setopt(curl, CURLOPT_URL, url);
    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
    curl_easy_setopt(curl, CURLOPT_POSTFIELDS, payload);
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
    curl_easy_setopt(curl, CURLOPT_USERAGENT, "un-cli/2.0");

    CURLcode res = curl_easy_perform(curl);
    long http_code = 0;
    curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);

    curl_slist_free_all(headers);
    curl_easy_cleanup(curl);
    free_credentials(creds);

    if (res != CURLE_OK || (http_code != 200 && http_code != 201)) {
        set_last_error("Failed to create session: HTTP %ld", http_code);
        free(response.data);
        return NULL;
    }

    unsandbox_session_t *session = calloc(1, sizeof(unsandbox_session_t));
    if (!session) {
        set_last_error("Out of memory");
        free(response.data);
        return NULL;
    }

    session->id = extract_json_string(response.data, "session_id");
    if (!session->id) session->id = extract_json_string(response.data, "id");
    session->container_name = extract_json_string(response.data, "container_name");
    session->status = extract_json_string(response.data, "status");
    session->network_mode = extract_json_string(response.data, "network_mode");
    session->vcpu = (int)extract_json_number(response.data, "vcpu");
    session->created_at = extract_json_number(response.data, "created_at");
    session->last_activity = extract_json_number(response.data, "last_activity");

    free(response.data);
    return session;
}

unsandbox_result_t *unsandbox_session_execute(
    const char *session_id, const char *command,
    const char *public_key, const char *secret_key) {

    if (!session_id || !command) {
        set_last_error("Session ID and command are required");
        return NULL;
    }

    UnsandboxCredentials *creds = get_credentials(public_key, secret_key, -1);
    if (!creds) {
        set_last_error("No credentials available");
        return NULL;
    }

    char *escaped_cmd = escape_json_string(command);
    if (!escaped_cmd) {
        set_last_error("Failed to escape command");
        free_credentials(creds);
        return NULL;
    }

    size_t payload_size = strlen(escaped_cmd) + 64;
    char *payload = malloc(payload_size);
    if (!payload) {
        set_last_error("Out of memory");
        free(escaped_cmd);
        free_credentials(creds);
        return NULL;
    }
    snprintf(payload, payload_size, "{\"command\":\"%s\"}", escaped_cmd);
    free(escaped_cmd);

    char url[256];
    snprintf(url, sizeof(url), "%s/sessions/%s/shell", API_BASE, session_id);

    char path[128];
    snprintf(path, sizeof(path), "/sessions/%s/shell", session_id);

    CURL *curl = curl_easy_init();
    if (!curl) {
        set_last_error("Failed to initialize curl");
        free(payload);
        free_credentials(creds);
        return NULL;
    }

    struct ResponseBuffer response = {0};
    response.data = malloc(1);
    response.size = 0;

    struct curl_slist *headers = NULL;
    headers = curl_slist_append(headers, "Content-Type: application/json");
    headers = add_hmac_auth_headers(headers, creds, "POST", path, payload);

    curl_easy_setopt(curl, CURLOPT_URL, url);
    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
    curl_easy_setopt(curl, CURLOPT_POSTFIELDS, payload);
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
    curl_easy_setopt(curl, CURLOPT_USERAGENT, "un-cli/2.0");
    curl_easy_setopt(curl, CURLOPT_TIMEOUT, 120L);

    CURLcode res = curl_easy_perform(curl);
    long http_code = 0;
    curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);

    curl_slist_free_all(headers);
    curl_easy_cleanup(curl);
    free(payload);
    free_credentials(creds);

    if (res != CURLE_OK || http_code != 200) {
        set_last_error("Failed to execute in session: HTTP %ld", http_code);
        free(response.data);
        return NULL;
    }

    unsandbox_result_t *result = parse_execute_response(response.data);
    free(response.data);
    return result;
}

/* Service - full library implementations */

unsandbox_service_list_t *unsandbox_service_list(
    const char *public_key, const char *secret_key) {

    UnsandboxCredentials *creds = get_credentials(public_key, secret_key, -1);
    if (!creds) {
        set_last_error("No credentials available");
        return NULL;
    }

    char url[256];
    snprintf(url, sizeof(url), "%s/services", API_BASE);

    CURL *curl = curl_easy_init();
    if (!curl) {
        set_last_error("Failed to initialize curl");
        free_credentials(creds);
        return NULL;
    }

    struct ResponseBuffer response = {0};
    response.data = malloc(1);
    response.size = 0;

    struct curl_slist *headers = NULL;
    headers = add_hmac_auth_headers(headers, creds, "GET", "/services", NULL);

    curl_easy_setopt(curl, CURLOPT_URL, url);
    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
    curl_easy_setopt(curl, CURLOPT_USERAGENT, "un-cli/2.0");

    CURLcode res = curl_easy_perform(curl);
    long http_code = 0;
    curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);

    curl_slist_free_all(headers);
    curl_easy_cleanup(curl);
    free_credentials(creds);

    if (res != CURLE_OK || http_code != 200) {
        set_last_error("Failed to list services: HTTP %ld", http_code);
        free(response.data);
        return NULL;
    }

    int count = count_json_array_objects(response.data, "services");
    unsandbox_service_list_t *list = calloc(1, sizeof(unsandbox_service_list_t));
    if (!list) {
        set_last_error("Out of memory");
        free(response.data);
        return NULL;
    }

    if (count > 0) {
        list->services = calloc(count, sizeof(unsandbox_service_t));
        if (!list->services) {
            set_last_error("Out of memory");
            free(list);
            free(response.data);
            return NULL;
        }
        list->count = count;

        const char *services_start = strstr(response.data, "\"services\":[");
        if (services_start) {
            const char *pos = services_start + 12;
            for (size_t i = 0; i < list->count && pos; i++) {
                pos = strchr(pos, '{');
                if (!pos) break;

                list->services[i].id = extract_json_string(pos, "id");
                list->services[i].name = extract_json_string(pos, "name");
                list->services[i].status = extract_json_string(pos, "status");
                list->services[i].container_name = extract_json_string(pos, "container_name");
                list->services[i].network_mode = extract_json_string(pos, "network_mode");
                list->services[i].ports = extract_json_string(pos, "ports");
                list->services[i].domains = extract_json_string(pos, "domains");
                list->services[i].vcpu = (int)extract_json_number(pos, "vcpu");
                list->services[i].locked = (int)extract_json_number(pos, "locked");
                list->services[i].unfreeze_on_demand = (int)extract_json_number(pos, "unfreeze_on_demand");
                list->services[i].created_at = extract_json_number(pos, "created_at");
                list->services[i].last_activity = extract_json_number(pos, "last_activity");

                pos = skip_json_object(pos);
            }
        }
    }

    free(response.data);
    return list;
}

unsandbox_service_t *unsandbox_service_get(
    const char *service_id,
    const char *public_key, const char *secret_key) {

    if (!service_id) {
        set_last_error("Service ID is required");
        return NULL;
    }

    UnsandboxCredentials *creds = get_credentials(public_key, secret_key, -1);
    if (!creds) {
        set_last_error("No credentials available");
        return NULL;
    }

    char url[256];
    snprintf(url, sizeof(url), "%s/services/%s", API_BASE, service_id);

    CURL *curl = curl_easy_init();
    if (!curl) {
        set_last_error("Failed to initialize curl");
        free_credentials(creds);
        return NULL;
    }

    struct ResponseBuffer response = {0};
    response.data = malloc(1);
    response.size = 0;

    char path[128];
    snprintf(path, sizeof(path), "/services/%s", service_id);

    struct curl_slist *headers = NULL;
    headers = add_hmac_auth_headers(headers, creds, "GET", path, NULL);

    curl_easy_setopt(curl, CURLOPT_URL, url);
    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
    curl_easy_setopt(curl, CURLOPT_USERAGENT, "un-cli/2.0");

    CURLcode res = curl_easy_perform(curl);
    long http_code = 0;
    curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);

    curl_slist_free_all(headers);
    curl_easy_cleanup(curl);
    free_credentials(creds);

    if (res != CURLE_OK || http_code != 200) {
        set_last_error("Failed to get service: HTTP %ld", http_code);
        free(response.data);
        return NULL;
    }

    unsandbox_service_t *service = calloc(1, sizeof(unsandbox_service_t));
    if (!service) {
        set_last_error("Out of memory");
        free(response.data);
        return NULL;
    }

    service->id = extract_json_string(response.data, "id");
    service->name = extract_json_string(response.data, "name");
    service->status = extract_json_string(response.data, "status");
    service->container_name = extract_json_string(response.data, "container_name");
    service->network_mode = extract_json_string(response.data, "network_mode");
    service->ports = extract_json_string(response.data, "ports");
    service->domains = extract_json_string(response.data, "domains");
    service->vcpu = (int)extract_json_number(response.data, "vcpu");
    service->locked = (int)extract_json_number(response.data, "locked");
    service->unfreeze_on_demand = (int)extract_json_number(response.data, "unfreeze_on_demand");
    service->created_at = extract_json_number(response.data, "created_at");
    service->last_activity = extract_json_number(response.data, "last_activity");

    free(response.data);
    return service;
}

char *unsandbox_service_create(
    const char *name, const char *ports, const char *domains,
    const char *bootstrap, const char *network_mode,
    const char *public_key, const char *secret_key) {
    UnsandboxCredentials *creds = get_credentials(public_key, secret_key, -1);
    if (!creds) {
        set_last_error("No credentials available");
        return NULL;
    }
    char *result = create_service(creds, name, ports, domains, bootstrap, NULL, network_mode, 0, NULL, NULL, 0, NULL, 0);
    free_credentials(creds);
    return result;
}

unsandbox_result_t *unsandbox_service_execute(
    const char *service_id, const char *command, int timeout_ms,
    const char *public_key, const char *secret_key) {

    if (!service_id || !command) {
        set_last_error("Service ID and command are required");
        return NULL;
    }

    UnsandboxCredentials *creds = get_credentials(public_key, secret_key, -1);
    if (!creds) {
        set_last_error("No credentials available");
        return NULL;
    }

    char *escaped_cmd = escape_json_string(command);
    if (!escaped_cmd) {
        set_last_error("Failed to escape command");
        free_credentials(creds);
        return NULL;
    }

    size_t payload_size = strlen(escaped_cmd) + 128;
    char *payload = malloc(payload_size);
    if (!payload) {
        set_last_error("Out of memory");
        free(escaped_cmd);
        free_credentials(creds);
        return NULL;
    }
    if (timeout_ms > 0) {
        snprintf(payload, payload_size, "{\"command\":\"%s\",\"timeout\":%d}", escaped_cmd, timeout_ms);
    } else {
        snprintf(payload, payload_size, "{\"command\":\"%s\"}", escaped_cmd);
    }
    free(escaped_cmd);

    char url[256];
    snprintf(url, sizeof(url), "%s/services/%s/execute", API_BASE, service_id);

    char path[128];
    snprintf(path, sizeof(path), "/services/%s/execute", service_id);

    CURL *curl = curl_easy_init();
    if (!curl) {
        set_last_error("Failed to initialize curl");
        free(payload);
        free_credentials(creds);
        return NULL;
    }

    struct ResponseBuffer response = {0};
    response.data = malloc(1);
    response.size = 0;

    struct curl_slist *headers = NULL;
    headers = curl_slist_append(headers, "Content-Type: application/json");
    headers = add_hmac_auth_headers(headers, creds, "POST", path, payload);

    curl_easy_setopt(curl, CURLOPT_URL, url);
    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
    curl_easy_setopt(curl, CURLOPT_POSTFIELDS, payload);
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
    curl_easy_setopt(curl, CURLOPT_USERAGENT, "un-cli/2.0");
    curl_easy_setopt(curl, CURLOPT_TIMEOUT, (timeout_ms > 0) ? (timeout_ms / 1000 + 30) : 120L);

    CURLcode res = curl_easy_perform(curl);
    long http_code = 0;
    curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);

    curl_slist_free_all(headers);
    curl_easy_cleanup(curl);
    free(payload);
    free_credentials(creds);

    if (res != CURLE_OK || http_code != 200) {
        set_last_error("Failed to execute in service: HTTP %ld", http_code);
        free(response.data);
        return NULL;
    }

    unsandbox_result_t *result = parse_execute_response(response.data);
    free(response.data);
    return result;
}

char *unsandbox_service_env_get(
    const char *service_id,
    const char *public_key, const char *secret_key) {

    if (!service_id) {
        set_last_error("Service ID is required");
        return NULL;
    }

    UnsandboxCredentials *creds = get_credentials(public_key, secret_key, -1);
    if (!creds) {
        set_last_error("No credentials available");
        return NULL;
    }

    char url[256];
    snprintf(url, sizeof(url), "%s/services/%s/env", API_BASE, service_id);

    CURL *curl = curl_easy_init();
    if (!curl) {
        set_last_error("Failed to initialize curl");
        free_credentials(creds);
        return NULL;
    }

    struct ResponseBuffer response = {0};
    response.data = malloc(1);
    response.size = 0;

    char path[128];
    snprintf(path, sizeof(path), "/services/%s/env", service_id);

    struct curl_slist *headers = NULL;
    headers = add_hmac_auth_headers(headers, creds, "GET", path, NULL);

    curl_easy_setopt(curl, CURLOPT_URL, url);
    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
    curl_easy_setopt(curl, CURLOPT_USERAGENT, "un-cli/2.0");

    CURLcode res = curl_easy_perform(curl);
    long http_code = 0;
    curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);

    curl_slist_free_all(headers);
    curl_easy_cleanup(curl);
    free_credentials(creds);

    if (res != CURLE_OK || http_code != 200) {
        set_last_error("Failed to get env: HTTP %ld", http_code);
        free(response.data);
        return NULL;
    }

    // Return the raw JSON response (caller can parse as needed)
    return response.data;
}

char *unsandbox_service_env_export(
    const char *service_id,
    const char *public_key, const char *secret_key) {

    if (!service_id) {
        set_last_error("Service ID is required");
        return NULL;
    }

    UnsandboxCredentials *creds = get_credentials(public_key, secret_key, -1);
    if (!creds) {
        set_last_error("No credentials available");
        return NULL;
    }

    char url[256];
    snprintf(url, sizeof(url), "%s/services/%s/env?format=export", API_BASE, service_id);

    CURL *curl = curl_easy_init();
    if (!curl) {
        set_last_error("Failed to initialize curl");
        free_credentials(creds);
        return NULL;
    }

    struct ResponseBuffer response = {0};
    response.data = malloc(1);
    response.size = 0;

    char path[128];
    snprintf(path, sizeof(path), "/services/%s/env", service_id);

    struct curl_slist *headers = NULL;
    headers = add_hmac_auth_headers(headers, creds, "GET", path, NULL);

    curl_easy_setopt(curl, CURLOPT_URL, url);
    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
    curl_easy_setopt(curl, CURLOPT_USERAGENT, "un-cli/2.0");

    CURLcode res = curl_easy_perform(curl);
    long http_code = 0;
    curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);

    curl_slist_free_all(headers);
    curl_easy_cleanup(curl);
    free_credentials(creds);

    if (res != CURLE_OK || http_code != 200) {
        set_last_error("Failed to export env: HTTP %ld", http_code);
        free(response.data);
        return NULL;
    }

    // Return the export-formatted env content
    return response.data;
}

/* Snapshot - full library implementations */

unsandbox_snapshot_list_t *unsandbox_snapshot_list(
    const char *public_key, const char *secret_key) {

    UnsandboxCredentials *creds = get_credentials(public_key, secret_key, -1);
    if (!creds) {
        set_last_error("No credentials available");
        return NULL;
    }

    char url[256];
    snprintf(url, sizeof(url), "%s/snapshots", API_BASE);

    CURL *curl = curl_easy_init();
    if (!curl) {
        set_last_error("Failed to initialize curl");
        free_credentials(creds);
        return NULL;
    }

    struct ResponseBuffer response = {0};
    response.data = malloc(1);
    response.size = 0;

    struct curl_slist *headers = NULL;
    headers = add_hmac_auth_headers(headers, creds, "GET", "/snapshots", NULL);

    curl_easy_setopt(curl, CURLOPT_URL, url);
    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
    curl_easy_setopt(curl, CURLOPT_USERAGENT, "un-cli/2.0");

    CURLcode res = curl_easy_perform(curl);
    long http_code = 0;
    curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);

    curl_slist_free_all(headers);
    curl_easy_cleanup(curl);
    free_credentials(creds);

    if (res != CURLE_OK || http_code != 200) {
        set_last_error("Failed to list snapshots: HTTP %ld", http_code);
        free(response.data);
        return NULL;
    }

    int count = count_json_array_objects(response.data, "snapshots");
    unsandbox_snapshot_list_t *list = calloc(1, sizeof(unsandbox_snapshot_list_t));
    if (!list) {
        set_last_error("Out of memory");
        free(response.data);
        return NULL;
    }

    if (count > 0) {
        list->snapshots = calloc(count, sizeof(unsandbox_snapshot_t));
        if (!list->snapshots) {
            set_last_error("Out of memory");
            free(list);
            free(response.data);
            return NULL;
        }
        list->count = count;

        const char *snapshots_start = strstr(response.data, "\"snapshots\":[");
        if (snapshots_start) {
            const char *pos = snapshots_start + 13;
            for (size_t i = 0; i < list->count && pos; i++) {
                pos = strchr(pos, '{');
                if (!pos) break;

                list->snapshots[i].id = extract_json_string(pos, "id");
                list->snapshots[i].name = extract_json_string(pos, "name");
                list->snapshots[i].type = extract_json_string(pos, "type");
                list->snapshots[i].source_id = extract_json_string(pos, "source_id");
                list->snapshots[i].hot = (int)extract_json_number(pos, "hot");
                list->snapshots[i].locked = (int)extract_json_number(pos, "locked");
                list->snapshots[i].created_at = extract_json_number(pos, "created_at");
                list->snapshots[i].size_bytes = extract_json_number(pos, "size_bytes");

                pos = skip_json_object(pos);
            }
        }
    }

    free(response.data);
    return list;
}

unsandbox_snapshot_t *unsandbox_snapshot_get(
    const char *snapshot_id,
    const char *public_key, const char *secret_key) {

    if (!snapshot_id) {
        set_last_error("Snapshot ID is required");
        return NULL;
    }

    UnsandboxCredentials *creds = get_credentials(public_key, secret_key, -1);
    if (!creds) {
        set_last_error("No credentials available");
        return NULL;
    }

    char url[256];
    snprintf(url, sizeof(url), "%s/snapshots/%s", API_BASE, snapshot_id);

    CURL *curl = curl_easy_init();
    if (!curl) {
        set_last_error("Failed to initialize curl");
        free_credentials(creds);
        return NULL;
    }

    struct ResponseBuffer response = {0};
    response.data = malloc(1);
    response.size = 0;

    char path[128];
    snprintf(path, sizeof(path), "/snapshots/%s", snapshot_id);

    struct curl_slist *headers = NULL;
    headers = add_hmac_auth_headers(headers, creds, "GET", path, NULL);

    curl_easy_setopt(curl, CURLOPT_URL, url);
    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
    curl_easy_setopt(curl, CURLOPT_USERAGENT, "un-cli/2.0");

    CURLcode res = curl_easy_perform(curl);
    long http_code = 0;
    curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);

    curl_slist_free_all(headers);
    curl_easy_cleanup(curl);
    free_credentials(creds);

    if (res != CURLE_OK || http_code != 200) {
        set_last_error("Failed to get snapshot: HTTP %ld", http_code);
        free(response.data);
        return NULL;
    }

    unsandbox_snapshot_t *snapshot = calloc(1, sizeof(unsandbox_snapshot_t));
    if (!snapshot) {
        set_last_error("Out of memory");
        free(response.data);
        return NULL;
    }

    snapshot->id = extract_json_string(response.data, "id");
    snapshot->name = extract_json_string(response.data, "name");
    snapshot->type = extract_json_string(response.data, "type");
    snapshot->source_id = extract_json_string(response.data, "source_id");
    snapshot->hot = (int)extract_json_number(response.data, "hot");
    snapshot->locked = (int)extract_json_number(response.data, "locked");
    snapshot->created_at = extract_json_number(response.data, "created_at");
    snapshot->size_bytes = extract_json_number(response.data, "size_bytes");

    free(response.data);
    return snapshot;
}

char *unsandbox_snapshot_session(
    const char *session_id, const char *name, int hot,
    const char *public_key, const char *secret_key) {

    if (!session_id) {
        set_last_error("Session ID is required");
        return NULL;
    }

    UnsandboxCredentials *creds = get_credentials(public_key, secret_key, -1);
    if (!creds) {
        set_last_error("No credentials available");
        return NULL;
    }

    // Build payload
    char payload[512];
    char *p = payload;
    p += sprintf(p, "{\"source_type\":\"session\",\"source_id\":\"%s\"", session_id);
    if (name && strlen(name) > 0) {
        char *esc_name = escape_json_string(name);
        p += sprintf(p, ",\"name\":\"%s\"", esc_name);
        free(esc_name);
    }
    if (hot) {
        p += sprintf(p, ",\"hot\":true");
    }
    p += sprintf(p, "}");

    char url[256];
    snprintf(url, sizeof(url), "%s/snapshots", API_BASE);

    CURL *curl = curl_easy_init();
    if (!curl) {
        set_last_error("Failed to initialize curl");
        free_credentials(creds);
        return NULL;
    }

    struct ResponseBuffer response = {0};
    response.data = malloc(1);
    response.size = 0;

    struct curl_slist *headers = NULL;
    headers = curl_slist_append(headers, "Content-Type: application/json");
    headers = add_hmac_auth_headers(headers, creds, "POST", "/snapshots", payload);

    curl_easy_setopt(curl, CURLOPT_URL, url);
    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
    curl_easy_setopt(curl, CURLOPT_POSTFIELDS, payload);
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
    curl_easy_setopt(curl, CURLOPT_USERAGENT, "un-cli/2.0");

    CURLcode res = curl_easy_perform(curl);
    long http_code = 0;
    curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);

    curl_slist_free_all(headers);
    curl_easy_cleanup(curl);
    free_credentials(creds);

    if (res != CURLE_OK || (http_code != 200 && http_code != 201)) {
        set_last_error("Failed to create snapshot: HTTP %ld", http_code);
        free(response.data);
        return NULL;
    }

    char *snapshot_id = extract_json_string(response.data, "id");
    free(response.data);
    return snapshot_id;
}

char *unsandbox_snapshot_service(
    const char *service_id, const char *name, int hot,
    const char *public_key, const char *secret_key) {

    if (!service_id) {
        set_last_error("Service ID is required");
        return NULL;
    }

    UnsandboxCredentials *creds = get_credentials(public_key, secret_key, -1);
    if (!creds) {
        set_last_error("No credentials available");
        return NULL;
    }

    // Build payload
    char payload[512];
    char *p = payload;
    p += sprintf(p, "{\"source_type\":\"service\",\"source_id\":\"%s\"", service_id);
    if (name && strlen(name) > 0) {
        char *esc_name = escape_json_string(name);
        p += sprintf(p, ",\"name\":\"%s\"", esc_name);
        free(esc_name);
    }
    if (hot) {
        p += sprintf(p, ",\"hot\":true");
    }
    p += sprintf(p, "}");

    char url[256];
    snprintf(url, sizeof(url), "%s/snapshots", API_BASE);

    CURL *curl = curl_easy_init();
    if (!curl) {
        set_last_error("Failed to initialize curl");
        free_credentials(creds);
        return NULL;
    }

    struct ResponseBuffer response = {0};
    response.data = malloc(1);
    response.size = 0;

    struct curl_slist *headers = NULL;
    headers = curl_slist_append(headers, "Content-Type: application/json");
    headers = add_hmac_auth_headers(headers, creds, "POST", "/snapshots", payload);

    curl_easy_setopt(curl, CURLOPT_URL, url);
    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
    curl_easy_setopt(curl, CURLOPT_POSTFIELDS, payload);
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
    curl_easy_setopt(curl, CURLOPT_USERAGENT, "un-cli/2.0");

    CURLcode res = curl_easy_perform(curl);
    long http_code = 0;
    curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);

    curl_slist_free_all(headers);
    curl_easy_cleanup(curl);
    free_credentials(creds);

    if (res != CURLE_OK || (http_code != 200 && http_code != 201)) {
        set_last_error("Failed to create snapshot: HTTP %ld", http_code);
        free(response.data);
        return NULL;
    }

    char *snapshot_id = extract_json_string(response.data, "id");
    free(response.data);
    return snapshot_id;
}

/* ============================================================================
 * Image API - Library Implementations
 * ============================================================================ */

unsandbox_image_list_t *unsandbox_image_list(
    const char *filter,
    const char *public_key, const char *secret_key) {

    UnsandboxCredentials *creds = get_credentials(public_key, secret_key, -1);
    if (!creds) {
        set_last_error("No credentials available");
        return NULL;
    }

    char url[256];
    char path[128];
    if (filter && strlen(filter) > 0) {
        snprintf(url, sizeof(url), "%s/images?filter=%s", API_BASE, filter);
        snprintf(path, sizeof(path), "/images?filter=%s", filter);
    } else {
        snprintf(url, sizeof(url), "%s/images", API_BASE);
        snprintf(path, sizeof(path), "/images");
    }

    CURL *curl = curl_easy_init();
    if (!curl) {
        set_last_error("Failed to initialize curl");
        free_credentials(creds);
        return NULL;
    }

    struct ResponseBuffer response = {0};
    response.data = malloc(1);
    response.size = 0;

    struct curl_slist *headers = NULL;
    headers = add_hmac_auth_headers(headers, creds, "GET", "/images", NULL);

    curl_easy_setopt(curl, CURLOPT_URL, url);
    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
    curl_easy_setopt(curl, CURLOPT_USERAGENT, "un-cli/2.0");

    CURLcode res = curl_easy_perform(curl);
    long http_code = 0;
    curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);

    curl_slist_free_all(headers);
    curl_easy_cleanup(curl);
    free_credentials(creds);

    if (res != CURLE_OK || http_code != 200) {
        set_last_error("Failed to list images: HTTP %ld", http_code);
        free(response.data);
        return NULL;
    }

    int count = count_json_array_objects(response.data, "images");
    unsandbox_image_list_t *list = calloc(1, sizeof(unsandbox_image_list_t));
    if (!list) {
        set_last_error("Out of memory");
        free(response.data);
        return NULL;
    }

    if (count > 0) {
        list->images = calloc(count, sizeof(unsandbox_image_t));
        if (!list->images) {
            set_last_error("Out of memory");
            free(list);
            free(response.data);
            return NULL;
        }
        list->count = count;

        const char *images_start = strstr(response.data, "\"images\":[");
        if (images_start) {
            const char *pos = images_start + 10;
            for (size_t i = 0; i < list->count && pos; i++) {
                pos = strchr(pos, '{');
                if (!pos) break;

                list->images[i].id = extract_json_string(pos, "id");
                list->images[i].name = extract_json_string(pos, "name");
                list->images[i].description = extract_json_string(pos, "description");
                list->images[i].visibility = extract_json_string(pos, "visibility");
                list->images[i].source_type = extract_json_string(pos, "source_type");
                list->images[i].source_id = extract_json_string(pos, "source_id");
                list->images[i].owner_api_key = extract_json_string(pos, "owner_api_key");
                list->images[i].locked = (int)extract_json_number(pos, "locked");
                list->images[i].created_at = extract_json_number(pos, "created_at");
                list->images[i].size_bytes = extract_json_number(pos, "size_bytes");

                pos = skip_json_object(pos);
            }
        }
    }

    free(response.data);
    return list;
}

unsandbox_image_t *unsandbox_image_get(
    const char *image_id,
    const char *public_key, const char *secret_key) {

    if (!image_id) {
        set_last_error("Image ID is required");
        return NULL;
    }

    UnsandboxCredentials *creds = get_credentials(public_key, secret_key, -1);
    if (!creds) {
        set_last_error("No credentials available");
        return NULL;
    }

    char url[256];
    snprintf(url, sizeof(url), "%s/images/%s", API_BASE, image_id);

    CURL *curl = curl_easy_init();
    if (!curl) {
        set_last_error("Failed to initialize curl");
        free_credentials(creds);
        return NULL;
    }

    struct ResponseBuffer response = {0};
    response.data = malloc(1);
    response.size = 0;

    char path[128];
    snprintf(path, sizeof(path), "/images/%s", image_id);

    struct curl_slist *headers = NULL;
    headers = add_hmac_auth_headers(headers, creds, "GET", path, NULL);

    curl_easy_setopt(curl, CURLOPT_URL, url);
    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
    curl_easy_setopt(curl, CURLOPT_USERAGENT, "un-cli/2.0");

    CURLcode res = curl_easy_perform(curl);
    long http_code = 0;
    curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);

    curl_slist_free_all(headers);
    curl_easy_cleanup(curl);
    free_credentials(creds);

    if (res != CURLE_OK || http_code != 200) {
        set_last_error("Failed to get image: HTTP %ld", http_code);
        free(response.data);
        return NULL;
    }

    unsandbox_image_t *image = calloc(1, sizeof(unsandbox_image_t));
    if (!image) {
        set_last_error("Out of memory");
        free(response.data);
        return NULL;
    }

    image->id = extract_json_string(response.data, "id");
    image->name = extract_json_string(response.data, "name");
    image->description = extract_json_string(response.data, "description");
    image->visibility = extract_json_string(response.data, "visibility");
    image->source_type = extract_json_string(response.data, "source_type");
    image->source_id = extract_json_string(response.data, "source_id");
    image->owner_api_key = extract_json_string(response.data, "owner_api_key");
    image->locked = (int)extract_json_number(response.data, "locked");
    image->created_at = extract_json_number(response.data, "created_at");
    image->size_bytes = extract_json_number(response.data, "size_bytes");

    free(response.data);
    return image;
}

char *unsandbox_image_publish(
    const char *source_type, const char *source_id,
    const char *name, const char *description,
    const char *public_key, const char *secret_key) {

    if (!source_type || !source_id) {
        set_last_error("Source type and ID are required");
        return NULL;
    }

    UnsandboxCredentials *creds = get_credentials(public_key, secret_key, -1);
    if (!creds) {
        set_last_error("No credentials available");
        return NULL;
    }

    // Use internal function
    char *result = image_publish(creds, source_type, source_id, name, description);
    free_credentials(creds);

    if (!result) {
        set_last_error("Failed to publish image");
        return NULL;
    }

    char *image_id = extract_json_string(result, "id");
    free(result);
    return image_id;
}

int unsandbox_image_delete(
    const char *image_id,
    const char *public_key, const char *secret_key) {

    if (!image_id) {
        set_last_error("Image ID is required");
        return -1;
    }

    UnsandboxCredentials *creds = get_credentials(public_key, secret_key, -1);
    if (!creds) {
        set_last_error("No credentials available");
        return -1;
    }

    int result = delete_image(creds, image_id);
    free_credentials(creds);
    return result;
}

int unsandbox_image_lock(
    const char *image_id,
    const char *public_key, const char *secret_key) {

    if (!image_id) {
        set_last_error("Image ID is required");
        return -1;
    }

    UnsandboxCredentials *creds = get_credentials(public_key, secret_key, -1);
    if (!creds) {
        set_last_error("No credentials available");
        return -1;
    }

    int result = lock_image(creds, image_id);
    free_credentials(creds);
    return result;
}

int unsandbox_image_unlock(
    const char *image_id,
    const char *public_key, const char *secret_key) {

    if (!image_id) {
        set_last_error("Image ID is required");
        return -1;
    }

    UnsandboxCredentials *creds = get_credentials(public_key, secret_key, -1);
    if (!creds) {
        set_last_error("No credentials available");
        return -1;
    }

    int result = unlock_image(creds, image_id);
    free_credentials(creds);
    return result;
}

int unsandbox_image_set_visibility(
    const char *image_id, const char *visibility,
    const char *public_key, const char *secret_key) {

    if (!image_id || !visibility) {
        set_last_error("Image ID and visibility are required");
        return -1;
    }

    UnsandboxCredentials *creds = get_credentials(public_key, secret_key, -1);
    if (!creds) {
        set_last_error("No credentials available");
        return -1;
    }

    int result = set_image_visibility(creds, image_id, visibility);
    free_credentials(creds);
    return result;
}

int unsandbox_image_grant_access(
    const char *image_id, const char *trusted_api_key,
    const char *public_key, const char *secret_key) {

    if (!image_id || !trusted_api_key) {
        set_last_error("Image ID and trusted API key are required");
        return -1;
    }

    UnsandboxCredentials *creds = get_credentials(public_key, secret_key, -1);
    if (!creds) {
        set_last_error("No credentials available");
        return -1;
    }

    int result = grant_image_access(creds, image_id, trusted_api_key);
    free_credentials(creds);
    return result;
}

int unsandbox_image_revoke_access(
    const char *image_id, const char *trusted_api_key,
    const char *public_key, const char *secret_key) {

    if (!image_id || !trusted_api_key) {
        set_last_error("Image ID and trusted API key are required");
        return -1;
    }

    UnsandboxCredentials *creds = get_credentials(public_key, secret_key, -1);
    if (!creds) {
        set_last_error("No credentials available");
        return -1;
    }

    int result = revoke_image_access(creds, image_id, trusted_api_key);
    free_credentials(creds);
    return result;
}

char **unsandbox_image_list_trusted(
    const char *image_id, size_t *count,
    const char *public_key, const char *secret_key) {

    if (!image_id || !count) {
        set_last_error("Image ID and count pointer are required");
        return NULL;
    }

    *count = 0;

    UnsandboxCredentials *creds = get_credentials(public_key, secret_key, -1);
    if (!creds) {
        set_last_error("No credentials available");
        return NULL;
    }

    char *result = list_image_trusted(creds, image_id);
    free_credentials(creds);

    if (!result) {
        set_last_error("Failed to list trusted keys");
        return NULL;
    }

    // Count keys in response
    const char *keys_start = strstr(result, "\"trusted_keys\":[");
    if (!keys_start) {
        free(result);
        return NULL;
    }

    size_t key_count = 0;
    const char *p = strchr(keys_start, '[');
    if (p) {
        p++;
        while (*p && *p != ']') {
            if (*p == '"') {
                key_count++;
                p++;
                while (*p && *p != '"') p++;
            }
            if (*p) p++;
        }
    }

    if (key_count == 0) {
        free(result);
        return NULL;
    }

    char **keys = calloc(key_count, sizeof(char *));
    if (!keys) {
        set_last_error("Out of memory");
        free(result);
        return NULL;
    }

    // Parse keys
    p = strchr(keys_start, '[');
    if (p) {
        p++;
        size_t i = 0;
        while (*p && *p != ']' && i < key_count) {
            if (*p == '"') {
                p++;
                const char *end = strchr(p, '"');
                if (end) {
                    size_t len = end - p;
                    keys[i] = malloc(len + 1);
                    if (keys[i]) {
                        memcpy(keys[i], p, len);
                        keys[i][len] = '\0';
                        i++;
                    }
                    p = end;
                }
            }
            if (*p) p++;
        }
        *count = i;
    }

    free(result);
    return keys;
}

int unsandbox_image_transfer(
    const char *image_id, const char *to_api_key,
    const char *public_key, const char *secret_key) {

    if (!image_id || !to_api_key) {
        set_last_error("Image ID and destination API key are required");
        return -1;
    }

    UnsandboxCredentials *creds = get_credentials(public_key, secret_key, -1);
    if (!creds) {
        set_last_error("No credentials available");
        return -1;
    }

    int result = transfer_image(creds, image_id, to_api_key);
    free_credentials(creds);
    return result;
}

char *unsandbox_image_spawn(
    const char *image_id, const char *name, const char *ports,
    const char *bootstrap, const char *network_mode,
    const char *public_key, const char *secret_key) {

    if (!image_id) {
        set_last_error("Image ID is required");
        return NULL;
    }

    UnsandboxCredentials *creds = get_credentials(public_key, secret_key, -1);
    if (!creds) {
        set_last_error("No credentials available");
        return NULL;
    }

    char *result = spawn_from_image(creds, image_id, name, ports, bootstrap, network_mode);
    free_credentials(creds);

    if (!result) {
        set_last_error("Failed to spawn from image");
        return NULL;
    }

    char *service_id = extract_json_string(result, "id");
    free(result);
    return service_id;
}

char *unsandbox_image_clone(
    const char *image_id, const char *name, const char *description,
    const char *public_key, const char *secret_key) {

    if (!image_id) {
        set_last_error("Image ID is required");
        return NULL;
    }

    UnsandboxCredentials *creds = get_credentials(public_key, secret_key, -1);
    if (!creds) {
        set_last_error("No credentials available");
        return NULL;
    }

    char *result = clone_image(creds, image_id, name, description);
    free_credentials(creds);

    if (!result) {
        set_last_error("Failed to clone image");
        return NULL;
    }

    char *cloned_id = extract_json_string(result, "id");
    free(result);
    return cloned_id;
}

/* ============================================================================
 * Memory Management for Images
 * ============================================================================ */

void unsandbox_free_image(unsandbox_image_t *image) {
    if (!image) return;
    free(image->id);
    free(image->name);
    free(image->description);
    free(image->visibility);
    free(image->source_type);
    free(image->source_id);
    free(image->owner_api_key);
    free(image);
}

void unsandbox_free_image_list(unsandbox_image_list_t *images) {
    if (!images) return;
    for (size_t i = 0; i < images->count; i++) {
        free(images->images[i].id);
        free(images->images[i].name);
        free(images->images[i].description);
        free(images->images[i].visibility);
        free(images->images[i].source_type);
        free(images->images[i].source_id);
        free(images->images[i].owner_api_key);
    }
    free(images->images);
    free(images);
}

void unsandbox_free_trusted_keys(char **keys, size_t count) {
    if (!keys) return;
    for (size_t i = 0; i < count; i++) {
        free(keys[i]);
    }
    free(keys);
}

/* Utility */
const char *unsandbox_last_error(void) {
    return unsandbox_error_buffer[0] ? unsandbox_error_buffer : NULL;
}
#endif /* UNSANDBOX_LIBRARY - end of library API */

#ifndef UNSANDBOX_LIBRARY
int main(int argc, char *argv[]) {
    // Disable stdout buffering for real-time output
    setvbuf(stdout, NULL, _IONBF, 0);

    const char *filename = NULL;
    const char *cli_public_key = NULL;   // -p flag
    const char *cli_secret_key = NULL;   // -k flag
    int cli_account_index = -1;          // --account flag (-1 = use env or default)
    const char *artifact_dir = NULL;
    const char *network_mode = NULL;
    const char *shell = NULL;  // -s/--shell for language
    int vcpu = 0;  // 0 = default (1), valid values: 1, 2, 4, 8
    int ttl = 0;   // 0 = default (60s), valid values: 1-900
    int save_artifacts = 0;
    int skip_confirm = 0;  // -y flag to skip large upload confirmation

    struct InputFile input_files[MAX_INPUT_FILES];
    int input_file_count = 0;
    long total_input_size = 0;  // Track total input file size

    struct EnvVar env_vars[MAX_ENV_VARS];
    int env_var_count = 0;

    // Check for key command first
    if (argc >= 2 && strcmp(argv[1], "key") == 0) {
        int do_extend = 0;

        // Parse options
        for (int i = 2; i < argc; i++) {
            if (strcmp(argv[i], "-p") == 0 && i + 1 < argc) {
                i++;
                cli_public_key = argv[i];
            } else if (strcmp(argv[i], "-k") == 0 && i + 1 < argc) {
                i++;
                cli_secret_key = argv[i];
            } else if (strcmp(argv[i], "--account") == 0 && i + 1 < argc) {
                i++;
                cli_account_index = atoi(argv[i]);
            } else if (strcmp(argv[i], "--extend") == 0) {
                do_extend = 1;
            }
        }

        // Get credentials (priority: flags > env > file with --account)
        UnsandboxCredentials *creds = get_credentials(cli_public_key, cli_secret_key, cli_account_index);

        if (!creds || !creds->public_key || strlen(creds->public_key) == 0) {
            fprintf(stderr, "Error: API credentials required.\n");
            fprintf(stderr, "  Set UNSANDBOX_PUBLIC_KEY + UNSANDBOX_SECRET_KEY env vars, or\n");
            fprintf(stderr, "  Use -p PUBLIC_KEY -k SECRET_KEY flags, or\n");
            fprintf(stderr, "  Create ~/.unsandbox/accounts.csv with: public_key,secret_key\n");
            free_credentials(creds);
            return 1;
        }

        // Handle --extend: validate key to get public key, then open portal
        if (do_extend) {
            // If we have public_key from credentials, use it directly
            if (creds->public_key) {
                char extend_url[512];
                snprintf(extend_url, sizeof(extend_url), "%s/keys/extend?pk=%s", PORTAL_BASE, creds->public_key);
                printf("Opening extension page in browser...\n");
                printf("If browser doesn't open, visit: %s\n", extend_url);

                // Try to open URL in browser
                #ifdef __APPLE__
                char cmd[1024];
                snprintf(cmd, sizeof(cmd), "open '%s'", extend_url);
                if (system(cmd) != 0) { /* ignore */ }
                #elif defined(__linux__)
                char cmd[2048];
                snprintf(cmd, sizeof(cmd), "xdg-open '%s' 2>/dev/null || sensible-browser '%s' 2>/dev/null", extend_url, extend_url);
                if (system(cmd) != 0) { /* ignore */ }
                #elif defined(_WIN32)
                char cmd[1024];
                snprintf(cmd, sizeof(cmd), "start %s", extend_url);
                if (system(cmd) != 0) { /* ignore */ }
                #endif

                free_credentials(creds);
                return 0;
            }
        }

        // Default: validate key using HMAC authentication
        curl_global_init(CURL_GLOBAL_DEFAULT);
        int ret = validate_api_key(creds);
        curl_global_cleanup();
        free_credentials(creds);
        return ret;
    }

    // Check for languages command
    if (argc >= 2 && strcmp(argv[1], "languages") == 0) {
        int json_output = 0;

        // Parse options
        for (int i = 2; i < argc; i++) {
            if (strcmp(argv[i], "-p") == 0 && i + 1 < argc) {
                i++;
                cli_public_key = argv[i];
            } else if (strcmp(argv[i], "-k") == 0 && i + 1 < argc) {
                i++;
                cli_secret_key = argv[i];
            } else if (strcmp(argv[i], "--account") == 0 && i + 1 < argc) {
                i++;
                cli_account_index = atoi(argv[i]);
            } else if (strcmp(argv[i], "--json") == 0) {
                json_output = 1;
            } else if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) {
                fprintf(stderr, "Usage: %s languages [options]\n\n", argv[0]);
                fprintf(stderr, "List all available languages for code execution.\n\n");
                fprintf(stderr, "Options:\n");
                fprintf(stderr, "  --json    Output as JSON array (for scripts)\n");
                fprintf(stderr, "  -p KEY    Public key\n");
                fprintf(stderr, "  -k KEY    Secret key\n");
                fprintf(stderr, "  -h        Show this help\n");
                return 0;
            }
        }

        // Get credentials
        UnsandboxCredentials *creds = get_credentials(cli_public_key, cli_secret_key, cli_account_index);

        if (!creds || !creds->public_key || strlen(creds->public_key) == 0) {
            fprintf(stderr, "Error: API credentials required.\n");
            fprintf(stderr, "  Set UNSANDBOX_PUBLIC_KEY + UNSANDBOX_SECRET_KEY env vars, or\n");
            fprintf(stderr, "  Use -p PUBLIC_KEY -k SECRET_KEY flags, or\n");
            fprintf(stderr, "  Create ~/.unsandbox/accounts.csv with: public_key,secret_key\n");
            free_credentials(creds);
            return 1;
        }

        curl_global_init(CURL_GLOBAL_DEFAULT);
        int ret = list_languages_cli(creds, json_output);
        curl_global_cleanup();
        free_credentials(creds);
        return ret;
    }

    // Check for snapshot command
    if (argc >= 2 && strcmp(argv[1], "snapshot") == 0) {
        const char *snapshot_id = NULL;
        const char *clone_type = NULL;
        const char *clone_name = NULL;
        const char *clone_shell = NULL;
        const char *clone_ports = NULL;
        int do_list = 0;
        int do_info = 0;
        int do_delete = 0;
        int do_lock = 0;
        int do_unlock = 0;
        int do_clone = 0;
        int show_help = 0;

        // Parse snapshot-specific args
        for (int i = 2; i < argc; i++) {
            if (strcmp(argv[i], "-p") == 0 && i + 1 < argc) {
                i++;
                cli_public_key = argv[i];
            } else if (strcmp(argv[i], "-k") == 0 && i + 1 < argc) {
                i++;
                cli_secret_key = argv[i];
            } else if (strcmp(argv[i], "--account") == 0 && i + 1 < argc) {
                i++;
                cli_account_index = atoi(argv[i]);
            } else if (strcmp(argv[i], "-l") == 0 || strcmp(argv[i], "--list") == 0) {
                do_list = 1;
            } else if (strcmp(argv[i], "--info") == 0 && i + 1 < argc) {
                do_info = 1;
                i++;
                snapshot_id = argv[i];
            } else if (strcmp(argv[i], "--delete") == 0 && i + 1 < argc) {
                do_delete = 1;
                i++;
                snapshot_id = argv[i];
            } else if (strcmp(argv[i], "--lock") == 0 && i + 1 < argc) {
                do_lock = 1;
                i++;
                snapshot_id = argv[i];
            } else if (strcmp(argv[i], "--unlock") == 0 && i + 1 < argc) {
                do_unlock = 1;
                i++;
                snapshot_id = argv[i];
            } else if (strcmp(argv[i], "--clone") == 0 && i + 1 < argc) {
                do_clone = 1;
                i++;
                snapshot_id = argv[i];
            } else if (strcmp(argv[i], "--type") == 0 && i + 1 < argc) {
                i++;
                clone_type = argv[i];
            } else if (strcmp(argv[i], "--name") == 0 && i + 1 < argc) {
                i++;
                clone_name = argv[i];
            } else if (strcmp(argv[i], "--shell") == 0 && i + 1 < argc) {
                i++;
                clone_shell = argv[i];
            } else if (strcmp(argv[i], "--ports") == 0 && i + 1 < argc) {
                i++;
                clone_ports = argv[i];
            } else if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) {
                show_help = 1;
            } else if (argv[i][0] == '-') {
                fprintf(stderr, "Unknown option: %s\n", argv[i]);
                print_usage(argv[0]);
                return 1;
            }
        }

        if (show_help) {
            print_usage(argv[0]);
            return 0;
        }

        // Get credentials
        UnsandboxCredentials *creds = get_credentials(cli_public_key, cli_secret_key, cli_account_index);
        if (!creds || !creds->public_key || strlen(creds->public_key) == 0) {
            fprintf(stderr, "Error: API credentials required.\n");
            fprintf(stderr, "  Set UNSANDBOX_PUBLIC_KEY + UNSANDBOX_SECRET_KEY env vars, or\n");
            fprintf(stderr, "  Use -p PUBLIC_KEY -k SECRET_KEY flags, or\n");
            fprintf(stderr, "  Create ~/.unsandbox/accounts.csv with: public_key,secret_key\n");
            free_credentials(creds);
            return 1;
        }

        curl_global_init(CURL_GLOBAL_DEFAULT);
        int ret = 0;

        if (do_list) {
            ret = list_snapshots(creds);
        } else if (do_info) {
            ret = get_snapshot_info(creds, snapshot_id);
        } else if (do_delete) {
            ret = delete_snapshot(creds, snapshot_id);
        } else if (do_lock) {
            ret = lock_snapshot(creds, snapshot_id);
        } else if (do_unlock) {
            ret = unlock_snapshot(creds, snapshot_id);
        } else if (do_clone) {
            if (!clone_type) {
                fprintf(stderr, "Error: --type required for --clone (session or service)\n");
                curl_global_cleanup();
                free_credentials(creds);
                return 1;
            }
            if (strcmp(clone_type, "session") != 0 && strcmp(clone_type, "service") != 0) {
                fprintf(stderr, "Error: --type must be 'session' or 'service'\n");
                curl_global_cleanup();
                free_credentials(creds);
                return 1;
            }
            ret = clone_snapshot(creds, snapshot_id, clone_type, clone_name, clone_shell, clone_ports);
        } else {
            fprintf(stderr, "Error: No snapshot action specified. Use --list, --info, --delete, --lock, --unlock, or --clone\n");
            print_usage(argv[0]);
            curl_global_cleanup();
            free_credentials(creds);
            return 1;
        }

        curl_global_cleanup();
        free_credentials(creds);
        return ret;
    }

    // Check for image command
    if (argc >= 2 && strcmp(argv[1], "image") == 0) {
        const char *image_id = NULL;
        const char *visibility = NULL;
        const char *spawn_name = NULL;
        const char *spawn_ports = NULL;
        const char *source_type = NULL;
        int do_list = 0;
        int do_info = 0;
        int do_delete = 0;
        int do_lock = 0;
        int do_unlock = 0;
        int do_publish = 0;
        int do_visibility = 0;
        int do_spawn = 0;
        int do_clone = 0;
        int show_help = 0;

        // Parse image-specific args
        for (int i = 2; i < argc; i++) {
            if (strcmp(argv[i], "-p") == 0 && i + 1 < argc) {
                i++;
                cli_public_key = argv[i];
            } else if (strcmp(argv[i], "-k") == 0 && i + 1 < argc) {
                i++;
                cli_secret_key = argv[i];
            } else if (strcmp(argv[i], "--account") == 0 && i + 1 < argc) {
                i++;
                cli_account_index = atoi(argv[i]);
            } else if (strcmp(argv[i], "-l") == 0 || strcmp(argv[i], "--list") == 0) {
                do_list = 1;
            } else if (strcmp(argv[i], "--info") == 0 && i + 1 < argc) {
                do_info = 1;
                i++;
                image_id = argv[i];
            } else if (strcmp(argv[i], "--delete") == 0 && i + 1 < argc) {
                do_delete = 1;
                i++;
                image_id = argv[i];
            } else if (strcmp(argv[i], "--lock") == 0 && i + 1 < argc) {
                do_lock = 1;
                i++;
                image_id = argv[i];
            } else if (strcmp(argv[i], "--unlock") == 0 && i + 1 < argc) {
                do_unlock = 1;
                i++;
                image_id = argv[i];
            } else if (strcmp(argv[i], "--publish") == 0 && i + 1 < argc) {
                do_publish = 1;
                i++;
                image_id = argv[i];  // This is actually the source ID (service/snapshot)
            } else if (strcmp(argv[i], "--visibility") == 0 && i + 2 < argc) {
                do_visibility = 1;
                i++;
                image_id = argv[i];
                i++;
                visibility = argv[i];
            } else if (strcmp(argv[i], "--spawn") == 0 && i + 1 < argc) {
                do_spawn = 1;
                i++;
                image_id = argv[i];
            } else if (strcmp(argv[i], "--clone") == 0 && i + 1 < argc) {
                do_clone = 1;
                i++;
                image_id = argv[i];
            } else if (strcmp(argv[i], "--name") == 0 && i + 1 < argc) {
                i++;
                spawn_name = argv[i];
            } else if (strcmp(argv[i], "--ports") == 0 && i + 1 < argc) {
                i++;
                spawn_ports = argv[i];
            } else if (strcmp(argv[i], "--source-type") == 0 && i + 1 < argc) {
                i++;
                source_type = argv[i];
            } else if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) {
                show_help = 1;
            } else if (argv[i][0] == '-') {
                fprintf(stderr, "Unknown option: %s\n", argv[i]);
                print_usage(argv[0]);
                return 1;
            }
        }

        if (show_help) {
            print_usage(argv[0]);
            return 0;
        }

        // Get credentials
        UnsandboxCredentials *creds = get_credentials(cli_public_key, cli_secret_key, cli_account_index);
        if (!creds || !creds->public_key || strlen(creds->public_key) == 0) {
            fprintf(stderr, "Error: API credentials required.\n");
            fprintf(stderr, "  Set UNSANDBOX_PUBLIC_KEY + UNSANDBOX_SECRET_KEY env vars, or\n");
            fprintf(stderr, "  Use -p PUBLIC_KEY -k SECRET_KEY flags, or\n");
            fprintf(stderr, "  Create ~/.unsandbox/accounts.csv with: public_key,secret_key\n");
            free_credentials(creds);
            return 1;
        }

        curl_global_init(CURL_GLOBAL_DEFAULT);
        int ret = 0;

        if (do_list) {
            char *response = list_images(creds, NULL);
            if (response) {
                // Parse and display image list from JSON response
                const char *images_start = strstr(response, "\"images\":[");
                if (!images_start) {
                    printf("No images found.\n");
                    free(response);
                } else {
                    // Count images
                    int count = 0;
                    const char *p = images_start;
                    while ((p = strchr(p + 1, '{')) != NULL && strchr(images_start, ']') && p < strchr(images_start, ']')) {
                        count++;
                    }

                    if (count == 0) {
                        printf("No images found.\n");
                    } else {
                        printf("%-40s %-20s %-10s %-8s %-10s\n", "ID", "NAME", "VISIBILITY", "LOCKED", "SOURCE");
                        printf("%-40s %-20s %-10s %-8s %-10s\n", "----------------------------------------",
                               "--------------------", "----------", "--------", "----------");

                        // Parse each image object
                        const char *pos = strchr(images_start, '[');
                        if (pos) {
                            while ((pos = strchr(pos, '{')) != NULL) {
                                // Check we haven't passed the end of the array
                                const char *arr_end = strchr(images_start, ']');
                                if (arr_end && pos > arr_end) break;

                                char *img_id = extract_json_string(pos, "id");
                                char *img_name = extract_json_string(pos, "name");
                                char *img_visibility = extract_json_string(pos, "visibility");
                                char *img_source = extract_json_string(pos, "source_type");

                                // Extract locked boolean
                                const char *locked_str = strstr(pos, "\"locked\":");
                                int locked = 0;
                                if (locked_str && locked_str < strchr(pos, '}')) {
                                    locked_str += 9;
                                    while (*locked_str == ' ') locked_str++;
                                    locked = (strncmp(locked_str, "true", 4) == 0);
                                }

                                printf("%-40s %-20s %-10s %-8s %-10s\n",
                                       img_id ? img_id : "",
                                       img_name ? img_name : "",
                                       img_visibility ? img_visibility : "",
                                       locked ? "yes" : "no",
                                       img_source ? img_source : "");

                                if (img_id) free(img_id);
                                if (img_name) free(img_name);
                                if (img_visibility) free(img_visibility);
                                if (img_source) free(img_source);

                                pos++;
                            }
                        }
                    }
                    free(response);
                }
            } else {
                fprintf(stderr, "Error: Failed to list images\n");
                ret = 1;
            }
        } else if (do_info) {
            char *response = get_image(creds, image_id);
            if (response) {
                printf("%s\n", response);
                free(response);
            } else {
                fprintf(stderr, "Error: Failed to get image info\n");
                ret = 1;
            }
        } else if (do_delete) {
            ret = delete_image(creds, image_id);
            if (ret == 0) {
                printf("Image deleted: %s\n", image_id);
            }
        } else if (do_lock) {
            ret = lock_image(creds, image_id);
            if (ret == 0) {
                printf("Image locked: %s\n", image_id);
            }
        } else if (do_unlock) {
            ret = unlock_image(creds, image_id);
            if (ret == 0) {
                printf("Image unlocked: %s\n", image_id);
            }
        } else if (do_visibility) {
            if (!visibility || (strcmp(visibility, "private") != 0 &&
                               strcmp(visibility, "unlisted") != 0 &&
                               strcmp(visibility, "public") != 0)) {
                fprintf(stderr, "Error: visibility must be 'private', 'unlisted', or 'public'\n");
                ret = 1;
            } else {
                ret = set_image_visibility(creds, image_id, visibility);
                if (ret == 0) {
                    printf("Image visibility set to %s: %s\n", visibility, image_id);
                }
            }
        } else if (do_spawn) {
            char *result = spawn_from_image(creds, image_id, spawn_name, spawn_ports, NULL, NULL);
            if (result) {
                printf("%s\n", result);
                free(result);
            } else {
                fprintf(stderr, "Error: Failed to spawn from image\n");
                ret = 1;
            }
        } else if (do_publish) {
            if (!source_type) {
                fprintf(stderr, "Error: --source-type required for --publish (service or snapshot)\n");
                ret = 1;
            } else {
                char *result = image_publish(creds, source_type, image_id, spawn_name, NULL);
                if (result) {
                    printf("Image published: %s\n", result);
                    free(result);
                } else {
                    fprintf(stderr, "Error: Failed to publish image\n");
                    ret = 1;
                }
            }
        } else if (do_clone) {
            char *result = clone_image(creds, image_id, spawn_name, NULL);
            if (result) {
                printf("Image cloned: %s\n", result);
                free(result);
            } else {
                fprintf(stderr, "Error: Failed to clone image\n");
                ret = 1;
            }
        } else {
            fprintf(stderr, "Error: No image action specified. Use --list, --info, --delete, --lock, --unlock, --visibility, --spawn, --publish, or --clone\n");
            print_usage(argv[0]);
            ret = 1;
        }

        curl_global_cleanup();
        free_credentials(creds);
        return ret;
    }

    // Check for service command first
    if (argc >= 2 && strcmp(argv[1], "service") == 0) {
        const char *service_name = NULL;
        const char *service_ports = NULL;
        const char *service_domains = NULL;
        const char *service_type = NULL;
        const char *golden_image = NULL;
        const char *service_bootstrap = NULL;
        const char *bootstrap_file = NULL;
        const char *service_id = NULL;
        struct InputFile service_input_files[MAX_INPUT_FILES];
        int service_input_file_count = 0;
        int show_help = 0;
        int do_list = 0;
        int do_info = 0;
        int do_tail = 0;
        int do_logs = 0;
        int do_download_logs = 0;
        const char *download_logs_file = NULL;
        int do_freeze = 0;
        int do_unfreeze = 0;
        int do_destroy = 0;
        int do_lock = 0;
        int do_unlock = 0;
        int do_auto_unfreeze = 0;
        int do_no_auto_unfreeze = 0;
        int do_show_freeze_page = 0;
        int do_no_show_freeze_page = 0;
        int do_resize = 0;
        int do_redeploy = 0;
        int do_execute = 0;
        const char *execute_command = NULL;
        int execute_timeout = 30000;  // Default 30 seconds
        int do_dump_bootstrap = 0;
        const char *dump_bootstrap_file = NULL;
        int do_snapshot = 0;
        int do_restore = 0;
        const char *restore_snapshot_id = NULL;
        const char *snapshot_name = NULL;
        int hot_snapshot = 0;
        int do_add_domain = 0;
        int do_remove_domain = 0;
        int do_set_domains = 0;
        int create_unfreeze_on_demand = 0;  // For --unfreeze-on-demand flag on create

        // Environment variables for service create
        char *service_env_content = NULL;  // Accumulated env vars (KEY=VALUE\n...)
        size_t service_env_size = 0;
        size_t service_env_capacity = 0;
        const char *service_env_file = NULL;  // --env-file path

        // Env subcommand: un service env status|set|export|delete <name>
        const char *env_subcommand = NULL;
        const char *env_target_id = NULL;

        // Parse service-specific args (flags like session)
        for (int i = 2; i < argc; i++) {
            // Check for "env" subcommand: un service env <action> <id>
            if (strcmp(argv[i], "env") == 0 && i + 2 < argc) {
                env_subcommand = argv[i + 1];  // status, set, export, delete
                env_target_id = argv[i + 2];   // service name/id
                i += 2;
                // Continue parsing for --env-file in case of "set"
                continue;
            }
            if (strcmp(argv[i], "-p") == 0 && i + 1 < argc) {
                i++;
                cli_public_key = argv[i];
            } else if (strcmp(argv[i], "-k") == 0 && i + 1 < argc) {
                i++;
                cli_secret_key = argv[i];
            } else if (strcmp(argv[i], "--account") == 0 && i + 1 < argc) {
                i++;
                cli_account_index = atoi(argv[i]);
            } else if (strcmp(argv[i], "-n") == 0 && i + 1 < argc) {
                i++;
                network_mode = argv[i];
            } else if (strcmp(argv[i], "--name") == 0 && i + 1 < argc) {
                i++;
                service_name = argv[i];
            } else if (strcmp(argv[i], "--ports") == 0 && i + 1 < argc) {
                i++;
                service_ports = argv[i];
            } else if (strcmp(argv[i], "--domains") == 0 && i + 1 < argc) {
                i++;
                service_domains = argv[i];
            } else if (strcmp(argv[i], "--type") == 0 && i + 1 < argc) {
                i++;
                service_type = argv[i];
            } else if (strcmp(argv[i], "--golden-image") == 0 && i + 1 < argc) {
                i++;
                golden_image = argv[i];
            } else if (strcmp(argv[i], "--bootstrap") == 0 && i + 1 < argc) {
                i++;
                service_bootstrap = argv[i];
            } else if (strcmp(argv[i], "--bootstrap-file") == 0 && i + 1 < argc) {
                i++;
                bootstrap_file = argv[i];
            } else if (strcmp(argv[i], "-f") == 0 && i + 1 < argc) {
                i++;
                if (service_input_file_count >= MAX_INPUT_FILES) {
                    fprintf(stderr, "Error: too many input files (max %d)\n", MAX_INPUT_FILES);
                    return 1;
                }
                // Read file and base64 encode
                size_t fsize;
                char *content = read_file(argv[i], &fsize);
                if (!content) {
                    fprintf(stderr, "Error: cannot read file '%s'\n", argv[i]);
                    return 1;
                }
                size_t b64_len;
                char *b64 = base64_encode((unsigned char *)content, fsize, &b64_len);
                free(content);
                if (!b64) {
                    fprintf(stderr, "Error: failed to encode file '%s'\n", argv[i]);
                    return 1;
                }
                service_input_files[service_input_file_count].filename = strdup(get_basename(argv[i]));
                service_input_files[service_input_file_count].content_base64 = b64;
                service_input_file_count++;
            } else if (strcmp(argv[i], "-F") == 0 && i + 1 < argc) {
                // -F preserves relative path (for directory structures)
                i++;
                if (service_input_file_count >= MAX_INPUT_FILES) {
                    fprintf(stderr, "Error: too many input files (max %d)\n", MAX_INPUT_FILES);
                    return 1;
                }
                size_t fsize;
                char *content = read_file(argv[i], &fsize);
                if (!content) {
                    fprintf(stderr, "Error: cannot read file '%s'\n", argv[i]);
                    return 1;
                }
                size_t b64_len;
                char *b64 = base64_encode((unsigned char *)content, fsize, &b64_len);
                free(content);
                if (!b64) {
                    fprintf(stderr, "Error: failed to encode file '%s'\n", argv[i]);
                    return 1;
                }
                // Use full path instead of basename
                service_input_files[service_input_file_count].filename = strdup(argv[i]);
                service_input_files[service_input_file_count].content_base64 = b64;
                service_input_file_count++;
            } else if (strcmp(argv[i], "-l") == 0 || strcmp(argv[i], "--list") == 0) {
                do_list = 1;
            } else if (strcmp(argv[i], "--info") == 0 && i + 1 < argc) {
                do_info = 1;
                i++;
                service_id = argv[i];
            } else if (strcmp(argv[i], "--tail") == 0 && i + 1 < argc) {
                do_tail = 1;
                i++;
                service_id = argv[i];
            } else if (strcmp(argv[i], "--logs") == 0 && i + 1 < argc) {
                do_logs = 1;
                i++;
                service_id = argv[i];
            } else if (strcmp(argv[i], "--download-logs") == 0 && i + 2 < argc) {
                do_download_logs = 1;
                i++;
                service_id = argv[i];
                i++;
                download_logs_file = argv[i];
            } else if (strcmp(argv[i], "--freeze") == 0 && i + 1 < argc) {
                do_freeze = 1;
                i++;
                service_id = argv[i];
            } else if (strcmp(argv[i], "--unfreeze") == 0 && i + 1 < argc) {
                do_unfreeze = 1;
                i++;
                service_id = argv[i];
            } else if (strcmp(argv[i], "--destroy") == 0 && i + 1 < argc) {
                do_destroy = 1;
                i++;
                service_id = argv[i];
            } else if (strcmp(argv[i], "--lock") == 0 && i + 1 < argc) {
                do_lock = 1;
                i++;
                service_id = argv[i];
            } else if (strcmp(argv[i], "--unlock") == 0 && i + 1 < argc) {
                do_unlock = 1;
                i++;
                service_id = argv[i];
            } else if (strcmp(argv[i], "--add-domain") == 0 && i + 1 < argc) {
                do_add_domain = 1;
                i++;
                service_id = argv[i];
            } else if (strcmp(argv[i], "--remove-domain") == 0 && i + 1 < argc) {
                do_remove_domain = 1;
                i++;
                service_id = argv[i];
            } else if (strcmp(argv[i], "--set-domains") == 0 && i + 1 < argc) {
                do_set_domains = 1;
                i++;
                service_id = argv[i];
            } else if (strcmp(argv[i], "--auto-unfreeze") == 0 && i + 1 < argc) {
                do_auto_unfreeze = 1;
                i++;
                service_id = argv[i];
            } else if (strcmp(argv[i], "--no-auto-unfreeze") == 0 && i + 1 < argc) {
                do_no_auto_unfreeze = 1;
                i++;
                service_id = argv[i];
            } else if (strcmp(argv[i], "--show-freeze-page") == 0 && i + 1 < argc) {
                do_show_freeze_page = 1;
                i++;
                service_id = argv[i];
            } else if (strcmp(argv[i], "--no-show-freeze-page") == 0 && i + 1 < argc) {
                do_no_show_freeze_page = 1;
                i++;
                service_id = argv[i];
            } else if (strcmp(argv[i], "--resize") == 0 && i + 1 < argc) {
                do_resize = 1;
                i++;
                service_id = argv[i];
            } else if (strcmp(argv[i], "--redeploy") == 0 && i + 1 < argc) {
                do_redeploy = 1;
                i++;
                service_id = argv[i];
            } else if (strcmp(argv[i], "--execute") == 0 && i + 2 < argc) {
                do_execute = 1;
                i++;
                service_id = argv[i];
                i++;
                execute_command = argv[i];
            } else if ((strcmp(argv[i], "-t") == 0 || strcmp(argv[i], "--timeout") == 0) && i + 1 < argc) {
                i++;
                execute_timeout = atoi(argv[i]) * 1000;  // Convert seconds to ms
                if (execute_timeout < 0) execute_timeout = 0;  // 0 = unlimited
                // No max cap - let it run as long as needed
            } else if (strcmp(argv[i], "--dump-bootstrap") == 0 && i + 1 < argc) {
                do_dump_bootstrap = 1;
                i++;
                service_id = argv[i];
                // Optional file argument
                if (i + 1 < argc && argv[i + 1][0] != '-') {
                    i++;
                    dump_bootstrap_file = argv[i];
                }
            } else if ((strcmp(argv[i], "-v") == 0 || strcmp(argv[i], "--vcpu") == 0) && i + 1 < argc) {
                i++;
                vcpu = atoi(argv[i]);
                if (vcpu < 1 || vcpu > 8) {
                    fprintf(stderr, "Error: -v/--vcpu must be 1-8\n");
                    return 1;
                }
            } else if (strcmp(argv[i], "--snapshot") == 0 && i + 1 < argc) {
                do_snapshot = 1;
                i++;
                service_id = argv[i];
            } else if (strcmp(argv[i], "--restore") == 0 && i + 1 < argc) {
                do_restore = 1;
                i++;
                restore_snapshot_id = argv[i];
            } else if (strcmp(argv[i], "--snapshot-name") == 0 && i + 1 < argc) {
                i++;
                snapshot_name = argv[i];
            } else if (strcmp(argv[i], "--hot") == 0) {
                hot_snapshot = 1;
            } else if (strcmp(argv[i], "--unfreeze-on-demand") == 0) {
                create_unfreeze_on_demand = 1;
            } else if ((strcmp(argv[i], "-e") == 0 || strcmp(argv[i], "--env") == 0) && i + 1 < argc) {
                // -e KEY=VALUE or --env KEY=VALUE - accumulate env vars
                i++;
                const char *env_pair = argv[i];
                // Validate format: must contain '='
                if (!strchr(env_pair, '=')) {
                    fprintf(stderr, "Error: Invalid environment variable format '%s'. Use KEY=VALUE\n", env_pair);
                    return 1;
                }
                // Add to accumulated env content
                size_t pair_len = strlen(env_pair);
                size_t needed = service_env_size + pair_len + 2;  // +1 for newline, +1 for null
                if (needed > service_env_capacity) {
                    service_env_capacity = needed > 4096 ? needed * 2 : 4096;
                    char *new_content = realloc(service_env_content, service_env_capacity);
                    if (!new_content) {
                        fprintf(stderr, "Error: Out of memory\n");
                        free(service_env_content);
                        return 1;
                    }
                    service_env_content = new_content;
                }
                memcpy(service_env_content + service_env_size, env_pair, pair_len);
                service_env_size += pair_len;
                service_env_content[service_env_size++] = '\n';
                service_env_content[service_env_size] = '\0';
            } else if (strcmp(argv[i], "--env-file") == 0 && i + 1 < argc) {
                i++;
                service_env_file = argv[i];
            } else if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) {
                show_help = 1;
            }
        }

        // Show help if requested or no action specified
        if (show_help) {
            print_usage(argv[0]);
            return 0;
        }

        // Get credentials (priority: env > flags > file)
        UnsandboxCredentials *creds = get_credentials(cli_public_key, cli_secret_key, cli_account_index);
        if (!creds || !creds->public_key || strlen(creds->public_key) == 0) {
            fprintf(stderr, "Error: API credentials required.\n");
            fprintf(stderr, "  Set UNSANDBOX_PUBLIC_KEY + UNSANDBOX_SECRET_KEY env vars, or\n");
            fprintf(stderr, "  Use -p PUBLIC_KEY -k SECRET_KEY flags, or\n");
            fprintf(stderr, "  Create ~/.unsandbox/accounts.csv with: public_key,secret_key\n");
            free_credentials(creds);
            return 1;
        }

        curl_global_init(CURL_GLOBAL_DEFAULT);
        int ret = 0;

        // Handle env subcommand first
        if (env_subcommand) {
            if (strcmp(env_subcommand, "status") == 0) {
                ret = service_env_status(creds, env_target_id);
            } else if (strcmp(env_subcommand, "set") == 0) {
                // Read env content from --env-file, -e flags, or stdin
                char *env_content = NULL;
                if (service_env_file) {
                    env_content = read_env_file(service_env_file);
                } else if (service_env_content && service_env_size > 0) {
                    env_content = service_env_content;
                    service_env_content = NULL;  // Transfer ownership
                } else {
                    // Check if stdin is a TTY
                    if (isatty(STDIN_FILENO)) {
                        fprintf(stderr, "Reading environment variables from stdin (Ctrl+D to finish):\n");
                    }
                    env_content = read_env_stdin();
                }
                if (env_content) {
                    ret = service_env_set(creds, env_target_id, env_content);
                    free(env_content);
                } else {
                    ret = 1;
                }
            } else if (strcmp(env_subcommand, "export") == 0) {
                ret = service_env_export(creds, env_target_id);
            } else if (strcmp(env_subcommand, "delete") == 0) {
                ret = service_env_delete(creds, env_target_id);
            } else {
                fprintf(stderr, "Error: Unknown env subcommand '%s'. Use: status, set, export, delete\n", env_subcommand);
                ret = 1;
            }
            free(service_env_content);
            curl_global_cleanup();
            free_credentials(creds);
            return ret;
        }

        if (do_list) {
            ret = list_services(creds);
        } else if (do_info) {
            ret = get_service_info(creds, service_id);
        } else if (do_tail) {
            // --tail: last 9000 lines (default)
            char *log = get_service_logs(creds, service_id, 0);
            if (log) {
                printf("%s", log);
                free(log);
                ret = 0;
            } else {
                fprintf(stderr, "Error: Failed to fetch logs (service not found or no logs available)\n");
                ret = 1;
            }
        } else if (do_logs) {
            // --logs: all logs
            char *log = get_service_logs(creds, service_id, 1);
            if (log) {
                printf("%s", log);
                free(log);
                ret = 0;
            } else {
                fprintf(stderr, "Error: Failed to fetch logs (service not found or no logs available)\n");
                ret = 1;
            }
        } else if (do_download_logs) {
            // --download-logs: all logs to file
            char *log = get_service_logs(creds, service_id, 1);
            if (log) {
                FILE *f = fopen(download_logs_file, "w");
                if (f) {
                    fprintf(f, "%s", log);
                    fclose(f);
                    printf("Logs saved to %s\n", download_logs_file);
                    ret = 0;
                } else {
                    fprintf(stderr, "Error: Could not open file %s for writing\n", download_logs_file);
                    ret = 1;
                }
                free(log);
            } else {
                fprintf(stderr, "Error: Failed to fetch logs (service not found or no logs available)\n");
                ret = 1;
            }
        } else if (do_freeze) {
            ret = freeze_service(creds, service_id);
        } else if (do_unfreeze) {
            ret = unfreeze_service(creds, service_id);
        } else if (do_destroy) {
            ret = destroy_service(creds, service_id);
        } else if (do_lock) {
            ret = lock_service(creds, service_id);
        } else if (do_unlock) {
            ret = unlock_service(creds, service_id);
        } else if (do_add_domain) {
            if (!service_domains) {
                fprintf(stderr, "Error: --domains required with --add-domain\n");
                curl_global_cleanup();
                free_credentials(creds);
                return 2;
            }
            ret = update_service_domains(creds, service_id, "add", service_domains);
        } else if (do_remove_domain) {
            if (!service_domains) {
                fprintf(stderr, "Error: --domains required with --remove-domain\n");
                curl_global_cleanup();
                free_credentials(creds);
                return 2;
            }
            ret = update_service_domains(creds, service_id, "remove", service_domains);
        } else if (do_set_domains) {
            if (!service_domains) {
                fprintf(stderr, "Error: --domains required with --set-domains\n");
                curl_global_cleanup();
                free_credentials(creds);
                return 2;
            }
            ret = update_service_domains(creds, service_id, "custom_domains", service_domains);
        } else if (do_auto_unfreeze) {
            ret = set_unfreeze_on_demand(creds, service_id, 1);
        } else if (do_no_auto_unfreeze) {
            ret = set_unfreeze_on_demand(creds, service_id, 0);
        } else if (do_show_freeze_page) {
            ret = set_show_freeze_page(creds, service_id, 1);
        } else if (do_no_show_freeze_page) {
            ret = set_show_freeze_page(creds, service_id, 0);
        } else if (do_resize) {
            if (vcpu < 1 || vcpu > 8) {
                fprintf(stderr, "Error: --vcpu must be 1-8 for resize\n");
                curl_global_cleanup();
                free_credentials(creds);
                return 1;
            }
            ret = resize_service(creds, service_id, vcpu);
        } else if (do_redeploy) {
            // Bootstrap is optional for redeploy:
            // - If provided via --bootstrap or --bootstrap-file, use it
            // - If omitted, API will use the stored encrypted bootstrap
            const char *bootstrap_to_use = bootstrap_file ? bootstrap_file : service_bootstrap;
            ret = redeploy_service(creds, service_id, bootstrap_to_use);
        } else if (do_execute) {
            // Pass input files if provided (written to /tmp/input/ before command runs)
            ret = execute_service(creds, service_id, execute_command, execute_timeout, service_input_files, service_input_file_count);
            // Free input file memory
            for (int i = 0; i < service_input_file_count; i++) {
                free(service_input_files[i].filename);
                free(service_input_files[i].content_base64);
            }
        } else if (do_dump_bootstrap) {
            // Dump bootstrap script from /tmp/bootstrap.sh inside the service
            // This is useful for migrations - the bootstrap is stored at the same path on all instances
            fprintf(stderr, "Fetching bootstrap script from %s...\n", service_id);

            // Use execute to cat the bootstrap file
            char *bootstrap = execute_service_capture(creds, service_id, "cat /tmp/bootstrap.sh", 30000);
            if (bootstrap) {
                if (dump_bootstrap_file) {
                    // Write to file
                    FILE *f = fopen(dump_bootstrap_file, "w");
                    if (f) {
                        fprintf(f, "%s", bootstrap);
                        fclose(f);
                        // Make executable
                        chmod(dump_bootstrap_file, 0755);
                        printf("Bootstrap saved to %s\n", dump_bootstrap_file);
                        ret = 0;
                    } else {
                        fprintf(stderr, "Error: Could not open file %s for writing\n", dump_bootstrap_file);
                        ret = 1;
                    }
                } else {
                    // Print to stdout
                    printf("%s", bootstrap);
                    ret = 0;
                }
                free(bootstrap);
            } else {
                fprintf(stderr, "Error: Failed to fetch bootstrap (service not running or no bootstrap file)\n");
                ret = 1;
            }
        } else if (do_snapshot) {
            ret = create_service_snapshot(creds, service_id, snapshot_name, hot_snapshot);
        } else if (do_restore) {
            ret = restore_from_snapshot(creds, restore_snapshot_id, "Service");
        } else if (service_name) {
            // Create service (default action when --name is provided)
            char *bootstrap_content = NULL;

            // If --bootstrap-file provided, read its contents
            if (bootstrap_file && strlen(bootstrap_file) > 0) {
                struct stat st;
                if (stat(bootstrap_file, &st) != 0 || !S_ISREG(st.st_mode)) {
                    fprintf(stderr, "Error: Bootstrap file not found: '%s'\n", bootstrap_file);
                    curl_global_cleanup();
                    free_credentials(creds);
                    return 1;
                }
                size_t fsize;
                bootstrap_content = read_file(bootstrap_file, &fsize);
                if (!bootstrap_content) {
                    fprintf(stderr, "Error: Failed to read bootstrap file '%s'\n", bootstrap_file);
                    curl_global_cleanup();
                    free_credentials(creds);
                    return 1;
                }
                fprintf(stderr, "Read bootstrap script (%zu bytes) from %s\n", fsize, bootstrap_file);
            }

            fprintf(stderr, "Creating service '%s'...", service_name);
            if (service_input_file_count > 0) {
                fprintf(stderr, " (%d files)...", service_input_file_count);
            }
            fflush(stderr);
            char *created_id = create_service(creds, service_name, service_ports, service_domains, service_bootstrap, bootstrap_content, network_mode, vcpu, service_type, service_input_files, service_input_file_count, golden_image, create_unfreeze_on_demand);
            if (bootstrap_content) free(bootstrap_content);
            // Free input file memory
            for (int i = 0; i < service_input_file_count; i++) {
                free(service_input_files[i].filename);
                free(service_input_files[i].content_base64);
            }

            if (created_id) {
                fprintf(stderr, " done\n");
                printf("\033[32mService created successfully\033[0m\n");
                printf("Service ID: %s\n", created_id);

                // Set environment vault if env vars were provided
                char *env_content_to_set = NULL;
                if (service_env_file) {
                    env_content_to_set = read_env_file(service_env_file);
                } else if (service_env_content && service_env_size > 0) {
                    env_content_to_set = service_env_content;
                    service_env_content = NULL;  // Transfer ownership
                }
                if (env_content_to_set) {
                    fprintf(stderr, "Setting environment vault...\n");
                    int env_ret = service_env_set(creds, created_id, env_content_to_set);
                    free(env_content_to_set);
                    if (env_ret != 0) {
                        fprintf(stderr, "\033[33mWarning: Failed to set environment vault\033[0m\n");
                    }
                }

                // Wait a moment then check bootstrap logs
                if ((service_bootstrap && strlen(service_bootstrap) > 0) ||
                    (bootstrap_file && strlen(bootstrap_file) > 0)) {
                    fprintf(stderr, "Checking bootstrap status...\n");
                    sleep(2);  // Give bootstrap time to start
                    char *log = get_service_logs(creds, created_id, 0);
                    if (log && strlen(log) > 0) {
                        printf("\n--- Bootstrap Log ---\n%s\n--- End Log ---\n", log);
                        free(log);
                    }
                }

                free(created_id);
                ret = 0;
            } else {
                fprintf(stderr, " failed\n");
                // Try to get logs if we can find the service by name
                // The service might exist even if create returned error
                fprintf(stderr, "Attempting to fetch bootstrap logs...\n");
                char *log = get_service_logs(creds, service_name, 0);
                if (log && strlen(log) > 0) {
                    fprintf(stderr, "\n\033[31m--- Bootstrap Log ---\033[0m\n%s\n\033[31m--- End Log ---\033[0m\n", log);
                    free(log);
                }
                ret = 1;
            }
            // Clean up env content if not transferred
            free(service_env_content);
        } else {
            // No action specified, show help
            free(service_env_content);
            fprintf(stderr, "Error: No service action specified. Use --list, --info, --name, etc.\n");
            print_usage(argv[0]);
            curl_global_cleanup();
            free_credentials(creds);
            return 1;
        }

        curl_global_cleanup();
        free_credentials(creds);
        return ret;
    }

    // Check for jobs command (async job management)
    if (argc >= 2 && strcmp(argv[1], "jobs") == 0) {
        const char *get_id = NULL;
        const char *cancel_id = NULL;

        // Parse options
        for (int i = 2; i < argc; i++) {
            if (strcmp(argv[i], "-p") == 0 && i + 1 < argc) {
                i++;
                cli_public_key = argv[i];
            } else if (strcmp(argv[i], "-k") == 0 && i + 1 < argc) {
                i++;
                cli_secret_key = argv[i];
            } else if (strcmp(argv[i], "--account") == 0 && i + 1 < argc) {
                i++;
                cli_account_index = atoi(argv[i]);
            } else if (strcmp(argv[i], "--get") == 0 && i + 1 < argc) {
                i++;
                get_id = argv[i];
            } else if (strcmp(argv[i], "--cancel") == 0 && i + 1 < argc) {
                i++;
                cancel_id = argv[i];
            } else if (strcmp(argv[i], "-l") == 0 || strcmp(argv[i], "--list") == 0) {
                // --list is default, no-op
            } else if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) {
                fprintf(stderr, "Usage: %s jobs [options]\n\n", argv[0]);
                fprintf(stderr, "Commands:\n");
                fprintf(stderr, "  (default)          List all jobs\n");
                fprintf(stderr, "  -l, --list         List all jobs\n");
                fprintf(stderr, "  --get ID           Get job status and result\n");
                fprintf(stderr, "  --cancel ID        Cancel a running job\n");
                return 0;
            }
        }

        UnsandboxCredentials *creds = get_credentials(cli_public_key, cli_secret_key, cli_account_index);
        if (!creds || !creds->public_key || strlen(creds->public_key) == 0) {
            fprintf(stderr, "Error: API credentials required.\n");
            fprintf(stderr, "  Set UNSANDBOX_PUBLIC_KEY + UNSANDBOX_SECRET_KEY env vars, or\n");
            fprintf(stderr, "  Use -p PUBLIC_KEY -k SECRET_KEY flags, or\n");
            fprintf(stderr, "  Create ~/.unsandbox/accounts.csv with: public_key,secret_key\n");
            free_credentials(creds);
            return 1;
        }

        curl_global_init(CURL_GLOBAL_DEFAULT);
        int ret = 0;

        if (cancel_id) {
            // un jobs --cancel ID — DELETE /jobs/:id
            char path[256], url[512];
            snprintf(path, sizeof(path), "/jobs/%s", cancel_id);
            snprintf(url, sizeof(url), "%s%s", API_BASE, path);

            CURL *curl = curl_easy_init();
            if (!curl) { ret = 1; goto jobs_cleanup; }

            struct curl_slist *hdrs = NULL;
            hdrs = add_hmac_auth_headers(hdrs, creds, "DELETE", path, NULL);

            curl_easy_setopt(curl, CURLOPT_URL, url);
            curl_easy_setopt(curl, CURLOPT_HTTPHEADER, hdrs);
            curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "DELETE");
            curl_easy_setopt(curl, CURLOPT_USERAGENT, "un-cli/2.0");

            CURLcode cres = curl_easy_perform(curl);
            long hcode = 0;
            curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &hcode);
            curl_slist_free_all(hdrs);
            curl_easy_cleanup(curl);

            if (cres == CURLE_OK && (hcode == 200 || hcode == 204)) {
                printf("Job %s cancelled\n", cancel_id);
            } else {
                fprintf(stderr, "Error: Failed to cancel job %s (HTTP %ld)\n", cancel_id, hcode);
                ret = 1;
            }
        } else if (get_id) {
            // un jobs --get ID — GET /jobs/:id
            char path[256], url[512];
            snprintf(path, sizeof(path), "/jobs/%s", get_id);
            snprintf(url, sizeof(url), "%s%s", API_BASE, path);

            CURL *curl = curl_easy_init();
            if (!curl) { ret = 1; goto jobs_cleanup; }

            struct ResponseBuffer resp = {0};
            resp.data = malloc(1);
            resp.size = 0;

            struct curl_slist *hdrs = NULL;
            hdrs = add_hmac_auth_headers(hdrs, creds, "GET", path, NULL);

            curl_easy_setopt(curl, CURLOPT_URL, url);
            curl_easy_setopt(curl, CURLOPT_HTTPHEADER, hdrs);
            curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
            curl_easy_setopt(curl, CURLOPT_WRITEDATA, &resp);
            curl_easy_setopt(curl, CURLOPT_USERAGENT, "un-cli/2.0");

            CURLcode cres = curl_easy_perform(curl);
            long hcode = 0;
            curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &hcode);
            curl_slist_free_all(hdrs);
            curl_easy_cleanup(curl);

            if (cres != CURLE_OK || hcode != 200) {
                fprintf(stderr, "Error: Failed to get job %s (HTTP %ld)\n", get_id, hcode);
                free(resp.data);
                ret = 1;
            } else {
                char *jid = extract_json_string(resp.data, "job_id");
                char *jstatus = extract_json_string(resp.data, "status");
                char *jlang = extract_json_string(resp.data, "language");
                char *jerror = extract_json_string(resp.data, "error");
                int64_t created = extract_json_number(resp.data, "created_at");
                int64_t completed = extract_json_number(resp.data, "completed_at");

                printf("%-12s %s\n", "Job ID:", jid ? jid : get_id);
                printf("%-12s %s\n", "Status:", jstatus ? jstatus : "unknown");
                if (jlang) printf("%-12s %s\n", "Language:", jlang);
                if (created > 0) printf("%-12s %ld\n", "Created:", (long)created);
                if (completed > 0) printf("%-12s %ld\n", "Completed:", (long)completed);
                if (jerror) printf("%-12s %s\n", "Error:", jerror);

                // If completed, also show stdout/stderr
                if (jstatus && strcmp(jstatus, "completed") == 0) {
                    char *jstdout = extract_json_string(resp.data, "stdout");
                    char *jstderr = extract_json_string(resp.data, "stderr");
                    if (jstdout && strlen(jstdout) > 0) {
                        printf("\n--- stdout ---\n%s", jstdout);
                        if (jstdout[strlen(jstdout)-1] != '\n') printf("\n");
                    }
                    if (jstderr && strlen(jstderr) > 0) {
                        fprintf(stderr, "\n--- stderr ---\n%s", jstderr);
                        if (jstderr[strlen(jstderr)-1] != '\n') fprintf(stderr, "\n");
                    }
                    free(jstdout);
                    free(jstderr);
                }

                free(jid);
                free(jstatus);
                free(jlang);
                free(jerror);
                free(resp.data);
            }
        } else {
            // un jobs --list (default) — GET /jobs
            char url[256];
            snprintf(url, sizeof(url), "%s/jobs", API_BASE);

            CURL *curl = curl_easy_init();
            if (!curl) { ret = 1; goto jobs_cleanup; }

            struct ResponseBuffer resp = {0};
            resp.data = malloc(1);
            resp.size = 0;

            struct curl_slist *hdrs = NULL;
            hdrs = add_hmac_auth_headers(hdrs, creds, "GET", "/jobs", NULL);

            curl_easy_setopt(curl, CURLOPT_URL, url);
            curl_easy_setopt(curl, CURLOPT_HTTPHEADER, hdrs);
            curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
            curl_easy_setopt(curl, CURLOPT_WRITEDATA, &resp);
            curl_easy_setopt(curl, CURLOPT_USERAGENT, "un-cli/2.0");

            CURLcode cres = curl_easy_perform(curl);
            long hcode = 0;
            curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &hcode);
            curl_slist_free_all(hdrs);
            curl_easy_cleanup(curl);

            if (cres != CURLE_OK || hcode != 200) {
                fprintf(stderr, "Error: Failed to list jobs (HTTP %ld)\n", hcode);
                free(resp.data);
                ret = 1;
            } else {
                int count = count_json_array_objects(resp.data, "jobs");
                if (count <= 0) {
                    printf("No jobs found\n");
                } else {
                    printf("%-38s %-12s %-14s %s\n", "JOB ID", "STATUS", "LANGUAGE", "CREATED");
                    printf("%-38s %-12s %-14s %s\n", "------", "------", "--------", "-------");

                    const char *jobs_start = strstr(resp.data, "\"jobs\":[");
                    if (jobs_start) {
                        const char *pos = jobs_start + 8;
                        for (int i = 0; i < count && pos; i++) {
                            pos = strchr(pos, '{');
                            if (!pos) break;
                            char *jid = extract_json_string(pos, "job_id");
                            char *jstatus = extract_json_string(pos, "status");
                            char *jlang = extract_json_string(pos, "language");
                            int64_t created = extract_json_number(pos, "created_at");
                            printf("%-38s %-12s %-14s %ld\n",
                                jid ? jid : "?",
                                jstatus ? jstatus : "?",
                                jlang ? jlang : "?",
                                (long)created);
                            free(jid);
                            free(jstatus);
                            free(jlang);
                            pos = skip_json_object(pos);
                        }
                    }
                }
                free(resp.data);
            }
        }

jobs_cleanup:
        curl_global_cleanup();
        free_credentials(creds);
        return ret;
    }

    // Check for paas command (PaaS platform management)
    if (argc >= 2 && strcmp(argv[1], "paas") == 0) {
        // paas requires a sub-subcommand
        if (argc < 3) {
            fprintf(stderr, "Usage: %s paas <command> [options]\n\n", argv[0]);
            fprintf(stderr, "Commands:\n");
            fprintf(stderr, "  logs    Fetch or stream production logs\n");
            fprintf(stderr, "\nRun '%s paas <command> -h' for help on a specific command.\n", argv[0]);
            return 1;
        }

        // paas logs
        if (strcmp(argv[2], "logs") == 0) {
            const char *source = "all";
            int lines = 100;
            const char *since = NULL;
            int since_explicit = 0;  // whether user passed --since
            const char *grep_filter = NULL;
            const char *level = NULL;
            int do_stream = 0;
            int json_output = 0;
            int show_help = 0;

            // Parse options (start at argv[3])
            for (int i = 3; i < argc; i++) {
                if (strcmp(argv[i], "-p") == 0 && i + 1 < argc) {
                    i++;
                    cli_public_key = argv[i];
                } else if (strcmp(argv[i], "-k") == 0 && i + 1 < argc) {
                    i++;
                    cli_secret_key = argv[i];
                } else if (strcmp(argv[i], "--account") == 0 && i + 1 < argc) {
                    i++;
                    cli_account_index = atoi(argv[i]);
                } else if (strcmp(argv[i], "--source") == 0 && i + 1 < argc) {
                    i++;
                    source = argv[i];
                } else if (strcmp(argv[i], "--all") == 0) {
                    source = "all";
                } else if (strcmp(argv[i], "--api") == 0) {
                    source = "api";
                } else if (strcmp(argv[i], "--portal") == 0) {
                    source = "portal";
                } else if (strcmp(argv[i], "--pool") == 0 && i + 1 < argc) {
                    i++;
                    static char pool_source[128];
                    snprintf(pool_source, sizeof(pool_source), "pool/%s", argv[i]);
                    source = pool_source;
                } else if (strcmp(argv[i], "--lines") == 0 && i + 1 < argc) {
                    i++;
                    lines = atoi(argv[i]);
                    if (lines < 1) lines = 1;
                    if (lines > 10000) lines = 10000;
                } else if (strcmp(argv[i], "-n") == 0 && i + 1 < argc) {
                    i++;
                    lines = atoi(argv[i]);
                    if (lines < 1) lines = 1;
                    if (lines > 10000) lines = 10000;
                } else if (strcmp(argv[i], "--since") == 0 && i + 1 < argc) {
                    i++;
                    since = argv[i];
                    since_explicit = 1;
                } else if (strcmp(argv[i], "--grep") == 0 && i + 1 < argc) {
                    i++;
                    grep_filter = argv[i];
                } else if ((strcmp(argv[i], "--level") == 0 || strcmp(argv[i], "-l") == 0) && i + 1 < argc) {
                    i++;
                    level = argv[i];
                } else if (strcmp(argv[i], "--follow") == 0 || strcmp(argv[i], "-f") == 0) {
                    do_stream = 1;
                } else if (strcmp(argv[i], "--json") == 0) {
                    json_output = 1;
                } else if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) {
                    show_help = 1;
                }
            }

            if (show_help) {
                fprintf(stderr, "Usage: %s paas logs [options]\n\n", argv[0]);
                fprintf(stderr, "Fetch or stream PaaS production logs.\n\n");
                fprintf(stderr, "Sources:\n");
                fprintf(stderr, "  --all              All sources (default)\n");
                fprintf(stderr, "  --api              API server logs\n");
                fprintf(stderr, "  --portal           Portal server logs\n");
                fprintf(stderr, "  --pool NODE        Pool node logs (e.g., cammy, ai)\n");
                fprintf(stderr, "  --source SOURCE    Source string (api, portal, pool/cammy, all)\n");
                fprintf(stderr, "\nOptions:\n");
                fprintf(stderr, "  --lines N, -n N    Number of lines (default: 100, max: 10000)\n");
                fprintf(stderr, "  --since TIME       Time window: 1m, 5m, 15m, 1h, 6h, 1d (default: 5m)\n");
                fprintf(stderr, "  --grep PATTERN     Filter log lines by pattern\n");
                fprintf(stderr, "  --level LVL, -l    Min log level: debug, info, notice, warning, err, crit\n");
                fprintf(stderr, "  --follow, -f       Follow logs in real-time (Ctrl+C to stop)\n");
                fprintf(stderr, "  --json             Output raw JSON response\n");
                fprintf(stderr, "  -p KEY             Public key\n");
                fprintf(stderr, "  -k KEY             Secret key\n");
                fprintf(stderr, "  -h                 Show this help\n");
                fprintf(stderr, "\nExamples:\n");
                fprintf(stderr, "  %s paas logs                             # last 100 lines from all sources\n", argv[0]);
                fprintf(stderr, "  %s paas logs --api --lines 500           # last 500 API log lines\n", argv[0]);
                fprintf(stderr, "  %s paas logs --portal --grep error       # portal logs matching 'error'\n", argv[0]);
                fprintf(stderr, "  %s paas logs --pool cammy --since 1h     # cammy pool logs from last hour\n", argv[0]);
                fprintf(stderr, "  %s paas logs -l warning                  # warnings and above from all sources\n", argv[0]);
                fprintf(stderr, "  %s paas logs --follow                    # follow all logs in real-time\n", argv[0]);
                fprintf(stderr, "  %s paas logs --follow -l warning         # follow warnings+ in real-time\n", argv[0]);
                fprintf(stderr, "\nRequires a partner API key with log access enabled.\n");
                return 0;
            }

            // Get credentials
            UnsandboxCredentials *creds = get_credentials(cli_public_key, cli_secret_key, cli_account_index);

            if (!creds || !creds->public_key || strlen(creds->public_key) == 0) {
                fprintf(stderr, "Error: API credentials required.\n");
                fprintf(stderr, "  Set UNSANDBOX_PUBLIC_KEY + UNSANDBOX_SECRET_KEY env vars, or\n");
                fprintf(stderr, "  Use -p PUBLIC_KEY -k SECRET_KEY flags, or\n");
                fprintf(stderr, "  Create ~/.unsandbox/accounts.csv with: public_key,secret_key\n");
                free_credentials(creds);
                return 1;
            }

            // Resolve since: explicit --since > cursor > default 5m
            if (!since_explicit) {
                char *cursor = read_log_cursor();
                if (cursor) {
                    since = cursor;
                } else {
                    since = "5m";
                }
            }

            curl_global_init(CURL_GLOBAL_DEFAULT);
            int ret;
            if (do_stream) {
                ret = stream_paas_logs(creds, source, grep_filter, level);
            } else {
                ret = fetch_paas_logs(creds, source, lines, since, grep_filter, json_output, level);
            }
            curl_global_cleanup();
            free_credentials(creds);
            return ret;
        }

        // Unknown paas subcommand
        fprintf(stderr, "Error: Unknown paas command '%s'\n", argv[2]);
        fprintf(stderr, "Run '%s paas' for available commands.\n", argv[0]);
        return 1;
    }

    // Check for session command
    if (argc >= 2 && strcmp(argv[1], "session") == 0) {
        int audit_history = 0;
        int list_only = 0;
        const char *shell = NULL;
        const char *attach_to = NULL;
        const char *kill_target = NULL;
        const char *freeze_target = NULL;
        const char *unfreeze_target = NULL;
        const char *boost_target = NULL;
        int boost_vcpu = 0;
        const char *unboost_target = NULL;
        const char *multiplexer = NULL;  // NULL = no multiplexer, "tmux", or "screen"
        struct InputFile session_input_files[MAX_INPUT_FILES];
        int session_input_file_count = 0;
        const char *snapshot_target = NULL;
        const char *restore_snapshot_id = NULL;
        const char *snapshot_name = NULL;
        int hot_snapshot = 0;
        // Parse shell-specific args
        for (int i = 2; i < argc; i++) {
            if (strcmp(argv[i], "-p") == 0 && i + 1 < argc) {
                i++;
                cli_public_key = argv[i];
            } else if (strcmp(argv[i], "-k") == 0 && i + 1 < argc) {
                i++;
                cli_secret_key = argv[i];
            } else if (strcmp(argv[i], "--account") == 0 && i + 1 < argc) {
                i++;
                cli_account_index = atoi(argv[i]);
            } else if (strcmp(argv[i], "-n") == 0 && i + 1 < argc) {
                i++;
                network_mode = argv[i];
            } else if ((strcmp(argv[i], "-s") == 0 || strcmp(argv[i], "--shell") == 0) && i + 1 < argc) {
                i++;
                shell = argv[i];
            } else if (strcmp(argv[i], "-a") == 0 || strcmp(argv[i], "--artifacts") == 0) {
                save_artifacts = 1;
            } else if (strcmp(argv[i], "--audit") == 0) {
                audit_history = 1;
                save_artifacts = 1;  // --audit implies -a
            } else if (strcmp(argv[i], "-o") == 0 && i + 1 < argc) {
                i++;
                artifact_dir = argv[i];
                save_artifacts = 1;  // -o implies -a
            } else if (strcmp(argv[i], "-l") == 0 || strcmp(argv[i], "--list") == 0) {
                list_only = 1;
            } else if (strcmp(argv[i], "--attach") == 0 && i + 1 < argc) {
                i++;
                attach_to = argv[i];
            } else if (strcmp(argv[i], "--kill") == 0 && i + 1 < argc) {
                i++;
                kill_target = argv[i];
            } else if (strcmp(argv[i], "--freeze") == 0 && i + 1 < argc) {
                i++;
                freeze_target = argv[i];
            } else if (strcmp(argv[i], "--unfreeze") == 0 && i + 1 < argc) {
                i++;
                unfreeze_target = argv[i];
            } else if (strcmp(argv[i], "--boost") == 0 && i + 1 < argc) {
                i++;
                boost_target = argv[i];
            } else if (strcmp(argv[i], "--boost-vcpu") == 0 && i + 1 < argc) {
                i++;
                boost_vcpu = atoi(argv[i]);
            } else if (strcmp(argv[i], "--unboost") == 0 && i + 1 < argc) {
                i++;
                unboost_target = argv[i];
            } else if (strcmp(argv[i], "--tmux") == 0) {
                multiplexer = "tmux";
            } else if (strcmp(argv[i], "--screen") == 0) {
                multiplexer = "screen";
            } else if (strcmp(argv[i], "-f") == 0 && i + 1 < argc) {
                i++;
                if (session_input_file_count >= MAX_INPUT_FILES) {
                    fprintf(stderr, "Error: too many input files (max %d)\n", MAX_INPUT_FILES);
                    return 1;
                }
                size_t fsize;
                char *content = read_file(argv[i], &fsize);
                if (!content) {
                    fprintf(stderr, "Error: cannot read file '%s'\n", argv[i]);
                    return 1;
                }
                size_t b64_len;
                char *b64 = base64_encode((unsigned char *)content, fsize, &b64_len);
                free(content);
                if (!b64) {
                    fprintf(stderr, "Error: failed to encode file '%s'\n", argv[i]);
                    return 1;
                }
                session_input_files[session_input_file_count].filename = strdup(get_basename(argv[i]));
                session_input_files[session_input_file_count].content_base64 = b64;
                session_input_file_count++;
            } else if (strcmp(argv[i], "-F") == 0 && i + 1 < argc) {
                // -F preserves relative path (for directory structures)
                i++;
                if (session_input_file_count >= MAX_INPUT_FILES) {
                    fprintf(stderr, "Error: too many input files (max %d)\n", MAX_INPUT_FILES);
                    return 1;
                }
                size_t fsize;
                char *content = read_file(argv[i], &fsize);
                if (!content) {
                    fprintf(stderr, "Error: cannot read file '%s'\n", argv[i]);
                    return 1;
                }
                size_t b64_len;
                char *b64 = base64_encode((unsigned char *)content, fsize, &b64_len);
                free(content);
                if (!b64) {
                    fprintf(stderr, "Error: failed to encode file '%s'\n", argv[i]);
                    return 1;
                }
                // Use full path instead of basename
                session_input_files[session_input_file_count].filename = strdup(argv[i]);
                session_input_files[session_input_file_count].content_base64 = b64;
                session_input_file_count++;
            } else if ((strcmp(argv[i], "-v") == 0 || strcmp(argv[i], "--vcpu") == 0) && i + 1 < argc) {
                i++;
                vcpu = atoi(argv[i]);
                if (vcpu < 1 || vcpu > 8) {
                    fprintf(stderr, "Error: -v/--vcpu must be 1-8\n");
                    return 1;
                }
            } else if (strcmp(argv[i], "--snapshot") == 0 && i + 1 < argc) {
                i++;
                snapshot_target = argv[i];
            } else if (strcmp(argv[i], "--restore") == 0 && i + 1 < argc) {
                i++;
                restore_snapshot_id = argv[i];
            } else if (strcmp(argv[i], "--snapshot-name") == 0 && i + 1 < argc) {
                i++;
                snapshot_name = argv[i];
            } else if (strcmp(argv[i], "--hot") == 0) {
                hot_snapshot = 1;
            } else if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) {
                print_usage(argv[0]);
                return 0;
            } else if (argv[i][0] == '-') {
                fprintf(stderr, "Unknown option: %s\n", argv[i]);
                print_usage(argv[0]);
                return 1;
            }
        }

        // Get credentials (priority: env > flags > file)
        UnsandboxCredentials *creds = get_credentials(cli_public_key, cli_secret_key, cli_account_index);
        if (!creds || !creds->public_key || strlen(creds->public_key) == 0) {
            fprintf(stderr, "Error: API credentials required.\n");
            fprintf(stderr, "  Set UNSANDBOX_PUBLIC_KEY + UNSANDBOX_SECRET_KEY env vars, or\n");
            fprintf(stderr, "  Use -p PUBLIC_KEY -k SECRET_KEY flags, or\n");
            fprintf(stderr, "  Create ~/.unsandbox/accounts.csv with: public_key,secret_key\n");
            free_credentials(creds);
            return 1;
        }

        curl_global_init(CURL_GLOBAL_DEFAULT);
        int ret;
        if (list_only) {
            ret = list_sessions(creds);
        } else if (kill_target) {
            ret = kill_session(creds, kill_target);
        } else if (freeze_target) {
            ret = freeze_session(creds, freeze_target);
        } else if (unfreeze_target) {
            ret = unfreeze_session(creds, unfreeze_target);
        } else if (boost_target) {
            if (boost_vcpu == 0) boost_vcpu = 2;  // Default boost: 2 vCPU
            ret = boost_session(creds, boost_target, boost_vcpu);
        } else if (unboost_target) {
            ret = unboost_session(creds, unboost_target);
        } else if (snapshot_target) {
            ret = create_session_snapshot(creds, snapshot_target, snapshot_name, hot_snapshot);
        } else if (restore_snapshot_id) {
            ret = restore_from_snapshot(creds, restore_snapshot_id, "Session");
        } else if (attach_to) {
            ret = reconnect_session(creds, attach_to, save_artifacts, artifact_dir, audit_history);
        } else {
            ret = shell_command(creds, network_mode, save_artifacts, artifact_dir, audit_history, shell, multiplexer, vcpu, session_input_files, session_input_file_count);
        }
        curl_global_cleanup();
        free_credentials(creds);
        return ret;
    }

    // Parse arguments for execute command (default)
    for (int i = 1; i < argc; i++) {
        if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) {
            print_usage(argv[0]);
            return 0;
        } else if (strcmp(argv[i], "-n") == 0 && i + 1 < argc) {
            i++;
            network_mode = argv[i];
        } else if (strcmp(argv[i], "-e") == 0 && i + 1 < argc) {
            i++;
            char *eq = strchr(argv[i], '=');
            if (!eq) {
                fprintf(stderr, "Error: -e requires KEY=VALUE format\n");
                return 1;
            }
            if (env_var_count >= MAX_ENV_VARS) {
                fprintf(stderr, "Error: too many env vars (max %d)\n", MAX_ENV_VARS);
                return 1;
            }
            env_vars[env_var_count].key = strndup(argv[i], eq - argv[i]);
            env_vars[env_var_count].value = strdup(eq + 1);
            env_var_count++;
        } else if (strcmp(argv[i], "-f") == 0 && i + 1 < argc) {
            i++;
            if (input_file_count >= MAX_INPUT_FILES) {
                fprintf(stderr, "Error: too many input files (max %d)\n", MAX_INPUT_FILES);
                return 1;
            }
            size_t fsize;
            char *content = read_file(argv[i], &fsize);
            if (!content) return 1;

            // Check total size limit
            total_input_size += fsize;
            if (total_input_size > MAX_TOTAL_INPUT_SIZE) {
                fprintf(stderr, "Error: total input file size exceeds limit (max 4GB)\n");
                free(content);
                return 1;
            }

            size_t b64_len;
            char *b64 = base64_encode((unsigned char*)content, fsize, &b64_len);
            free(content);
            if (!b64) {
                fprintf(stderr, "Error: failed to encode file\n");
                return 1;
            }

            input_files[input_file_count].filename = strdup(get_basename(argv[i]));
            input_files[input_file_count].content_base64 = b64;
            input_file_count++;
        } else if (strcmp(argv[i], "-F") == 0 && i + 1 < argc) {
            // -F preserves relative path (for directory structures)
            i++;
            if (input_file_count >= MAX_INPUT_FILES) {
                fprintf(stderr, "Error: too many input files (max %d)\n", MAX_INPUT_FILES);
                return 1;
            }
            size_t fsize;
            char *content = read_file(argv[i], &fsize);
            if (!content) return 1;

            // Check total size limit
            total_input_size += fsize;
            if (total_input_size > MAX_TOTAL_INPUT_SIZE) {
                fprintf(stderr, "Error: total input file size exceeds limit (max 4GB)\n");
                free(content);
                return 1;
            }

            size_t b64_len;
            char *b64 = base64_encode((unsigned char*)content, fsize, &b64_len);
            free(content);
            if (!b64) {
                fprintf(stderr, "Error: failed to encode file\n");
                return 1;
            }

            // Use full path instead of basename
            input_files[input_file_count].filename = strdup(argv[i]);
            input_files[input_file_count].content_base64 = b64;
            input_file_count++;
        } else if (strcmp(argv[i], "-a") == 0 || strcmp(argv[i], "--artifacts") == 0) {
            save_artifacts = 1;
        } else if (strcmp(argv[i], "-y") == 0 || strcmp(argv[i], "--yes") == 0) {
            skip_confirm = 1;
        } else if (strcmp(argv[i], "-o") == 0 && i + 1 < argc) {
            i++;
            artifact_dir = argv[i];
        } else if (strcmp(argv[i], "-p") == 0 && i + 1 < argc) {
            i++;
            cli_public_key = argv[i];
        } else if (strcmp(argv[i], "-k") == 0 && i + 1 < argc) {
            i++;
            cli_secret_key = argv[i];
        } else if (strcmp(argv[i], "--account") == 0 && i + 1 < argc) {
            i++;
            cli_account_index = atoi(argv[i]);
        } else if ((strcmp(argv[i], "-v") == 0 || strcmp(argv[i], "--vcpu") == 0) && i + 1 < argc) {
            i++;
            vcpu = atoi(argv[i]);
            if (vcpu < 1 || vcpu > 8) {
                fprintf(stderr, "Error: -v/--vcpu must be 1-8\n");
                return 1;
            }
        } else if ((strcmp(argv[i], "-t") == 0 || strcmp(argv[i], "--ttl") == 0) && i + 1 < argc) {
            i++;
            ttl = atoi(argv[i]);
            if (ttl < 1 || ttl > 900) {
                fprintf(stderr, "Error: -t/--ttl must be 1-900 seconds\n");
                return 1;
            }
        } else if ((strcmp(argv[i], "-s") == 0 || strcmp(argv[i], "--shell") == 0) && i + 1 < argc) {
            i++;
            shell = argv[i];
        } else if (argv[i][0] != '-') {
            filename = argv[i];
        } else {
            fprintf(stderr, "Unknown option: %s\n", argv[i]);
            print_usage(argv[0]);
            return 1;
        }
    }

    if (!filename) {
        print_usage(argv[0]);
        return 1;
    }

    // Warn about large uploads and confirm
    if (total_input_size > LARGE_UPLOAD_WARN_SIZE && !skip_confirm) {
        fprintf(stderr, "Warning: uploading %.1f GB of input files. This may take a while (base64 encoded).\n",
                (double)total_input_size / (1024.0 * 1024.0 * 1024.0));
        fprintf(stderr, "Continue? [y/N] ");
        int c = getchar();
        if (c != 'y' && c != 'Y') {
            fprintf(stderr, "Aborted. Use -y to skip this confirmation.\n");
            return 1;
        }
        // Consume rest of line
        while (c != '\n' && c != EOF) c = getchar();
    }

    // Get credentials (priority: env > flags > file)
    UnsandboxCredentials *creds = get_credentials(cli_public_key, cli_secret_key, cli_account_index);
    if (!creds || !creds->public_key || strlen(creds->public_key) == 0) {
        fprintf(stderr, "Error: API credentials required.\n");
        fprintf(stderr, "  Set UNSANDBOX_PUBLIC_KEY + UNSANDBOX_SECRET_KEY env vars, or\n");
        fprintf(stderr, "  Use -p PUBLIC_KEY -k SECRET_KEY flags, or\n");
        fprintf(stderr, "  Create ~/.unsandbox/accounts.csv with: public_key,secret_key\n");
        free_credentials(creds);
        return 1;
    }

    // Get code: if -s is given OR file doesn't exist, treat as inline code
    size_t code_size;
    char *code;
    int inline_mode = 0;
    if (shell) {
        // Explicit -s flag: treat as inline code
        inline_mode = 1;
    } else if (access(filename, F_OK) != 0) {
        // File doesn't exist: assume bash inline code
        shell = "bash";
        inline_mode = 1;
    }

    if (inline_mode) {
        // Inline code mode: argument is the code itself
        code = strdup(filename);
        code_size = strlen(code);
    } else {
        // File mode: read code from file
        code = read_file(filename, &code_size);
        if (!code) return 1;
    }

    // Detect language (use -s/--shell if provided, otherwise auto-detect)
    const char *language = shell;
    if (!language) {
        language = detect_language_from_extension(filename);
    }
    if (!language) {
        language = detect_language_from_shebang(code);
    }
    if (!language) {
        fprintf(stderr, "Error: cannot detect language from file extension or shebang\n");
        fprintf(stderr, "  Use -s/--shell to specify the language (e.g., -s bash, -s python)\n");
        free(code);
        return 1;
    }

    // Escape code for JSON
    char *escaped_code = escape_json_string(code);
    free(code);
    if (!escaped_code) {
        fprintf(stderr, "Error: failed to escape code\n");
        return 1;
    }

    // Build JSON payload
    size_t payload_size = strlen(escaped_code) + 4096;
    for (int i = 0; i < input_file_count; i++) {
        payload_size += strlen(input_files[i].content_base64) + 256;
    }
    for (int i = 0; i < env_var_count; i++) {
        payload_size += strlen(env_vars[i].key) + strlen(env_vars[i].value) + 32;
    }

    char *json_payload = malloc(payload_size);
    if (!json_payload) {
        fprintf(stderr, "Error: out of memory\n");
        free(escaped_code);
        return 1;
    }

    char *p = json_payload;
    p += sprintf(p, "{\"language\":\"%s\",\"code\":\"%s\"", language, escaped_code);
    free(escaped_code);

    // Add input files
    if (input_file_count > 0) {
        p += sprintf(p, ",\"input_files\":[");
        for (int i = 0; i < input_file_count; i++) {
            if (i > 0) *p++ = ',';
            char *esc_filename = escape_json_string(input_files[i].filename);
            p += sprintf(p, "{\"filename\":\"%s\",\"content\":\"%s\"}",
                        esc_filename, input_files[i].content_base64);
            free(esc_filename);
            free(input_files[i].filename);
            free(input_files[i].content_base64);
        }
        p += sprintf(p, "]");
    }

    // Add env vars
    if (env_var_count > 0) {
        p += sprintf(p, ",\"env\":{");
        for (int i = 0; i < env_var_count; i++) {
            if (i > 0) *p++ = ',';
            char *esc_key = escape_json_string(env_vars[i].key);
            char *esc_val = escape_json_string(env_vars[i].value);
            p += sprintf(p, "\"%s\":\"%s\"", esc_key, esc_val);
            free(esc_key);
            free(esc_val);
            free(env_vars[i].key);
            free(env_vars[i].value);
        }
        p += sprintf(p, "}");
    }

    // Add artifact flag
    if (save_artifacts) {
        p += sprintf(p, ",\"return_artifact\":true");
    }

    // Add vcpu if specified (> 1)
    if (vcpu > 1) {
        p += sprintf(p, ",\"vcpu\":%d", vcpu);
    }

    // Add network_mode if specified
    if (network_mode && strlen(network_mode) > 0) {
        p += sprintf(p, ",\"network_mode\":\"%s\"", network_mode);
    }

    // Add TTL if specified
    if (ttl > 0) {
        p += sprintf(p, ",\"ttl\":%d", ttl);
    }

    p += sprintf(p, "}");

    // Initialize libcurl
    curl_global_init(CURL_GLOBAL_DEFAULT);
    CURL *curl = curl_easy_init();
    if (!curl) {
        fprintf(stderr, "Error: failed to initialize curl\n");
        free(json_payload);
        curl_global_cleanup();
        return 1;
    }

    // Set up response buffer
    struct ResponseBuffer response = {0};
    response.data = malloc(1);
    response.size = 0;

    // Set up request headers with HMAC authentication
    struct curl_slist *headers = NULL;
    headers = curl_slist_append(headers, "Content-Type: application/json");
    headers = add_hmac_auth_headers(headers, creds, "POST", "/execute", json_payload);

    // Configure curl
    curl_easy_setopt(curl, CURLOPT_URL, API_URL);
    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
    curl_easy_setopt(curl, CURLOPT_POSTFIELDS, json_payload);
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void *)&response);
    curl_easy_setopt(curl, CURLOPT_USERAGENT, "un-cli/2.0");

    // Set timeout based on TTL (or default to 120 seconds)
    long curl_timeout = (ttl > 0) ? (ttl + 30) : 120;  // Add 30s buffer
    curl_easy_setopt(curl, CURLOPT_TIMEOUT, curl_timeout);

    // Perform request
    CURLcode res = curl_easy_perform(curl);

    if (res != CURLE_OK) {
        fprintf(stderr, "Error: request failed: %s\n", curl_easy_strerror(res));
        curl_easy_cleanup(curl);
        curl_slist_free_all(headers);
        free(json_payload);
        free(response.data);
        curl_global_cleanup();
        return 1;
    }

    // Check HTTP status code
    long http_code = 0;
    curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);

    if (http_code != 200) {
        fprintf(stderr, "Error: HTTP %ld\n", http_code);
        if (response.data) {
            fprintf(stderr, "%s\n", response.data);
        }
        curl_easy_cleanup(curl);
        curl_slist_free_all(headers);
        free(json_payload);
        free(response.data);
        curl_global_cleanup();
        return 1;
    }

    // Check if response contains job_id (async execution)
    char *job_id = NULL;
    char *status = NULL;
    char *final_data = response.data;

    if (response.data) {
        job_id = extract_json_string(response.data, "job_id");
        status = extract_json_string(response.data, "status");
    }

    // If we got a job_id and status isn't terminal, we need to poll
    if (job_id && status) {
        int need_poll = (strcmp(status, "pending") == 0 ||
                        strcmp(status, "running") == 0);

        if (need_poll) {
            fprintf(stderr, "job %s\n", job_id);
            // Free initial response, poll for final result
            free(response.data);
            final_data = poll_job_status(creds, job_id);
        }
    }

    // Parse and print final response
    if (final_data) {
        parse_and_print_response(final_data, save_artifacts, artifact_dir, filename);
        if (final_data != response.data) {
            free(final_data);
        }
    }

    // Cleanup
    if (job_id) free(job_id);
    if (status) free(status);
    curl_easy_cleanup(curl);
    curl_slist_free_all(headers);
    free(json_payload);
    if (final_data == response.data) {
        free(response.data);
    }
    curl_global_cleanup();
    free_credentials(creds);

    return 0;
}
#endif /* UNSANDBOX_LIBRARY */

Documentation clarifications

Dependencies

C Binary (un1) — requires libcurl and libwebsockets:

sudo apt install build-essential libcurl4-openssl-dev libwebsockets-dev
wget unsandbox.com/downloads/un.c && gcc -O2 -o un un.c -lcurl -lwebsockets

SDK Implementations — most use stdlib only (Ruby, JS, Go, etc). Some require minimal deps:

pip install requests  # Python

Execute Code

Run a Script

./un hello.py
./un app.js
./un main.rs

With Environment Variables

./un -e DEBUG=1 -e NAME=World script.py

With Input Files (teleport files into sandbox)

./un -f data.csv -f config.json process.py

Get Compiled Binary (teleport artifacts out)

./un -a -o ./bin main.c

Interactive Sessions

Start a Shell Session

# Default bash shell
./un session

# Choose your shell
./un session --shell zsh
./un session --shell fish

# Jump into a REPL
./un session --shell python3
./un session --shell node
./un session --shell julia

Session with Network Access

./un session -n semitrusted

Session Auditing (full terminal recording)

# Record everything (including vim, interactive programs)
./un session --audit -o ./logs

# Replay session later
zcat session.log*.gz | less -R

Collect Artifacts from Session

# Files in /tmp/artifacts/ are collected on exit
./un session -a -o ./outputs

Session Persistence (tmux/screen)

# Default: session terminates on disconnect (clean exit)
./un session

# With tmux: session persists, can reconnect later
./un session --tmux
# Press Ctrl+b then d to detach

# With screen: alternative multiplexer
./un session --screen
# Press Ctrl+a then d to detach

List Active Sessions

./un session --list

# Output:
# Active sessions: 2
#
# SESSION ID                               CONTAINER            SHELL      TTL      STATUS
# abc123...                                unsb-vm-12345        python3    45m30s   active
# def456...                                unsb-vm-67890        bash       1h2m     active

Reconnect to Existing Session

# Reconnect by container name (requires --tmux or --screen)
./un session --attach unsb-vm-12345

# Use exit to terminate session, or detach to keep it running

Terminate a Session

./un session --kill unsb-vm-12345

Available Shells & REPLs

Shells: bash, dash, sh, zsh, fish, ksh, tcsh, csh, elvish, xonsh, ash

REPLs:  python3, bpython, ipython    # Python
        node                          # JavaScript
        ruby, irb                     # Ruby
        lua                           # Lua
        php                           # PHP
        perl                          # Perl
        guile, scheme                 # Scheme
        ghci                          # Haskell
        erl, iex                      # Erlang/Elixir
        sbcl, clisp                   # Common Lisp
        r                             # R
        julia                         # Julia
        clojure                       # Clojure

API Key Management

Check Key Status

# Check if your API key is valid
./un key

# Output:
# Valid: key expires in 30 days

Extend Expired Key

# Open the portal to extend an expired key
./un key --extend

# This opens the unsandbox.com portal where you can
# add more credits to extend your key's expiration

Authentication

Credentials are loaded in priority order (highest first):

# 1. CLI flags (highest priority)
./un -p unsb-pk-xxxx -k unsb-sk-xxxxx script.py

# 2. Environment variables
export UNSANDBOX_PUBLIC_KEY=unsb-pk-xxxx-xxxx-xxxx-xxxx
export UNSANDBOX_SECRET_KEY=unsb-sk-xxxxx-xxxxx-xxxxx-xxxxx
./un script.py

# 3. Config file (lowest priority)
# ~/.unsandbox/accounts.csv format: public_key,secret_key
mkdir -p ~/.unsandbox
echo "unsb-pk-xxxx-xxxx-xxxx-xxxx,unsb-sk-xxxxx-xxxxx-xxxxx-xxxxx" > ~/.unsandbox/accounts.csv
./un script.py

Requests are signed with HMAC-SHA256. The bearer token contains only the public key; the secret key computes the signature (never transmitted).

Resource Scaling

Set vCPU Count

# Default: 1 vCPU, 2GB RAM
./un script.py

# Scale up: 4 vCPUs, 8GB RAM
./un -v 4 script.py

# Maximum: 8 vCPUs, 16GB RAM
./un --vcpu 8 heavy_compute.py

Live Session Boosting

# Boost a running session to 2 vCPU, 4GB RAM
./un session --boost sandbox-abc

# Boost to specific vCPU count (4 vCPU, 8GB RAM)
./un session --boost sandbox-abc --boost-vcpu 4

# Return to base resources (1 vCPU, 2GB RAM)
./un session --unboost sandbox-abc

Session Freeze/Unfreeze

Freeze and Unfreeze Sessions

# Freeze a session (stop billing, preserve state)
./un session --freeze sandbox-abc

# Unfreeze a frozen session
./un session --unfreeze sandbox-abc

# Note: Requires --tmux or --screen for persistence

Persistent Services

Create a Service

# Web server with ports
./un service --name web --ports 80,443 --bootstrap "python -m http.server 80"

# With custom domains
./un service --name blog --ports 8000 --domains blog.example.com

# Game server with SRV records
./un service --name mc --type minecraft --bootstrap ./setup.sh

# Deploy app tarball with bootstrap script
./un service --name app --ports 8000 -f app.tar.gz --bootstrap-file ./setup.sh
# setup.sh: cd /tmp && tar xzf app.tar.gz && ./app/start.sh

Manage Services

# List all services
./un service --list

# Get service details
./un service --info abc123

# View bootstrap logs
./un service --logs abc123
./un service --tail abc123  # last 9000 lines

# Execute command in running service
./un service --execute abc123 'journalctl -u myapp -n 50'

# Dump bootstrap script (for migrations)
./un service --dump-bootstrap abc123
./un service --dump-bootstrap abc123 backup.sh

# Freeze/unfreeze service
./un service --freeze abc123
./un service --unfreeze abc123

# Service settings (auto-wake, freeze page display)
./un service --auto-unfreeze abc123      # enable auto-wake on HTTP
./un service --no-auto-unfreeze abc123   # disable auto-wake
./un service --show-freeze-page abc123   # show HTML payment page (default)
./un service --no-show-freeze-page abc123  # return JSON error instead

# Redeploy with new bootstrap
./un service --redeploy abc123 --bootstrap ./new-setup.sh

# Destroy service
./un service --destroy abc123

Snapshots

List Snapshots

./un snapshot --list

# Output:
# Snapshots: 3
#
# SNAPSHOT ID                              NAME             SOURCE     SIZE     CREATED
# unsb-snapshot-a1b2-c3d4-e5f6-g7h8        before-upgrade   session    512 MB   2h ago
# unsb-snapshot-i9j0-k1l2-m3n4-o5p6        stable-v1.0      service    1.2 GB   1d ago

Create Session Snapshot

# Snapshot with name
./un session --snapshot unsb-vm-12345 --name "before upgrade"

# Quick snapshot (auto-generated name)
./un session --snapshot unsb-vm-12345

Create Service Snapshot

# Standard snapshot (pauses container briefly)
./un service --snapshot unsb-service-abc123 --name "stable v1.0"

# Hot snapshot (no pause, may be inconsistent)
./un service --snapshot unsb-service-abc123 --hot

Restore from Snapshot

# Restore session from snapshot
./un session --restore unsb-snapshot-a1b2-c3d4-e5f6-g7h8

# Restore service from snapshot
./un service --restore unsb-snapshot-i9j0-k1l2-m3n4-o5p6

Delete Snapshot

./un snapshot --delete unsb-snapshot-a1b2-c3d4-e5f6-g7h8

Images

Images are independent, transferable container images that survive container deletion. Unlike snapshots (which live with their container), images can be shared with other users, transferred between API keys, or made public in the marketplace.

List Images

# List all images (owned + shared + public)
./un image --list

# List only your images
./un image --list owned

# List images shared with you
./un image --list shared

# List public marketplace images
./un image --list public

# Get image details
./un image --info unsb-image-xxxx-xxxx-xxxx-xxxx

Publish Images

# Publish from a stopped or frozen service
./un image --publish-service unsb-service-abc123 \
   --name "My App v1.0" --description "Production snapshot"

# Publish from a snapshot
./un image --publish-snapshot unsb-snapshot-xxxx-xxxx-xxxx-xxxx \
   --name "Stable Release"

# Note: Cannot publish from running containers - stop or freeze first

Create Services from Images

# Spawn a new service from an image
./un image --spawn unsb-image-xxxx-xxxx-xxxx-xxxx \
   --name new-service --ports 80,443

# Clone an image (creates a copy you own)
./un image --clone unsb-image-xxxx-xxxx-xxxx-xxxx

Image Protection

# Lock image to prevent accidental deletion
./un image --lock unsb-image-xxxx-xxxx-xxxx-xxxx

# Unlock image to allow deletion
./un image --unlock unsb-image-xxxx-xxxx-xxxx-xxxx

# Delete image (must be unlocked)
./un image --delete unsb-image-xxxx-xxxx-xxxx-xxxx

Visibility & Sharing

# Set visibility level
./un image --visibility unsb-image-xxxx-xxxx-xxxx-xxxx private   # owner only (default)
./un image --visibility unsb-image-xxxx-xxxx-xxxx-xxxx unlisted  # can be shared
./un image --visibility unsb-image-xxxx-xxxx-xxxx-xxxx public    # marketplace

# Share with specific user
./un image --grant unsb-image-xxxx-xxxx-xxxx-xxxx \
   --key unsb-pk-friend-friend-friend-friend

# Revoke access
./un image --revoke unsb-image-xxxx-xxxx-xxxx-xxxx \
   --key unsb-pk-friend-friend-friend-friend

# List who has access
./un image --trusted unsb-image-xxxx-xxxx-xxxx-xxxx

Transfer Ownership

# Transfer image to another API key
./un image --transfer unsb-image-xxxx-xxxx-xxxx-xxxx \
   --to unsb-pk-newowner-newowner-newowner-newowner

Usage Reference

Usage: ./un [options] <source_file>
       ./un session [options]
       ./un service [options]
       ./un snapshot [options]
       ./un image [options]
       ./un key

Commands:
  (default)        Execute source file in sandbox
  session          Open interactive shell/REPL session
  service          Manage persistent services
  snapshot         Manage container snapshots
  image            Manage container images (publish, share, transfer)
  key              Check API key validity and expiration

Options:
  -e KEY=VALUE     Set environment variable (can use multiple times)
  -f FILE          Add input file (can use multiple times)
  -a               Return and save artifacts from /tmp/artifacts/
  -o DIR           Output directory for artifacts (default: current dir)
  -p KEY           Public key (or set UNSANDBOX_PUBLIC_KEY env var)
  -k KEY           Secret key (or set UNSANDBOX_SECRET_KEY env var)
  -n MODE          Network mode: zerotrust (default) or semitrusted
  -v N, --vcpu N   vCPU count 1-8, each vCPU gets 2GB RAM (default: 1)
  -y               Skip confirmation for large uploads (>1GB)
  -h               Show this help

Authentication (priority order):
  1. -p and -k flags (public and secret key)
  2. UNSANDBOX_PUBLIC_KEY + UNSANDBOX_SECRET_KEY env vars
  3. ~/.unsandbox/accounts.csv (format: public_key,secret_key per line)

Session options:
  -s, --shell SHELL  Shell/REPL to use (default: bash)
  -l, --list         List active sessions
  --attach ID        Reconnect to existing session (ID or container name)
  --kill ID          Terminate a session (ID or container name)
  --freeze ID        Freeze a session (requires --tmux/--screen)
  --unfreeze ID      Unfreeze a frozen session
  --boost ID         Boost session resources (2 vCPU, 4GB RAM)
  --boost-vcpu N     Specify vCPU count for boost (1-8)
  --unboost ID       Return to base resources
  --audit            Record full session for auditing
  --tmux             Enable session persistence with tmux (allows reconnect)
  --screen           Enable session persistence with screen (allows reconnect)

Service options:
  --name NAME        Service name (creates new service)
  --ports PORTS      Comma-separated ports (e.g., 80,443)
  --domains DOMAINS  Custom domains (e.g., example.com,www.example.com)
  --type TYPE        Service type: minecraft, mumble, teamspeak, source, tcp, udp
  --bootstrap CMD    Bootstrap command/file/URL to run on startup
  -f FILE            Upload file to /tmp/ (can use multiple times)
  -l, --list         List all services
  --info ID          Get service details
  --tail ID          Get last 9000 lines of bootstrap logs
  --logs ID          Get all bootstrap logs
  --freeze ID        Freeze a service
  --unfreeze ID      Unfreeze a service
  --auto-unfreeze ID       Enable auto-wake on HTTP request
  --no-auto-unfreeze ID    Disable auto-wake on HTTP request
  --show-freeze-page ID    Show HTML payment page when frozen (default)
  --no-show-freeze-page ID Return JSON error when frozen
  --destroy ID       Destroy a service
  --redeploy ID      Re-run bootstrap script (requires --bootstrap)
  --execute ID CMD   Run a command in a running service
  --dump-bootstrap ID [FILE]  Dump bootstrap script (for migrations)
  --snapshot ID    Create snapshot of session or service
  --snapshot-name  User-friendly name for snapshot
  --hot            Create snapshot without pausing (may be inconsistent)
  --restore ID     Restore session/service from snapshot ID

Snapshot options:
  -l, --list       List all snapshots
  --info ID        Get snapshot details
  --delete ID      Delete a snapshot permanently

Image options:
  -l, --list [owned|shared|public]  List images (all, owned, shared, or public)
  --info ID        Get image details
  --publish-service ID   Publish image from stopped/frozen service
  --publish-snapshot ID  Publish image from snapshot
  --name NAME      Name for published image
  --description DESC  Description for published image
  --delete ID      Delete image (must be unlocked)
  --clone ID       Clone image (creates copy you own)
  --spawn ID       Create service from image (requires --name)
  --lock ID        Lock image to prevent deletion
  --unlock ID      Unlock image to allow deletion
  --visibility ID LEVEL  Set visibility (private|unlisted|public)
  --grant ID --key KEY   Grant access to another API key
  --revoke ID --key KEY  Revoke access from API key
  --transfer ID --to KEY Transfer ownership to API key
  --trusted ID     List API keys with access

Key options:
  (no options)       Check API key validity
  --extend           Open portal to extend an expired key

Examples:
  ./un script.py                       # execute Python script
  ./un -e DEBUG=1 script.py            # with environment variable
  ./un -f data.csv process.py          # with input file
  ./un -a -o ./bin main.c              # save compiled artifacts
  ./un -v 4 heavy.py                   # with 4 vCPUs, 8GB RAM
  ./un session                         # interactive bash session
  ./un session --tmux                  # bash with reconnect support
  ./un session --list                  # list active sessions
  ./un session --attach unsb-vm-12345  # reconnect to session
  ./un session --kill unsb-vm-12345    # terminate a session
  ./un session --freeze unsb-vm-12345  # freeze session
  ./un session --unfreeze unsb-vm-12345  # unfreeze session
  ./un session --boost unsb-vm-12345   # boost resources
  ./un session --unboost unsb-vm-12345 # return to base
  ./un session --shell python3         # Python REPL
  ./un session --shell node            # Node.js REPL
  ./un session -n semitrusted          # session with network access
  ./un session --audit -o ./logs       # record session for auditing
  ./un service --name web --ports 80   # create web service
  ./un service --list                  # list all services
  ./un service --logs abc123           # view bootstrap logs
  ./un key                             # check API key
  ./un key --extend                    # extend expired key
  ./un snapshot --list                 # list all snapshots
  ./un session --snapshot unsb-vm-123  # snapshot a session
  ./un service --snapshot abc123       # snapshot a service
  ./un session --restore unsb-snapshot-xxxx  # restore from snapshot
  ./un image --list                  # list all images
  ./un image --list owned            # list your images
  ./un image --publish-service abc   # publish image from service
  ./un image --spawn img123 --name x # create service from image
  ./un image --grant img --key pk    # share image with user

CLI Inception

The UN CLI has been implemented in 42 programming languages, demonstrating that the unsandbox API can be accessed from virtually any environment.

View All 42 Implementations →

License

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
that 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 all 42
programming languages through a unified interface, accessible to everyone. 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 its own software - with our permacomputer
monitoring itself, repairing itself, with minimal human guidance in the loop.
Our agents do their best.

Copyright 2025 TimeHexOn & foxhop & russell@unturf
https://www.timehexon.com
https://www.foxhop.net
https://www.unturf.com/software

Export Vault

Enter a password to encrypt your exported vault. You'll need this password to import the vault on another device.

Import Vault

Select an exported vault file and enter the export password to decrypt it.