CLI
Cliente de linha de comando rápido para execução de código e sessões interativas. Mais de 42 linguagens, mais de 30 shells/REPLs.
Documentação Oficial OpenAPI Swagger ↗Início Rápido — PHP
# Download + setup
curl -O https://git.unturf.com/engineering/unturf/un-inception/-/raw/main/clients/php/sync/src/un.php && chmod +x un.php && ln -sf un.php un
export UNSANDBOX_PUBLIC_KEY="unsb-pk-xxxx-xxxx-xxxx-xxxx"
export UNSANDBOX_SECRET_KEY="unsb-sk-xxxxx-xxxxx-xxxxx-xxxxx"
# Run code
./un script.php
Baixar
Guia de Instalação →Características:
- 42+ languages - Python, JS, Go, Rust, C++, Java...
- Sessions - 30+ shells/REPLs, tmux persistence
- Files - Upload files, collect artifacts
- Services - Persistent containers with domains
- Snapshots - Point-in-time backups
- Images - Publish, share, transfer
Início Rápido de Integração ⚡
Adicione superpoderes unsandbox ao seu app PHP existente:
curl -O https://git.unturf.com/engineering/unturf/un-inception/-/raw/main/clients/php/sync/src/un.php
# 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
<?php
// In your PHP app:
require_once 'un.php';
$client = new Unsandbox();
$result = $client->executeCode("php", "echo 'Hello from PHP running on unsandbox!';");
echo $result["stdout"]; // Hello from PHP running on unsandbox!
php myapp.php
7f8c7edda8f4e96b46594a824c5104c6
SHA256: a346e5afd157211e3e8a40d2d617d7dca97caf340e85434e67bd3bd53af0e22b
#!/usr/bin/env php
<?php
/**
* PUBLIC DOMAIN - NO LICENSE, NO WARRANTY
*
* unsandbox.com PHP SDK (Synchronous)
*
* Library Usage:
* require_once 'un.php';
* use Unsandbox\Unsandbox;
*
* $client = new Unsandbox();
*
* // Execute code synchronously
* $result = $client->executeCode('python', 'print("hello")');
*
* // Execute asynchronously
* $jobId = $client->executeAsync('javascript', 'console.log("hello")');
*
* // Wait for job completion with exponential backoff
* $result = $client->waitForJob($jobId);
*
* // List all jobs
* $jobs = $client->listJobs();
*
* // Get supported languages (cached for 1 hour)
* $languages = $client->getLanguages();
*
* // Snapshot operations
* $snapshotId = $client->sessionSnapshot($sessionId);
*
* Authentication Priority (4-tier):
* 1. Method arguments (publicKey, secretKey)
* 2. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY)
* 3. Config file (~/.unsandbox/accounts.csv, line 0 by default)
* 4. Local directory (./accounts.csv, line 0 by default)
*
* Request Authentication (HMAC-SHA256):
* Authorization: Bearer <public_key>
* X-Timestamp: <unix_seconds>
* X-Signature: HMAC-SHA256(secret_key, "timestamp:METHOD:path:body")
*
* Languages Cache:
* - Cached in ~/.unsandbox/languages.json
* - TTL: 1 hour
* - Updated on successful API calls
*/
namespace Unsandbox;
/**
* Exception thrown when credentials cannot be found or are invalid.
*/
class CredentialsException extends \Exception {}
/**
* Exception thrown when an API request fails.
*/
class ApiException extends \Exception {
private ?array $response;
public function __construct(string $message, int $code = 0, ?array $response = null, ?\Throwable $previous = null) {
parent::__construct($message, $code, $previous);
$this->response = $response;
}
public function getResponse(): ?array {
return $this->response;
}
}
/**
* Unsandbox PHP SDK - Synchronous Client
*
* Provides methods to execute code, manage jobs, and handle snapshots
* using the unsandbox.com API.
*/
class Unsandbox {
private const API_BASE = 'https://api.unsandbox.com';
private const LANGUAGES_CACHE_TTL = 3600; // 1 hour
private const POLL_DELAYS_MS = [300, 450, 700, 900, 650, 1600, 2000];
/**
* Language detection mapping (file extension -> language)
*/
private const LANGUAGE_MAP = [
'py' => 'python',
'js' => 'javascript',
'ts' => 'typescript',
'rb' => 'ruby',
'php' => 'php',
'pl' => 'perl',
'sh' => 'bash',
'r' => 'r',
'R' => 'r',
'lua' => 'lua',
'go' => 'go',
'rs' => 'rust',
'c' => 'c',
'cpp' => 'cpp',
'cc' => 'cpp',
'cxx' => 'cpp',
'java' => 'java',
'kt' => 'kotlin',
'm' => 'objc',
'cs' => 'csharp',
'fs' => 'fsharp',
'hs' => 'haskell',
'ml' => 'ocaml',
'clj' => 'clojure',
'scm' => 'scheme',
'ss' => 'scheme',
'erl' => 'erlang',
'ex' => 'elixir',
'exs' => 'elixir',
'jl' => 'julia',
'd' => 'd',
'nim' => 'nim',
'zig' => 'zig',
'v' => 'v',
'cr' => 'crystal',
'dart' => 'dart',
'groovy' => 'groovy',
'f90' => 'fortran',
'f95' => 'fortran',
'lisp' => 'commonlisp',
'lsp' => 'commonlisp',
'cob' => 'cobol',
'tcl' => 'tcl',
'raku' => 'raku',
'pro' => 'prolog',
'p' => 'prolog',
'4th' => 'forth',
'forth' => 'forth',
'fth' => 'forth',
];
private ?string $defaultPublicKey = null;
private ?string $defaultSecretKey = null;
private int $accountIndex = 0;
private bool $accountIndexExplicit = false;
/**
* Create a new Unsandbox client.
*
* @param string|null $publicKey Default public key (optional)
* @param string|null $secretKey Default secret key (optional)
* @param int $accountIndex Account index for CSV files (default: 0)
*/
public function __construct(?string $publicKey = null, ?string $secretKey = null, int $accountIndex = 0) {
$this->defaultPublicKey = $publicKey;
$this->defaultSecretKey = $secretKey;
$this->accountIndex = $accountIndex;
}
/**
* Execute code synchronously (awaits until completion).
*
* @param string $language Programming language (e.g., "python", "javascript", "go")
* @param string $code Source code to execute
* @param string|null $publicKey Optional API key (uses credentials resolution if not provided)
* @param string|null $secretKey Optional API secret (uses credentials resolution if not provided)
* @return array Response array containing stdout, stderr, exit code, etc.
* @throws CredentialsException Missing credentials
* @throws ApiException API request failed
*/
public function executeCode(string $language, string $code, ?string $publicKey = null, ?string $secretKey = null): array {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
$response = $this->makeRequest(
'POST',
'/execute',
$publicKey,
$secretKey,
['language' => $language, 'code' => $code]
);
// If we got a job_id, poll until completion
$jobId = $response['job_id'] ?? null;
$status = $response['status'] ?? null;
if ($jobId && in_array($status, ['pending', 'running'], true)) {
return $this->waitForJob($jobId, $publicKey, $secretKey);
}
return $response;
}
/**
* Execute code asynchronously (returns immediately with job_id).
*
* @param string $language Programming language (e.g., "python", "javascript")
* @param string $code Source code to execute
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @return string Job ID string
* @throws CredentialsException Missing credentials
* @throws ApiException API request failed
*/
public function executeAsync(string $language, string $code, ?string $publicKey = null, ?string $secretKey = null): string {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
$response = $this->makeRequest(
'POST',
'/execute',
$publicKey,
$secretKey,
['language' => $language, 'code' => $code]
);
return $response['job_id'] ?? '';
}
/**
* Get current status/result of a job (single poll, no waiting).
*
* @param string $jobId Job ID from executeAsync()
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @return array Job response array
* @throws CredentialsException Missing credentials
* @throws ApiException API request failed
*/
public function getJob(string $jobId, ?string $publicKey = null, ?string $secretKey = null): array {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
return $this->makeRequest('GET', "/jobs/{$jobId}", $publicKey, $secretKey);
}
/**
* Wait for job completion with exponential backoff polling.
*
* Polling delays (ms): [300, 450, 700, 900, 650, 1600, 2000, ...]
* Cumulative: 300, 750, 1450, 2350, 3000, 4600, 6600ms+
*
* @param string $jobId Job ID from executeAsync()
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @param int $timeout Maximum wait time in seconds (default: 3600)
* @return array Final job result when status is terminal (completed, failed, timeout, cancelled)
* @throws CredentialsException Missing credentials
* @throws ApiException API request failed or timeout exceeded
*/
public function waitForJob(string $jobId, ?string $publicKey = null, ?string $secretKey = null, int $timeout = 3600): array {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
$pollCount = 0;
$startTime = time();
while (true) {
// Check timeout
if ((time() - $startTime) >= $timeout) {
throw new ApiException("Timeout waiting for job {$jobId}");
}
// Sleep before polling
$delayIdx = min($pollCount, count(self::POLL_DELAYS_MS) - 1);
usleep(self::POLL_DELAYS_MS[$delayIdx] * 1000);
$pollCount++;
$response = $this->getJob($jobId, $publicKey, $secretKey);
$status = $response['status'] ?? null;
if (in_array($status, ['completed', 'failed', 'timeout', 'cancelled'], true)) {
return $response;
}
// Still running, continue polling
}
}
/**
* Cancel a running job.
*
* @param string $jobId Job ID to cancel
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @return array Response array with cancellation confirmation
* @throws CredentialsException Missing credentials
* @throws ApiException API request failed
*/
public function cancelJob(string $jobId, ?string $publicKey = null, ?string $secretKey = null): array {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
return $this->makeRequest('DELETE', "/jobs/{$jobId}", $publicKey, $secretKey);
}
/**
* List all jobs for the authenticated account.
*
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @return array List of job arrays
* @throws CredentialsException Missing credentials
* @throws ApiException API request failed
*/
public function listJobs(?string $publicKey = null, ?string $secretKey = null): array {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
$response = $this->makeRequest('GET', '/jobs', $publicKey, $secretKey);
return $response['jobs'] ?? [];
}
/**
* Get list of supported programming languages.
*
* Results are cached for 1 hour in ~/.unsandbox/languages.json
*
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @return array List of language identifiers (e.g., ["python", "javascript", "go", ...])
* @throws CredentialsException Missing credentials
* @throws ApiException API request failed
*/
public function getLanguages(?string $publicKey = null, ?string $secretKey = null): array {
// Try cache first
$cached = $this->loadLanguagesCache();
if ($cached !== null) {
return $cached;
}
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
$response = $this->makeRequest('GET', '/languages', $publicKey, $secretKey);
$languages = $response['languages'] ?? [];
// Cache the result
$this->saveLanguagesCache($languages);
return $languages;
}
/**
* Detect programming language from filename extension.
*
* @param string $filename Filename to detect language from (e.g., "script.py")
* @return string|null Language identifier (e.g., "python") or null if unknown
*/
public static function detectLanguage(string $filename): ?string {
if (empty($filename) || strpos($filename, '.') === false) {
return null;
}
$parts = explode('.', $filename);
$ext = end($parts);
return self::LANGUAGE_MAP[$ext] ?? null;
}
/**
* Create a snapshot of a session.
*
* @param string $sessionId Session ID to snapshot
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @param string|null $name Optional snapshot name
* @param bool $ephemeral If true, snapshot is ephemeral (hot snapshot)
* @return string Snapshot ID
* @throws CredentialsException Missing credentials
* @throws ApiException API request failed
*/
public function sessionSnapshot(
string $sessionId,
?string $publicKey = null,
?string $secretKey = null,
?string $name = null,
bool $ephemeral = false
): string {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
$data = ['session_id' => $sessionId, 'hot' => $ephemeral];
if ($name !== null) {
$data['name'] = $name;
}
$response = $this->makeRequest('POST', '/snapshots', $publicKey, $secretKey, $data);
return $response['snapshot_id'] ?? '';
}
/**
* Create a snapshot of a service.
*
* @param string $serviceId Service ID to snapshot
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @param string|null $name Optional snapshot name
* @return string Snapshot ID
* @throws CredentialsException Missing credentials
* @throws ApiException API request failed
*/
public function serviceSnapshot(
string $serviceId,
?string $publicKey = null,
?string $secretKey = null,
?string $name = null
): string {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
$data = ['service_id' => $serviceId];
if ($name !== null) {
$data['name'] = $name;
}
$response = $this->makeRequest('POST', '/snapshots', $publicKey, $secretKey, $data);
return $response['snapshot_id'] ?? '';
}
/**
* List all snapshots.
*
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @return array List of snapshot arrays
* @throws CredentialsException Missing credentials
* @throws ApiException API request failed
*/
public function listSnapshots(?string $publicKey = null, ?string $secretKey = null): array {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
$response = $this->makeRequest('GET', '/snapshots', $publicKey, $secretKey);
return $response['snapshots'] ?? [];
}
/**
* Get details of a specific snapshot.
*
* @param string $snapshotId Snapshot ID to get details for
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @return array Snapshot details array containing:
* - id: Snapshot ID
* - name: Snapshot name
* - type: "session" or "service"
* - source_id: Original resource ID
* - hot: Whether snapshot preserves running state
* - locked: Whether snapshot is locked
* - created_at: Creation timestamp
* - size_bytes: Size in bytes
* @throws CredentialsException Missing credentials
* @throws ApiException API request failed
*/
public function getSnapshot(string $snapshotId, ?string $publicKey = null, ?string $secretKey = null): array {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
return $this->makeRequest('GET', "/snapshots/{$snapshotId}", $publicKey, $secretKey);
}
/**
* Restore a snapshot.
*
* @param string $snapshotId Snapshot ID to restore
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @return array Response array with restored resource info
* @throws CredentialsException Missing credentials
* @throws ApiException API request failed
*/
public function restoreSnapshot(string $snapshotId, ?string $publicKey = null, ?string $secretKey = null): array {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
return $this->makeRequest('POST', "/snapshots/{$snapshotId}/restore", $publicKey, $secretKey, []);
}
/**
* Delete a snapshot.
*
* @param string $snapshotId Snapshot ID to delete
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @return array Response array with deletion confirmation
* @throws CredentialsException Missing credentials
* @throws ApiException API request failed
*/
public function deleteSnapshot(string $snapshotId, ?string $publicKey = null, ?string $secretKey = null): array {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
return $this->makeRequestWithSudo('DELETE', "/snapshots/{$snapshotId}", $publicKey, $secretKey);
}
/**
* Lock a snapshot to prevent deletion.
*
* @param string $snapshotId Snapshot ID to lock
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @return array Response array with lock confirmation
* @throws CredentialsException Missing credentials
* @throws ApiException API request failed
*/
public function lockSnapshot(string $snapshotId, ?string $publicKey = null, ?string $secretKey = null): array {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
return $this->makeRequest('POST', "/snapshots/{$snapshotId}/lock", $publicKey, $secretKey, []);
}
/**
* Unlock a snapshot to allow deletion.
*
* @param string $snapshotId Snapshot ID to unlock
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @return array Response array with unlock confirmation
* @throws CredentialsException Missing credentials
* @throws ApiException API request failed
*/
public function unlockSnapshot(string $snapshotId, ?string $publicKey = null, ?string $secretKey = null): array {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
return $this->makeRequestWithSudo('POST', "/snapshots/{$snapshotId}/unlock", $publicKey, $secretKey, []);
}
/**
* Clone a snapshot to create a new session or service.
*
* @param string $snapshotId Snapshot ID to clone
* @param string|null $name Optional name for the new resource
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @param array $opts Optional parameters: 'type' (session|service), 'shell', 'ports'
* @return array Response array with cloned resource info
* @throws CredentialsException Missing credentials
* @throws ApiException API request failed
*/
public function cloneSnapshot(
string $snapshotId,
?string $name = null,
?string $publicKey = null,
?string $secretKey = null,
array $opts = []
): array {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
$data = [];
if ($name !== null) {
$data['name'] = $name;
}
if (isset($opts['type'])) {
$data['type'] = $opts['type'];
}
if (isset($opts['shell'])) {
$data['shell'] = $opts['shell'];
}
if (isset($opts['ports'])) {
$data['ports'] = $opts['ports'];
}
return $this->makeRequest('POST', "/snapshots/{$snapshotId}/clone", $publicKey, $secretKey, $data);
}
// =========================================================================
// Images Methods (LXD Container Images)
// =========================================================================
/**
* Publish a new image from a session or service container.
*
* @param string $sourceType Source type: "session" or "service"
* @param string $sourceId Session ID or Service ID to publish from
* @param string|null $name Optional name for the image
* @param string|null $description Optional description for the image
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @return array Response array with image_id and image details
* @throws CredentialsException Missing credentials
* @throws ApiException API request failed
*/
public function imagePublish(
string $sourceType,
string $sourceId,
?string $name = null,
?string $description = null,
?string $publicKey = null,
?string $secretKey = null
): array {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
$data = [
'source_type' => $sourceType,
'source_id' => $sourceId,
];
if ($name !== null) {
$data['name'] = $name;
}
if ($description !== null) {
$data['description'] = $description;
}
return $this->makeRequest('POST', '/images', $publicKey, $secretKey, $data);
}
/**
* List all images owned by the authenticated account.
*
* @param string|null $filterType Optional filter: "owned", "shared", "public", or null for all
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @return array List of image arrays
* @throws CredentialsException Missing credentials
* @throws ApiException API request failed
*/
public function listImages(?string $filterType = null, ?string $publicKey = null, ?string $secretKey = null): array {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
$path = $filterType !== null ? "/images/{$filterType}" : '/images';
$response = $this->makeRequest('GET', $path, $publicKey, $secretKey);
return $response['images'] ?? [];
}
/**
* Get details of a specific image.
*
* @param string $imageId Image ID
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @return array Image details
* @throws CredentialsException Missing credentials
* @throws ApiException API request failed
*/
public function getImage(string $imageId, ?string $publicKey = null, ?string $secretKey = null): array {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
return $this->makeRequest('GET', "/images/{$imageId}", $publicKey, $secretKey);
}
/**
* Delete an image.
*
* @param string $imageId Image ID to delete
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @return array Response array with deletion confirmation
* @throws CredentialsException Missing credentials
* @throws ApiException API request failed
*/
public function deleteImage(string $imageId, ?string $publicKey = null, ?string $secretKey = null): array {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
return $this->makeRequestWithSudo('DELETE', "/images/{$imageId}", $publicKey, $secretKey);
}
/**
* Lock an image to prevent deletion.
*
* @param string $imageId Image ID to lock
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @return array Response array with lock confirmation
* @throws CredentialsException Missing credentials
* @throws ApiException API request failed
*/
public function lockImage(string $imageId, ?string $publicKey = null, ?string $secretKey = null): array {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
return $this->makeRequest('POST', "/images/{$imageId}/lock", $publicKey, $secretKey, []);
}
/**
* Unlock an image to allow deletion.
*
* @param string $imageId Image ID to unlock
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @return array Response array with unlock confirmation
* @throws CredentialsException Missing credentials
* @throws ApiException API request failed
*/
public function unlockImage(string $imageId, ?string $publicKey = null, ?string $secretKey = null): array {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
return $this->makeRequestWithSudo('POST', "/images/{$imageId}/unlock", $publicKey, $secretKey, []);
}
/**
* Set visibility of an image.
*
* @param string $imageId Image ID
* @param string $visibility Visibility level: "private" or "public"
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @return array Response array with visibility update confirmation
* @throws CredentialsException Missing credentials
* @throws ApiException API request failed
*/
public function setImageVisibility(
string $imageId,
string $visibility,
?string $publicKey = null,
?string $secretKey = null
): array {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
return $this->makeRequest('POST', "/images/{$imageId}/visibility", $publicKey, $secretKey, [
'visibility' => $visibility,
]);
}
/**
* Grant access to an image for another API key.
*
* @param string $imageId Image ID
* @param string $trustedApiKey The API key to grant access to
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @return array Response array with grant confirmation
* @throws CredentialsException Missing credentials
* @throws ApiException API request failed
*/
public function grantImageAccess(
string $imageId,
string $trustedApiKey,
?string $publicKey = null,
?string $secretKey = null
): array {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
return $this->makeRequest('POST', "/images/{$imageId}/grant", $publicKey, $secretKey, [
'trusted_api_key' => $trustedApiKey,
]);
}
/**
* Revoke access to an image from another API key.
*
* @param string $imageId Image ID
* @param string $trustedApiKey The API key to revoke access from
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @return array Response array with revoke confirmation
* @throws CredentialsException Missing credentials
* @throws ApiException API request failed
*/
public function revokeImageAccess(
string $imageId,
string $trustedApiKey,
?string $publicKey = null,
?string $secretKey = null
): array {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
return $this->makeRequest('POST', "/images/{$imageId}/revoke", $publicKey, $secretKey, [
'trusted_api_key' => $trustedApiKey,
]);
}
/**
* List API keys that have been granted access to an image.
*
* @param string $imageId Image ID
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @return array List of trusted API key information
* @throws CredentialsException Missing credentials
* @throws ApiException API request failed
*/
public function listImageTrusted(string $imageId, ?string $publicKey = null, ?string $secretKey = null): array {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
$response = $this->makeRequest('GET', "/images/{$imageId}/trusted", $publicKey, $secretKey);
return $response['trusted'] ?? [];
}
/**
* Transfer ownership of an image to another API key.
*
* @param string $imageId Image ID to transfer
* @param string $toApiKey The API key to transfer ownership to
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @return array Response array with transfer confirmation
* @throws CredentialsException Missing credentials
* @throws ApiException API request failed
*/
public function transferImage(
string $imageId,
string $toApiKey,
?string $publicKey = null,
?string $secretKey = null
): array {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
return $this->makeRequest('POST', "/images/{$imageId}/transfer", $publicKey, $secretKey, [
'to_api_key' => $toApiKey,
]);
}
/**
* Spawn a new service from an image.
*
* @param string $imageId Image ID to spawn from
* @param string|null $name Optional service name
* @param array|string|null $ports Optional port(s) to expose (array of ints or comma-separated string)
* @param string|null $bootstrap Optional bootstrap command or URL
* @param string $networkMode Network mode: "zerotrust" or "semitrusted"
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @return array Response array with spawned service info
* @throws CredentialsException Missing credentials
* @throws ApiException API request failed
*/
public function spawnFromImage(
string $imageId,
?string $name = null,
$ports = null,
?string $bootstrap = null,
string $networkMode = 'zerotrust',
?string $publicKey = null,
?string $secretKey = null
): array {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
$data = [
'network_mode' => $networkMode,
];
if ($name !== null) {
$data['name'] = $name;
}
if ($ports !== null) {
// Convert ports to array if string
if (is_string($ports)) {
$ports = array_map('intval', explode(',', $ports));
}
$data['ports'] = $ports;
}
if ($bootstrap !== null) {
$data['bootstrap'] = $bootstrap;
}
return $this->makeRequest('POST', "/images/{$imageId}/spawn", $publicKey, $secretKey, $data);
}
/**
* Clone an image to create a new image with a different name/description.
*
* @param string $imageId Image ID to clone
* @param string|null $name Optional name for the cloned image
* @param string|null $description Optional description for the cloned image
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @return array Response array with cloned image info
* @throws CredentialsException Missing credentials
* @throws ApiException API request failed
*/
public function cloneImage(
string $imageId,
?string $name = null,
?string $description = null,
?string $publicKey = null,
?string $secretKey = null
): array {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
$data = [];
if ($name !== null) {
$data['name'] = $name;
}
if ($description !== null) {
$data['description'] = $description;
}
return $this->makeRequest('POST', "/images/{$imageId}/clone", $publicKey, $secretKey, $data);
}
// =========================================================================
// Session Methods
// =========================================================================
/**
* List all active sessions for the authenticated account.
*
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @return array List of session arrays
* @throws CredentialsException Missing credentials
* @throws ApiException API request failed
*/
public function listSessions(?string $publicKey = null, ?string $secretKey = null): array {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
$response = $this->makeRequest('GET', '/sessions', $publicKey, $secretKey);
return $response['sessions'] ?? [];
}
/**
* Get details of a specific session.
*
* @param string $sessionId Session ID
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @return array Session details
* @throws CredentialsException Missing credentials
* @throws ApiException API request failed
*/
public function getSession(string $sessionId, ?string $publicKey = null, ?string $secretKey = null): array {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
return $this->makeRequest('GET', "/sessions/{$sessionId}", $publicKey, $secretKey);
}
/**
* Create a new interactive session.
*
* @param string $language Programming language or shell (e.g., "bash", "python3")
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @param array $opts Optional parameters: 'network_mode', 'ttl', 'shell', 'multiplexer', 'vcpu'
* @return array Session info including session_id and container_name
* @throws CredentialsException Missing credentials
* @throws ApiException API request failed
*/
public function createSession(
string $language,
?string $publicKey = null,
?string $secretKey = null,
array $opts = []
): array {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
$data = [
'network_mode' => $opts['network_mode'] ?? 'zerotrust',
'ttl' => $opts['ttl'] ?? 3600,
];
if (!empty($language)) {
$data['shell'] = $language;
}
if (isset($opts['shell'])) {
$data['shell'] = $opts['shell'];
}
if (isset($opts['multiplexer'])) {
$data['multiplexer'] = $opts['multiplexer'];
}
if (isset($opts['vcpu']) && $opts['vcpu'] > 1) {
$data['vcpu'] = $opts['vcpu'];
}
if (isset($opts['input_files'])) {
$data['input_files'] = $opts['input_files'];
}
return $this->makeRequest('POST', '/sessions', $publicKey, $secretKey, $data);
}
/**
* Delete (terminate) a session.
*
* @param string $sessionId Session ID to delete
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @return array Response array with deletion confirmation
* @throws CredentialsException Missing credentials
* @throws ApiException API request failed
*/
public function deleteSession(string $sessionId, ?string $publicKey = null, ?string $secretKey = null): array {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
return $this->makeRequest('DELETE', "/sessions/{$sessionId}", $publicKey, $secretKey);
}
/**
* Freeze a session to pause execution and reduce resource usage.
*
* @param string $sessionId Session ID to freeze
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @return array Response array with freeze confirmation
* @throws CredentialsException Missing credentials
* @throws ApiException API request failed
*/
public function freezeSession(string $sessionId, ?string $publicKey = null, ?string $secretKey = null): array {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
return $this->makeRequest('POST', "/sessions/{$sessionId}/freeze", $publicKey, $secretKey, []);
}
/**
* Unfreeze a session to resume execution.
*
* @param string $sessionId Session ID to unfreeze
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @return array Response array with unfreeze confirmation
* @throws CredentialsException Missing credentials
* @throws ApiException API request failed
*/
public function unfreezeSession(string $sessionId, ?string $publicKey = null, ?string $secretKey = null): array {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
return $this->makeRequest('POST', "/sessions/{$sessionId}/unfreeze", $publicKey, $secretKey, []);
}
/**
* Boost a session's resources (increase vCPU, memory is derived: vcpu * 2048MB).
*
* @param string $sessionId Session ID to boost
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @param int $vcpu Number of vCPUs (default: 2)
* @return array Response array with boost confirmation
* @throws CredentialsException Missing credentials
* @throws ApiException API request failed
*/
public function boostSession(
string $sessionId,
?string $publicKey = null,
?string $secretKey = null,
int $vcpu = 2
): array {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
return $this->makeRequest('POST', "/sessions/{$sessionId}/boost", $publicKey, $secretKey, ['vcpu' => $vcpu]);
}
/**
* Remove boost from a session (return to base resources).
*
* @param string $sessionId Session ID to unboost
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @return array Response array with unboost confirmation
* @throws CredentialsException Missing credentials
* @throws ApiException API request failed
*/
public function unboostSession(string $sessionId, ?string $publicKey = null, ?string $secretKey = null): array {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
return $this->makeRequest('POST', "/sessions/{$sessionId}/unboost", $publicKey, $secretKey, []);
}
/**
* Execute a shell command in an active session.
*
* Note: This is for one-shot commands. For interactive sessions, use WebSocket connection.
*
* @param string $sessionId Session ID
* @param string $command Command to execute
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @return array Response array with command output
* @throws CredentialsException Missing credentials
* @throws ApiException API request failed
*/
public function shellSession(
string $sessionId,
string $command,
?string $publicKey = null,
?string $secretKey = null
): array {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
return $this->makeRequest('POST', "/sessions/{$sessionId}/shell", $publicKey, $secretKey, ['command' => $command]);
}
// =========================================================================
// Service Methods
// =========================================================================
/**
* List all services for the authenticated account.
*
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @return array List of service arrays
* @throws CredentialsException Missing credentials
* @throws ApiException API request failed
*/
public function listServices(?string $publicKey = null, ?string $secretKey = null): array {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
$response = $this->makeRequest('GET', '/services', $publicKey, $secretKey);
return $response['services'] ?? [];
}
/**
* Create a new persistent service.
*
* @param string $name Service name
* @param array|string $ports Port(s) to expose (array of ints or comma-separated string)
* @param string $bootstrap Bootstrap command or URL
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @param array $opts Optional parameters: 'network_mode', 'vcpu', 'service_type', 'custom_domains', 'bootstrap_content', 'input_files'
* @return array Service info including service_id
* @throws CredentialsException Missing credentials
* @throws ApiException API request failed
*/
public function createService(
string $name,
$ports,
string $bootstrap,
?string $publicKey = null,
?string $secretKey = null,
array $opts = []
): array {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
// Convert ports to array if string
if (is_string($ports)) {
$ports = array_map('intval', explode(',', $ports));
}
$data = [
'name' => $name,
'ports' => $ports,
'bootstrap' => $bootstrap,
];
if (isset($opts['network_mode'])) {
$data['network_mode'] = $opts['network_mode'];
}
if (isset($opts['vcpu']) && $opts['vcpu'] > 1) {
$data['vcpu'] = $opts['vcpu'];
}
if (isset($opts['service_type'])) {
$data['service_type'] = $opts['service_type'];
}
if (isset($opts['custom_domains'])) {
$data['custom_domains'] = $opts['custom_domains'];
}
if (isset($opts['bootstrap_content'])) {
$data['bootstrap_content'] = $opts['bootstrap_content'];
}
if (isset($opts['input_files'])) {
$data['input_files'] = $opts['input_files'];
}
if (isset($opts['unfreeze_on_demand']) && $opts['unfreeze_on_demand']) {
$data['unfreeze_on_demand'] = true;
}
return $this->makeRequest('POST', '/services', $publicKey, $secretKey, $data);
}
/**
* Get details of a specific service.
*
* @param string $serviceId Service ID
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @return array Service details
* @throws CredentialsException Missing credentials
* @throws ApiException API request failed
*/
public function getService(string $serviceId, ?string $publicKey = null, ?string $secretKey = null): array {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
return $this->makeRequest('GET', "/services/{$serviceId}", $publicKey, $secretKey);
}
/**
* Update a service (e.g., resize vCPU).
*
* @param string $serviceId Service ID
* @param array $opts Update parameters: 'vcpu', 'name', etc.
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @return array Updated service details
* @throws CredentialsException Missing credentials
* @throws ApiException API request failed
*/
public function updateService(
string $serviceId,
array $opts,
?string $publicKey = null,
?string $secretKey = null
): array {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
return $this->makeRequest('PATCH', "/services/{$serviceId}", $publicKey, $secretKey, $opts);
}
/**
* Delete (destroy) a service.
*
* @param string $serviceId Service ID to delete
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @return array Response array with deletion confirmation
* @throws CredentialsException Missing credentials
* @throws ApiException API request failed
*/
public function deleteService(string $serviceId, ?string $publicKey = null, ?string $secretKey = null): array {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
return $this->makeRequestWithSudo('DELETE', "/services/{$serviceId}", $publicKey, $secretKey);
}
/**
* Freeze a service to pause execution and reduce resource usage.
*
* @param string $serviceId Service ID to freeze
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @return array Response array with freeze confirmation
* @throws CredentialsException Missing credentials
* @throws ApiException API request failed
*/
public function freezeService(string $serviceId, ?string $publicKey = null, ?string $secretKey = null): array {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
return $this->makeRequest('POST', "/services/{$serviceId}/freeze", $publicKey, $secretKey, []);
}
/**
* Unfreeze a service to resume execution.
*
* @param string $serviceId Service ID to unfreeze
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @return array Response array with unfreeze confirmation
* @throws CredentialsException Missing credentials
* @throws ApiException API request failed
*/
public function unfreezeService(string $serviceId, ?string $publicKey = null, ?string $secretKey = null): array {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
return $this->makeRequest('POST', "/services/{$serviceId}/unfreeze", $publicKey, $secretKey, []);
}
/**
* Lock a service to prevent deletion.
*
* @param string $serviceId Service ID to lock
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @return array Response array with lock confirmation
* @throws CredentialsException Missing credentials
* @throws ApiException API request failed
*/
public function lockService(string $serviceId, ?string $publicKey = null, ?string $secretKey = null): array {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
return $this->makeRequest('POST', "/services/{$serviceId}/lock", $publicKey, $secretKey, []);
}
/**
* Unlock a service to allow deletion.
*
* @param string $serviceId Service ID to unlock
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @return array Response array with unlock confirmation
* @throws CredentialsException Missing credentials
* @throws ApiException API request failed
*/
public function unlockService(string $serviceId, ?string $publicKey = null, ?string $secretKey = null): array {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
return $this->makeRequestWithSudo('POST', "/services/{$serviceId}/unlock", $publicKey, $secretKey, []);
}
/**
* Set unfreeze_on_demand flag for a service.
*
* @param string $serviceId Service ID
* @param bool $enabled Whether to enable unfreeze on demand
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @return array Response array with update confirmation
* @throws CredentialsException Missing credentials
* @throws ApiException API request failed
*/
public function setUnfreezeOnDemand(string $serviceId, bool $enabled, ?string $publicKey = null, ?string $secretKey = null): array {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
return $this->makeRequest('PATCH', "/services/{$serviceId}", $publicKey, $secretKey, ['unfreeze_on_demand' => $enabled]);
}
/**
* Set show_freeze_page flag for a service.
*
* @param string $serviceId Service ID
* @param bool $enabled Whether to enable show freeze page
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @return array Response array with update confirmation
* @throws CredentialsException Missing credentials
* @throws ApiException API request failed
*/
public function setShowFreezePage(string $serviceId, bool $enabled, ?string $publicKey = null, ?string $secretKey = null): array {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
return $this->makeRequest('PATCH', "/services/{$serviceId}", $publicKey, $secretKey, ['show_freeze_page' => $enabled]);
}
/**
* Get bootstrap logs for a service.
*
* @param string $serviceId Service ID
* @param bool $all If true, get all logs; if false, get last 9000 lines
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @return array Response array with log content
* @throws CredentialsException Missing credentials
* @throws ApiException API request failed
*/
public function getServiceLogs(
string $serviceId,
bool $all = false,
?string $publicKey = null,
?string $secretKey = null
): array {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
$path = "/services/{$serviceId}/logs" . ($all ? '?all=true' : '');
return $this->makeRequest('GET', $path, $publicKey, $secretKey);
}
/**
* Get environment vault status for a service.
*
* @param string $serviceId Service ID
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @return array Response array with vault status (has_vault, count, updated_at)
* @throws CredentialsException Missing credentials
* @throws ApiException API request failed
*/
public function getServiceEnv(string $serviceId, ?string $publicKey = null, ?string $secretKey = null): array {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
return $this->makeRequest('GET', "/services/{$serviceId}/env", $publicKey, $secretKey);
}
/**
* Set environment vault for a service.
*
* @param string $serviceId Service ID
* @param string $env Environment content in .env format (KEY=VALUE\nKEY2=VALUE2)
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @return array Response array with update confirmation
* @throws CredentialsException Missing credentials
* @throws ApiException API request failed
*/
public function setServiceEnv(
string $serviceId,
string $env,
?string $publicKey = null,
?string $secretKey = null
): array {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
return $this->makeRequestRaw('PUT', "/services/{$serviceId}/env", $publicKey, $secretKey, $env, 'text/plain');
}
/**
* Delete environment vault for a service.
*
* @param string $serviceId Service ID
* @param array|null $keys Optional specific keys to delete; if null, deletes entire vault
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @return array Response array with deletion confirmation
* @throws CredentialsException Missing credentials
* @throws ApiException API request failed
*/
public function deleteServiceEnv(
string $serviceId,
?array $keys = null,
?string $publicKey = null,
?string $secretKey = null
): array {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
return $this->makeRequest('DELETE', "/services/{$serviceId}/env", $publicKey, $secretKey);
}
/**
* Export environment vault for a service (returns .env format).
*
* @param string $serviceId Service ID
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @return array Response array with env content
* @throws CredentialsException Missing credentials
* @throws ApiException API request failed
*/
public function exportServiceEnv(string $serviceId, ?string $publicKey = null, ?string $secretKey = null): array {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
return $this->makeRequest('POST', "/services/{$serviceId}/env/export", $publicKey, $secretKey, []);
}
/**
* Redeploy a service (re-run bootstrap script).
*
* @param string $serviceId Service ID
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @param array $opts Optional parameters: 'input_files'
* @return array Response array with redeploy confirmation
* @throws CredentialsException Missing credentials
* @throws ApiException API request failed
*/
public function redeployService(string $serviceId, ?string $publicKey = null, ?string $secretKey = null, array $opts = []): array {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
$data = [];
if (isset($opts['input_files'])) {
$data['input_files'] = $opts['input_files'];
}
return $this->makeRequest('POST', "/services/{$serviceId}/redeploy", $publicKey, $secretKey, $data);
}
/**
* Execute a command in a running service container.
*
* @param string $serviceId Service ID
* @param string $command Command to execute
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @param int $timeout Timeout in milliseconds (default: 30000)
* @return array Response array with command output (stdout, stderr, exit_code)
* @throws CredentialsException Missing credentials
* @throws ApiException API request failed
*/
public function executeInService(
string $serviceId,
string $command,
?string $publicKey = null,
?string $secretKey = null,
int $timeout = 30000
): array {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
$response = $this->makeRequest('POST', "/services/{$serviceId}/execute", $publicKey, $secretKey, [
'command' => $command,
'timeout' => $timeout,
]);
// If we got a job_id, poll until completion
$jobId = $response['job_id'] ?? null;
if ($jobId) {
return $this->waitForJob($jobId, $publicKey, $secretKey);
}
return $response;
}
/**
* Resize a service's vCPU allocation.
*
* @param string $serviceId Service ID to resize
* @param int $vcpu Number of vCPUs (1-8 typically)
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @return array Response array with updated service info
* @throws CredentialsException Missing credentials
* @throws ApiException API request failed
*/
public function resizeService(string $serviceId, int $vcpu, ?string $publicKey = null, ?string $secretKey = null): array {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
return $this->makeRequest('PATCH', "/services/{$serviceId}", $publicKey, $secretKey, ['vcpu' => $vcpu]);
}
// =========================================================================
// Key Validation
// =========================================================================
/**
* Validate API keys.
*
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @return array Response array with validation result
* @throws CredentialsException Missing credentials
* @throws ApiException API request failed
*/
public function validateKeys(?string $publicKey = null, ?string $secretKey = null): array {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
return $this->makeRequest('POST', '/keys/validate', $publicKey, $secretKey, []);
}
// =========================================================================
// Image Generation
// =========================================================================
/**
* Generate images from text prompt using AI.
*
* @param string $prompt Text description of the image to generate
* @param string|null $model Model to use (optional)
* @param string $size Image size (default: "1024x1024")
* @param string $quality "standard" or "hd" (default: "standard")
* @param int $n Number of images to generate (default: 1)
* @param string|null $publicKey API public key
* @param string|null $secretKey API secret key
* @return array Result with 'images' array and 'created_at'
* @throws CredentialsException Missing credentials
* @throws ApiException API request failed
*/
public function image(
string $prompt,
?string $model = null,
string $size = "1024x1024",
string $quality = "standard",
int $n = 1,
?string $publicKey = null,
?string $secretKey = null
): array {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
$payload = [
'prompt' => $prompt,
'size' => $size,
'quality' => $quality,
'n' => $n,
];
if ($model !== null) {
$payload['model'] = $model;
}
return $this->makeRequest('POST', '/image', $publicKey, $secretKey, $payload);
}
// =========================================================================
// PaaS Logs Functions
// =========================================================================
private static ?string $lastError = null;
/**
* Fetch batch logs from the PaaS platform.
*
* @param string $source Log source - "all", "api", "portal", "pool/cammy", "pool/ai"
* @param int $lines Number of lines to fetch (1-10000)
* @param string $since Time window - "1m", "5m", "1h", "1d"
* @param string|null $grep Optional filter pattern
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @return array Log entries
* @throws CredentialsException Missing credentials
* @throws ApiException API request failed
*/
public function logsFetch(
string $source = 'all',
int $lines = 100,
string $since = '5m',
?string $grep = null,
?string $publicKey = null,
?string $secretKey = null
): array {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
$path = "/logs?source=" . urlencode($source) . "&lines={$lines}&since=" . urlencode($since);
if ($grep !== null) {
$path .= "&grep=" . urlencode($grep);
}
return $this->makeRequest('GET', $path, $publicKey, $secretKey);
}
/**
* Stream logs via Server-Sent Events.
*
* Blocks until interrupted or server closes connection.
*
* @param string $source Log source - "all", "api", "portal", "pool/cammy", "pool/ai"
* @param string|null $grep Optional filter pattern
* @param callable|null $callback Function called for each log line (signature: callback(source, line))
* @param string|null $publicKey Optional API key
* @param string|null $secretKey Optional API secret
* @throws CredentialsException Missing credentials
* @throws ApiException API request failed
*/
public function logsStream(
string $source = 'all',
?string $grep = null,
?callable $callback = null,
?string $publicKey = null,
?string $secretKey = null
): void {
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
$path = "/logs/stream?source=" . urlencode($source);
if ($grep !== null) {
$path .= "&grep=" . urlencode($grep);
}
$timestamp = time();
$signature = $this->signRequest($secretKey, $timestamp, 'GET', $path);
$ch = curl_init(self::API_BASE . $path);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => false,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer {$publicKey}",
"X-Timestamp: {$timestamp}",
"X-Signature: {$signature}",
"Accept: text/event-stream",
],
CURLOPT_WRITEFUNCTION => function($ch, $data) use ($source, $callback) {
$lines = explode("\n", $data);
foreach ($lines as $line) {
if (strpos($line, 'data: ') === 0) {
$json = substr($line, 6);
$entry = @json_decode($json, true);
$entrySource = $entry['source'] ?? $source;
$entryLine = $entry['line'] ?? $json;
if ($callback !== null) {
$callback($entrySource, $entryLine);
} else {
echo "[{$entrySource}] {$entryLine}\n";
}
}
}
return strlen($data);
},
]);
curl_exec($ch);
curl_close($ch);
}
// =========================================================================
// Utility Functions
// =========================================================================
public const SDK_VERSION = '4.2.0';
/**
* Get the SDK version string.
*
* @return string Version string (e.g., "4.2.0")
*/
public static function version(): string {
return self::SDK_VERSION;
}
/**
* Check if the API is healthy and responding.
*
* @return bool true if API is healthy, false otherwise
*/
public static function healthCheck(): bool {
$ch = curl_init(self::API_BASE . '/health');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
curl_close($ch);
if ($error) {
self::$lastError = "Health check failed: {$error}";
return false;
}
if ($httpCode !== 200) {
self::$lastError = "Health check failed: HTTP {$httpCode}";
return false;
}
return true;
}
/**
* Get the last error message.
*
* @return string|null Last error message or null
*/
public static function lastError(): ?string {
return self::$lastError;
}
/**
* Sign a message using HMAC-SHA256.
*
* This is the underlying signing function used for request authentication.
* Exposed for testing and debugging purposes.
*
* @param string $secretKey The secret key for signing
* @param string $message The message to sign
* @return string 64-character lowercase hex string
*/
public static function hmacSign(string $secretKey, string $message): string {
return hash_hmac('sha256', $message, $secretKey);
}
/**
* Get path to ~/.unsandbox directory, creating if necessary.
*
* @return string Path to unsandbox directory
*/
private function getUnsandboxDir(): string {
$home = getenv('HOME') ?: (getenv('USERPROFILE') ?: '');
if (empty($home)) {
$home = posix_getpwuid(posix_getuid())['dir'] ?? '/tmp';
}
$dir = $home . '/.unsandbox';
if (!is_dir($dir)) {
mkdir($dir, 0700, true);
}
return $dir;
}
/**
* Load credentials from a CSV file.
*
* @param string $csvPath Path to CSV file
* @param int $accountIndex Account index (0-based)
* @return array|null [publicKey, secretKey] or null if not found
*/
private function loadCredentialsFromCsv(string $csvPath, int $accountIndex = 0): ?array {
if (!file_exists($csvPath)) {
return null;
}
$handle = fopen($csvPath, 'r');
if ($handle === false) {
return null;
}
$currentIndex = 0;
while (($line = fgets($handle)) !== false) {
$line = trim($line);
if (empty($line) || $line[0] === '#') {
continue;
}
if ($currentIndex === $accountIndex) {
$parts = explode(',', $line);
if (count($parts) >= 2) {
$publicKey = trim($parts[0]);
$secretKey = trim($parts[1]);
fclose($handle);
return [$publicKey, $secretKey];
}
}
$currentIndex++;
}
fclose($handle);
return null;
}
/**
* Resolve credentials from 4-tier priority system.
*
* Priority:
* 1. Method arguments / constructor defaults
* 2. $accountIndex >= 0 → load from accounts.csv row N
* 3. Environment variables (UNSANDBOX_PUBLIC_KEY / UNSANDBOX_SECRET_KEY)
* 4. Default CSV lookup (account 0)
*
* @param string|null $publicKey Public key from method argument
* @param string|null $secretKey Secret key from method argument
* @param int|null $accountIndex Explicit account index (overrides env and default)
* @return array [publicKey, secretKey]
* @throws CredentialsException If no credentials found
*/
private function resolveCredentials(?string $publicKey, ?string $secretKey, ?int $accountIndex = null): array {
// Tier 1: Method arguments
if (!empty($publicKey) && !empty($secretKey)) {
return [$publicKey, $secretKey];
}
// Use default keys if provided to constructor
if (!empty($this->defaultPublicKey) && !empty($this->defaultSecretKey)) {
return [$this->defaultPublicKey, $this->defaultSecretKey];
}
// Tier 2: Explicit account index (--account N flag or constructor accountIndex != 0)
// Resolve the effective account index: explicit arg > UNSANDBOX_ACCOUNT env > $this->accountIndex
$effectiveIndex = null;
if ($accountIndex !== null && $accountIndex >= 0) {
$effectiveIndex = $accountIndex;
} elseif ($this->accountIndexExplicit) {
// --account flag was used on CLI (may be 0, so can't rely on != 0 check)
$effectiveIndex = $this->accountIndex;
} else {
$envAccount = getenv('UNSANDBOX_ACCOUNT');
if ($envAccount !== false && $envAccount !== '') {
$effectiveIndex = (int)$envAccount;
} elseif ($this->accountIndex !== 0) {
$effectiveIndex = $this->accountIndex;
}
}
if ($effectiveIndex !== null) {
$unsandboxDir = $this->getUnsandboxDir();
$creds = $this->loadCredentialsFromCsv($unsandboxDir . '/accounts.csv', $effectiveIndex);
if ($creds !== null) {
return $creds;
}
$creds = $this->loadCredentialsFromCsv('./accounts.csv', $effectiveIndex);
if ($creds !== null) {
return $creds;
}
}
// Tier 3: Environment variables
$envPk = getenv('UNSANDBOX_PUBLIC_KEY');
$envSk = getenv('UNSANDBOX_SECRET_KEY');
if (!empty($envPk) && !empty($envSk)) {
return [$envPk, $envSk];
}
// Tier 4: Default CSV lookup (account 0)
$unsandboxDir = $this->getUnsandboxDir();
$creds = $this->loadCredentialsFromCsv($unsandboxDir . '/accounts.csv', 0);
if ($creds !== null) {
return $creds;
}
$creds = $this->loadCredentialsFromCsv('./accounts.csv', 0);
if ($creds !== null) {
return $creds;
}
throw new CredentialsException(
"No credentials found. Please provide via:\n" .
" 1. Method arguments (publicKey, secretKey)\n" .
" 2. --account N flag or UNSANDBOX_ACCOUNT env var (CSV row N)\n" .
" 3. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY)\n" .
" 4. ~/.unsandbox/accounts.csv or ./accounts.csv (row 0)"
);
}
/**
* Sign a request using HMAC-SHA256.
*
* Message format: "timestamp:METHOD:path:body"
*
* @param string $secretKey Secret key for signing
* @param int $timestamp Unix timestamp
* @param string $method HTTP method
* @param string $path API endpoint path
* @param string|null $body Request body (optional)
* @return string 64-character hex signature
*/
private function signRequest(string $secretKey, int $timestamp, string $method, string $path, ?string $body = null): string {
$bodyStr = $body ?? '';
$message = "{$timestamp}:{$method}:{$path}:{$bodyStr}";
return hash_hmac('sha256', $message, $secretKey);
}
/**
* Make an authenticated HTTP request to the API.
*
* @param string $method HTTP method (GET, POST, DELETE)
* @param string $path API endpoint path
* @param string $publicKey API public key
* @param string $secretKey API secret key
* @param array|null $data Request data (optional)
* @return array Decoded JSON response
* @throws ApiException On network errors or non-2xx response
*/
private function makeRequest(string $method, string $path, string $publicKey, string $secretKey, ?array $data = null): array {
$url = self::API_BASE . $path;
$timestamp = time();
$body = $data !== null ? json_encode($data) : '';
$signature = $this->signRequest($secretKey, $timestamp, $method, $path, $data !== null ? $body : null);
$headers = [
'Authorization: Bearer ' . $publicKey,
'X-Timestamp: ' . $timestamp,
'X-Signature: ' . $signature,
'Content-Type: application/json',
];
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_TIMEOUT, 120);
switch ($method) {
case 'POST':
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
break;
case 'PUT':
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT');
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
break;
case 'PATCH':
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PATCH');
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
break;
case 'DELETE':
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');
break;
case 'GET':
default:
// GET is the default
break;
}
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
curl_close($ch);
if ($response === false) {
throw new ApiException("cURL error: {$error}");
}
$decoded = json_decode($response, true);
if ($decoded === null && json_last_error() !== JSON_ERROR_NONE) {
throw new ApiException("Invalid JSON response: " . json_last_error_msg());
}
if ($httpCode >= 400) {
$errorMessage = $decoded['error'] ?? $decoded['message'] ?? "HTTP {$httpCode}";
throw new ApiException($errorMessage, $httpCode, $decoded);
}
return $decoded;
}
/**
* Make an authenticated HTTP request with sudo OTP challenge handling.
*
* If the server returns 428 (Precondition Required), prompts for OTP
* and retries the request with X-Sudo-OTP and X-Sudo-Challenge headers.
*
* Used for destructive operations: service destroy/unlock, snapshot delete/unlock,
* image delete/unlock.
*
* @param string $method HTTP method (GET, POST, DELETE, etc.)
* @param string $path API endpoint path
* @param string $publicKey API public key
* @param string $secretKey API secret key
* @param array|null $data Request data (optional)
* @return array Decoded JSON response
* @throws ApiException On network errors or non-2xx response
*/
private function makeRequestWithSudo(string $method, string $path, string $publicKey, string $secretKey, ?array $data = null): array {
$url = self::API_BASE . $path;
$timestamp = time();
$body = $data !== null ? json_encode($data) : '';
$signature = $this->signRequest($secretKey, $timestamp, $method, $path, $data !== null ? $body : null);
$headers = [
'Authorization: Bearer ' . $publicKey,
'X-Timestamp: ' . $timestamp,
'X-Signature: ' . $signature,
'Content-Type: application/json',
];
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_TIMEOUT, 120);
switch ($method) {
case 'POST':
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
break;
case 'PUT':
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT');
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
break;
case 'PATCH':
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PATCH');
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
break;
case 'DELETE':
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');
break;
case 'GET':
default:
break;
}
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
curl_close($ch);
if ($response === false) {
throw new ApiException("cURL error: {$error}");
}
// Handle 428 sudo OTP challenge
if ($httpCode === 428) {
$challengeId = '';
$challengeData = json_decode($response, true);
if ($challengeData !== null && isset($challengeData['challenge_id'])) {
$challengeId = $challengeData['challenge_id'];
}
fwrite(STDERR, "\033[33mConfirmation required. Check your email for a one-time code.\033[0m\n");
fwrite(STDERR, "Enter OTP: ");
$otp = '';
if (stream_isatty(STDIN)) {
$otp = trim(fgets(STDIN));
} else {
throw new ApiException("Cannot read OTP in non-interactive mode");
}
if (empty($otp)) {
throw new ApiException("Operation cancelled");
}
// Retry with sudo headers
$retryTimestamp = time();
$retrySignature = $this->signRequest($secretKey, $retryTimestamp, $method, $path, $data !== null ? $body : null);
$retryHeaders = [
'Authorization: Bearer ' . $publicKey,
'X-Timestamp: ' . $retryTimestamp,
'X-Signature: ' . $retrySignature,
'Content-Type: application/json',
'X-Sudo-OTP: ' . $otp,
'X-Sudo-Challenge: ' . $challengeId,
];
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, $retryHeaders);
curl_setopt($ch, CURLOPT_TIMEOUT, 120);
switch ($method) {
case 'POST':
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
break;
case 'PUT':
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT');
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
break;
case 'PATCH':
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PATCH');
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
break;
case 'DELETE':
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');
break;
case 'GET':
default:
break;
}
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
curl_close($ch);
if ($response === false) {
throw new ApiException("cURL error: {$error}");
}
}
$decoded = json_decode($response, true);
if ($decoded === null && json_last_error() !== JSON_ERROR_NONE) {
throw new ApiException("Invalid JSON response: " . json_last_error_msg());
}
if ($httpCode >= 400) {
$errorMessage = $decoded['error'] ?? $decoded['message'] ?? "HTTP {$httpCode}";
throw new ApiException($errorMessage, $httpCode, $decoded);
}
return $decoded;
}
/**
* Make an authenticated HTTP request with raw body content (non-JSON).
*
* @param string $method HTTP method (PUT, POST, etc.)
* @param string $path API endpoint path
* @param string $publicKey API public key
* @param string $secretKey API secret key
* @param string $body Raw request body
* @param string $contentType Content type header (e.g., 'text/plain')
* @return array Decoded JSON response
* @throws ApiException On network errors or non-2xx response
*/
private function makeRequestRaw(string $method, string $path, string $publicKey, string $secretKey, string $body, string $contentType = 'text/plain'): array {
$url = self::API_BASE . $path;
$timestamp = time();
$signature = $this->signRequest($secretKey, $timestamp, $method, $path, $body);
$headers = [
'Authorization: Bearer ' . $publicKey,
'X-Timestamp: ' . $timestamp,
'X-Signature: ' . $signature,
'Content-Type: ' . $contentType,
];
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_TIMEOUT, 120);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
curl_close($ch);
if ($response === false) {
throw new ApiException("cURL error: {$error}");
}
$decoded = json_decode($response, true);
if ($decoded === null && json_last_error() !== JSON_ERROR_NONE) {
throw new ApiException("Invalid JSON response: " . json_last_error_msg());
}
if ($httpCode >= 400) {
$errorMessage = $decoded['error'] ?? $decoded['message'] ?? "HTTP {$httpCode}";
throw new ApiException($errorMessage, $httpCode, $decoded);
}
return $decoded;
}
/**
* Get path to languages cache file.
*
* @return string Path to cache file
*/
private function getLanguagesCachePath(): string {
return $this->getUnsandboxDir() . '/languages.json';
}
/**
* Load languages from cache if valid (< 1 hour old).
*
* @return array|null List of languages or null if cache invalid
*/
private function loadLanguagesCache(): ?array {
$cachePath = $this->getLanguagesCachePath();
if (!file_exists($cachePath)) {
return null;
}
$mtime = filemtime($cachePath);
$ageSeconds = time() - $mtime;
if ($ageSeconds >= self::LANGUAGES_CACHE_TTL) {
return null;
}
$content = file_get_contents($cachePath);
if ($content === false) {
return null;
}
$data = json_decode($content, true);
if ($data === null) {
return null;
}
return $data['languages'] ?? null;
}
/**
* Save languages to cache.
*
* @param array $languages List of languages
*/
private function saveLanguagesCache(array $languages): void {
$cachePath = $this->getLanguagesCachePath();
$data = [
'languages' => $languages,
'timestamp' => time(),
];
file_put_contents($cachePath, json_encode($data));
}
// =========================================================================
// CLI Methods
// =========================================================================
/**
* Main CLI entry point.
*
* @param array $argv Command line arguments
*/
public function cliMain(array $argv): void {
$script = array_shift($argv);
if (empty($argv)) {
$this->cliShowHelp();
exit(0);
}
// Parse global options and determine command
$globalOpts = $this->cliParseGlobalOptions($argv);
$args = $globalOpts['args'];
$opts = $globalOpts['opts'];
// Handle help flag
if ($opts['help']) {
$this->cliShowHelp();
exit(0);
}
// Set credentials from options
if (!empty($opts['public_key'])) {
$this->defaultPublicKey = $opts['public_key'];
}
if (!empty($opts['secret_key'])) {
$this->defaultSecretKey = $opts['secret_key'];
}
if ($opts['account'] !== null) {
$this->accountIndex = $opts['account'];
$this->accountIndexExplicit = true;
}
// Determine the command
if (empty($args)) {
$this->cliShowHelp();
exit(0);
}
$command = $args[0];
try {
switch ($command) {
case 'session':
$this->cliHandleSession(array_slice($args, 1), $opts);
break;
case 'service':
$this->cliHandleService(array_slice($args, 1), $opts);
break;
case 'snapshot':
$this->cliHandleSnapshot(array_slice($args, 1), $opts);
break;
case 'image':
$this->cliHandleImage(array_slice($args, 1), $opts);
break;
case 'key':
$this->cliHandleKey($opts);
break;
case 'languages':
$this->cliHandleLanguages(array_slice($args, 1), $opts);
break;
case '-h':
case '--help':
case 'help':
$this->cliShowHelp();
break;
default:
// Default: execute code file or inline code
$this->cliHandleExecute($args, $opts);
break;
}
} catch (CredentialsException $e) {
$this->cliError("Authentication error: " . $e->getMessage());
exit(3);
} catch (ApiException $e) {
$this->cliError("API error: " . $e->getMessage());
exit(4);
} catch (\Exception $e) {
$this->cliError("Error: " . $e->getMessage());
exit(1);
}
}
/**
* Parse global CLI options.
*
* @param array $argv Arguments to parse
* @return array ['opts' => [...], 'args' => [...]]
*/
private function cliParseGlobalOptions(array $argv): array {
$opts = [
'shell' => null,
'env' => [],
'files' => [],
'files_path' => [],
'artifacts' => false,
'output' => null,
'public_key' => null,
'secret_key' => null,
'network' => 'zerotrust',
'vcpu' => 1,
'yes' => false,
'help' => false,
'account' => null,
];
$args = [];
$i = 0;
while ($i < count($argv)) {
$arg = $argv[$i];
if ($arg === '-s' || $arg === '--shell') {
$i++;
$opts['shell'] = $argv[$i] ?? null;
} elseif ($arg === '-e' || $arg === '--env') {
$i++;
if (isset($argv[$i])) {
$opts['env'][] = $argv[$i];
}
} elseif ($arg === '-f' || $arg === '--file') {
$i++;
if (isset($argv[$i])) {
$opts['files'][] = $argv[$i];
}
} elseif ($arg === '-F' || $arg === '--file-path') {
$i++;
if (isset($argv[$i])) {
$opts['files_path'][] = $argv[$i];
}
} elseif ($arg === '-a' || $arg === '--artifacts') {
$opts['artifacts'] = true;
} elseif ($arg === '-o' || $arg === '--output') {
$i++;
$opts['output'] = $argv[$i] ?? null;
} elseif ($arg === '-p' || $arg === '--public-key') {
$i++;
$opts['public_key'] = $argv[$i] ?? null;
} elseif ($arg === '-k' || $arg === '--secret-key') {
$i++;
$opts['secret_key'] = $argv[$i] ?? null;
} elseif ($arg === '-n' || $arg === '--network') {
$i++;
$opts['network'] = $argv[$i] ?? 'zerotrust';
} elseif ($arg === '-v' || $arg === '--vcpu') {
$i++;
$opts['vcpu'] = (int)($argv[$i] ?? 1);
} elseif ($arg === '-y' || $arg === '--yes') {
$opts['yes'] = true;
} elseif ($arg === '-h' || $arg === '--help') {
$opts['help'] = true;
} elseif ($arg === '--account') {
$i++;
if (isset($argv[$i])) {
$opts['account'] = (int)$argv[$i];
}
} elseif (strpos($arg, '-') !== 0) {
$args[] = $arg;
}
$i++;
}
return ['opts' => $opts, 'args' => $args];
}
/**
* Handle execute command (default command).
*
* @param array $args Positional arguments
* @param array $opts Options
*/
private function cliHandleExecute(array $args, array $opts): void {
$code = null;
$language = null;
// If shell option is set, treat first arg as code
if (!empty($opts['shell'])) {
$language = $opts['shell'];
$code = $args[0] ?? '';
} else {
// First arg is a file
$file = $args[0] ?? '';
if (empty($file)) {
$this->cliError("No source file specified");
exit(2);
}
if (!file_exists($file)) {
$this->cliError("File not found: {$file}");
exit(2);
}
$code = file_get_contents($file);
if ($code === false) {
$this->cliError("Cannot read file: {$file}");
exit(2);
}
$language = self::detectLanguage($file);
if ($language === null) {
$this->cliError("Cannot detect language from file extension: {$file}");
exit(2);
}
}
// Build request options
$requestOpts = [
'network_mode' => $opts['network'],
];
if ($opts['vcpu'] > 1) {
$requestOpts['vcpu'] = $opts['vcpu'];
}
// Handle environment variables
$envVars = [];
foreach ($opts['env'] as $envPair) {
$pos = strpos($envPair, '=');
if ($pos !== false) {
$key = substr($envPair, 0, $pos);
$val = substr($envPair, $pos + 1);
$envVars[$key] = $val;
}
}
// Handle input files
$inputFiles = [];
foreach ($opts['files'] as $filepath) {
if (file_exists($filepath)) {
$content = file_get_contents($filepath);
$inputFiles[] = [
'name' => basename($filepath),
'content' => base64_encode($content),
];
}
}
foreach ($opts['files_path'] as $filepath) {
if (file_exists($filepath)) {
$content = file_get_contents($filepath);
$inputFiles[] = [
'name' => $filepath,
'content' => base64_encode($content),
'preserve_path' => true,
];
}
}
$result = $this->executeCode($language, $code);
// Output result
if (isset($result['stdout'])) {
echo $result['stdout'];
}
if (isset($result['stderr']) && !empty($result['stderr'])) {
fwrite(STDERR, $result['stderr']);
}
echo "---\n";
echo "Exit code: " . ($result['exit_code'] ?? 0) . "\n";
if (isset($result['execution_time'])) {
echo "Execution time: " . $result['execution_time'] . "ms\n";
}
exit($result['exit_code'] ?? 0);
}
/**
* Handle session subcommand.
*
* @param array $args Positional arguments
* @param array $opts Options
*/
private function cliHandleSession(array $args, array $opts): void {
// Parse session-specific options
$sessionOpts = $this->cliParseSessionOptions($args);
if ($sessionOpts['list']) {
$sessions = $this->listSessions();
$this->cliPrintList($sessions, 'session');
return;
}
if ($sessionOpts['attach']) {
$session = $this->getSession($sessionOpts['attach']);
$this->cliPrintSessionInfo($session);
echo "\nNote: Interactive attachment requires WebSocket support\n";
return;
}
if ($sessionOpts['kill']) {
$result = $this->deleteSession($sessionOpts['kill']);
echo "Session terminated: " . $sessionOpts['kill'] . "\n";
return;
}
if ($sessionOpts['freeze']) {
$result = $this->freezeSession($sessionOpts['freeze']);
echo "Session frozen: " . $sessionOpts['freeze'] . "\n";
return;
}
if ($sessionOpts['unfreeze']) {
$result = $this->unfreezeSession($sessionOpts['unfreeze']);
echo "Session unfrozen: " . $sessionOpts['unfreeze'] . "\n";
return;
}
if ($sessionOpts['boost']) {
$result = $this->boostSession($sessionOpts['boost'], null, null, $opts['vcpu'] ?: 2);
echo "Session boosted: " . $sessionOpts['boost'] . "\n";
return;
}
if ($sessionOpts['unboost']) {
$result = $this->unboostSession($sessionOpts['unboost']);
echo "Session unboost: " . $sessionOpts['unboost'] . "\n";
return;
}
if ($sessionOpts['snapshot']) {
$snapshotId = $this->sessionSnapshot(
$sessionOpts['snapshot'],
null,
null,
$sessionOpts['snapshot_name'],
$sessionOpts['hot']
);
echo "Snapshot created: {$snapshotId}\n";
return;
}
// Create new session
$createOpts = [
'network_mode' => $opts['network'],
];
if (!empty($sessionOpts['shell'])) {
$createOpts['shell'] = $sessionOpts['shell'];
}
if ($sessionOpts['tmux']) {
$createOpts['multiplexer'] = 'tmux';
} elseif ($sessionOpts['screen']) {
$createOpts['multiplexer'] = 'screen';
}
if ($opts['vcpu'] > 1) {
$createOpts['vcpu'] = $opts['vcpu'];
}
$session = $this->createSession($sessionOpts['shell'] ?? 'bash', null, null, $createOpts);
$this->cliPrintSessionInfo($session);
}
/**
* Parse session-specific options.
*
* @param array $args Arguments to parse
* @return array Parsed options
*/
private function cliParseSessionOptions(array $args): array {
$opts = [
'list' => false,
'attach' => null,
'kill' => null,
'freeze' => null,
'unfreeze' => null,
'boost' => null,
'unboost' => null,
'snapshot' => null,
'snapshot_name' => null,
'hot' => false,
'tmux' => false,
'screen' => false,
'shell' => null,
'audit' => false,
];
$i = 0;
while ($i < count($args)) {
$arg = $args[$i];
if ($arg === '--list' || $arg === '-l') {
$opts['list'] = true;
} elseif ($arg === '--attach') {
$i++;
$opts['attach'] = $args[$i] ?? null;
} elseif ($arg === '--kill') {
$i++;
$opts['kill'] = $args[$i] ?? null;
} elseif ($arg === '--freeze') {
$i++;
$opts['freeze'] = $args[$i] ?? null;
} elseif ($arg === '--unfreeze') {
$i++;
$opts['unfreeze'] = $args[$i] ?? null;
} elseif ($arg === '--boost') {
$i++;
$opts['boost'] = $args[$i] ?? null;
} elseif ($arg === '--unboost') {
$i++;
$opts['unboost'] = $args[$i] ?? null;
} elseif ($arg === '--snapshot') {
$i++;
$opts['snapshot'] = $args[$i] ?? null;
} elseif ($arg === '--snapshot-name') {
$i++;
$opts['snapshot_name'] = $args[$i] ?? null;
} elseif ($arg === '--hot') {
$opts['hot'] = true;
} elseif ($arg === '--tmux') {
$opts['tmux'] = true;
} elseif ($arg === '--screen') {
$opts['screen'] = true;
} elseif ($arg === '--shell') {
$i++;
$opts['shell'] = $args[$i] ?? null;
} elseif ($arg === '--audit') {
$opts['audit'] = true;
}
$i++;
}
return $opts;
}
/**
* Handle service subcommand.
*
* @param array $args Positional arguments
* @param array $opts Global options
*/
private function cliHandleService(array $args, array $opts): void {
// Check for env subcommand
if (!empty($args) && $args[0] === 'env') {
$this->cliHandleServiceEnv(array_slice($args, 1), $opts);
return;
}
// Parse service-specific options
$serviceOpts = $this->cliParseServiceOptions($args);
if ($serviceOpts['list']) {
$services = $this->listServices();
$this->cliPrintList($services, 'service');
return;
}
if ($serviceOpts['info']) {
$service = $this->getService($serviceOpts['info']);
$this->cliPrintServiceInfo($service);
return;
}
if ($serviceOpts['logs']) {
$logs = $this->getServiceLogs($serviceOpts['logs'], true);
echo $logs['logs'] ?? '';
return;
}
if ($serviceOpts['tail']) {
$logs = $this->getServiceLogs($serviceOpts['tail'], false);
echo $logs['logs'] ?? '';
return;
}
if ($serviceOpts['freeze']) {
$result = $this->freezeService($serviceOpts['freeze']);
echo "Service frozen: " . $serviceOpts['freeze'] . "\n";
return;
}
if ($serviceOpts['unfreeze']) {
$result = $this->unfreezeService($serviceOpts['unfreeze']);
echo "Service unfrozen: " . $serviceOpts['unfreeze'] . "\n";
return;
}
if ($serviceOpts['destroy']) {
if (!$opts['yes']) {
fwrite(STDERR, "Warning: This will permanently destroy the service. Use -y to confirm.\n");
exit(2);
}
$result = $this->deleteService($serviceOpts['destroy']);
echo "Service destroyed: " . $serviceOpts['destroy'] . "\n";
return;
}
if ($serviceOpts['lock']) {
$result = $this->lockService($serviceOpts['lock']);
echo "Service locked: " . $serviceOpts['lock'] . "\n";
return;
}
if ($serviceOpts['unlock']) {
$result = $this->unlockService($serviceOpts['unlock']);
echo "Service unlocked: " . $serviceOpts['unlock'] . "\n";
return;
}
if ($serviceOpts['execute']) {
$result = $this->executeInService($serviceOpts['execute'], $serviceOpts['execute_cmd']);
if (isset($result['stdout'])) {
echo $result['stdout'];
}
if (isset($result['stderr']) && !empty($result['stderr'])) {
fwrite(STDERR, $result['stderr']);
}
exit($result['exit_code'] ?? 0);
}
if ($serviceOpts['redeploy']) {
$redeployOpts = [];
$inputFiles = [];
foreach ($opts['files'] as $filepath) {
if (file_exists($filepath)) {
$content = file_get_contents($filepath);
$inputFiles[] = [
'name' => basename($filepath),
'content' => base64_encode($content),
];
}
}
foreach ($opts['files_path'] as $filepath) {
if (file_exists($filepath)) {
$content = file_get_contents($filepath);
$inputFiles[] = [
'name' => $filepath,
'content' => base64_encode($content),
'preserve_path' => true,
];
}
}
if (!empty($inputFiles)) {
$redeployOpts['input_files'] = $inputFiles;
}
$result = $this->redeployService($serviceOpts['redeploy'], null, null, $redeployOpts);
echo "Service redeploying: " . $serviceOpts['redeploy'] . "\n";
return;
}
if ($serviceOpts['snapshot']) {
$snapshotId = $this->serviceSnapshot($serviceOpts['snapshot'], null, null, $serviceOpts['snapshot_name']);
echo "Snapshot created: {$snapshotId}\n";
return;
}
if ($serviceOpts['resize']) {
$result = $this->updateService($serviceOpts['resize'], ['vcpu' => $opts['vcpu']]);
echo "Service resized: " . $serviceOpts['resize'] . "\n";
return;
}
if ($serviceOpts['set_unfreeze_on_demand']) {
$enabled = in_array($serviceOpts['set_unfreeze_on_demand_enabled'], ['true', '1'], true);
$result = $this->setUnfreezeOnDemand($serviceOpts['set_unfreeze_on_demand'], $enabled);
$status = $enabled ? 'enabled' : 'disabled';
echo "Unfreeze-on-demand {$status} for service: " . $serviceOpts['set_unfreeze_on_demand'] . "\n";
return;
}
// Create new service
if (!empty($serviceOpts['name'])) {
$createOpts = [];
if (!empty($opts['network'])) {
$createOpts['network_mode'] = $opts['network'];
}
if ($opts['vcpu'] > 1) {
$createOpts['vcpu'] = $opts['vcpu'];
}
if (!empty($serviceOpts['type'])) {
$createOpts['service_type'] = $serviceOpts['type'];
}
if (!empty($serviceOpts['domains'])) {
$createOpts['custom_domains'] = explode(',', $serviceOpts['domains']);
}
if ($serviceOpts['unfreeze_on_demand']) {
$createOpts['unfreeze_on_demand'] = true;
}
// Handle input files
$inputFiles = [];
foreach ($opts['files'] as $filepath) {
if (file_exists($filepath)) {
$content = file_get_contents($filepath);
$inputFiles[] = [
'name' => basename($filepath),
'content' => base64_encode($content),
];
}
}
foreach ($opts['files_path'] as $filepath) {
if (file_exists($filepath)) {
$content = file_get_contents($filepath);
$inputFiles[] = [
'name' => $filepath,
'content' => base64_encode($content),
'preserve_path' => true,
];
}
}
if (!empty($inputFiles)) {
$createOpts['input_files'] = $inputFiles;
}
// Handle bootstrap from file
$bootstrap = $serviceOpts['bootstrap'] ?? '';
if (!empty($serviceOpts['bootstrap_file'])) {
if (file_exists($serviceOpts['bootstrap_file'])) {
$bootstrap = file_get_contents($serviceOpts['bootstrap_file']);
} else {
$this->cliError("Bootstrap file not found: " . $serviceOpts['bootstrap_file']);
exit(2);
}
}
// Handle env file
if (!empty($serviceOpts['env_file'])) {
if (file_exists($serviceOpts['env_file'])) {
// Will be applied after creation
}
}
$service = $this->createService(
$serviceOpts['name'],
$serviceOpts['ports'] ?? '80',
$bootstrap,
null,
null,
$createOpts
);
$this->cliPrintServiceInfo($service);
return;
}
$this->cliError("No action specified for service command. Use --list, --info, --name, etc.");
exit(2);
}
/**
* Parse service-specific options.
*
* @param array $args Arguments to parse
* @return array Parsed options
*/
private function cliParseServiceOptions(array $args): array {
$opts = [
'list' => false,
'name' => null,
'ports' => null,
'domains' => null,
'type' => null,
'bootstrap' => null,
'bootstrap_file' => null,
'env_file' => null,
'info' => null,
'logs' => null,
'tail' => null,
'freeze' => null,
'unfreeze' => null,
'destroy' => null,
'lock' => null,
'unlock' => null,
'resize' => null,
'redeploy' => null,
'execute' => null,
'execute_cmd' => null,
'snapshot' => null,
'snapshot_name' => null,
'unfreeze_on_demand' => false,
'set_unfreeze_on_demand' => null,
'set_unfreeze_on_demand_enabled' => null,
];
$i = 0;
while ($i < count($args)) {
$arg = $args[$i];
if ($arg === '--list' || $arg === '-l') {
$opts['list'] = true;
} elseif ($arg === '--name') {
$i++;
$opts['name'] = $args[$i] ?? null;
} elseif ($arg === '--ports') {
$i++;
$opts['ports'] = $args[$i] ?? null;
} elseif ($arg === '--domains') {
$i++;
$opts['domains'] = $args[$i] ?? null;
} elseif ($arg === '--type') {
$i++;
$opts['type'] = $args[$i] ?? null;
} elseif ($arg === '--bootstrap') {
$i++;
$opts['bootstrap'] = $args[$i] ?? null;
} elseif ($arg === '--bootstrap-file') {
$i++;
$opts['bootstrap_file'] = $args[$i] ?? null;
} elseif ($arg === '--env-file') {
$i++;
$opts['env_file'] = $args[$i] ?? null;
} elseif ($arg === '--info') {
$i++;
$opts['info'] = $args[$i] ?? null;
} elseif ($arg === '--logs') {
$i++;
$opts['logs'] = $args[$i] ?? null;
} elseif ($arg === '--tail') {
$i++;
$opts['tail'] = $args[$i] ?? null;
} elseif ($arg === '--freeze') {
$i++;
$opts['freeze'] = $args[$i] ?? null;
} elseif ($arg === '--unfreeze') {
$i++;
$opts['unfreeze'] = $args[$i] ?? null;
} elseif ($arg === '--destroy') {
$i++;
$opts['destroy'] = $args[$i] ?? null;
} elseif ($arg === '--lock') {
$i++;
$opts['lock'] = $args[$i] ?? null;
} elseif ($arg === '--unlock') {
$i++;
$opts['unlock'] = $args[$i] ?? null;
} elseif ($arg === '--resize') {
$i++;
$opts['resize'] = $args[$i] ?? null;
} elseif ($arg === '--redeploy') {
$i++;
$opts['redeploy'] = $args[$i] ?? null;
} elseif ($arg === '--execute') {
$i++;
$opts['execute'] = $args[$i] ?? null;
// Next argument is the command
$i++;
$opts['execute_cmd'] = $args[$i] ?? '';
} elseif ($arg === '--snapshot') {
$i++;
$opts['snapshot'] = $args[$i] ?? null;
} elseif ($arg === '--snapshot-name') {
$i++;
$opts['snapshot_name'] = $args[$i] ?? null;
} elseif ($arg === '--unfreeze-on-demand') {
$opts['unfreeze_on_demand'] = true;
} elseif ($arg === '--set-unfreeze-on-demand') {
$i++;
$opts['set_unfreeze_on_demand'] = $args[$i] ?? null;
$i++;
$opts['set_unfreeze_on_demand_enabled'] = $args[$i] ?? null;
}
$i++;
}
return $opts;
}
/**
* Handle service env subcommand.
*
* @param array $args Positional arguments
* @param array $opts Global options
*/
private function cliHandleServiceEnv(array $args, array $opts): void {
if (empty($args)) {
$this->cliError("Usage: service env <status|set|export|delete> <service_id>");
exit(2);
}
$action = $args[0];
$serviceId = $args[1] ?? null;
if (empty($serviceId)) {
$this->cliError("Service ID required");
exit(2);
}
switch ($action) {
case 'status':
$result = $this->getServiceEnv($serviceId);
echo "Has vault: " . ($result['has_vault'] ? 'yes' : 'no') . "\n";
if (isset($result['count'])) {
echo "Variables: " . $result['count'] . "\n";
}
if (isset($result['updated_at'])) {
echo "Updated: " . $result['updated_at'] . "\n";
}
break;
case 'set':
// Read from stdin or --env-file
$envContent = '';
if (stream_isatty(STDIN)) {
$this->cliError("Provide env content via stdin or --env-file");
exit(2);
} else {
$envContent = file_get_contents('php://stdin');
}
$result = $this->setServiceEnv($serviceId, $envContent);
echo "Environment vault updated\n";
break;
case 'export':
$result = $this->exportServiceEnv($serviceId);
echo $result['env'] ?? '';
break;
case 'delete':
if (!$opts['yes']) {
fwrite(STDERR, "Warning: This will delete the environment vault. Use -y to confirm.\n");
exit(2);
}
$result = $this->deleteServiceEnv($serviceId);
echo "Environment vault deleted\n";
break;
default:
$this->cliError("Unknown env action: {$action}");
exit(2);
}
}
/**
* Handle snapshot subcommand.
*
* @param array $args Positional arguments
* @param array $opts Global options
*/
private function cliHandleSnapshot(array $args, array $opts): void {
$snapshotOpts = $this->cliParseSnapshotOptions($args);
if ($snapshotOpts['list']) {
$snapshots = $this->listSnapshots();
$this->cliPrintList($snapshots, 'snapshot');
return;
}
if ($snapshotOpts['info']) {
// Get snapshot info via restore endpoint without actually restoring
$snapshots = $this->listSnapshots();
foreach ($snapshots as $snapshot) {
if (($snapshot['snapshot_id'] ?? $snapshot['id'] ?? '') === $snapshotOpts['info']) {
$this->cliPrintSnapshotInfo($snapshot);
return;
}
}
$this->cliError("Snapshot not found: " . $snapshotOpts['info']);
exit(4);
}
if ($snapshotOpts['delete']) {
if (!$opts['yes']) {
fwrite(STDERR, "Warning: This will permanently delete the snapshot. Use -y to confirm.\n");
exit(2);
}
$result = $this->deleteSnapshot($snapshotOpts['delete']);
echo "Snapshot deleted: " . $snapshotOpts['delete'] . "\n";
return;
}
if ($snapshotOpts['lock']) {
$result = $this->lockSnapshot($snapshotOpts['lock']);
echo "Snapshot locked: " . $snapshotOpts['lock'] . "\n";
return;
}
if ($snapshotOpts['unlock']) {
$result = $this->unlockSnapshot($snapshotOpts['unlock']);
echo "Snapshot unlocked: " . $snapshotOpts['unlock'] . "\n";
return;
}
if ($snapshotOpts['clone']) {
$cloneOpts = [];
if (!empty($snapshotOpts['type'])) {
$cloneOpts['type'] = $snapshotOpts['type'];
}
if (!empty($snapshotOpts['shell'])) {
$cloneOpts['shell'] = $snapshotOpts['shell'];
}
if (!empty($snapshotOpts['ports'])) {
$cloneOpts['ports'] = explode(',', $snapshotOpts['ports']);
}
$result = $this->cloneSnapshot($snapshotOpts['clone'], $snapshotOpts['name'], null, null, $cloneOpts);
echo "Snapshot cloned:\n";
echo json_encode($result, JSON_PRETTY_PRINT) . "\n";
return;
}
$this->cliError("No action specified for snapshot command. Use --list, --info, --delete, --clone, etc.");
exit(2);
}
/**
* Parse snapshot-specific options.
*
* @param array $args Arguments to parse
* @return array Parsed options
*/
private function cliParseSnapshotOptions(array $args): array {
$opts = [
'list' => false,
'info' => null,
'delete' => null,
'lock' => null,
'unlock' => null,
'clone' => null,
'type' => null,
'name' => null,
'shell' => null,
'ports' => null,
];
$i = 0;
while ($i < count($args)) {
$arg = $args[$i];
if ($arg === '--list' || $arg === '-l') {
$opts['list'] = true;
} elseif ($arg === '--info') {
$i++;
$opts['info'] = $args[$i] ?? null;
} elseif ($arg === '--delete') {
$i++;
$opts['delete'] = $args[$i] ?? null;
} elseif ($arg === '--lock') {
$i++;
$opts['lock'] = $args[$i] ?? null;
} elseif ($arg === '--unlock') {
$i++;
$opts['unlock'] = $args[$i] ?? null;
} elseif ($arg === '--clone') {
$i++;
$opts['clone'] = $args[$i] ?? null;
} elseif ($arg === '--type') {
$i++;
$opts['type'] = $args[$i] ?? null;
} elseif ($arg === '--name') {
$i++;
$opts['name'] = $args[$i] ?? null;
} elseif ($arg === '--shell') {
$i++;
$opts['shell'] = $args[$i] ?? null;
} elseif ($arg === '--ports') {
$i++;
$opts['ports'] = $args[$i] ?? null;
}
$i++;
}
return $opts;
}
/**
* Handle image subcommand.
*
* @param array $args Command arguments
* @param array $opts Global options
*/
private function cliHandleImage(array $args, array $opts): void {
$imageOpts = $this->cliParseImageOptions($args);
if ($imageOpts['list']) {
$images = $this->listImages();
$this->cliPrintImageList($images);
return;
}
if ($imageOpts['info']) {
$image = $this->getImage($imageOpts['info']);
$this->cliPrintImageInfo($image);
return;
}
if ($imageOpts['delete']) {
if (!$opts['yes']) {
fwrite(STDERR, "Warning: This will permanently delete the image. Use -y to confirm.\n");
exit(2);
}
$result = $this->deleteImage($imageOpts['delete']);
echo "Image deleted: " . $imageOpts['delete'] . "\n";
return;
}
if ($imageOpts['lock']) {
$result = $this->lockImage($imageOpts['lock']);
echo "Image locked: " . $imageOpts['lock'] . "\n";
return;
}
if ($imageOpts['unlock']) {
$result = $this->unlockImage($imageOpts['unlock']);
echo "Image unlocked: " . $imageOpts['unlock'] . "\n";
return;
}
if ($imageOpts['publish']) {
if (empty($imageOpts['sourceType'])) {
$this->cliError("--source-type required for --publish");
exit(2);
}
$result = $this->imagePublish($imageOpts['sourceType'], $imageOpts['publish'], $imageOpts['name']);
$imageId = $result['image_id'] ?? $result['id'] ?? '';
echo "Image published: {$imageId}\n";
return;
}
if ($imageOpts['visibility'] && $imageOpts['visibilityMode']) {
if (!in_array($imageOpts['visibilityMode'], ['private', 'unlisted', 'public'])) {
$this->cliError("visibility must be private, unlisted, or public");
exit(2);
}
$result = $this->setImageVisibility($imageOpts['visibility'], $imageOpts['visibilityMode']);
echo "Image {$imageOpts['visibility']} visibility set to {$imageOpts['visibilityMode']}\n";
return;
}
if ($imageOpts['spawn']) {
if (empty($imageOpts['name'])) {
$this->cliError("--name required for --spawn");
exit(2);
}
$ports = null;
if (!empty($imageOpts['ports'])) {
$ports = array_map('intval', explode(',', $imageOpts['ports']));
}
$result = $this->spawnFromImage($imageOpts['spawn'], $imageOpts['name'], $ports);
$serviceId = $result['service_id'] ?? $result['id'] ?? '';
echo "Service spawned: {$serviceId}\n";
return;
}
if ($imageOpts['clone']) {
$result = $this->cloneImage($imageOpts['clone'], $imageOpts['name']);
$imageId = $result['image_id'] ?? $result['id'] ?? '';
echo "Image cloned: {$imageId}\n";
return;
}
$this->cliError("No action specified for image command. Use --list, --info, --delete, --publish, --spawn, --clone, etc.");
exit(2);
}
/**
* Parse image-specific options.
*
* @param array $args Arguments to parse
* @return array Parsed options
*/
private function cliParseImageOptions(array $args): array {
$opts = [
'list' => false,
'info' => null,
'delete' => null,
'lock' => null,
'unlock' => null,
'publish' => null,
'sourceType' => null,
'visibility' => null,
'visibilityMode' => null,
'spawn' => null,
'clone' => null,
'name' => null,
'ports' => null,
];
$i = 0;
while ($i < count($args)) {
$arg = $args[$i];
if ($arg === '--list' || $arg === '-l') {
$opts['list'] = true;
} elseif ($arg === '--info') {
$i++;
$opts['info'] = $args[$i] ?? null;
} elseif ($arg === '--delete') {
$i++;
$opts['delete'] = $args[$i] ?? null;
} elseif ($arg === '--lock') {
$i++;
$opts['lock'] = $args[$i] ?? null;
} elseif ($arg === '--unlock') {
$i++;
$opts['unlock'] = $args[$i] ?? null;
} elseif ($arg === '--publish') {
$i++;
$opts['publish'] = $args[$i] ?? null;
} elseif ($arg === '--source-type') {
$i++;
$opts['sourceType'] = $args[$i] ?? null;
} elseif ($arg === '--visibility') {
$i++;
$opts['visibility'] = $args[$i] ?? null;
// Check if next arg is the mode (not a flag)
if (isset($args[$i + 1]) && !str_starts_with($args[$i + 1], '-')) {
$i++;
$opts['visibilityMode'] = $args[$i];
}
} elseif ($arg === '--spawn') {
$i++;
$opts['spawn'] = $args[$i] ?? null;
} elseif ($arg === '--clone') {
$i++;
$opts['clone'] = $args[$i] ?? null;
} elseif ($arg === '--name') {
$i++;
$opts['name'] = $args[$i] ?? null;
} elseif ($arg === '--ports') {
$i++;
$opts['ports'] = $args[$i] ?? null;
}
$i++;
}
return $opts;
}
/**
* Print images list in tabular format.
*
* @param array $images Images to print
*/
private function cliPrintImageList(array $images): void {
if (empty($images)) {
echo "No images found.\n";
return;
}
printf("%-38s %-20s %-10s %-10s %s\n", 'ID', 'NAME', 'VISIBILITY', 'SOURCE', 'CREATED');
foreach ($images as $image) {
printf(
"%-38s %-20s %-10s %-10s %s\n",
substr($image['image_id'] ?? $image['id'] ?? '-', 0, 38),
substr($image['name'] ?? '-', 0, 20),
substr($image['visibility'] ?? 'private', 0, 10),
substr($image['source_type'] ?? '-', 0, 10),
$image['created_at'] ?? '-'
);
}
}
/**
* Print image info.
*
* @param array $image Image data
*/
private function cliPrintImageInfo(array $image): void {
echo "ID: " . ($image['image_id'] ?? $image['id'] ?? '-') . "\n";
if (!empty($image['name'])) {
echo "Name: " . $image['name'] . "\n";
}
if (!empty($image['visibility'])) {
echo "Visibility: " . $image['visibility'] . "\n";
}
if (!empty($image['source_type'])) {
echo "Source Type: " . $image['source_type'] . "\n";
}
if (!empty($image['source_id'])) {
echo "Source ID: " . $image['source_id'] . "\n";
}
if (!empty($image['size'])) {
echo "Size: " . $image['size'] . "\n";
}
if (isset($image['locked'])) {
echo "Locked: " . ($image['locked'] ? 'yes' : 'no') . "\n";
}
if (!empty($image['created_at'])) {
echo "Created: " . $image['created_at'] . "\n";
}
}
/**
* Handle key subcommand.
*
* @param array $opts Global options
*/
private function cliHandleKey(array $opts): void {
$result = $this->validateKeys();
echo "API Key Valid: " . ($result['valid'] ? 'yes' : 'no') . "\n";
if (isset($result['account_id'])) {
echo "Account ID: " . $result['account_id'] . "\n";
}
if (isset($result['email'])) {
echo "Email: " . $result['email'] . "\n";
}
}
/**
* Handle languages command.
*
* @param array $args Command arguments
* @param array $opts Global options
*/
private function cliHandleLanguages(array $args, array $opts): void {
// Check for --json flag
$jsonOutput = in_array('--json', $args);
$languages = $this->getLanguages();
if ($jsonOutput) {
// Output as JSON array
echo json_encode($languages) . "\n";
} else {
// Output one language per line (pipe-friendly)
foreach ($languages as $lang) {
echo $lang . "\n";
}
}
}
/**
* Print list of resources in tabular format.
*
* @param array $items Items to print
* @param string $type Resource type (session, service, snapshot)
*/
private function cliPrintList(array $items, string $type): void {
if (empty($items)) {
echo "No {$type}s found.\n";
return;
}
// Print header
printf("%-40s %-20s %-12s %s\n", 'ID', 'NAME', 'STATUS', 'CREATED');
echo str_repeat('-', 90) . "\n";
foreach ($items as $item) {
$id = $item["{$type}_id"] ?? $item['id'] ?? 'N/A';
$name = $item['name'] ?? 'N/A';
$status = $item['status'] ?? 'N/A';
$created = $item['created_at'] ?? $item['created'] ?? 'N/A';
printf("%-40s %-20s %-12s %s\n", $id, $name, $status, $created);
}
}
/**
* Print session information.
*
* @param array $session Session data
*/
private function cliPrintSessionInfo(array $session): void {
echo "Session ID: " . ($session['session_id'] ?? $session['id'] ?? 'N/A') . "\n";
if (isset($session['container_name'])) {
echo "Container: " . $session['container_name'] . "\n";
}
if (isset($session['status'])) {
echo "Status: " . $session['status'] . "\n";
}
if (isset($session['shell'])) {
echo "Shell: " . $session['shell'] . "\n";
}
if (isset($session['network_mode'])) {
echo "Network: " . $session['network_mode'] . "\n";
}
if (isset($session['websocket_url'])) {
echo "WebSocket URL: " . $session['websocket_url'] . "\n";
}
}
/**
* Print service information.
*
* @param array $service Service data
*/
private function cliPrintServiceInfo(array $service): void {
echo "Service ID: " . ($service['service_id'] ?? $service['id'] ?? 'N/A') . "\n";
if (isset($service['name'])) {
echo "Name: " . $service['name'] . "\n";
}
if (isset($service['status'])) {
echo "Status: " . $service['status'] . "\n";
}
if (isset($service['ports'])) {
echo "Ports: " . (is_array($service['ports']) ? implode(', ', $service['ports']) : $service['ports']) . "\n";
}
if (isset($service['url'])) {
echo "URL: " . $service['url'] . "\n";
}
if (isset($service['network_mode'])) {
echo "Network: " . $service['network_mode'] . "\n";
}
}
/**
* Print snapshot information.
*
* @param array $snapshot Snapshot data
*/
private function cliPrintSnapshotInfo(array $snapshot): void {
echo "Snapshot ID: " . ($snapshot['snapshot_id'] ?? $snapshot['id'] ?? 'N/A') . "\n";
if (isset($snapshot['name'])) {
echo "Name: " . $snapshot['name'] . "\n";
}
if (isset($snapshot['type'])) {
echo "Type: " . $snapshot['type'] . "\n";
}
if (isset($snapshot['status'])) {
echo "Status: " . $snapshot['status'] . "\n";
}
if (isset($snapshot['size'])) {
echo "Size: " . $snapshot['size'] . "\n";
}
if (isset($snapshot['locked'])) {
echo "Locked: " . ($snapshot['locked'] ? 'yes' : 'no') . "\n";
}
if (isset($snapshot['created_at'])) {
echo "Created: " . $snapshot['created_at'] . "\n";
}
}
/**
* Print error message to stderr.
*
* @param string $message Error message
*/
private function cliError(string $message): void {
fwrite(STDERR, "Error: {$message}\n");
}
/**
* Show CLI help.
*/
private function cliShowHelp(): void {
$help = <<<HELP
Unsandbox PHP CLI
USAGE:
php un.php [options] <source_file>
php un.php -s <language> '<code>'
php un.php session [options]
php un.php service [options]
php un.php snapshot [options]
php un.php key
php un.php languages [--json]
GLOBAL OPTIONS:
-s, --shell LANG Language for inline code execution
-e, --env KEY=VAL Set environment variable (can be repeated)
-f, --file FILE Add input file to /tmp/ (can be repeated)
-F, --file-path FILE Add input file preserving path (can be repeated)
-a, --artifacts Return compiled artifacts
-o, --output DIR Output directory for artifacts
-p, --public-key KEY API public key
-k, --secret-key KEY API secret key
-n, --network MODE Network mode: zerotrust (default) or semitrusted
-v, --vcpu N vCPU count (1-8)
-y, --yes Skip confirmation prompts
-h, --help Show this help
COMMANDS:
<file> Execute code file (default command)
session Manage interactive sessions
service Manage persistent services
snapshot Manage snapshots
key Validate API key
languages [--json] List available languages
SESSION OPTIONS:
--list, -l List active sessions
--attach ID Reconnect to session
--kill ID Terminate session
--freeze ID Pause session
--unfreeze ID Resume session
--boost ID Boost session resources
--unboost ID Remove session boost
--snapshot ID Create session snapshot
--snapshot-name NAME Name for snapshot
--hot Hot snapshot (no freeze)
--tmux Use tmux for persistence
--screen Use screen for persistence
--shell SHELL Shell/REPL to use
SERVICE OPTIONS:
--list, -l List all services
--name NAME Create service with name
--ports PORTS Ports to expose (comma-separated)
--domains DOMAINS Custom domains (comma-separated)
--type TYPE Service type (minecraft, tcp, udp)
--bootstrap CMD Bootstrap command
--bootstrap-file FILE Bootstrap from file
--env-file FILE Load environment from file
--info ID Get service details
--logs ID Get all service logs
--tail ID Get last 9000 lines of logs
--freeze ID Pause service
--unfreeze ID Resume service
--destroy ID Delete service (requires -y)
--lock ID Prevent service deletion
--unlock ID Allow service deletion
--resize ID Resize service (with --vcpu)
--redeploy ID Re-run bootstrap (supports -f/-F for input files)
--execute ID 'cmd' Execute command in service
--snapshot ID Create service snapshot
SERVICE ENV SUBCOMMAND:
service env status ID Show vault status
service env set ID Set vault (stdin or --env-file)
service env export ID Export vault to stdout
service env delete ID Delete vault (requires -y)
SNAPSHOT OPTIONS:
--list, -l List all snapshots
--info ID Get snapshot details
--delete ID Delete snapshot (requires -y)
--lock ID Prevent snapshot deletion
--unlock ID Allow snapshot deletion
--clone ID Clone snapshot
--type TYPE Clone type: session or service
--name NAME Name for cloned resource
--shell SHELL Shell for cloned session
--ports PORTS Ports for cloned service
EXAMPLES:
php un.php script.py
php un.php -s bash 'echo hello'
php un.php -n semitrusted crawler.py
php un.php session --shell python3 --tmux
php un.php session --list
php un.php service --name myapp --ports 80 --bootstrap 'python -m http.server 80'
php un.php service --execute <id> 'ls -la'
php un.php snapshot --list
php un.php key
HELP;
echo $help;
}
}
// CLI entry point
if (php_sapi_name() === 'cli' && basename(__FILE__) === basename($argv[0])) {
$client = new Unsandbox();
$client->cliMain($argv);
}
Esclarecimentos de documentação
Dependências
C Binary (un1) — requer libcurl e libwebsockets:
sudo apt install build-essential libcurl4-openssl-dev libwebsockets-dev
wget unsandbox.com/downloads/un.c && gcc -O2 -o un un.c -lcurl -lwebsockets
Implementações SDK — a maioria usa apenas stdlib (Ruby, JS, Go, etc). Alguns requerem dependências mínimas:
pip install requests # Python
Executar Código
Executar um script
./un hello.py
./un app.js
./un main.rs
Com variáveis de ambiente
./un -e DEBUG=1 -e NAME=World script.py
Com arquivos de entrada (teletransportar arquivos para sandbox)
./un -f data.csv -f config.json process.py
Obter binário compilado
./un -a -o ./bin main.c
Sessões interativas
Iniciar uma sessão de shell
# Default bash shell
./un session
# Choose your shell
./un session --shell zsh
./un session --shell fish
# Jump into a REPL
./un session --shell python3
./un session --shell node
./un session --shell julia
Sessão com acesso à rede
./un session -n semitrusted
Auditoria de sessão (gravação completa do terminal)
# Record everything (including vim, interactive programs)
./un session --audit -o ./logs
# Replay session later
zcat session.log*.gz | less -R
Coletar artefatos da sessão
# Files in /tmp/artifacts/ are collected on exit
./un session -a -o ./outputs
Persistência de sessão (tmux/screen)
# Default: session terminates on disconnect (clean exit)
./un session
# With tmux: session persists, can reconnect later
./un session --tmux
# Press Ctrl+b then d to detach
# With screen: alternative multiplexer
./un session --screen
# Press Ctrl+a then d to detach
Listar Trabalhos Ativos
./un session --list
# Output:
# Active sessions: 2
#
# SESSION ID CONTAINER SHELL TTL STATUS
# abc123... unsb-vm-12345 python3 45m30s active
# def456... unsb-vm-67890 bash 1h2m active
Reconectar à sessão existente
# Reconnect by container name (requires --tmux or --screen)
./un session --attach unsb-vm-12345
# Use exit to terminate session, or detach to keep it running
Encerrar uma sessão
./un session --kill unsb-vm-12345
Shells e REPLs disponíveis
Shells: bash, dash, sh, zsh, fish, ksh, tcsh, csh, elvish, xonsh, ash
REPLs: python3, bpython, ipython # Python
node # JavaScript
ruby, irb # Ruby
lua # Lua
php # PHP
perl # Perl
guile, scheme # Scheme
ghci # Haskell
erl, iex # Erlang/Elixir
sbcl, clisp # Common Lisp
r # R
julia # Julia
clojure # Clojure
Gerenciamento de Chave API
Verificar Status do Pagamento
# Check if your API key is valid
./un key
# Output:
# Valid: key expires in 30 days
Estender Chave Expirada
# Open the portal to extend an expired key
./un key --extend
# This opens the unsandbox.com portal where you can
# add more credits to extend your key's expiration
Autenticação
As credenciais são carregadas em ordem de prioridade (maior primeiro):
# 1. CLI flags (highest priority)
./un -p unsb-pk-xxxx -k unsb-sk-xxxxx script.py
# 2. Environment variables
export UNSANDBOX_PUBLIC_KEY=unsb-pk-xxxx-xxxx-xxxx-xxxx
export UNSANDBOX_SECRET_KEY=unsb-sk-xxxxx-xxxxx-xxxxx-xxxxx
./un script.py
# 3. Config file (lowest priority)
# ~/.unsandbox/accounts.csv format: public_key,secret_key
mkdir -p ~/.unsandbox
echo "unsb-pk-xxxx-xxxx-xxxx-xxxx,unsb-sk-xxxxx-xxxxx-xxxxx-xxxxx" > ~/.unsandbox/accounts.csv
./un script.py
As requisições são assinadas com HMAC-SHA256. O token bearer contém apenas a chave pública; a chave secreta calcula a assinatura (nunca é transmitida).
Escalonamento de Recursos
Definir Quantidade de vCPU
# Default: 1 vCPU, 2GB RAM
./un script.py
# Scale up: 4 vCPUs, 8GB RAM
./un -v 4 script.py
# Maximum: 8 vCPUs, 16GB RAM
./un --vcpu 8 heavy_compute.py
Reforço de Sessão Ao Vivo
# Boost a running session to 2 vCPU, 4GB RAM
./un session --boost sandbox-abc
# Boost to specific vCPU count (4 vCPU, 8GB RAM)
./un session --boost sandbox-abc --boost-vcpu 4
# Return to base resources (1 vCPU, 2GB RAM)
./un session --unboost sandbox-abc
Congelar/Descongelar Sessão
Congelar e Descongelar Sessões
# Freeze a session (stop billing, preserve state)
./un session --freeze sandbox-abc
# Unfreeze a frozen session
./un session --unfreeze sandbox-abc
# Note: Requires --tmux or --screen for persistence
Serviços Persistentes
Criar um Serviço
# Web server with ports
./un service --name web --ports 80,443 --bootstrap "python -m http.server 80"
# With custom domains
./un service --name blog --ports 8000 --domains blog.example.com
# Game server with SRV records
./un service --name mc --type minecraft --bootstrap ./setup.sh
# Deploy app tarball with bootstrap script
./un service --name app --ports 8000 -f app.tar.gz --bootstrap-file ./setup.sh
# setup.sh: cd /tmp && tar xzf app.tar.gz && ./app/start.sh
Gerenciar Serviços
# List all services
./un service --list
# Get service details
./un service --info abc123
# View bootstrap logs
./un service --logs abc123
./un service --tail abc123 # last 9000 lines
# Execute command in running service
./un service --execute abc123 'journalctl -u myapp -n 50'
# Dump bootstrap script (for migrations)
./un service --dump-bootstrap abc123
./un service --dump-bootstrap abc123 backup.sh
# Freeze/unfreeze service
./un service --freeze abc123
./un service --unfreeze abc123
# Service settings (auto-wake, freeze page display)
./un service --auto-unfreeze abc123 # enable auto-wake on HTTP
./un service --no-auto-unfreeze abc123 # disable auto-wake
./un service --show-freeze-page abc123 # show HTML payment page (default)
./un service --no-show-freeze-page abc123 # return JSON error instead
# Redeploy with new bootstrap
./un service --redeploy abc123 --bootstrap ./new-setup.sh
# Destroy service
./un service --destroy abc123
Snapshots
Listar Snapshots
./un snapshot --list
# Output:
# Snapshots: 3
#
# SNAPSHOT ID NAME SOURCE SIZE CREATED
# unsb-snapshot-a1b2-c3d4-e5f6-g7h8 before-upgrade session 512 MB 2h ago
# unsb-snapshot-i9j0-k1l2-m3n4-o5p6 stable-v1.0 service 1.2 GB 1d ago
Criar Snapshot da Sessão
# Snapshot with name
./un session --snapshot unsb-vm-12345 --name "before upgrade"
# Quick snapshot (auto-generated name)
./un session --snapshot unsb-vm-12345
Criar Snapshot do Serviço
# Standard snapshot (pauses container briefly)
./un service --snapshot unsb-service-abc123 --name "stable v1.0"
# Hot snapshot (no pause, may be inconsistent)
./un service --snapshot unsb-service-abc123 --hot
Restaurar a partir do Snapshot
# Restore session from snapshot
./un session --restore unsb-snapshot-a1b2-c3d4-e5f6-g7h8
# Restore service from snapshot
./un service --restore unsb-snapshot-i9j0-k1l2-m3n4-o5p6
Excluir Snapshot
./un snapshot --delete unsb-snapshot-a1b2-c3d4-e5f6-g7h8
Imagens
Imagens são imagens de container independentes e transferíveis que sobrevivem à exclusão do container. Diferente dos snapshots (que permanecem com seu container), imagens podem ser compartilhadas com outros usuários, transferidas entre chaves de API ou tornadas públicas no marketplace.
Listar Imagens
# List all images (owned + shared + public)
./un image --list
# List only your images
./un image --list owned
# List images shared with you
./un image --list shared
# List public marketplace images
./un image --list public
# Get image details
./un image --info unsb-image-xxxx-xxxx-xxxx-xxxx
Publicar Imagens
# Publish from a stopped or frozen service
./un image --publish-service unsb-service-abc123 \
--name "My App v1.0" --description "Production snapshot"
# Publish from a snapshot
./un image --publish-snapshot unsb-snapshot-xxxx-xxxx-xxxx-xxxx \
--name "Stable Release"
# Note: Cannot publish from running containers - stop or freeze first
Criar Serviços a partir de Imagens
# Spawn a new service from an image
./un image --spawn unsb-image-xxxx-xxxx-xxxx-xxxx \
--name new-service --ports 80,443
# Clone an image (creates a copy you own)
./un image --clone unsb-image-xxxx-xxxx-xxxx-xxxx
Proteção de Imagem
# Lock image to prevent accidental deletion
./un image --lock unsb-image-xxxx-xxxx-xxxx-xxxx
# Unlock image to allow deletion
./un image --unlock unsb-image-xxxx-xxxx-xxxx-xxxx
# Delete image (must be unlocked)
./un image --delete unsb-image-xxxx-xxxx-xxxx-xxxx
Visibilidade e Compartilhamento
# Set visibility level
./un image --visibility unsb-image-xxxx-xxxx-xxxx-xxxx private # owner only (default)
./un image --visibility unsb-image-xxxx-xxxx-xxxx-xxxx unlisted # can be shared
./un image --visibility unsb-image-xxxx-xxxx-xxxx-xxxx public # marketplace
# Share with specific user
./un image --grant unsb-image-xxxx-xxxx-xxxx-xxxx \
--key unsb-pk-friend-friend-friend-friend
# Revoke access
./un image --revoke unsb-image-xxxx-xxxx-xxxx-xxxx \
--key unsb-pk-friend-friend-friend-friend
# List who has access
./un image --trusted unsb-image-xxxx-xxxx-xxxx-xxxx
Transferir Propriedade
# Transfer image to another API key
./un image --transfer unsb-image-xxxx-xxxx-xxxx-xxxx \
--to unsb-pk-newowner-newowner-newowner-newowner
Referência de uso
Usage: ./un [options] <source_file>
./un session [options]
./un service [options]
./un snapshot [options]
./un image [options]
./un key
Commands:
(default) Execute source file in sandbox
session Open interactive shell/REPL session
service Manage persistent services
snapshot Manage container snapshots
image Manage container images (publish, share, transfer)
key Check API key validity and expiration
Options:
-e KEY=VALUE Set environment variable (can use multiple times)
-f FILE Add input file (can use multiple times)
-a Return and save artifacts from /tmp/artifacts/
-o DIR Output directory for artifacts (default: current dir)
-p KEY Public key (or set UNSANDBOX_PUBLIC_KEY env var)
-k KEY Secret key (or set UNSANDBOX_SECRET_KEY env var)
-n MODE Network mode: zerotrust (default) or semitrusted
-v N, --vcpu N vCPU count 1-8, each vCPU gets 2GB RAM (default: 1)
-y Skip confirmation for large uploads (>1GB)
-h Show this help
Authentication (priority order):
1. -p and -k flags (public and secret key)
2. UNSANDBOX_PUBLIC_KEY + UNSANDBOX_SECRET_KEY env vars
3. ~/.unsandbox/accounts.csv (format: public_key,secret_key per line)
Session options:
-s, --shell SHELL Shell/REPL to use (default: bash)
-l, --list List active sessions
--attach ID Reconnect to existing session (ID or container name)
--kill ID Terminate a session (ID or container name)
--freeze ID Freeze a session (requires --tmux/--screen)
--unfreeze ID Unfreeze a frozen session
--boost ID Boost session resources (2 vCPU, 4GB RAM)
--boost-vcpu N Specify vCPU count for boost (1-8)
--unboost ID Return to base resources
--audit Record full session for auditing
--tmux Enable session persistence with tmux (allows reconnect)
--screen Enable session persistence with screen (allows reconnect)
Service options:
--name NAME Service name (creates new service)
--ports PORTS Comma-separated ports (e.g., 80,443)
--domains DOMAINS Custom domains (e.g., example.com,www.example.com)
--type TYPE Service type: minecraft, mumble, teamspeak, source, tcp, udp
--bootstrap CMD Bootstrap command/file/URL to run on startup
-f FILE Upload file to /tmp/ (can use multiple times)
-l, --list List all services
--info ID Get service details
--tail ID Get last 9000 lines of bootstrap logs
--logs ID Get all bootstrap logs
--freeze ID Freeze a service
--unfreeze ID Unfreeze a service
--auto-unfreeze ID Enable auto-wake on HTTP request
--no-auto-unfreeze ID Disable auto-wake on HTTP request
--show-freeze-page ID Show HTML payment page when frozen (default)
--no-show-freeze-page ID Return JSON error when frozen
--destroy ID Destroy a service
--redeploy ID Re-run bootstrap script (requires --bootstrap)
--execute ID CMD Run a command in a running service
--dump-bootstrap ID [FILE] Dump bootstrap script (for migrations)
--snapshot ID Create snapshot of session or service
--snapshot-name User-friendly name for snapshot
--hot Create snapshot without pausing (may be inconsistent)
--restore ID Restore session/service from snapshot ID
Snapshot options:
-l, --list List all snapshots
--info ID Get snapshot details
--delete ID Delete a snapshot permanently
Image options:
-l, --list [owned|shared|public] List images (all, owned, shared, or public)
--info ID Get image details
--publish-service ID Publish image from stopped/frozen service
--publish-snapshot ID Publish image from snapshot
--name NAME Name for published image
--description DESC Description for published image
--delete ID Delete image (must be unlocked)
--clone ID Clone image (creates copy you own)
--spawn ID Create service from image (requires --name)
--lock ID Lock image to prevent deletion
--unlock ID Unlock image to allow deletion
--visibility ID LEVEL Set visibility (private|unlisted|public)
--grant ID --key KEY Grant access to another API key
--revoke ID --key KEY Revoke access from API key
--transfer ID --to KEY Transfer ownership to API key
--trusted ID List API keys with access
Key options:
(no options) Check API key validity
--extend Open portal to extend an expired key
Examples:
./un script.py # execute Python script
./un -e DEBUG=1 script.py # with environment variable
./un -f data.csv process.py # with input file
./un -a -o ./bin main.c # save compiled artifacts
./un -v 4 heavy.py # with 4 vCPUs, 8GB RAM
./un session # interactive bash session
./un session --tmux # bash with reconnect support
./un session --list # list active sessions
./un session --attach unsb-vm-12345 # reconnect to session
./un session --kill unsb-vm-12345 # terminate a session
./un session --freeze unsb-vm-12345 # freeze session
./un session --unfreeze unsb-vm-12345 # unfreeze session
./un session --boost unsb-vm-12345 # boost resources
./un session --unboost unsb-vm-12345 # return to base
./un session --shell python3 # Python REPL
./un session --shell node # Node.js REPL
./un session -n semitrusted # session with network access
./un session --audit -o ./logs # record session for auditing
./un service --name web --ports 80 # create web service
./un service --list # list all services
./un service --logs abc123 # view bootstrap logs
./un key # check API key
./un key --extend # extend expired key
./un snapshot --list # list all snapshots
./un session --snapshot unsb-vm-123 # snapshot a session
./un service --snapshot abc123 # snapshot a service
./un session --restore unsb-snapshot-xxxx # restore from snapshot
./un image --list # list all images
./un image --list owned # list your images
./un image --publish-service abc # publish image from service
./un image --spawn img123 --name x # create service from image
./un image --grant img --key pk # share image with user
CLI Inception
O UN CLI foi implementado em 42 linguagens de programação, demonstrando que a API do unsandbox pode ser acessada de praticamente qualquer ambiente.
Ver Todas as 42 Implementações →
Licença
DOMÍNIO PÚBLICO - SEM LICENÇA, SEM GARANTIA
Este é software gratuito de domínio público para o bem público de um permacomputador hospedado
em permacomputer.com - um computador sempre ativo pelo povo, para o povo. Um que é
durável, fácil de reparar e distribuído como água da torneira para inteligência de
aprendizado de máquina.
O permacomputador é infraestrutura de propriedade comunitária otimizada em torno de quatro valores:
VERDADE - Primeiros princípios, matemática & ciência, código aberto distribuído livremente
LIBERDADE - Parcerias voluntárias, liberdade da tirania e controle corporativo
HARMONIA - Desperdício mínimo, sistemas auto-renováveis com diversas conexões prósperas
AMOR - Seja você mesmo sem ferir os outros, cooperação através da lei natural
Este software contribui para essa visão ao permitir a execução de código em mais de 42
linguagens de programação através de uma interface unificada, acessível a todos. Código são
sementes que brotam em qualquer tecnologia abandonada.
Saiba mais: https://www.permacomputer.com
Qualquer pessoa é livre para copiar, modificar, publicar, usar, compilar, vender ou distribuir
este software, seja em forma de código-fonte ou como binário compilado, para qualquer propósito,
comercial ou não comercial, e por qualquer meio.
SEM GARANTIA. O SOFTWARE É FORNECIDO "COMO ESTÁ" SEM GARANTIA DE QUALQUER TIPO.
Dito isso, a camada de membrana digital do nosso permacomputador executa continuamente testes
unitários, de integração e funcionais em todo o seu próprio software - com nosso permacomputador
monitorando a si mesmo, reparando a si mesmo, com orientação humana mínima no ciclo.
Nossos agentes fazem o seu melhor.
Copyright 2025 TimeHexOn & foxhop & russell@unturf
https://www.timehexon.com
https://www.foxhop.net
https://www.unturf.com/software