un-inception/un.php
russell@unturf.com 783ca52963 Refactor 5 SDKs: Ruby, Perl, PHP, Lua, Bash with library exports + HMAC auth
- Ruby (un.rb): Full module with execute, executeAsync, wait, job management
- Perl (un.pl): Complete SDK with 4-tier credential system, HMAC signing, caching
- PHP (un.php): Static class-based API with proper error handling
- Lua (un.lua): Module-based implementation with socket and JSON support
- Bash (un.sh): Shell functions using curl, openssl, and jq for JSON

All implementations:
- 4-tier credential loading (args > env > home > local)
- HMAC-SHA256 request signatures
- 1-hour cache for /languages endpoint in ~/.unsandbox/
- Exponential backoff job polling
- Preserved original CLI functionality
- Language-appropriate docstrings/comments
- Full permacomputer public domain license headers
2026-01-15 15:17:02 -05:00

221 lines
7.9 KiB
PHP
Executable file

<?php
// PUBLIC DOMAIN - NO LICENSE, NO WARRANTY
//
// This is free public domain software for the public good of a permacomputer hosted
// at permacomputer.com - an always-on computer by the people, for the people. One
// which is durable, easy to repair, and distributed like tap water for machine
// learning intelligence.
//
// The permacomputer is community-owned infrastructure optimized around four values:
//
// TRUTH - First principles, math & science, open source code freely distributed
// FREEDOM - Voluntary partnerships, freedom from tyranny & corporate control
// HARMONY - Minimal waste, self-renewing systems with diverse thriving connections
// LOVE - Be yourself without hurting others, cooperation through natural law
//
// This software contributes to that vision by enabling code execution across 42+
// programming languages through a unified interface, accessible to all. Code is
// seeds to sprout on any abandoned technology.
//
// Learn more: https://www.permacomputer.com
//
// Anyone is free to copy, modify, publish, use, compile, sell, or distribute this
// software, either in source code form or as a compiled binary, for any purpose,
// commercial or non-commercial, and by any means.
//
// NO WARRANTY. THE SOFTWARE IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND.
//
// That said, our permacomputer's digital membrane stratum continuously runs unit,
// integration, and functional tests on all of it's own software - with our
// permacomputer monitoring itself, repairing itself, with minimal human in the
// loop guidance. Our agents do their best.
//
// Copyright 2025 TimeHexOn & foxhop & russell@unturf
// https://www.timehexon.com
// https://www.foxhop.net
// https://www.unturf.com/software
//
// unsandbox SDK for PHP - Execute code in secure sandboxes
// https://unsandbox.com | https://api.unsandbox.com/openapi
class Un {
const API_BASE = 'https://api.unsandbox.com';
const VERSION = '2.0.0';
public static function loadAccountsCsv($path = null) {
$path = $path ?: getenv('HOME') . '/.unsandbox/accounts.csv';
if (!file_exists($path)) return [];
$accounts = [];
$lines = file($path);
foreach ($lines as $line) {
$line = trim($line);
if (!$line) continue;
list($pk, $sk) = explode(',', $line, 2);
$accounts[] = [trim($pk), trim($sk)];
}
return $accounts;
}
public static function getCredentials($publicKey = null, $secretKey = null) {
// Tier 1: Arguments
if ($publicKey && $secretKey) return [$publicKey, $secretKey];
// Tier 2: Environment
$pk = getenv('UNSANDBOX_PUBLIC_KEY');
$sk = getenv('UNSANDBOX_SECRET_KEY');
if ($pk && $sk) return [$pk, $sk];
// Tier 3: Home directory
$accounts = self::loadAccountsCsv();
if ($accounts) return $accounts[0];
// Tier 4: Local directory
$accounts = self::loadAccountsCsv('./accounts.csv');
if ($accounts) return $accounts[0];
throw new Exception("No credentials found\n");
}
public static function signRequest($secret, $timestamp, $method, $endpoint, $body) {
$message = "$timestamp:$method:$endpoint:$body";
return hash_hmac('sha256', $message, $secret);
}
public static function apiRequest($method, $endpoint, $body = null, $opts = []) {
list($pk, $sk) = self::getCredentials(
$opts['publicKey'] ?? null,
$opts['secretKey'] ?? null
);
$timestamp = time();
$url = self::API_BASE . $endpoint;
$bodyStr = $body ? json_encode($body) : '{}';
$signature = self::signRequest($sk, $timestamp, $method, $endpoint, $bodyStr);
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Authorization: Bearer ' . $pk,
'X-Timestamp: ' . $timestamp,
'X-Signature: ' . $signature,
'Content-Type: application/json'
]);
if ($body) {
curl_setopt($ch, CURLOPT_POSTFIELDS, $bodyStr);
}
$response = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($code != 200) throw new Exception("API error ($code)");
return json_decode($response, true);
}
public static function languages($cacheTtl = 3600) {
$cacheDir = getenv('HOME') . '/.unsandbox';
$cachePath = $cacheDir . '/languages.json';
if (file_exists($cachePath)) {
$age = time() - filemtime($cachePath);
if ($age < $cacheTtl) {
return json_decode(file_get_contents($cachePath), true);
}
}
$result = self::apiRequest('GET', '/languages');
$langs = $result['languages'] ?? [];
if (!is_dir($cacheDir)) mkdir($cacheDir, 0700, true);
file_put_contents($cachePath, json_encode($langs));
return $langs;
}
public static function execute($language, $code, $opts = []) {
$body = [
'language' => $language,
'code' => $code,
'network_mode' => $opts['networkMode'] ?? 'zerotrust',
'ttl' => $opts['ttl'] ?? 60
];
return self::apiRequest('POST', '/execute', $body, $opts);
}
public static function executeAsync($language, $code, $opts = []) {
$body = [
'language' => $language,
'code' => $code,
'network_mode' => $opts['networkMode'] ?? 'zerotrust',
'ttl' => $opts['ttl'] ?? 300
];
return self::apiRequest('POST', '/execute/async', $body, $opts);
}
public static function run($file, $opts = []) {
$code = file_get_contents($file);
return self::execute(self::detectLanguage($file), $code, $opts);
}
public static function getJob($jobId, $opts = []) {
return self::apiRequest('GET', "/jobs/$jobId", null, $opts);
}
public static function wait($jobId, $timeout = 3600, $opts = []) {
$delays = [300, 450, 700, 900, 650, 1600, 2000];
$start = time();
for ($i = 0; $i < 120; $i++) {
$job = self::getJob($jobId, $opts);
if ($job['status'] === 'completed') return $job;
if ($job['status'] === 'failed') throw new Exception("Job failed");
if ($job['status'] === 'timeout') throw new Exception("Job timeout");
if (time() - $start > $timeout) throw new Exception("Polling timeout");
$delay = $delays[$i] ?? 2000;
usleep($delay * 1000);
}
throw new Exception("Max polls exceeded");
}
public static function cancelJob($jobId, $opts = []) {
return self::apiRequest('DELETE', "/jobs/$jobId", null, $opts);
}
public static function detectLanguage($filename) {
$ext = pathinfo($filename, PATHINFO_EXTENSION);
$map = ['py' => 'python', 'rb' => 'ruby', 'js' => 'javascript', 'php' => 'php',
'lua' => 'lua', 'sh' => 'bash', 'go' => 'go', 'pl' => 'perl'];
return $map[$ext] ?? throw new Exception("Unknown file type");
}
public static function image($code, $format = 'png', $opts = []) {
return self::apiRequest('POST', '/image', ['code' => $code, 'format' => $format], $opts);
}
}
// CLI
if (php_sapi_name() === 'cli' && !empty($GLOBALS['argv'])) {
array_shift($GLOBALS['argv']);
if (empty($GLOBALS['argv'])) {
echo "Usage: php un.php <file>\n";
exit(1);
}
try {
$result = Un::run($GLOBALS['argv'][0]);
if (!empty($result['stdout'])) echo $result['stdout'];
if (!empty($result['stderr'])) fwrite(STDERR, $result['stderr']);
exit($result['exit_code'] ?? 0);
} catch (Exception $e) {
fwrite(STDERR, "Error: " . $e->getMessage() . "\n");
exit(1);
}
}
?>