diff --git a/.gitignore b/.gitignore index acbe267..bfff866 100644 --- a/.gitignore +++ b/.gitignore @@ -15,8 +15,6 @@ /Un.class __pycache__/ -.venv/ -*.egg-info/ # Build directories _build/ @@ -42,11 +40,8 @@ Thumbs.db /output/ __pycache__/ *.pyc -science-results/ -science-results.xml clients/c/un clients/c/examples/fibonacci clients/c/examples/hello_world clients/c/tests/test_library build/ -.claude/ diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 7b8f150..3ef7e66 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -157,10 +157,6 @@ science-validate-examples: needs: - build script: - # Set up Python venv with dependencies for example validation - - python3 -m venv .venv - - source .venv/bin/activate - - pip install --quiet requests aiohttp - bash scripts/validate-examples.sh artifacts: reports: @@ -168,6 +164,7 @@ science-validate-examples: paths: - science-results/ expire_in: 30 days + allow_failure: true only: - main - /^\d+\.\d+\.\d+$/ @@ -230,6 +227,7 @@ validate-examples: paths: - science-results/ expire_in: 30 days + allow_failure: true only: - main - /^\d+\.\d+\.\d+$/ diff --git a/AGGREGATED-PERFORMANCE.md b/AGGREGATED-PERFORMANCE.md index 808ce30..6508256 100644 --- a/AGGREGATED-PERFORMANCE.md +++ b/AGGREGATED-PERFORMANCE.md @@ -1,13 +1,13 @@ # UN Inception: Aggregated Performance Analysis -**Analysis Date:** 1771084652.659423 -**Reports Analyzed:** 4.2.0, 4.2.10, 4.2.11, 4.2.12, 4.2.13, 4.2.14, 4.2.15, 4.2.16, 4.2.17, 4.2.18, 4.2.19, 4.2.20, 4.2.21, 4.2.22, 4.2.23, 4.2.24, 4.2.25, 4.2.26, 4.2.27, 4.2.28, 4.2.29, 4.2.3, 4.2.30, 4.2.31, 4.2.32, 4.2.36, 4.2.37, 4.2.38, 4.2.4, 4.2.46, 4.2.5, 4.2.50, 4.2.51, 4.2.52, 4.2.6, 4.2.7, 4.2.8, 4.2.9, 4.3.0, 4.3.1, 4.3.2, 4.3.3, 4.3.4 +**Analysis Date:** 1770391601.721544 +**Reports Analyzed:** 4.2.0, 4.2.10, 4.2.11, 4.2.12, 4.2.13, 4.2.14, 4.2.15, 4.2.16, 4.2.17, 4.2.18, 4.2.19, 4.2.20, 4.2.21, 4.2.22, 4.2.23, 4.2.24, 4.2.25, 4.2.26, 4.2.27, 4.2.28, 4.2.29, 4.2.3, 4.2.30, 4.2.31, 4.2.32, 4.2.36, 4.2.37, 4.2.38, 4.2.4, 4.2.46, 4.2.5, 4.2.50, 4.2.51, 4.2.52, 4.2.6, 4.2.7, 4.2.8, 4.2.9, 4.3.0 --- ## Executive Summary -Analysis of 43 performance reports reveals **significant variance** in execution metrics across releases. Different languages rank as slowest/fastest in different runs, indicating **non-deterministic execution patterns** likely caused by: +Analysis of 39 performance reports reveals **significant variance** in execution metrics across releases. Different languages rank as slowest/fastest in different runs, indicating **non-deterministic execution patterns** likely caused by: 1. **Orchestrator placement on CPU-bound pool** (not an SRE best practice) 2. **Resource contention** between the orchestrator & test jobs @@ -61,10 +61,6 @@ Analysis of 43 performance reports reveals **significant variance** in execution | 4.2.8 | 111s | kotlin (313s) | fortran (28s) | -6s (-5.1%) | | 4.2.9 | 107s | ruby (279s) | d (19s) | -4s (-3.6%) | | 4.3.0 | 376s | typescript (829s) | c (47s) | +269s (+251.4%) | -| 4.3.1 | 238s | go (414s) | prolog (58s) | -138s (-36.7%) | -| 4.3.2 | 132s | crystal (314s) | c (66s) | -106s (-44.5%) | -| 4.3.3 | 175s | go (447s) | v (65s) | +43s (+32.6%) | -| 4.3.4 | 204s | perl (456s) | php (49s) | +29s (+16.6%) | **Observation:** Average duration increased **0.0%** from 0s to 0s. @@ -120,10 +116,6 @@ The same language changes dramatically in rank between runs: - 4.2.8: 253s - 4.2.9: 166s - 4.3.0: 196s - - 4.3.1: 164s - - 4.3.2: 193s - - 4.3.3: 202s - - 4.3.4: 198s - **Range:** 32s → 2173s (6690.6% variance) **R:** @@ -166,10 +158,6 @@ The same language changes dramatically in rank between runs: - 4.2.8: 126s - 4.2.9: 54s - 4.3.0: 453s - - 4.3.1: 163s - - 4.3.2: 179s - - 4.3.3: 81s - - 4.3.4: 106s - **Range:** 9s → 1834s (20277.8% variance) **SCHEME:** @@ -212,10 +200,6 @@ The same language changes dramatically in rank between runs: - 4.2.8: 55s - 4.2.9: 153s - 4.3.0: 171s - - 4.3.1: 276s - - 4.3.2: 132s - - 4.3.3: 196s - - 4.3.4: 148s - **Range:** 15s → 1574s (10393.3% variance) **PYTHON:** @@ -258,10 +242,6 @@ The same language changes dramatically in rank between runs: - 4.2.8: 253s - 4.2.9: 165s - 4.3.0: 671s - - 4.3.1: 148s - - 4.3.2: 85s - - 4.3.3: 158s - - 4.3.4: 307s - **Range:** 19s → 1574s (8184.2% variance) **TCL:** @@ -304,10 +284,6 @@ The same language changes dramatically in rank between runs: - 4.2.8: 247s - 4.2.9: 54s - 4.3.0: 476s - - 4.3.1: 132s - - 4.3.2: 80s - - 4.3.3: 186s - - 4.3.4: 247s - **Range:** 20s → 1572s (7760.0% variance) @@ -356,10 +332,6 @@ The same language changes dramatically in rank between runs: 4.2.8: fortran, groovy, crystal, java, powershell 4.2.9: d, julia, csharp, v, objc 4.3.0: c, bash, php, fortran, ruby -4.3.1: prolog, awk, powershell, typescript, dart -4.3.2: c, cpp, fsharp, perl, csharp -4.3.3: v, r, dart, perl, rust -4.3.4: php, powershell, v, bash, fortran **Slowest Languages by Run:** @@ -402,10 +374,6 @@ The same language changes dramatically in rank between runs: 4.2.8: kotlin, python, javascript, tcl, raku 4.2.9: ruby, deno, rust, crystal, java 4.3.0: typescript, go, python, java, objc -4.3.1: go, groovy, perl, deno, objc -4.3.2: crystal, erlang, elixir, rust, groovy -4.3.3: go, crystal, raku, typescript, kotlin -4.3.4: perl, ruby, go, cobol, kotlin **Conclusion:** No consistent "fast" or "slow" languages across runs. This proves: - Execution order is random or system-dependent @@ -416,9 +384,9 @@ The same language changes dramatically in rank between runs: ### 4. API Health Trends -**Overall API Health:** 5.2/100 (avg across 12 releases) +**Overall API Health:** 7.8/100 (avg across 8 releases) **Trend:** STABLE -**Total Retries (all releases):** 5332 +**Total Retries (all releases):** 3644 | Release | Health Score | Total Retries | 429 (Rate Limit) | 5xx (Server) | Timeout | Connection | |---------|--------------|---------------|------------------|--------------|---------|------------| @@ -430,10 +398,6 @@ The same language changes dramatically in rank between runs: | 4.2.51 | 28/100 | 36 | 0 | 36 | 0 | 0 | | 4.2.52 | 34/100 | 33 | 0 | 33 | 0 | 0 | | 4.3.0 | 0/100 | 876 | 839 | 27 | 10 | 0 | -| 4.3.1 | 0/100 | 649 | 634 | 5 | 10 | 0 | -| 4.3.2 | 0/100 | 217 | 207 | 0 | 10 | 0 | -| 4.3.3 | 0/100 | 330 | 320 | 0 | 10 | 0 | -| 4.3.4 | 0/100 | 492 | 482 | 0 | 10 | 0 | **Interpretation:** - **Score 95-100:** API healthy, tests pass on first attempt @@ -589,26 +553,26 @@ Keep it as-is for stress testing, but in separate test environment. | Language | Min (s) | Max (s) | Avg (s) | Range (s) | Variance % | |----------|---------|---------|---------|-----------|------------| -| DOTNET | 5 | 1539 | 171.3 | 1534 | 30680.0% | -| R | 9 | 1834 | 219.7 | 1825 | 20277.8% | -| NIM | 8 | 1557 | 173.6 | 1549 | 19362.5% | -| CLOJURE | 8 | 1549 | 190.6 | 1541 | 19262.5% | -| FORTRAN | 8 | 1547 | 138.9 | 1539 | 19237.5% | -| PERL | 8 | 1546 | 183.8 | 1538 | 19225.0% | -| D | 8 | 1545 | 142.7 | 1537 | 19212.5% | -| ZIG | 8 | 1542 | 215.1 | 1534 | 19175.0% | -| FORTH | 8 | 1540 | 172.4 | 1532 | 19150.0% | -| KOTLIN | 8 | 1536 | 168.5 | 1528 | 19100.0% | -| CSHARP | 8 | 1534 | 159.8 | 1526 | 19075.0% | -| LUA | 9 | 1548 | 208.0 | 1539 | 17100.0% | -| PROLOG | 9 | 1540 | 127.8 | 1531 | 17011.1% | -| RUST | 9 | 1537 | 189.3 | 1528 | 16977.8% | -| SCHEME | 15 | 1574 | 167.3 | 1559 | 10393.3% | -| OBJC | 17 | 1559 | 165.9 | 1542 | 9070.6% | -| POWERSHELL | 14 | 1269 | 124.9 | 1255 | 8964.3% | -| PYTHON | 19 | 1574 | 175.4 | 1555 | 8184.2% | -| OCAML | 19 | 1537 | 168.0 | 1518 | 7989.5% | -| TCL | 20 | 1572 | 186.0 | 1552 | 7760.0% | +| DOTNET | 5 | 1539 | 168.1 | 1534 | 30680.0% | +| R | 9 | 1834 | 228.6 | 1825 | 20277.8% | +| NIM | 8 | 1557 | 176.5 | 1549 | 19362.5% | +| CLOJURE | 8 | 1549 | 184.3 | 1541 | 19262.5% | +| FORTRAN | 8 | 1547 | 135.6 | 1539 | 19237.5% | +| PERL | 8 | 1546 | 176.8 | 1538 | 19225.0% | +| D | 8 | 1545 | 139.8 | 1537 | 19212.5% | +| ZIG | 8 | 1542 | 216.4 | 1534 | 19175.0% | +| FORTH | 8 | 1540 | 173.3 | 1532 | 19150.0% | +| KOTLIN | 8 | 1536 | 157.5 | 1528 | 19100.0% | +| CSHARP | 8 | 1534 | 163.1 | 1526 | 19075.0% | +| LUA | 9 | 1548 | 206.9 | 1539 | 17100.0% | +| PROLOG | 9 | 1540 | 125.9 | 1531 | 17011.1% | +| RUST | 9 | 1537 | 188.2 | 1528 | 16977.8% | +| SCHEME | 15 | 1574 | 165.2 | 1559 | 10393.3% | +| OBJC | 17 | 1559 | 163.3 | 1542 | 9070.6% | +| POWERSHELL | 14 | 1269 | 128.6 | 1255 | 8964.3% | +| PYTHON | 19 | 1574 | 175.5 | 1555 | 8184.2% | +| OCAML | 19 | 1537 | 168.6 | 1518 | 7989.5% | +| TCL | 20 | 1572 | 188.5 | 1552 | 7760.0% | --- @@ -702,10 +666,6 @@ Individual Reports → Aggregation Script → Chart Generation (via UN) → Fina - `reports/4.2.8/perf.json` - 645 tests, generated 2026-01-23T10:01:33Z - `reports/4.2.9/perf.json` - 645 tests, generated 2026-01-23T10:05:34Z - `reports/4.3.0/perf.json` - 820 tests, generated 2026-02-06T15:25:06Z -- `reports/4.3.1/perf.json` - 824 tests, generated 2026-02-08T11:44:58Z -- `reports/4.3.2/perf.json` - 860 tests, generated 2026-02-08T18:34:37Z -- `reports/4.3.3/perf.json` - 848 tests, generated 2026-02-08T19:28:06Z -- `reports/4.3.4/perf.json` - 844 tests, generated 2026-02-14T15:56:41Z Each `perf.json` contains: @@ -902,5 +862,5 @@ For questions about this methodology or to report issues: --- **Generated by UN Inception Performance Analysis Pipeline** -**Analysis Date:** 2026-02-14T10:57:32.791573 +**Analysis Date:** 2026-02-06T10:26:41.836993 **Report Version:** 1.0.0 diff --git a/CLAUDE.md b/CLAUDE.md index 13ae7eb..849299b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2,9 +2,9 @@ ## ⚠️ CRITICAL: QR TEST FILES USE NATIVE LIBRARIES - NEVER SHELL OUT -**QR test files (`test/qr.*`) MUST use each language's native QR library.** Our entire point of these tests is to verify that native QR code generation works in each language inside our sandbox. Shelling out to `qrencode` CLI defeats our purpose. +**QR test files (`test/qr.*`) MUST use each language's native QR library.** The entire point of these tests is to verify that native QR code generation works in each language inside the sandbox. Shelling out to `qrencode` CLI defeats the purpose. -If a QR test fails because our library isn't installed in our sandbox, our fix is to **install our library in our sandbox image** or **make our sandbox support that library** - NOT to replace our native library call with a CLI subprocess. +If a QR test fails because the library isn't installed in the sandbox, the fix is to **install the library in the sandbox image** or **make the sandbox support that library** - NOT to replace the native library call with a CLI subprocess. ```bash # ❌ FORBIDDEN - shelling out defeats the test @@ -18,9 +18,9 @@ q.add_data("unsandbox-qr-ok") ## ⚠️ CRITICAL: SCIENTIFIC INTEGRITY - TESTS MUST NEVER LIE -**Science is our foundation of this project.** Tests exist to tell us our truth about our code. A test that lies is worse than no test at all. +**Science is the foundation of this project.** Tests exist to tell us the truth about our code. A test that lies is worse than no test at all. -### Our Cardinal Rule +### The Cardinal Rule **If a test cannot verify its assertion, it MUST FAIL or RETRY - never silently pass.** @@ -47,7 +47,7 @@ fi On 2026-01-28, we discovered our "100% pass rate" was a lie: - **780 tests "passed"** across 42 languages - **270 were soft passes** (35%) - masked failures -- Our test matrix was telling us everything worked when it didn't +- The test matrix was telling us everything worked when it didn't **Soft passes are scientific fraud.** They: - Hide real bugs in SDKs @@ -60,7 +60,7 @@ On 2026-01-28, we discovered our "100% pass rate" was a lie: 1. **Retry ALL transient errors** - HTTP 429, 500, 502, 503, 504, timeouts 2. **Use exponential backoff** - Start at 2s, cap at 60s 3. **Max retries = 10** - Then FAIL, don't fake pass -4. **No soft passes** - If our expected output isn't there, it's a FAIL +4. **No soft passes** - If the expected output isn't there, it's a FAIL 5. **Track retry stats** - So we can see API health over time ### Acceptable Test Outcomes @@ -73,28 +73,6 @@ On 2026-01-28, we discovered our "100% pass rate" was a lie: **Never**: `PASS (API issue)`, `PASS (timeout)`, `PASS (sandbox state)` -### Green Christmas Tree Policy - -**Every failure stays visible until fixed.** No skipping. No `allow_failure: true` to hide problems. No workarounds. - -Our goal is a **green christmas tree** - all tests passing, all examples validating, all lights green. Until then: - -1. **Failures are features** - They tell us what to fix next -2. **Red stays red** - Don't mask failures to make CI "pass" -3. **Iterate until green** - Keep fixing until everything works -4. **Log errors loudly** - Show stderr, stdout, API errors on every failure - -When CI fails, our response is: -- ❌ NOT: "Let's skip this test" or "Let's allow this to fail" -- ✅ YES: "Let's fix this test" or "Let's fix our code" - -**Current known issues to fix (2026-02-13):** -- SDK client examples fail in sandbox (no credentials, no SDK installed) -- PHP examples have ` 0) { - if (line ~ /^[[:space:]]*$/ || line ~ /^[[:space:]]*#/) continue - count++ - if (count == index) { - split(line, fields, ",") - pk = fields[1]; sk = fields[2] - gsub(/^[[:space:]]+|[[:space:]]+$/, "", pk) - gsub(/^[[:space:]]+|[[:space:]]+$/, "", sk) - close(path) - GLOBAL_PUBLIC_KEY = pk; GLOBAL_SECRET_KEY = sk - return 1 - } - } - close(path) - # Try ./accounts.csv as fallback - count = -1; path = "accounts.csv" - while ((getline line < path) > 0) { - if (line ~ /^[[:space:]]*$/ || line ~ /^[[:space:]]*#/) continue - count++ - if (count == index) { - split(line, fields, ",") - pk = fields[1]; sk = fields[2] - gsub(/^[[:space:]]+|[[:space:]]+$/, "", pk) - gsub(/^[[:space:]]+|[[:space:]]+$/, "", sk) - close(path) - GLOBAL_PUBLIC_KEY = pk; GLOBAL_SECRET_KEY = sk - return 1 - } - } - close(path) - return 0 -} +function get_api_keys( public_key, secret_key, cmd) { + # Get public key + cmd = "echo -n $UNSANDBOX_PUBLIC_KEY" + cmd | getline public_key + close(cmd) -function get_api_keys( public_key, secret_key, cmd, default_index) { - # Priority 1: already set via -p/-k flags - if (GLOBAL_PUBLIC_KEY != "" && GLOBAL_SECRET_KEY != "") { return } + # Get secret key + cmd = "echo -n $UNSANDBOX_SECRET_KEY" + cmd | getline secret_key + close(cmd) - # Priority 2: --account N bypasses env vars - if (GLOBAL_ACCOUNT_INDEX >= 0) { - if (load_accounts_csv(GLOBAL_ACCOUNT_INDEX)) { - if (GLOBAL_PUBLIC_KEY != "") return - } - print RED "Error: Account index " GLOBAL_ACCOUNT_INDEX " not found in accounts.csv" RESET > "/dev/stderr" + # Fallback to old UNSANDBOX_API_KEY for backwards compat + if (public_key == "") { + cmd = "echo -n $UNSANDBOX_API_KEY" + cmd | getline public_key + close(cmd) + secret_key = "" + } + + if (public_key == "") { + print RED "Error: UNSANDBOX_PUBLIC_KEY or UNSANDBOX_API_KEY not set" RESET > "/dev/stderr" exit 1 } - # Priority 3: env vars UNSANDBOX_PUBLIC_KEY / UNSANDBOX_SECRET_KEY - cmd = "echo -n $UNSANDBOX_PUBLIC_KEY"; cmd | getline public_key; close(cmd) - cmd = "echo -n $UNSANDBOX_SECRET_KEY"; cmd | getline secret_key; close(cmd) - if (public_key != "" && secret_key != "") { - GLOBAL_PUBLIC_KEY = public_key; GLOBAL_SECRET_KEY = secret_key; return - } - - # Fallback to legacy UNSANDBOX_API_KEY for backwards compat - if (public_key == "") { - cmd = "echo -n $UNSANDBOX_API_KEY"; cmd | getline public_key; close(cmd) - secret_key = "" - } - if (public_key != "") { - GLOBAL_PUBLIC_KEY = public_key; GLOBAL_SECRET_KEY = secret_key; return - } - - # Priority 4/5: accounts.csv with UNSANDBOX_ACCOUNT env var or row 0 - cmd = "echo -n $UNSANDBOX_ACCOUNT"; cmd | getline default_index; close(cmd) - if (default_index == "") default_index = 0 - if (load_accounts_csv(default_index + 0)) { - if (GLOBAL_PUBLIC_KEY != "") return - } - - print RED "Error: UNSANDBOX_PUBLIC_KEY or UNSANDBOX_API_KEY not set" RESET > "/dev/stderr" - exit 1 + GLOBAL_PUBLIC_KEY = public_key + GLOBAL_SECRET_KEY = secret_key } function get_extension(filename) { @@ -2414,20 +2359,6 @@ END { exit 0 } - # Pre-scan ARGV for global flags: --account N, -p KEY, -k KEY - for (_gi = 1; _gi < ARGC; _gi++) { - if (ARGV[_gi] == "--account" && _gi + 1 < ARGC) { - GLOBAL_ACCOUNT_INDEX = ARGV[_gi + 1] + 0 - _gi++ - } else if (ARGV[_gi] == "-p" && _gi + 1 < ARGC) { - GLOBAL_PUBLIC_KEY = ARGV[_gi + 1] - _gi++ - } else if (ARGV[_gi] == "-k" && _gi + 1 < ARGC) { - GLOBAL_SECRET_KEY = ARGV[_gi + 1] - _gi++ - } - } - if (ARGV[1] == "session") { if (ARGC >= 3 && ARGV[2] == "--list") { session_list() diff --git a/clients/bash/sync/src/un.sh b/clients/bash/sync/src/un.sh index 858d730..a2b3a3b 100644 --- a/clients/bash/sync/src/un.sh +++ b/clients/bash/sync/src/un.sh @@ -1,19 +1,21 @@ #!/bin/bash -# This is free 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, & distributed like tap water -# for machine learning intelligence. +# PUBLIC DOMAIN - NO LICENSE, NO WARRANTY # -# The permacomputer is community-owned infrastructure optimized around -# four values: +# unsandbox.com Bash SDK (Synchronous) +# Full API with execution, sessions, services, snapshots, and images. # -# 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 +# Library Usage: +# source un.sh +# result=$(execute "python" "print(42)") +# echo "$result" | jq -r '.stdout' # -# 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. +# CLI Usage: +# bash un.sh script.py +# bash un.sh -s python 'print(42)' +# bash un.sh session --list +# bash un.sh service --list +# +# Copyright 2025 TimeHexOn & foxhop & russell@unturf set -euo pipefail @@ -21,7 +23,6 @@ VERSION="4.2.50" API_BASE="https://api.unsandbox.com" PORTAL_BASE="https://unsandbox.com" LAST_ERROR="" -ACCOUNT_INDEX=-1 # Colors BLUE='\033[34m' @@ -105,19 +106,8 @@ hmac_sign() { load_accounts_csv() { local path="${1:-$HOME/.unsandbox/accounts.csv}" - local row="${2:-0}" [ -f "$path" ] || return 1 - local n=0 - while IFS= read -r line; do - [[ "$line" =~ ^# ]] && continue - [ -z "$line" ] && continue - if [ "$n" -eq "$row" ]; then - echo "$line" - return 0 - fi - n=$((n + 1)) - done < "$path" - return 1 + head -1 "$path" 2>/dev/null | grep -v '^#' } get_credentials() { @@ -127,20 +117,7 @@ get_credentials() { return fi - # Tier 2: --account N flag → bypass env vars, load CSV row N directly - if [ "$ACCOUNT_INDEX" -ge 0 ] 2>/dev/null; then - local creds - creds=$(load_accounts_csv "$HOME/.unsandbox/accounts.csv" "$ACCOUNT_INDEX" 2>/dev/null || true) - [ -z "$creds" ] && creds=$(load_accounts_csv "./accounts.csv" "$ACCOUNT_INDEX" 2>/dev/null || true) - if [ -n "$creds" ]; then - echo "$creds" - return - fi - set_error "Account index $ACCOUNT_INDEX not found in accounts.csv" - return 1 - fi - - # Tier 3: Environment + # Tier 2: Environment if [ -n "${UNSANDBOX_PUBLIC_KEY:-}" ] && [ -n "${UNSANDBOX_SECRET_KEY:-}" ]; then echo "$UNSANDBOX_PUBLIC_KEY:$UNSANDBOX_SECRET_KEY" return @@ -152,7 +129,7 @@ get_credentials() { return fi - # Tier 4: Home directory + # Tier 3: Home directory local creds creds=$(load_accounts_csv "$HOME/.unsandbox/accounts.csv" 2>/dev/null || true) if [ -n "$creds" ]; then @@ -160,7 +137,7 @@ get_credentials() { return fi - # Tier 5: Local directory + # Tier 4: Local directory creds=$(load_accounts_csv "./accounts.csv" 2>/dev/null || true) if [ -n "$creds" ]; then echo "$creds" @@ -449,7 +426,6 @@ service_create() { local name="$1" local ports="${2:-}" local bootstrap="${3:-}" - local input_files_json="${4:-}" local body body=$(jq -n --arg name "$name" '{name: $name}') @@ -460,9 +436,6 @@ service_create() { if [ -n "$bootstrap" ]; then body=$(echo "$body" | jq --arg boot "$bootstrap" '. + {bootstrap: $boot}') fi - if [ -n "$input_files_json" ]; then - body=$(echo "$body" | jq --argjson files "$input_files_json" '. + {input_files: $files}') - fi api_request "POST" "/services" "$body" } @@ -501,14 +474,10 @@ service_set_unfreeze_on_demand() { service_redeploy() { local service_id="$1" local bootstrap="${2:-}" - local input_files_json="${3:-}" local body="{}" if [ -n "$bootstrap" ]; then body=$(jq -n --arg boot "$bootstrap" '{bootstrap: $boot}') fi - if [ -n "$input_files_json" ]; then - body=$(echo "$body" | jq --argjson files "$input_files_json" '. + {input_files: $files}') - fi api_request "POST" "/services/$service_id/redeploy" "$body" } @@ -935,9 +904,6 @@ cmd_service() { local target="" local name="" local ports="" - local bootstrap="" - local bootstrap_file="" - local -a files=() while [ $# -gt 0 ]; do case "$1" in @@ -949,47 +915,13 @@ cmd_service() { --lock) action="lock"; target="$2"; shift ;; --unlock) action="unlock"; target="$2"; shift ;; --logs) action="logs"; target="$2"; shift ;; - --redeploy) action="redeploy"; target="$2"; shift ;; --name) name="$2"; shift ;; --ports) ports="$2"; shift ;; - --bootstrap) bootstrap="$2"; shift ;; - --bootstrap-file) bootstrap_file="$2"; shift ;; - -f|--file) files+=("$2"); shift ;; *) ;; esac shift done - # Build input_files JSON from -f args - local input_files_json="" - if [ ${#files[@]} -gt 0 ]; then - input_files_json="[" - local first=1 - for fpath in "${files[@]}"; do - if [ ! -f "$fpath" ]; then - echo -e "${RED}Error: File not found: $fpath${RESET}" >&2 - exit 1 - fi - local encoded - encoded=$(base64 -w0 "$fpath" 2>/dev/null || base64 "$fpath" 2>/dev/null) - local fname - fname=$(basename "$fpath") - [ "$first" -eq 0 ] && input_files_json="$input_files_json," - input_files_json="$input_files_json{\"filename\":$(echo "$fname" | jq -Rs .),\"content\":$(echo "$encoded" | jq -Rs .)}" - first=0 - done - input_files_json="$input_files_json]" - fi - - # Resolve bootstrap from file if provided - if [ -n "$bootstrap_file" ]; then - if [ ! -f "$bootstrap_file" ]; then - echo -e "${RED}Error: Bootstrap file not found: $bootstrap_file${RESET}" >&2 - exit 1 - fi - bootstrap=$(cat "$bootstrap_file") - fi - case "$action" in list) local result @@ -1024,19 +956,14 @@ cmd_service() { result=$(service_logs "$target") echo "$result" | jq -r '.logs // empty' ;; - redeploy) - local result - result=$(service_redeploy "$target" "$bootstrap" "$input_files_json") - echo -e "${GREEN}Service redeployed: $target${RESET}" - ;; *) if [ -n "$name" ]; then local result - result=$(service_create "$name" "$ports" "$bootstrap" "$input_files_json") + result=$(service_create "$name" "$ports" "") echo -e "${GREEN}Service created${RESET}" echo "$result" | jq -r '"ID: \(.id)\nName: \(.name)"' else - echo "Usage: bash un.sh service --list|--info ID|--destroy ID|--redeploy ID|--name NAME" >&2 + echo "Usage: bash un.sh service --list|--info ID|--destroy ID|--name NAME" >&2 exit 1 fi ;; @@ -1213,12 +1140,8 @@ Service options: --lock ID Lock service --unlock ID Unlock service --logs ID Get service logs - --redeploy ID Re-run bootstrap (supports -f, --bootstrap) --name NAME Create service with name --ports PORTS Service ports (comma-separated) - --bootstrap CMD Bootstrap command - --bootstrap-file FILE Bootstrap from file - -f, --file FILE Add input file (can repeat) Snapshot options: --list List all snapshots @@ -1255,21 +1178,6 @@ if [ "${BASH_SOURCE[0]}" = "$0" ]; then exit 1 fi - # Pre-scan for --account N before dispatching - _args=("$@") - _new_args=() - _i=0 - while [ $_i -lt ${#_args[@]} ]; do - if [ "${_args[$_i]}" = "--account" ]; then - _i=$((_i + 1)) - ACCOUNT_INDEX="${_args[$_i]}" - else - _new_args+=("${_args[$_i]}") - fi - _i=$((_i + 1)) - done - set -- "${_new_args[@]+"${_new_args[@]}"}" - case "$1" in languages) shift diff --git a/clients/bash/tests/test_library.sh b/clients/bash/tests/test_library.sh index d7dd299..ca63d54 100755 --- a/clients/bash/tests/test_library.sh +++ b/clients/bash/tests/test_library.sh @@ -1,20 +1,4 @@ #!/bin/bash -# This is free 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, & 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. - # Unit Tests for un.sh Library Functions # # Tests the ACTUAL exported functions from Un module. diff --git a/clients/c/Makefile b/clients/c/Makefile index 470bab6..3c3a1bf 100644 --- a/clients/c/Makefile +++ b/clients/c/Makefile @@ -108,14 +108,6 @@ test: build $(TEST_DIR)/test_library test-library: test -test-integration: build - @echo "" - @echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" - @echo "INTEGRATION: Testing --account flag priority" - @echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" - @echo "" - @bash $(TEST_DIR)/test_account_flag.sh - test-functional: build $(TEST_DIR)/test_functional @echo "" @echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" diff --git a/clients/c/src/un.c b/clients/c/src/un.c index 9ee1802..44bff49 100644 --- a/clients/c/src/un.c +++ b/clients/c/src/un.c @@ -396,14 +396,7 @@ static UnsandboxCredentials* get_credentials(const char *cli_pk, const char *cli return creds; } - // Priority 2: --account N flag → explicit CSV lookup - // When the user explicitly selects an account, go straight to accounts.csv. - // Env vars are intentionally bypassed — an explicit flag must win over ambient env. - if (account_index >= 0) { - return load_credentials_from_csv(account_index); - } - - // Priority 3: Environment variables (keys) + // Priority 2: Environment variables (keys) const char *env_pk = getenv("UNSANDBOX_PUBLIC_KEY"); const char *env_sk = getenv("UNSANDBOX_SECRET_KEY"); @@ -423,12 +416,16 @@ static UnsandboxCredentials* get_credentials(const char *cli_pk, const char *cli return creds; } - // Priority 4: Config file (~/.unsandbox/accounts.csv) - // Use UNSANDBOX_ACCOUNT env var, or default to account 0 - int csv_index = 0; - const char *env_account = getenv("UNSANDBOX_ACCOUNT"); - if (env_account && strlen(env_account) > 0) { - csv_index = atoi(env_account); + // Priority 3: Config file (~/.unsandbox/accounts.csv) + // Use account_index from --account flag, or UNSANDBOX_ACCOUNT env var, or default to 0 + int csv_index = account_index; + if (csv_index < 0) { + const char *env_account = getenv("UNSANDBOX_ACCOUNT"); + if (env_account && strlen(env_account) > 0) { + csv_index = atoi(env_account); + } else { + csv_index = 0; + } } return load_credentials_from_csv(csv_index); } @@ -4646,7 +4643,7 @@ static char* read_env_stdin(void) { // Redeploy a service (re-run bootstrap script) // Bootstrap scripts should be idempotent for proper upgrade behavior -static int redeploy_service(const UnsandboxCredentials *creds, const char *service_id, const char *bootstrap, struct InputFile *input_files, int input_file_count) { +static int redeploy_service(const UnsandboxCredentials *creds, const char *service_id, const char *bootstrap) { CURL *curl = curl_easy_init(); if (!curl) return 1; @@ -4695,9 +4692,6 @@ static int redeploy_service(const UnsandboxCredentials *creds, const char *servi } else if (bootstrap_url) { payload_size += strlen(bootstrap_url) * 2 + 100; } - for (int i = 0; i < input_file_count; i++) { - payload_size += strlen(input_files[i].content_base64) + 256; - } // Build JSON payload manually (matching create_service pattern) char *payload = malloc(payload_size); @@ -4710,31 +4704,15 @@ static int redeploy_service(const UnsandboxCredentials *creds, const char *servi char *p = payload; p += sprintf(p, "{"); - int has_field = 0; if (bootstrap_content) { char *esc_content = escape_json_string(bootstrap_content); p += sprintf(p, "\"bootstrap_content\":\"%s\"", esc_content); free(esc_content); free(bootstrap_content); - has_field = 1; } else if (bootstrap_url) { char *esc_url = escape_json_string(bootstrap_url); p += sprintf(p, "\"bootstrap\":\"%s\"", esc_url); free(esc_url); - has_field = 1; - } - - if (input_file_count > 0) { - if (has_field) p += sprintf(p, ","); - p += sprintf(p, "\"input_files\":["); - for (int i = 0; i < input_file_count; i++) { - if (i > 0) p += sprintf(p, ","); - char *esc_filename = escape_json_string(input_files[i].filename); - p += sprintf(p, "{\"filename\":\"%s\",\"content\":\"%s\"}", - esc_filename, input_files[i].content_base64); - free(esc_filename); - } - p += sprintf(p, "]"); } p += sprintf(p, "}"); @@ -6860,7 +6838,7 @@ void print_usage(const char *prog) { * ============================================================================ */ const char *unsandbox_version(void) { - return "4.3.4"; + return "4.3.1"; } const char *unsandbox_detect_language(const char *filename) { @@ -7122,7 +7100,7 @@ int unsandbox_service_redeploy(const char *service_id, const char *bootstrap, const char *public_key, const char *secret_key) { UnsandboxCredentials *creds = get_credentials(public_key, secret_key, -1); if (!creds) return -1; - int result = redeploy_service(creds, service_id, bootstrap, NULL, 0); + int result = redeploy_service(creds, service_id, bootstrap); free_credentials(creds); return result; } @@ -10511,12 +10489,7 @@ int main(int argc, char *argv[]) { // - If provided via --bootstrap or --bootstrap-file, use it // - If omitted, API will use the stored encrypted bootstrap const char *bootstrap_to_use = bootstrap_file ? bootstrap_file : service_bootstrap; - ret = redeploy_service(creds, service_id, bootstrap_to_use, service_input_files, service_input_file_count); - // Free input file memory - for (int i = 0; i < service_input_file_count; i++) { - free(service_input_files[i].filename); - free(service_input_files[i].content_base64); - } + ret = redeploy_service(creds, service_id, bootstrap_to_use); } else if (do_execute) { // Pass input files if provided (written to /tmp/input/ before command runs) ret = execute_service(creds, service_id, execute_command, execute_timeout, service_input_files, service_input_file_count); diff --git a/clients/c/tests/test_account_flag.sh b/clients/c/tests/test_account_flag.sh deleted file mode 100755 index 966be65..0000000 --- a/clients/c/tests/test_account_flag.sh +++ /dev/null @@ -1,111 +0,0 @@ -#!/usr/bin/env bash -# Integration test: --account N flag must take priority over env vars -# -# Requires: UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY (real credentials) -# Run: make test-integration OR bash tests/test_account_flag.sh -# -# The defect this guards against: get_credentials() checked env vars before -# account_index, so --account N was silently ignored when env vars existed. - -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -UN_BIN="$SCRIPT_DIR/../un" - -RED='\033[31m' -GREEN='\033[32m' -NC='\033[0m' - -pass=0 -fail=0 - -check() { - local desc="$1" result="$2" - if [ "$result" = "pass" ]; then - printf " ${GREEN}✓${NC} %s\n" "$desc" - pass=$((pass + 1)) - else - printf " ${RED}✗${NC} %s\n" "$desc" - fail=$((fail + 1)) - fi -} - -# Require real credentials to be available -if [ -z "${UNSANDBOX_PUBLIC_KEY:-}" ] || [ -z "${UNSANDBOX_SECRET_KEY:-}" ]; then - echo "SKIP: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set" - exit 0 -fi - -if [ ! -x "$UN_BIN" ]; then - echo "FAIL: UN binary not found at $UN_BIN — run make first" - exit 1 -fi - -REAL_PK="$UNSANDBOX_PUBLIC_KEY" -REAL_SK="$UNSANDBOX_SECRET_KEY" - -# Temporary HOME with accounts.csv: -# index 0: garbage credentials (will always 401) -# index 1: real credentials (will succeed) -TMPHOME="$(mktemp -d)" -mkdir -p "$TMPHOME/.unsandbox" -trap 'rm -rf "$TMPHOME"' EXIT - -cat > "$TMPHOME/.unsandbox/accounts.csv" <&1 || true) - -if ! echo "$OUT" | grep -qi "401\|unauthorized\|invalid_credential\|No credentials"; then - check "--account 1 uses CSV row 1 (real creds) over garbage env vars" "pass" -else - check "--account 1 uses CSV row 1 (real creds) over garbage env vars" "fail" - echo " output: $OUT" -fi - -# --- Test 2: --account 0 should use CSV row 0 (garbage creds) → 401 --- -# Even though real env vars are set, explicit --account 0 should pick garbage creds -OUT=$(HOME="$TMPHOME" \ - UNSANDBOX_PUBLIC_KEY="$REAL_PK" \ - UNSANDBOX_SECRET_KEY="$REAL_SK" \ - "$UN_BIN" --account 0 key 2>&1 || true) - -if echo "$OUT" | grep -qi "401\|unauthorized\|invalid_credential\|invalid key"; then - check "--account 0 uses CSV row 0 (garbage creds) despite real env vars, 401 as expected" "pass" -else - check "--account 0 uses CSV row 0 (garbage creds) despite real env vars, 401 as expected" "fail" - echo " output: $OUT" -fi - -# --- Test 3: no --account flag, real env vars → env vars win over garbage CSV row 0 --- -OUT=$(HOME="$TMPHOME" \ - UNSANDBOX_PUBLIC_KEY="$REAL_PK" \ - UNSANDBOX_SECRET_KEY="$REAL_SK" \ - "$UN_BIN" key 2>&1 || true) - -if ! echo "$OUT" | grep -qi "401\|unauthorized\|invalid_credential\|No credentials"; then - check "No --account flag: env vars used, succeeds" "pass" -else - check "No --account flag: env vars used, succeeds" "fail" - echo " output: $OUT" -fi - -echo "" -echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" -printf "Passed: ${GREEN}%d${NC} Failed: ${RED}%d${NC}\n" "$pass" "$fail" -echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" - -[ "$fail" -eq 0 ] diff --git a/clients/clojure/sync/src/un.clj b/clients/clojure/sync/src/un.clj index a733b60..20ad8aa 100644 --- a/clients/clojure/sync/src/un.clj +++ b/clients/clojure/sync/src/un.clj @@ -118,51 +118,17 @@ (or (second (re-find pattern-str json-str)) (second (re-find pattern-num json-str))))) -(def ^:dynamic *account-index* nil) - -(defn load-accounts-csv [path index] - (when (.exists (io/file path)) - (try - (let [lines (str/split-lines (slurp path)) - rows (->> lines - (filter #(and (> (count %) 0) - (not (str/starts-with? % "#")))) - (map #(str/split % #"," 2)) - (filter #(>= (count %) 2)))] - (when (< index (count rows)) - (let [row (nth rows index)] - [(str/trim (first row)) (str/trim (second row))]))) - (catch Exception _ nil)))) - (defn get-api-keys [] (let [public-key (System/getenv "UNSANDBOX_PUBLIC_KEY") secret-key (System/getenv "UNSANDBOX_SECRET_KEY") - api-key (System/getenv "UNSANDBOX_API_KEY") - home (System/getenv "HOME") - account-idx (or *account-index* 0)] + api-key (System/getenv "UNSANDBOX_API_KEY")] (cond - ;; --account N: load row N from accounts.csv, bypasses env vars - (some? *account-index*) - (or (load-accounts-csv (str home "/.unsandbox/accounts.csv") account-idx) - (load-accounts-csv "./accounts.csv" account-idx) - (do - (binding [*out* *err*] - (println (str "Error: account " account-idx " not found in accounts.csv"))) - (System/exit 1))) - ;; env vars (and public-key secret-key) [public-key secret-key] api-key [api-key nil] - ;; accounts.csv fallback (row 0 or UNSANDBOX_ACCOUNT) - :else - (let [row-idx (if-let [acc (System/getenv "UNSANDBOX_ACCOUNT")] - (try (Integer/parseInt acc) (catch Exception _ 0)) - 0)] - (or (load-accounts-csv (str home "/.unsandbox/accounts.csv") row-idx) - (load-accounts-csv "./accounts.csv" row-idx) - (do + :else (do (binding [*out* *err*] (println "Error: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set (or UNSANDBOX_API_KEY for backwards compat)")) - (System/exit 1))))))) + (System/exit 1))))) (defn get-api-key [] (first (get-api-keys))) @@ -1250,14 +1216,4 @@ (recur (rest args) file env-vars artifacts out-dir network vcpu session-action session-id session-shell session-input-files service-action service-id service-name service-ports service-bootstrap service-bootstrap-file service-type service-input-files service-envs service-env-file service-unfreeze-on-demand key-extend image-action image-id image-source-type image-visibility image-name image-ports mode)))) -(let [raw-args *command-line-args* - account-val (second (drop-while #(not= % "--account") raw-args)) - account-idx (when account-val - (try (Integer/parseInt account-val) (catch Exception _ nil))) - filtered-args (loop [in raw-args out []] - (cond - (empty? in) out - (= (first in) "--account") (recur (drop 2 in) out) - :else (recur (rest in) (conj out (first in)))))] - (binding [*account-index* account-idx] - (parse-args filtered-args))) +(parse-args *command-line-args*) diff --git a/clients/cobol/sync/src/un.cob b/clients/cobol/sync/src/un.cob index 8aae80a..6a3401f 100644 --- a/clients/cobol/sync/src/un.cob +++ b/clients/cobol/sync/src/un.cob @@ -45,21 +45,15 @@ SELECT SOURCE-FILE ASSIGN TO WS-FILENAME ORGANIZATION IS LINE SEQUENTIAL FILE STATUS IS WS-FILE-STATUS. - SELECT CRED-FILE ASSIGN TO "/tmp/unsb_creds.txt" - ORGANIZATION IS LINE SEQUENTIAL - FILE STATUS IS WS-CRED-STATUS. DATA DIVISION. FILE SECTION. FD SOURCE-FILE. 01 SOURCE-LINE PIC X(1024). - FD CRED-FILE. - 01 CRED-LINE PIC X(512). WORKING-STORAGE SECTION. 01 WS-FILENAME PIC X(256). 01 WS-FILE-STATUS PIC XX. - 01 WS-CRED-STATUS PIC XX. 01 WS-API-KEY PIC X(256). 01 WS-PUBLIC-KEY PIC X(256). 01 WS-SECRET-KEY PIC X(256). @@ -103,23 +97,12 @@ 01 WS-UOD-ENABLED PIC X(8). 01 WS-TYPE PIC X(32). 01 WS-SHELL PIC X(32). - 01 WS-ACCOUNT-INDEX PIC S9(4) VALUE -1. - 01 WS-ACCOUNT-STR PIC X(16). - 01 WS-ACCT-POS PIC 9(4) VALUE 0. - 01 WS-ERROR-MSG PIC X(256). PROCEDURE DIVISION. MAIN-PROCEDURE. - * Get first command line argument + * Get command line argument (first argument) ACCEPT WS-ARG1 FROM COMMAND-LINE. - * Pre-scan: handle --account N global flag before subcommand - IF WS-ARG1 = "--account" - ACCEPT WS-ACCOUNT-STR FROM ARGUMENT-VALUE - MOVE FUNCTION NUMVAL(WS-ACCOUNT-STR) TO WS-ACCOUNT-INDEX - ACCEPT WS-ARG1 FROM ARGUMENT-VALUE - END-IF. - IF WS-ARG1 = SPACES DISPLAY "Usage: un.cob " UPON SYSERR DISPLAY " un.cob session [options]" UPON SYSERR @@ -164,94 +147,7 @@ PERFORM HANDLE-EXECUTE. STOP RUN. - GET-CREDENTIALS. - * If already set, return immediately - IF WS-PUBLIC-KEY NOT = SPACES AND WS-SECRET-KEY NOT = SPACES - EXIT PARAGRAPH - END-IF. - - * Build shell script to resolve credentials with full priority - IF WS-ACCOUNT-INDEX >= 0 - MOVE WS-ACCOUNT-INDEX TO WS-ACCOUNT-STR - STRING "IDX=" FUNCTION TRIM(WS-ACCOUNT-STR) "; " - "PK=''; SK=''; CNT=-1; " - "for CSV in \"$HOME/.unsandbox/accounts.csv\" " - "\"./accounts.csv\"; do " - "[ -f \"$CSV\" ] || continue; " - "while IFS= read -r line || [ -n \"$line\" ]; do " - "case \"$line\" in \"#\"*|\"\"|\" \"*) continue ;; esac; " - "CNT=$((CNT+1)); " - "if [ \"$CNT\" -eq \"$IDX\" ]; then " - "PK=$(echo \"$line\" | cut -d',' -f1 | tr -d ' '); " - "SK=$(echo \"$line\" | cut -d',' -f2 | tr -d ' '); " - "break 2; fi; " - "done < \"$CSV\"; done; " - "if [ -z \"$PK\" ]; then " - "echo -e '\\x1b[31mError: Account index " - FUNCTION TRIM(WS-ACCOUNT-STR) - " not found in accounts.csv\\x1b[0m' >&2; exit 1; fi; " - "printf '%s\\n%s\\n' \"$PK\" \"$SK\" " - "> /tmp/unsb_creds.txt" - DELIMITED BY SIZE INTO WS-CURL-CMD - END-STRING - ELSE - STRING "PK=\"$UNSANDBOX_PUBLIC_KEY\"; " - "SK=\"$UNSANDBOX_SECRET_KEY\"; " - "if [ -z \"$PK\" ]; then PK=\"$UNSANDBOX_API_KEY\"; SK=''; fi; " - "if [ -z \"$PK\" ]; then " - "IDX=\"${UNSANDBOX_ACCOUNT:-0}\"; " - "CNT=-1; " - "for CSV in \"$HOME/.unsandbox/accounts.csv\" " - "\"./accounts.csv\"; do " - "[ -f \"$CSV\" ] || continue; " - "while IFS= read -r line || [ -n \"$line\" ]; do " - "case \"$line\" in \"#\"*|\"\"|\" \"*) continue ;; esac; " - "CNT=$((CNT+1)); " - "if [ \"$CNT\" -eq \"$IDX\" ]; then " - "PK=$(echo \"$line\" | cut -d',' -f1 | tr -d ' '); " - "SK=$(echo \"$line\" | cut -d',' -f2 | tr -d ' '); " - "break 2; fi; " - "done < \"$CSV\"; done; fi; " - "if [ -z \"$PK\" ]; then " - "echo -e '\\x1b[31mError: No credentials found\\x1b[0m' " - ">&2; exit 1; fi; " - "printf '%s\\n%s\\n' \"$PK\" \"$SK\" " - "> /tmp/unsb_creds.txt" - DELIMITED BY SIZE INTO WS-CURL-CMD - END-STRING - END-IF. - - CALL "SYSTEM" USING WS-CURL-CMD - RETURNING WS-EXIT-CODE. - IF WS-EXIT-CODE NOT = 0 - MOVE WS-EXIT-CODE TO RETURN-CODE - STOP RUN - END-IF. - - * Read resolved credentials from temp file - MOVE SPACES TO WS-PUBLIC-KEY. - MOVE SPACES TO WS-SECRET-KEY. - OPEN INPUT CRED-FILE. - IF WS-CRED-STATUS = "00" - READ CRED-FILE INTO WS-PUBLIC-KEY - READ CRED-FILE INTO WS-SECRET-KEY - CLOSE CRED-FILE - END-IF. - - IF WS-PUBLIC-KEY = SPACES - DISPLAY "Error: Could not resolve credentials" - UPON SYSERR - MOVE 1 TO RETURN-CODE - STOP RUN - END-IF. - HANDLE-EXECUTE. - * Get credentials - PERFORM GET-CREDENTIALS. - IF WS-API-KEY = SPACES - MOVE WS-PUBLIC-KEY TO WS-API-KEY - END-IF. - * Check if file exists OPEN INPUT SOURCE-FILE. IF WS-FILE-STATUS NOT = "00" @@ -272,14 +168,25 @@ STOP RUN END-IF. + * Get API key from environment + ACCEPT WS-API-KEY FROM ENVIRONMENT "UNSANDBOX_API_KEY". + + IF WS-API-KEY = SPACES + DISPLAY "Error: UNSANDBOX_API_KEY not set" UPON SYSERR + MOVE 1 TO RETURN-CODE + STOP RUN + END-IF. + * Use curl to make request PERFORM MAKE-EXECUTE-REQUEST. HANDLE-SESSION. - * Get credentials - PERFORM GET-CREDENTIALS. + * Get API key + ACCEPT WS-API-KEY FROM ENVIRONMENT "UNSANDBOX_API_KEY". IF WS-API-KEY = SPACES - MOVE WS-PUBLIC-KEY TO WS-API-KEY + DISPLAY "Error: UNSANDBOX_API_KEY not set" UPON SYSERR + MOVE 1 TO RETURN-CODE + STOP RUN END-IF. * Initialize session parameters @@ -302,8 +209,27 @@ END-IF. HANDLE-SERVICE. - * Get credentials - PERFORM GET-CREDENTIALS. + * Get API keys (try new format first, fall back to old) + ACCEPT WS-PUBLIC-KEY FROM ENVIRONMENT "UNSANDBOX_PUBLIC_KEY". + IF WS-PUBLIC-KEY NOT = SPACES + ACCEPT WS-SECRET-KEY FROM ENVIRONMENT "UNSANDBOX_SECRET_KEY" + IF WS-SECRET-KEY = SPACES + DISPLAY "Error: UNSANDBOX_SECRET_KEY not set" + UPON SYSERR + MOVE 1 TO RETURN-CODE + STOP RUN + END-IF + ELSE + ACCEPT WS-API-KEY FROM ENVIRONMENT "UNSANDBOX_API_KEY" + IF WS-API-KEY = SPACES + DISPLAY "Error: UNSANDBOX_PUBLIC_KEY/SECRET_KEY or " + "UNSANDBOX_API_KEY not set" UPON SYSERR + MOVE 1 TO RETURN-CODE + STOP RUN + END-IF + MOVE WS-API-KEY TO WS-PUBLIC-KEY + MOVE WS-API-KEY TO WS-SECRET-KEY + END-IF. * Initialize service parameters MOVE SPACES TO WS-NAME. @@ -406,7 +332,26 @@ END-IF. MAKE-EXECUTE-REQUEST. - * Credentials already resolved by caller (GET-CREDENTIALS) + * Get public/secret keys with fallback + ACCEPT WS-PUBLIC-KEY FROM ENVIRONMENT "UNSANDBOX_PUBLIC_KEY". + IF WS-PUBLIC-KEY NOT = SPACES + ACCEPT WS-SECRET-KEY FROM ENVIRONMENT "UNSANDBOX_SECRET_KEY" + IF WS-SECRET-KEY = SPACES + DISPLAY "Error: UNSANDBOX_SECRET_KEY not set" + UPON SYSERR + MOVE 1 TO RETURN-CODE + STOP RUN + END-IF + ELSE + ACCEPT WS-PUBLIC-KEY FROM ENVIRONMENT "UNSANDBOX_API_KEY" + IF WS-PUBLIC-KEY = SPACES + DISPLAY "Error: UNSANDBOX_PUBLIC_KEY/SECRET_KEY or " + "UNSANDBOX_API_KEY not set" UPON SYSERR + MOVE 1 TO RETURN-CODE + STOP RUN + END-IF + MOVE WS-PUBLIC-KEY TO WS-SECRET-KEY + END-IF. * Build curl command using shell with HMAC signature STRING "TS=$(date +%s); " @@ -998,10 +943,12 @@ CALL "SYSTEM" USING WS-CURL-CMD. HANDLE-KEY. - * Get credentials - PERFORM GET-CREDENTIALS. + * Get API key + ACCEPT WS-API-KEY FROM ENVIRONMENT "UNSANDBOX_API_KEY". IF WS-API-KEY = SPACES - MOVE WS-PUBLIC-KEY TO WS-API-KEY + DISPLAY "Error: UNSANDBOX_API_KEY not set" UPON SYSERR + MOVE 1 TO RETURN-CODE + STOP RUN END-IF. * Parse key arguments @@ -1177,8 +1124,27 @@ CALL "SYSTEM" USING WS-CURL-CMD. HANDLE-LANGUAGES. - * Get credentials - PERFORM GET-CREDENTIALS. + * Get API keys + ACCEPT WS-PUBLIC-KEY FROM ENVIRONMENT "UNSANDBOX_PUBLIC_KEY". + IF WS-PUBLIC-KEY NOT = SPACES + ACCEPT WS-SECRET-KEY FROM ENVIRONMENT "UNSANDBOX_SECRET_KEY" + IF WS-SECRET-KEY = SPACES + DISPLAY "Error: UNSANDBOX_SECRET_KEY not set" + UPON SYSERR + MOVE 1 TO RETURN-CODE + STOP RUN + END-IF + ELSE + ACCEPT WS-API-KEY FROM ENVIRONMENT "UNSANDBOX_API_KEY" + IF WS-API-KEY = SPACES + DISPLAY "Error: UNSANDBOX_PUBLIC_KEY/SECRET_KEY or " + "UNSANDBOX_API_KEY not set" UPON SYSERR + MOVE 1 TO RETURN-CODE + STOP RUN + END-IF + MOVE WS-API-KEY TO WS-PUBLIC-KEY + MOVE WS-API-KEY TO WS-SECRET-KEY + END-IF. * Parse --json flag MOVE SPACES TO WS-JSON-OUTPUT. @@ -1253,8 +1219,27 @@ CALL "SYSTEM" USING WS-CURL-CMD. HANDLE-IMAGE. - * Get credentials - PERFORM GET-CREDENTIALS. + * Get API keys (try new format first, fall back to old) + ACCEPT WS-PUBLIC-KEY FROM ENVIRONMENT "UNSANDBOX_PUBLIC_KEY". + IF WS-PUBLIC-KEY NOT = SPACES + ACCEPT WS-SECRET-KEY FROM ENVIRONMENT "UNSANDBOX_SECRET_KEY" + IF WS-SECRET-KEY = SPACES + DISPLAY "Error: UNSANDBOX_SECRET_KEY not set" + UPON SYSERR + MOVE 1 TO RETURN-CODE + STOP RUN + END-IF + ELSE + ACCEPT WS-API-KEY FROM ENVIRONMENT "UNSANDBOX_API_KEY" + IF WS-API-KEY = SPACES + DISPLAY "Error: UNSANDBOX_PUBLIC_KEY/SECRET_KEY or " + "UNSANDBOX_API_KEY not set" UPON SYSERR + MOVE 1 TO RETURN-CODE + STOP RUN + END-IF + MOVE WS-API-KEY TO WS-PUBLIC-KEY + MOVE WS-API-KEY TO WS-SECRET-KEY + END-IF. * Initialize image parameters MOVE SPACES TO WS-ID. @@ -1691,7 +1676,15 @@ HANDLE-SNAPSHOT. * Get credentials - PERFORM GET-CREDENTIALS. + ACCEPT WS-PUBLIC-KEY FROM ENVIRONMENT + "UNSANDBOX_PUBLIC_KEY". + ACCEPT WS-SECRET-KEY FROM ENVIRONMENT + "UNSANDBOX_SECRET_KEY". + IF WS-PUBLIC-KEY = SPACES OR WS-SECRET-KEY = SPACES + DISPLAY "Error: API keys not set" UPON SYSERR + MOVE 1 TO RETURN-CODE + STOP RUN + END-IF. * Get second argument (operation or --list) ACCEPT WS-ARG2 FROM ARGUMENT-VALUE. diff --git a/clients/cobol/tests/test_un.sh b/clients/cobol/tests/test_un.sh index 8986144..a8af64b 100755 --- a/clients/cobol/tests/test_un.sh +++ b/clients/cobol/tests/test_un.sh @@ -1,20 +1,4 @@ #!/bin/bash -# This is free 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, & 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. - # Test suite for COBOL Unsandbox SDK # Run: bash tests/test_un.sh diff --git a/clients/cpp/sync/src/un.cpp b/clients/cpp/sync/src/un.cpp index 59a9fed..e4ffaef 100644 --- a/clients/cpp/sync/src/un.cpp +++ b/clients/cpp/sync/src/un.cpp @@ -103,27 +103,6 @@ string read_file(const string& filename) { return buf.str(); } -// Load a row from an accounts.csv file (format: public_key,secret_key per line). -// Lines starting with '#' and blank lines are skipped. Returns the Nth data row. -pair loadAccountsCSV(const string& path, int index) { - ifstream f(path); - if (!f) return {"", ""}; - string line; - int row = 0; - while (getline(f, line)) { - // Trim trailing carriage return - if (!line.empty() && line.back() == '\r') line.pop_back(); - if (line.empty() || line[0] == '#') continue; - if (row == index) { - size_t comma = line.find(','); - if (comma == string::npos) return {"", ""}; - return {line.substr(0, comma), line.substr(comma + 1)}; - } - row++; - } - return {"", ""}; -} - string escape_json(const string& s) { ostringstream o; for (char c : s) { @@ -726,32 +705,9 @@ string service_unlock(const string& service_id, const string& public_key, const return exec_curl(cmd); } -string service_redeploy(const string& service_id, const string& bootstrap, const vector& input_files, const string& public_key, const string& secret_key) { +string service_redeploy(const string& service_id, const string& bootstrap, const string& public_key, const string& secret_key) { string path = "/services/" + service_id + "/redeploy"; - ostringstream json; - json << "{"; - bool has_field = false; - if (!bootstrap.empty()) { - json << "\"bootstrap\":\"" << escape_json(bootstrap) << "\""; - has_field = true; - } - if (!input_files.empty()) { - if (has_field) json << ","; - json << "\"input_files\":["; - for (size_t i = 0; i < input_files.size(); i++) { - if (i > 0) json << ","; - ifstream file(input_files[i], ios::binary); - if (!file) continue; - ostringstream content; - content << file.rdbuf(); - string b64 = base64_encode(content.str()); - string filename = input_files[i].substr(input_files[i].find_last_of("/\\") + 1); - json << "{\"filename\":\"" << filename << "\",\"content_base64\":\"" << b64 << "\"}"; - } - json << "]"; - } - json << "}"; - string body = json.str(); + string body = bootstrap.empty() ? "{}" : "{\"bootstrap\":\"" + escape_json(bootstrap) + "\"}"; string auth_headers = build_auth_headers("POST", path, body, public_key, secret_key); string cmd = "curl -s -X POST '" + API_BASE + path + "' " "-H 'Content-Type: application/json' " @@ -1246,7 +1202,7 @@ void cmd_session(bool list, const string& kill, const string& shell, const strin cout << exec_curl(cmd) << endl; } -void cmd_service(const string& name, const string& ports, const string& type, const string& bootstrap, const string& bootstrap_file, const vector& files, bool list, const string& info, const string& logs, const string& tail, const string& sleep, const string& wake, const string& destroy, const string& resize, const string& execute, const string& command, const string& dump_bootstrap, const string& dump_file, const string& redeploy, const string& network, int vcpu, const vector& envs, const string& env_file, const string& env_action, const string& env_target, const string& set_unfreeze_on_demand_id, int set_unfreeze_on_demand_enabled, int unfreeze_on_demand, const string& public_key, const string& secret_key) { +void cmd_service(const string& name, const string& ports, const string& type, const string& bootstrap, const string& bootstrap_file, const vector& files, bool list, const string& info, const string& logs, const string& tail, const string& sleep, const string& wake, const string& destroy, const string& resize, const string& execute, const string& command, const string& dump_bootstrap, const string& dump_file, const string& network, int vcpu, const vector& envs, const string& env_file, const string& env_action, const string& env_target, const string& set_unfreeze_on_demand_id, int set_unfreeze_on_demand_enabled, int unfreeze_on_demand, const string& public_key, const string& secret_key) { // Handle service env subcommand if (!env_action.empty()) { cmd_service_env(env_action, env_target, envs, env_file, public_key, secret_key); @@ -1372,62 +1328,6 @@ void cmd_service(const string& name, const string& ports, const string& type, co return; } - if (!redeploy.empty()) { - // Bootstrap is optional for redeploy: - // - If provided via --bootstrap or --bootstrap-file, use it - // - If omitted, API will use the stored encrypted bootstrap - string bootstrap_to_use = bootstrap; - if (!bootstrap_file.empty()) { - struct stat st; - if (stat(bootstrap_file.c_str(), &st) == 0) { - bootstrap_to_use = read_file(bootstrap_file); - } else { - cerr << RED << "Error: Bootstrap file not found: " << bootstrap_file << RESET << endl; - exit(1); - } - } - cout << YELLOW << "Redeploying service " << redeploy << "..." << RESET << endl; - ostringstream json; - json << "{"; - bool has_field = false; - if (!bootstrap_to_use.empty()) { - if (!bootstrap_file.empty()) { - json << "\"bootstrap_content\":\"" << escape_json(bootstrap_to_use) << "\""; - } else { - json << "\"bootstrap\":\"" << escape_json(bootstrap_to_use) << "\""; - } - has_field = true; - } - if (!files.empty()) { - if (has_field) json << ","; - json << "\"input_files\":["; - for (size_t i = 0; i < files.size(); i++) { - if (i > 0) json << ","; - ifstream file(files[i], ios::binary); - if (!file) { - cerr << RED << "Error: Input file not found: " << files[i] << RESET << endl; - exit(1); - } - ostringstream content; - content << file.rdbuf(); - string b64 = base64_encode(content.str()); - string filename = files[i].substr(files[i].find_last_of("/\\") + 1); - json << "{\"filename\":\"" << filename << "\",\"content_base64\":\"" << b64 << "\"}"; - } - json << "]"; - } - json << "}"; - string path = "/services/" + redeploy + "/redeploy"; - string auth_headers = build_auth_headers("POST", path, json.str(), public_key, secret_key); - string cmd = "curl -s -X POST '" + API_BASE + path + "' " - "-H 'Content-Type: application/json' " - + auth_headers + " " - "-d '" + json.str() + "'"; - string result = exec_curl(cmd); - cout << result << endl; - return; - } - if (!dump_bootstrap.empty()) { cerr << "Fetching bootstrap script from " << dump_bootstrap << "..." << endl; string json_body = "{\"command\":\"cat /tmp/bootstrap.sh\"}"; @@ -1802,62 +1702,12 @@ void cmd_validate_key(bool extend, const string& public_key, const string& secre } int main(int argc, char* argv[]) { - string public_key; - string secret_key; - int account_index = -1; // -1 = not set + string public_key = getenv("UNSANDBOX_PUBLIC_KEY") ? getenv("UNSANDBOX_PUBLIC_KEY") : ""; + string secret_key = getenv("UNSANDBOX_SECRET_KEY") ? getenv("UNSANDBOX_SECRET_KEY") : ""; - // First pass: scan for --account N and -p/-k flags before full arg parsing - for (int i = 1; i < argc; i++) { - string a = argv[i]; - if (a == "--account" && i+1 < argc) { - account_index = atoi(argv[++i]); - } else if (a == "-p" && i+1 < argc) { - public_key = argv[++i]; - } - } - - if (account_index >= 0) { - // --account N: load from ~/.unsandbox/accounts.csv, bypassing env vars - const char* home = getenv("HOME"); - string csv_path = string(home ? home : ".") + "/.unsandbox/accounts.csv"; - auto creds = loadAccountsCSV(csv_path, account_index); - if (creds.first.empty()) { - // fall back to ./accounts.csv - creds = loadAccountsCSV("accounts.csv", account_index); - } - if (!creds.first.empty()) { - if (public_key.empty()) public_key = creds.first; - secret_key = creds.second; - } - } else { - // Priority: env vars, then ~/.unsandbox/accounts.csv row 0, then ./accounts.csv row 0 - if (public_key.empty()) { - public_key = getenv("UNSANDBOX_PUBLIC_KEY") ? getenv("UNSANDBOX_PUBLIC_KEY") : ""; - } - secret_key = getenv("UNSANDBOX_SECRET_KEY") ? getenv("UNSANDBOX_SECRET_KEY") : ""; - - // Fall back to UNSANDBOX_API_KEY for backwards compatibility - if (public_key.empty()) { - public_key = getenv("UNSANDBOX_API_KEY") ? getenv("UNSANDBOX_API_KEY") : ""; - } - - // Try UNSANDBOX_ACCOUNT env var to pick a row - int env_account = -1; - const char* env_acct = getenv("UNSANDBOX_ACCOUNT"); - if (env_acct) env_account = atoi(env_acct); - - if (public_key.empty()) { - const char* home = getenv("HOME"); - string csv_path = string(home ? home : ".") + "/.unsandbox/accounts.csv"; - auto creds = loadAccountsCSV(csv_path, env_account >= 0 ? env_account : 0); - if (creds.first.empty()) { - creds = loadAccountsCSV("accounts.csv", env_account >= 0 ? env_account : 0); - } - if (!creds.first.empty()) { - public_key = creds.first; - secret_key = creds.second; - } - } + // Fall back to UNSANDBOX_API_KEY for backwards compatibility + if (public_key.empty()) { + public_key = getenv("UNSANDBOX_API_KEY") ? getenv("UNSANDBOX_API_KEY") : ""; } if (argc < 2) { @@ -1895,7 +1745,6 @@ int main(int argc, char* argv[]) { else if (arg == "--name" && i+1 < argc) name = argv[++i]; else if (arg == "--ports" && i+1 < argc) ports = argv[++i]; else if (arg == "-k" && i+1 < argc) public_key = argv[++i]; - else if (arg == "--account" && i+1 < argc) i++; // already handled in first pass } cmd_image(list, info, del, lock, unlock, publish, source_type, visibility_id, visibility, spawn, clone, name, ports, public_key, secret_key); @@ -1920,7 +1769,6 @@ int main(int argc, char* argv[]) { else if (arg == "--tmux") tmux = true; else if (arg == "--screen") screen = true; else if (arg == "-k" && i+1 < argc) public_key = argv[++i]; - else if (arg == "--account" && i+1 < argc) i++; // already handled in first pass } cmd_session(list, kill, shell, network, vcpu, tmux, screen, files, public_key, secret_key); @@ -1930,7 +1778,7 @@ int main(int argc, char* argv[]) { if (cmd_type == "service") { string name, ports, type, bootstrap, bootstrap_file; bool list = false; - string info, logs, tail, sleep, wake, destroy, resize, execute, command, dump_bootstrap, dump_file, network, redeploy; + string info, logs, tail, sleep, wake, destroy, resize, execute, command, dump_bootstrap, dump_file, network; int vcpu = 0; vector files; vector envs; @@ -1968,7 +1816,6 @@ int main(int argc, char* argv[]) { else if (arg == "--resize" && i+1 < argc) resize = argv[++i]; else if (arg == "--execute" && i+1 < argc) execute = argv[++i]; else if (arg == "--command" && i+1 < argc) command = argv[++i]; - else if (arg == "--redeploy" && i+1 < argc) redeploy = argv[++i]; else if (arg == "--dump-bootstrap" && i+1 < argc) dump_bootstrap = argv[++i]; else if (arg == "--dump-file" && i+1 < argc) dump_file = argv[++i]; else if (arg == "-n" && i+1 < argc) network = argv[++i]; @@ -1983,10 +1830,9 @@ int main(int argc, char* argv[]) { unfreeze_on_demand = (val == "true") ? 1 : 0; } else if (arg == "-k" && i+1 < argc) public_key = argv[++i]; - else if (arg == "--account" && i+1 < argc) i++; // already handled in first pass } - cmd_service(name, ports, type, bootstrap, bootstrap_file, files, list, info, logs, tail, sleep, wake, destroy, resize, execute, command, dump_bootstrap, dump_file, redeploy, network, vcpu, envs, env_file, env_action, env_target, set_unfreeze_on_demand_id, set_unfreeze_on_demand_enabled, unfreeze_on_demand, public_key, secret_key); + cmd_service(name, ports, type, bootstrap, bootstrap_file, files, list, info, logs, tail, sleep, wake, destroy, resize, execute, command, dump_bootstrap, dump_file, network, vcpu, envs, env_file, env_action, env_target, set_unfreeze_on_demand_id, set_unfreeze_on_demand_enabled, unfreeze_on_demand, public_key, secret_key); return 0; } @@ -1997,7 +1843,6 @@ int main(int argc, char* argv[]) { string arg = argv[i]; if (arg == "--extend") extend = true; else if (arg == "-k" && i+1 < argc) public_key = argv[++i]; - else if (arg == "--account" && i+1 < argc) i++; // already handled in first pass } cmd_validate_key(extend, public_key, secret_key); @@ -2011,7 +1856,6 @@ int main(int argc, char* argv[]) { string arg = argv[i]; if (arg == "--json") json_output = true; else if (arg == "-k" && i+1 < argc) public_key = argv[++i]; - else if (arg == "--account" && i+1 < argc) i++; // already handled in first pass } cmd_languages(json_output, public_key, secret_key); @@ -2032,7 +1876,6 @@ int main(int argc, char* argv[]) { else if (arg == "-n" && i+1 < argc) network = argv[++i]; else if (arg == "-v" && i+1 < argc) vcpu = stoi(argv[++i]); else if (arg == "-k" && i+1 < argc) public_key = argv[++i]; - else if (arg == "--account" && i+1 < argc) i++; // already handled in first pass else if (arg[0] == '-') { cerr << RED << "Unknown option: " << arg << RESET << endl; return 1; diff --git a/clients/crystal/sync/src/un.cr b/clients/crystal/sync/src/un.cr index f27a4c5..40b8ed5 100644 --- a/clients/crystal/sync/src/un.cr +++ b/clients/crystal/sync/src/un.cr @@ -132,72 +132,21 @@ def save_languages_cache(response : JSON::Any) end end -def load_accounts_csv(path : String, index : Int32) : {String, String}? - return nil unless File.exists?(path) - begin - lines = File.read(path).split('\n').select do |l| - t = l.strip - !t.empty? && !t.starts_with?('#') +def get_api_keys(args_key : String?) : {String, String?} + public_key = ENV["UNSANDBOX_PUBLIC_KEY"]? + secret_key = ENV["UNSANDBOX_SECRET_KEY"]? + + # Fall back to UNSANDBOX_API_KEY for backwards compatibility + if public_key.nil? || public_key.empty? || secret_key.nil? || secret_key.empty? + legacy_key = args_key || ENV["UNSANDBOX_API_KEY"]? + if legacy_key.nil? || legacy_key.empty? + STDERR.puts "#{RED}Error: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set#{RESET}" + exit 1 end - return nil if index < 0 || index >= lines.size - parts = lines[index].split(',') - return nil if parts.size < 2 - pk = parts[0].strip - sk = parts[1].strip - return nil if pk.empty? || sk.empty? - {pk, sk} - rescue - nil - end -end - -def get_api_keys(args_key : String?, args_public_key : String? = nil, account : Int32? = nil) : {String, String?} - # Tier 1: explicit -p/-k flags - if args_public_key && !args_public_key.empty? && args_key && !args_key.empty? - return {args_public_key, args_key} - end - - # Tier 2: --account N → accounts.csv row N (bypasses env vars) - if !account.nil? - idx = account.not_nil! - home = ENV["HOME"]? - if home && !home.empty? - result = load_accounts_csv(File.join(home, ".unsandbox", "accounts.csv"), idx) - return {result[0], result[1]} if result - end - result = load_accounts_csv("accounts.csv", idx) - return {result[0], result[1]} if result - STDERR.puts "#{RED}Error: --account #{idx} not found in accounts.csv#{RESET}" - exit 1 - end - - # Tier 3: env vars - env_pk = ENV["UNSANDBOX_PUBLIC_KEY"]? - env_sk = ENV["UNSANDBOX_SECRET_KEY"]? - if env_pk && !env_pk.empty? && env_sk && !env_sk.empty? - return {env_pk, env_sk} - end - - # Tier 4: ~/.unsandbox/accounts.csv row 0 (or UNSANDBOX_ACCOUNT env var) - home = ENV["HOME"]? - def_index = (ENV["UNSANDBOX_ACCOUNT"]?.try(&.to_i?) || 0).to_i32 - if home && !home.empty? - result = load_accounts_csv(File.join(home, ".unsandbox", "accounts.csv"), def_index) - return {result[0], result[1]} if result - end - - # Tier 5: ./accounts.csv row 0 - result = load_accounts_csv("accounts.csv", def_index) - return {result[0], result[1]} if result - - # Legacy UNSANDBOX_API_KEY fallback - legacy_key = args_key || ENV["UNSANDBOX_API_KEY"]? - if legacy_key && !legacy_key.empty? return {legacy_key, nil} end - STDERR.puts "#{RED}Error: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set#{RESET}" - exit 1 + {public_key, secret_key} end def extract_challenge_id(response_body : String) : String? @@ -445,7 +394,7 @@ def build_env_content(envs : Array(String), env_file : String?) : String end def cmd_service_env(args) - public_key, secret_key = get_api_keys(args[:api_key]?.as?(String), args_public_key: args[:public_key]?.as?(String), account: args[:account]?.as?(Int32)) + public_key, secret_key = get_api_keys(args[:api_key]?.as?(String)) action = args[:env_action]?.as?(String) || "" target = args[:env_target]?.as?(String) || "" @@ -514,7 +463,7 @@ def cmd_service_env(args) end def cmd_execute(args) - public_key, secret_key = get_api_keys(args[:api_key]?.as?(String), args_public_key: args[:public_key]?.as?(String), account: args[:account]?.as?(Int32)) + public_key, secret_key = get_api_keys(args[:api_key]?) filename = args[:source_file].as(String) unless File.exists?(filename) @@ -604,7 +553,7 @@ def cmd_execute(args) end def cmd_session(args) - public_key, secret_key = get_api_keys(args[:api_key]?.as?(String), args_public_key: args[:public_key]?.as?(String), account: args[:account]?.as?(Int32)) + public_key, secret_key = get_api_keys(args[:api_key]?) if args[:list]?.as?(Bool) result = api_request("/sessions", public_key, secret_key) @@ -712,7 +661,7 @@ def cmd_session(args) end def cmd_languages(args) - public_key, secret_key = get_api_keys(args[:api_key]?.as?(String), args_public_key: args[:public_key]?.as?(String), account: args[:account]?.as?(Int32)) + public_key, secret_key = get_api_keys(args[:api_key]?) # Try to load from cache first cached_response = load_languages_cache @@ -745,7 +694,7 @@ def cmd_languages(args) end def cmd_key(args) - public_key, secret_key = get_api_keys(args[:api_key]?.as?(String), args_public_key: args[:public_key]?.as?(String), account: args[:account]?.as?(Int32)) + public_key, secret_key = get_api_keys(args[:api_key]?) # Validate key url = URI.parse(PORTAL_BASE + "/keys/validate") @@ -839,7 +788,7 @@ def cmd_key(args) end def cmd_image(args) - public_key, secret_key = get_api_keys(args[:api_key]?.as?(String), args_public_key: args[:public_key]?.as?(String), account: args[:account]?.as?(Int32)) + public_key, secret_key = get_api_keys(args[:api_key]?) if args[:list]?.as?(Bool) result = api_request("/images", public_key, secret_key) @@ -1000,7 +949,7 @@ def cmd_image(args) end def cmd_snapshot(args) - public_key, secret_key = get_api_keys(args[:api_key]?.as?(String), args_public_key: args[:public_key]?.as?(String), account: args[:account]?.as?(Int32)) + public_key, secret_key = get_api_keys(args[:api_key]?) if args[:list]?.as?(Bool) result = api_request("/snapshots", public_key, secret_key) @@ -1123,7 +1072,7 @@ def cmd_snapshot(args) end def cmd_logs(args) - public_key, secret_key = get_api_keys(args[:api_key]?.as?(String), args_public_key: args[:public_key]?.as?(String), account: args[:account]?.as?(Int32)) + public_key, secret_key = get_api_keys(args[:api_key]?) source = args[:logs_source]?.as?(String) || "all" lines = args[:logs_lines]?.as?(Int32) || 100 @@ -1238,7 +1187,7 @@ def cmd_service(args) end end - public_key, secret_key = get_api_keys(args[:api_key]?.as?(String), args_public_key: args[:public_key]?.as?(String), account: args[:account]?.as?(Int32)) + public_key, secret_key = get_api_keys(args[:api_key]?) if args[:list]?.as?(Bool) result = api_request("/services", public_key, secret_key) @@ -1492,8 +1441,6 @@ def main args = { source_file: nil, api_key: nil, - public_key: nil, - account: nil, network: nil, env: [] of String, files: [] of String, @@ -1582,9 +1529,7 @@ def main parser = OptionParser.new do |opts| opts.banner = "Usage: un.cr [options] \n un.cr languages [--json]\n un.cr session [options]\n un.cr service [options]\n un.cr service env [options]\n un.cr snapshot [options]\n un.cr image [options]\n un.cr logs [options]\n un.cr key [options]\n un.cr health\n un.cr version\n\nService env commands:\n env status Show vault status\n env set Set vault (-e KEY=VALUE or --env-file FILE)\n env export Export vault contents\n env delete Delete vault" - opts.on("-k API_KEY", "--api-key=API_KEY", "Secret/API key") { |k| args[:api_key] = k } - opts.on("-p PUBLIC_KEY", "--public-key=PUBLIC_KEY", "Public key (use with -k for secret key)") { |k| args[:public_key] = k } - opts.on("--account=N", "Use row N from accounts.csv (0-based)") { |n| args[:account] = n.to_i32 } + opts.on("-k API_KEY", "--api-key=API_KEY", "API key") { |k| args[:api_key] = k } opts.on("-n NETWORK", "--network=NETWORK", "Network mode") { |n| args[:network] = n } opts.on("-e ENV", "--env=ENV", "Environment variable (KEY=VALUE)") { |e| args[:env].as(Array(String)) << e diff --git a/clients/csharp/sync/src/Un.cs b/clients/csharp/sync/src/Un.cs index 327e4f6..3d1a303 100644 --- a/clients/csharp/sync/src/Un.cs +++ b/clients/csharp/sync/src/Un.cs @@ -92,10 +92,6 @@ class Un { CmdKey(parsedArgs); } - else if (parsedArgs.Command == "languages") - { - CmdLanguages(parsedArgs); - } else if (parsedArgs.SourceFile != null) { CmdExecute(parsedArgs); @@ -115,7 +111,7 @@ class Un static void CmdExecute(Args args) { - var (publicKey, secretKey) = GetApiKeys(args.ApiKey, args.Account); + var (publicKey, secretKey) = GetApiKeys(args.ApiKey); string code = File.ReadAllText(args.SourceFile); string language = DetectLanguage(args.SourceFile); @@ -202,7 +198,7 @@ class Un static void CmdSession(Args args) { - var (publicKey, secretKey) = GetApiKeys(args.ApiKey, args.Account); + var (publicKey, secretKey) = GetApiKeys(args.ApiKey); if (args.SessionList) { @@ -255,7 +251,7 @@ class Un static void CmdKey(Args args) { - var (publicKey, secretKey) = GetApiKeys(args.ApiKey, args.Account); + var (publicKey, secretKey) = GetApiKeys(args.ApiKey); var result = ApiRequest("/keys/validate", "POST", null, publicKey, secretKey); @@ -340,7 +336,7 @@ class Un static void CmdService(Args args) { - var (publicKey, secretKey) = GetApiKeys(args.ApiKey, args.Account); + var (publicKey, secretKey) = GetApiKeys(args.ApiKey); // Handle env subcommand if (!string.IsNullOrEmpty(args.EnvAction)) @@ -442,32 +438,6 @@ class Un return; } - if (args.ServiceRedeploy != null) - { - var payload = new Dictionary(); - if (args.ServiceBootstrap != null) - { - payload["bootstrap"] = args.ServiceBootstrap; - } - if (args.Files.Count > 0) - { - var inputFiles = new List>(); - foreach (var filepath in args.Files) - { - var content = File.ReadAllBytes(filepath); - inputFiles.Add(new Dictionary - { - ["filename"] = Path.GetFileName(filepath), - ["content"] = Convert.ToBase64String(content) - }); - } - payload["input_files"] = inputFiles; - } - ApiRequest($"/services/{args.ServiceRedeploy}/redeploy", "POST", payload.Count > 0 ? payload : null, publicKey, secretKey); - Console.WriteLine($"{GREEN}Service redeployed: {args.ServiceRedeploy}{RESET}"); - return; - } - if (args.ServiceExecute != null) { var payload = new Dictionary @@ -559,20 +529,6 @@ class Un { payload["unfreeze_on_demand"] = true; } - if (args.Files.Count > 0) - { - var inputFiles = new List>(); - foreach (var filepath in args.Files) - { - var content = File.ReadAllBytes(filepath); - inputFiles.Add(new Dictionary - { - ["filename"] = Path.GetFileName(filepath), - ["content"] = Convert.ToBase64String(content) - }); - } - payload["input_files"] = inputFiles; - } var result = ApiRequest("/services", "POST", payload, publicKey, secretKey); string serviceId = result.ContainsKey("id") ? (string)result["id"] : null; @@ -606,77 +562,24 @@ class Un Environment.Exit(1); } - static (string, string) LoadAccountsCSV(string path, int index) + static (string, string) GetApiKeys(string argsKey) { - if (!File.Exists(path)) return (null, null); - int row = 0; - foreach (string rawLine in File.ReadAllLines(path)) - { - string line = rawLine.Trim(); - if (line.Length == 0 || line.StartsWith("#")) continue; - if (row == index) - { - string[] parts = line.Split(','); - if (parts.Length >= 2) - return (parts[0].Trim(), parts[1].Trim()); - } - row++; - } - return (null, null); - } - - static (string, string) GetApiKeys(string argsKey, int accountIndex = -1) - { - // Tier 1: explicit -p/-k flags (argsKey covers legacy -k/--api-key) - // (handled by callers that pass explicit keys directly to ApiRequest) - - // Tier 2: --account N → accounts.csv row N (bypasses env vars) - if (accountIndex >= 0) - { - string home = Environment.GetEnvironmentVariable("HOME") - ?? Environment.GetEnvironmentVariable("USERPROFILE") ?? "."; - string homeCsv = Path.Combine(home, ".unsandbox", "accounts.csv"); - var (pk1, sk1) = LoadAccountsCSV(homeCsv, accountIndex); - if (!string.IsNullOrEmpty(pk1) && !string.IsNullOrEmpty(sk1)) - return (pk1, sk1); - var (pk2, sk2) = LoadAccountsCSV("accounts.csv", accountIndex); - if (!string.IsNullOrEmpty(pk2) && !string.IsNullOrEmpty(sk2)) - return (pk2, sk2); - Console.Error.WriteLine($"{RED}Error: No account at index {accountIndex} in accounts.csv{RESET}"); - Environment.Exit(1); - } - - // Tier 3: environment variables string publicKey = Environment.GetEnvironmentVariable("UNSANDBOX_PUBLIC_KEY"); string secretKey = Environment.GetEnvironmentVariable("UNSANDBOX_SECRET_KEY"); - if (!string.IsNullOrEmpty(publicKey) && !string.IsNullOrEmpty(secretKey)) - return (publicKey, secretKey); - - // Tier 4: ~/.unsandbox/accounts.csv row 0 (or UNSANDBOX_ACCOUNT env var) - int defaultIdx = 0; - string acctEnv = Environment.GetEnvironmentVariable("UNSANDBOX_ACCOUNT"); - if (!string.IsNullOrEmpty(acctEnv) && int.TryParse(acctEnv, out int parsedIdx)) - defaultIdx = parsedIdx; - string home2 = Environment.GetEnvironmentVariable("HOME") - ?? Environment.GetEnvironmentVariable("USERPROFILE") ?? "."; - string homeCsv2 = Path.Combine(home2, ".unsandbox", "accounts.csv"); - var (pk3, sk3) = LoadAccountsCSV(homeCsv2, defaultIdx); - if (!string.IsNullOrEmpty(pk3) && !string.IsNullOrEmpty(sk3)) - return (pk3, sk3); - - // Tier 5: ./accounts.csv row 0 - var (pk4, sk4) = LoadAccountsCSV("accounts.csv", defaultIdx); - if (!string.IsNullOrEmpty(pk4) && !string.IsNullOrEmpty(sk4)) - return (pk4, sk4); // Fall back to UNSANDBOX_API_KEY for backwards compatibility - string legacyKey = argsKey ?? Environment.GetEnvironmentVariable("UNSANDBOX_API_KEY"); - if (string.IsNullOrEmpty(legacyKey)) + if (string.IsNullOrEmpty(publicKey) || string.IsNullOrEmpty(secretKey)) { - Console.Error.WriteLine($"{RED}Error: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set{RESET}"); - Environment.Exit(1); + string legacyKey = argsKey ?? Environment.GetEnvironmentVariable("UNSANDBOX_API_KEY"); + if (string.IsNullOrEmpty(legacyKey)) + { + Console.Error.WriteLine($"{RED}Error: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set{RESET}"); + Environment.Exit(1); + } + return (legacyKey, null); } - return (legacyKey, null); + + return (publicKey, secretKey); } static string DetectLanguage(string filename) @@ -1331,13 +1234,10 @@ class Un public string ServiceShowFreezePage = null; public bool ServiceShowFreezePageEnabled = true; public bool ServiceCreateUnfreezeOnDemand = false; - public string ServiceRedeploy = null; public string EnvFile = null; public string EnvAction = null; public string EnvTarget = null; public bool KeyExtend = false; - public bool LanguagesJson = false; - public int Account = -1; } static Args ParseArgs(string[] args) @@ -1349,7 +1249,6 @@ class Un if (arg == "session") result.Command = "session"; else if (arg == "service") result.Command = "service"; else if (arg == "key") result.Command = "key"; - else if (arg == "languages") result.Command = "languages"; else if (arg == "env" && result.Command == "service") { // Parse: service env @@ -1396,99 +1295,12 @@ class Un else if (arg == "--show-freeze-page") result.ServiceShowFreezePage = args[++i]; else if (arg == "--show-freeze-page-enabled") result.ServiceShowFreezePageEnabled = args[++i].ToLower() == "true"; else if (arg == "--with-unfreeze-on-demand") result.ServiceCreateUnfreezeOnDemand = true; - else if (arg == "--redeploy") result.ServiceRedeploy = args[++i]; else if (arg == "--extend") result.KeyExtend = true; - else if (arg == "--json") result.LanguagesJson = true; - else if (arg == "--account") result.Account = int.Parse(args[++i]); else if (!arg.StartsWith("-")) result.SourceFile = arg; } return result; } - static string GetLanguagesCachePath() - { - string home = Environment.GetEnvironmentVariable("HOME") - ?? Environment.GetEnvironmentVariable("USERPROFILE") - ?? "."; - return Path.Combine(home, ".unsandbox", "languages.json"); - } - - static List LoadLanguagesCache() - { - string cachePath = GetLanguagesCachePath(); - if (!File.Exists(cachePath)) return null; - - try - { - string content = File.ReadAllText(cachePath); - double mtime = new DateTimeOffset(File.GetLastWriteTimeUtc(cachePath)).ToUnixTimeSeconds(); - double now = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); - if (now - mtime < 3600) - { - var data = ParseJson(content); - if (data.ContainsKey("languages") && data["languages"] is List langs) - return langs.ConvertAll(x => x.ToString()); - } - } - catch { } - return null; - } - - static void SaveLanguagesCache(List languages) - { - try - { - string cachePath = GetLanguagesCachePath(); - string cacheDir = Path.GetDirectoryName(cachePath); - if (!Directory.Exists(cacheDir)) Directory.CreateDirectory(cacheDir); - - long timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); - var sb = new StringBuilder(); - sb.Append("{\"languages\":["); - for (int i = 0; i < languages.Count; i++) - { - if (i > 0) sb.Append(","); - sb.Append("\"").Append(languages[i]).Append("\""); - } - sb.Append("],\"timestamp\":").Append(timestamp).Append("}"); - File.WriteAllText(cachePath, sb.ToString()); - } - catch { } - } - - static void CmdLanguages(Args args) - { - // Try cache first - var languages = LoadLanguagesCache(); - - if (languages == null) - { - var (publicKey, secretKey) = GetApiKeys(args.ApiKey, args.Account); - var result = ApiRequest("/languages", "GET", null, publicKey, secretKey); - languages = new List(); - if (result.ContainsKey("languages") && result["languages"] is List langs) - languages = langs.ConvertAll(x => x.ToString()); - SaveLanguagesCache(languages); - } - - if (args.LanguagesJson) - { - var sb = new StringBuilder("["); - for (int i = 0; i < languages.Count; i++) - { - if (i > 0) sb.Append(","); - sb.Append("\"").Append(languages[i]).Append("\""); - } - sb.Append("]"); - Console.WriteLine(sb.ToString()); - } - else - { - foreach (var lang in languages) - Console.WriteLine(lang); - } - } - static void PrintHelp() { Console.WriteLine(@"Usage: Un [options] @@ -1496,7 +1308,6 @@ class Un Un service [options] Un service env [options] Un key [options] - Un languages [--json] Execute options: -e KEY=VALUE Set environment variable @@ -1529,7 +1340,6 @@ Service options: --show-freeze-page-enabled BOOL Enable/disable (default: true) --with-unfreeze-on-demand Enable unfreeze-on-demand when creating service --destroy ID Destroy service - --redeploy ID Re-run bootstrap (with optional --bootstrap, -f) --execute ID Execute command in service --command CMD Command to execute (with --execute) --dump-bootstrap ID Dump bootstrap script @@ -1544,10 +1354,7 @@ Service env commands: env delete ID Delete vault Key options: - --extend Open browser to extend expired key - -Languages options: - --json Output as JSON array"); + --extend Open browser to extend expired key"); } } @@ -1561,7 +1368,7 @@ Languages options: public static class Unsandbox { private const string API_BASE = "https://api.unsandbox.com"; - private const string VERSION = "4.3.4"; + private const string VERSION = "4.3.1"; private static string _lastError; /// Extension map for language detection @@ -1679,76 +1486,15 @@ public static class Unsandbox catch (Exception ex) { _lastError = ex.Message; return new List(); } } - private const int LANGUAGES_CACHE_TTL = 3600; // 1 hour - - private static string GetLanguagesCachePath() - { - string home = Environment.GetEnvironmentVariable("HOME") - ?? Environment.GetEnvironmentVariable("USERPROFILE") - ?? "."; - return Path.Combine(home, ".unsandbox", "languages.json"); - } - - private static List LoadLanguagesCache() - { - string cachePath = GetLanguagesCachePath(); - if (!File.Exists(cachePath)) return null; - - try - { - string content = File.ReadAllText(cachePath); - double mtime = new DateTimeOffset(File.GetLastWriteTimeUtc(cachePath)).ToUnixTimeSeconds(); - double now = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); - if (now - mtime < LANGUAGES_CACHE_TTL) - { - var data = ParseJson(content); - if (data.ContainsKey("languages") && data["languages"] is List langs) - return langs.ConvertAll(x => x.ToString()); - } - } - catch { } - return null; - } - - private static void SaveLanguagesCache(List languages) - { - try - { - string cachePath = GetLanguagesCachePath(); - string cacheDir = Path.GetDirectoryName(cachePath); - if (!Directory.Exists(cacheDir)) Directory.CreateDirectory(cacheDir); - - long timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); - var sb = new StringBuilder(); - sb.Append("{\"languages\":["); - for (int i = 0; i < languages.Count; i++) - { - if (i > 0) sb.Append(","); - sb.Append("\"").Append(languages[i]).Append("\""); - } - sb.Append("],\"timestamp\":").Append(timestamp).Append("}"); - File.WriteAllText(cachePath, sb.ToString()); - } - catch { } - } - - /// Get available programming languages (cached for 1 hour) + /// Get available programming languages public static List GetLanguages(string publicKey = null, string secretKey = null) { - // Try cache first - var cached = LoadLanguagesCache(); - if (cached != null) return cached; - var (pk, sk) = ResolveKeys(publicKey, secretKey); try { var result = ApiCall("/languages", "GET", null, pk, sk); if (result.ContainsKey("languages") && result["languages"] is List langs) - { - var languages = langs.ConvertAll(x => x.ToString()); - SaveLanguagesCache(languages); - return languages; - } + return langs.ConvertAll(x => x.ToString()); return new List(); } catch (Exception ex) { _lastError = ex.Message; return new List(); } @@ -1886,7 +1632,7 @@ public static class Unsandbox catch (Exception ex) { _lastError = ex.Message; return null; } } - public static string ServiceCreate(string name, string ports = null, string domains = null, string bootstrap = null, string networkMode = null, List> inputFiles = null, string publicKey = null, string secretKey = null) + public static string ServiceCreate(string name, string ports = null, string domains = null, string bootstrap = null, string networkMode = null, string publicKey = null, string secretKey = null) { var (pk, sk) = ResolveKeys(publicKey, secretKey); var payload = new Dictionary { ["name"] = name }; @@ -1899,7 +1645,6 @@ public static class Unsandbox if (domains != null) payload["domains"] = domains; if (bootstrap != null) payload["bootstrap"] = bootstrap; if (networkMode != null) payload["network"] = networkMode; - if (inputFiles != null && inputFiles.Count > 0) payload["input_files"] = inputFiles; try { var result = ApiCall("/services", "POST", payload, pk, sk); @@ -1951,16 +1696,10 @@ public static class Unsandbox catch (Exception ex) { _lastError = ex.Message; return false; } } - public static bool ServiceRedeploy(string serviceId, string bootstrap = null, List> inputFiles = null, string publicKey = null, string secretKey = null) + public static bool ServiceRedeploy(string serviceId, string bootstrap = null, string publicKey = null, string secretKey = null) { var (pk, sk) = ResolveKeys(publicKey, secretKey); - Dictionary payload = null; - if (bootstrap != null || (inputFiles != null && inputFiles.Count > 0)) - { - payload = new Dictionary(); - if (bootstrap != null) payload["bootstrap"] = bootstrap; - if (inputFiles != null && inputFiles.Count > 0) payload["input_files"] = inputFiles; - } + var payload = bootstrap != null ? new Dictionary { ["bootstrap"] = bootstrap } : null; try { ApiCall($"/services/{serviceId}/redeploy", "POST", payload, pk, sk); return true; } catch (Exception ex) { _lastError = ex.Message; return false; } } diff --git a/clients/d/sync/src/un.d b/clients/d/sync/src/un.d index 2f3e4fc..a510d56 100644 --- a/clients/d/sync/src/un.d +++ b/clients/d/sync/src/un.d @@ -52,7 +52,6 @@ import std.string; import std.conv; import std.array; import std.algorithm; -import std.typecons; immutable string API_BASE = "https://api.unsandbox.com"; immutable string PORTAL_BASE = "https://unsandbox.com"; @@ -1556,82 +1555,13 @@ void validateKey(string publicKey, string secretKey, bool extend) { } } -// Load a row from an accounts.csv file (format: public_key,secret_key per line). -// Lines starting with '#' and blank lines are skipped. Returns the Nth data row. -Tuple!(string, string) loadAccountsCSV(string path, int index) { - import std.file : exists, readText; - import std.range : empty; - if (!exists(path)) return tuple("", ""); - string content = readText(path); - int row = 0; - foreach (line; content.splitLines()) { - string stripped = line.strip(); - if (stripped.empty || stripped[0] == '#') continue; - if (row == index) { - auto parts = stripped.findSplit(","); - if (!parts[1].empty) return tuple(parts[0], parts[2]); - return tuple("", ""); - } - row++; - } - return tuple("", ""); -} - int main(string[] args) { - string publicKey; - string secretKey; - int accountIndex = -1; // -1 = not set - string explicitPublicKey; + string publicKey = environment.get("UNSANDBOX_PUBLIC_KEY", ""); + string secretKey = environment.get("UNSANDBOX_SECRET_KEY", ""); - // First pass: scan for --account N and -p flags - for (size_t i = 1; i < args.length; i++) { - if (args[i] == "--account" && i+1 < args.length) { - accountIndex = to!int(args[++i]); - } else if (args[i] == "-p" && i+1 < args.length) { - explicitPublicKey = args[++i]; - } - } - - if (accountIndex >= 0) { - // --account N: load from ~/.unsandbox/accounts.csv, bypassing env vars - string home = environment.get("HOME", "."); - string csvPath = home ~ "/.unsandbox/accounts.csv"; - auto creds = loadAccountsCSV(csvPath, accountIndex); - if (creds[0].empty) { - creds = loadAccountsCSV("accounts.csv", accountIndex); - } - if (!creds[0].empty) { - publicKey = explicitPublicKey.empty ? creds[0] : explicitPublicKey; - secretKey = creds[1]; - } - } else { - publicKey = explicitPublicKey.empty - ? environment.get("UNSANDBOX_PUBLIC_KEY", "") - : explicitPublicKey; - secretKey = environment.get("UNSANDBOX_SECRET_KEY", ""); - - // Fall back to UNSANDBOX_API_KEY for backwards compatibility - if (publicKey.empty) { - publicKey = environment.get("UNSANDBOX_API_KEY", ""); - } - - // Try UNSANDBOX_ACCOUNT env var to pick a row - int envAccount = -1; - string envAcct = environment.get("UNSANDBOX_ACCOUNT", ""); - if (!envAcct.empty) envAccount = to!int(envAcct); - - if (publicKey.empty) { - string home = environment.get("HOME", "."); - string csvPath = home ~ "/.unsandbox/accounts.csv"; - auto creds = loadAccountsCSV(csvPath, envAccount >= 0 ? envAccount : 0); - if (creds[0].empty) { - creds = loadAccountsCSV("accounts.csv", envAccount >= 0 ? envAccount : 0); - } - if (!creds[0].empty) { - publicKey = creds[0]; - secretKey = creds[1]; - } - } + // Fall back to UNSANDBOX_API_KEY for backwards compatibility + if (publicKey.empty) { + publicKey = environment.get("UNSANDBOX_API_KEY", ""); } if (args.length < 2) { @@ -1668,7 +1598,6 @@ int main(string[] args) { else if (args[i] == "--screen") screen = true; else if (args[i] == "-f" && i+1 < args.length) inputFiles ~= args[++i]; else if (args[i] == "-k" && i+1 < args.length) publicKey = args[++i]; - else if (args[i] == "--account" && i+1 < args.length) i++; // already handled in first pass } cmdSession(list, kill, shell, network, vcpu, tmux, screen, inputFiles, publicKey, secretKey); @@ -1696,7 +1625,6 @@ int main(string[] args) { if (args[i] == "-e" && i+1 < args.length) svcEnvs ~= args[++i]; else if (args[i] == "--env-file" && i+1 < args.length) svcEnvFile = args[++i]; else if (args[i] == "-k" && i+1 < args.length) publicKey = args[++i]; - else if (args[i] == "--account" && i+1 < args.length) i++; // already handled in first pass } cmdService(name, ports, bootstrap, bootstrapFile, type, list, info, logs, tail, sleep, wake, destroy, resize, resizeVcpu, execute, command, dumpBootstrap, dumpFile, unfreezeOnDemand, unfreezeOnDemandEnabled, createUnfreezeOnDemand, network, vcpu, inputFiles, svcEnvs, svcEnvFile, envAction, envTarget, publicKey, secretKey); return 0; @@ -1730,7 +1658,6 @@ int main(string[] args) { else if (args[i] == "-e" && i+1 < args.length) svcEnvs ~= args[++i]; else if (args[i] == "--env-file" && i+1 < args.length) svcEnvFile = args[++i]; else if (args[i] == "-k" && i+1 < args.length) publicKey = args[++i]; - else if (args[i] == "--account" && i+1 < args.length) i++; // already handled in first pass } cmdService(name, ports, bootstrap, bootstrapFile, type, list, info, logs, tail, sleep, wake, destroy, resize, resizeVcpu, execute, command, dumpBootstrap, dumpFile, unfreezeOnDemand, unfreezeOnDemandEnabled, createUnfreezeOnDemand, network, vcpu, inputFiles, svcEnvs, svcEnvFile, envAction, envTarget, publicKey, secretKey); @@ -1743,7 +1670,6 @@ int main(string[] args) { for (size_t i = 2; i < args.length; i++) { if (args[i] == "--extend") extend = true; else if (args[i] == "-k" && i+1 < args.length) publicKey = args[++i]; - else if (args[i] == "--account" && i+1 < args.length) i++; // already handled in first pass } if (publicKey.empty) { @@ -1761,7 +1687,6 @@ int main(string[] args) { for (size_t i = 2; i < args.length; i++) { if (args[i] == "--json") jsonOutput = true; else if (args[i] == "-k" && i+1 < args.length) publicKey = args[++i]; - else if (args[i] == "--account" && i+1 < args.length) i++; // already handled in first pass } if (publicKey.empty) { @@ -1795,7 +1720,6 @@ int main(string[] args) { else if (args[i] == "--name" && i+1 < args.length) name = args[++i]; else if (args[i] == "--ports" && i+1 < args.length) ports = args[++i]; else if (args[i] == "-k" && i+1 < args.length) publicKey = args[++i]; - else if (args[i] == "--account" && i+1 < args.length) i++; // already handled in first pass } if (publicKey.empty) { @@ -1819,7 +1743,6 @@ int main(string[] args) { else if (args[i] == "-n" && i+1 < args.length) network = args[++i]; else if (args[i] == "-v" && i+1 < args.length) vcpu = to!int(args[++i]); else if (args[i] == "-k" && i+1 < args.length) publicKey = args[++i]; - else if (args[i] == "--account" && i+1 < args.length) i++; // already handled in first pass else if (args[i].startsWith("-")) { stderr.writefln("%sUnknown option: %s%s", RED, args[i], RESET); return 1; diff --git a/clients/dart/sync/src/un.dart b/clients/dart/sync/src/un.dart index 2c37ba1..deba76c 100644 --- a/clients/dart/sync/src/un.dart +++ b/clients/dart/sync/src/un.dart @@ -73,8 +73,6 @@ class Args { String? command; String? sourceFile; String? apiKey; - String? publicKey; - int? account; String? network; int vcpu = 0; List env = []; @@ -143,73 +141,21 @@ class Args { bool snapshotHot = false; } -Map? loadAccountsCSV(String path, int index) { - try { - final file = File(path); - if (!file.existsSync()) return null; - final lines = file.readAsLinesSync().where((l) { - final t = l.trim(); - return t.isNotEmpty && !t.startsWith('#'); - }).toList(); - if (index < 0 || index >= lines.length) return null; - final parts = lines[index].split(','); - if (parts.length < 2) return null; - final pk = parts[0].trim(); - final sk = parts[1].trim(); - if (pk.isEmpty || sk.isEmpty) return null; - return {'pk': pk, 'sk': sk}; - } catch (e) { - return null; - } -} +List getApiKeys(String? argsKey) { + final publicKey = Platform.environment['UNSANDBOX_PUBLIC_KEY']; + final secretKey = Platform.environment['UNSANDBOX_SECRET_KEY']; -List getApiKeys(String? argsKey, {String? argsPublicKey, int? account}) { - // Tier 1: explicit -p/-k flags - if (argsPublicKey != null && argsPublicKey.isNotEmpty && argsKey != null && argsKey.isNotEmpty) { - return [argsPublicKey, argsKey]; - } - - // Tier 2: --account N → accounts.csv row N (bypasses env vars) - if (account != null) { - final home = Platform.environment['HOME'] ?? ''; - if (home.isNotEmpty) { - final fromHome = loadAccountsCSV('$home/.unsandbox/accounts.csv', account); - if (fromHome != null) return [fromHome['pk'], fromHome['sk']]; + // Fall back to UNSANDBOX_API_KEY for backwards compatibility + if (publicKey == null || publicKey.isEmpty || secretKey == null || secretKey.isEmpty) { + final legacyKey = argsKey ?? Platform.environment['UNSANDBOX_API_KEY']; + if (legacyKey == null || legacyKey.isEmpty) { + stderr.writeln('${red}Error: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set$reset'); + exit(1); } - final fromLocal = loadAccountsCSV('./accounts.csv', account); - if (fromLocal != null) return [fromLocal['pk'], fromLocal['sk']]; - stderr.writeln('${red}Error: --account $account not found in accounts.csv$reset'); - exit(1); - } - - // Tier 3: env vars - final envPk = Platform.environment['UNSANDBOX_PUBLIC_KEY']; - final envSk = Platform.environment['UNSANDBOX_SECRET_KEY']; - if (envPk != null && envPk.isNotEmpty && envSk != null && envSk.isNotEmpty) { - return [envPk, envSk]; - } - - // Tier 4: ~/.unsandbox/accounts.csv row 0 (or UNSANDBOX_ACCOUNT env var) - final home = Platform.environment['HOME'] ?? ''; - final defIndexStr = Platform.environment['UNSANDBOX_ACCOUNT'] ?? '0'; - final defIndex = int.tryParse(defIndexStr) ?? 0; - if (home.isNotEmpty) { - final fromHome = loadAccountsCSV('$home/.unsandbox/accounts.csv', defIndex); - if (fromHome != null) return [fromHome['pk'], fromHome['sk']]; - } - - // Tier 5: ./accounts.csv row 0 - final fromLocal = loadAccountsCSV('./accounts.csv', defIndex); - if (fromLocal != null) return [fromLocal['pk'], fromLocal['sk']]; - - // Legacy UNSANDBOX_API_KEY fallback - final legacyKey = argsKey ?? Platform.environment['UNSANDBOX_API_KEY']; - if (legacyKey != null && legacyKey.isNotEmpty) { return [legacyKey, null]; } - stderr.writeln('${red}Error: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set$reset'); - exit(1); + return [publicKey, secretKey]; } String detectLanguage(String filename) { @@ -563,7 +509,7 @@ Future serviceEnvDelete(String serviceId, String publicKey, String? secret } Future cmdServiceEnv(Args args) async { - final keys = getApiKeys(args.apiKey, argsPublicKey: args.publicKey, account: args.account); + final keys = getApiKeys(args.apiKey); final publicKey = keys[0]!; final secretKey = keys[1]; final action = args.envAction; @@ -633,7 +579,7 @@ Future cmdServiceEnv(Args args) async { } Future cmdExecute(Args args) async { - final keys = getApiKeys(args.apiKey, argsPublicKey: args.publicKey, account: args.account); + final keys = getApiKeys(args.apiKey); final publicKey = keys[0]!; final secretKey = keys[1]; final code = await File(args.sourceFile!).readAsString(); @@ -711,7 +657,7 @@ Future cmdExecute(Args args) async { } Future cmdSession(Args args) async { - final keys = getApiKeys(args.apiKey, argsPublicKey: args.publicKey, account: args.account); + final keys = getApiKeys(args.apiKey); final publicKey = keys[0]!; final secretKey = keys[1]; @@ -771,7 +717,7 @@ Future cmdSession(Args args) async { } Future cmdService(Args args) async { - final keys = getApiKeys(args.apiKey, argsPublicKey: args.publicKey, account: args.account); + final keys = getApiKeys(args.apiKey); final publicKey = keys[0]!; final secretKey = keys[1]; @@ -995,7 +941,7 @@ Future cmdService(Args args) async { } Future cmdLanguages(Args args) async { - final keys = getApiKeys(args.apiKey, argsPublicKey: args.publicKey, account: args.account); + final keys = getApiKeys(args.apiKey); final publicKey = keys[0]!; final secretKey = keys[1]; @@ -1027,7 +973,7 @@ Future cmdLanguages(Args args) async { } Future cmdImage(Args args) async { - final keys = getApiKeys(args.apiKey, argsPublicKey: args.publicKey, account: args.account); + final keys = getApiKeys(args.apiKey); final publicKey = keys[0]!; final secretKey = keys[1]; @@ -1169,7 +1115,7 @@ Future imageTransfer(String id, String toKey, String publicKey, String? se // Snapshot functions Future cmdSnapshot(Args args) async { - final keys = getApiKeys(args.apiKey, argsPublicKey: args.publicKey, account: args.account); + final keys = getApiKeys(args.apiKey); final publicKey = keys[0]!; final secretKey = keys[1]; @@ -1373,7 +1319,7 @@ String sdkVersion() { } Future cmdKey(Args args) async { - final keys = getApiKeys(args.apiKey, argsPublicKey: args.publicKey, account: args.account); + final keys = getApiKeys(args.apiKey); final publicKey = keys[0]!; final secretKey = keys[1]; @@ -1462,13 +1408,6 @@ Args parseArgs(List argv) { case '--api-key': args.apiKey = argv[++i]; break; - case '-p': - case '--public-key': - args.publicKey = argv[++i]; - break; - case '--account': - args.account = int.parse(argv[++i]); - break; case '-n': case '--network': args.network = argv[++i]; @@ -1719,9 +1658,7 @@ Execute options: -o DIR Output directory for artifacts -n MODE Network mode (zerotrust/semitrusted) -v N vCPU count (1-8) - -p KEY Public key (use with -k for secret key) - -k KEY Secret/API key - --account N Use row N from accounts.csv (0-based) + -k KEY API key Session options: --list List active sessions diff --git a/clients/dotnet/async/src/Un.cs b/clients/dotnet/async/src/Un.cs index e1f4802..f493adf 100644 --- a/clients/dotnet/async/src/Un.cs +++ b/clients/dotnet/async/src/Un.cs @@ -12,7 +12,7 @@ using System.Text.Json.Serialization; const string API_BASE = "https://api.unsandbox.com"; const string PORTAL_BASE = "https://unsandbox.com"; -const string VERSION = "4.3.4"; +const string VERSION = "4.3.1"; // ANSI colors const string BLUE = "\x1B[34m"; diff --git a/clients/dotnet/sync/src/Un.cs b/clients/dotnet/sync/src/Un.cs index 9b3faf9..1aa0c9b 100644 --- a/clients/dotnet/sync/src/Un.cs +++ b/clients/dotnet/sync/src/Un.cs @@ -12,7 +12,7 @@ using System.Text.Json.Serialization; const string API_BASE = "https://api.unsandbox.com"; const string PORTAL_BASE = "https://unsandbox.com"; -const string VERSION = "4.3.4"; +const string VERSION = "4.3.1"; // ANSI colors const string BLUE = "\x1B[34m"; @@ -87,7 +87,7 @@ catch (Exception ex) void CmdExecute(Args args) { - var (publicKey, secretKey) = GetApiKeys(args.ApiKey, args.Account); + var (publicKey, secretKey) = GetApiKeys(args.ApiKey); var code = File.ReadAllText(args.SourceFile!); var language = DetectLanguage(args.SourceFile!); @@ -148,7 +148,7 @@ void CmdExecute(Args args) void CmdSession(Args args) { - var (publicKey, secretKey) = GetApiKeys(args.ApiKey, args.Account); + var (publicKey, secretKey) = GetApiKeys(args.ApiKey); if (args.SessionList) { @@ -224,7 +224,7 @@ void CmdSession(Args args) void CmdKey(Args args) { - var (publicKey, secretKey) = GetApiKeys(args.ApiKey, args.Account); + var (publicKey, secretKey) = GetApiKeys(args.ApiKey); var result = ApiRequest("/keys/validate", HttpMethod.Post, null, publicKey, secretKey); if (!result.TryGetValue("valid", out var validObj) || validObj is not JsonElement validEl) @@ -281,7 +281,7 @@ void OpenBrowser(string url) void CmdService(Args args) { - var (publicKey, secretKey) = GetApiKeys(args.ApiKey, args.Account); + var (publicKey, secretKey) = GetApiKeys(args.ApiKey); if (!string.IsNullOrEmpty(args.EnvAction)) { @@ -392,22 +392,7 @@ void CmdService(Args args) if (args.ServiceRedeploy != null) { - var payload = new Dictionary(); - if (args.Files.Count > 0) - { - var inputFiles = new List>(); - foreach (var filepath in args.Files) - { - var content = File.ReadAllBytes(filepath); - inputFiles.Add(new Dictionary - { - ["filename"] = Path.GetFileName(filepath), - ["content"] = Convert.ToBase64String(content) - }); - } - payload["input_files"] = inputFiles; - } - ApiRequest($"/services/{args.ServiceRedeploy}/redeploy", HttpMethod.Post, payload.Count > 0 ? payload : null, publicKey, secretKey); + ApiRequest($"/services/{args.ServiceRedeploy}/redeploy", HttpMethod.Post, null, publicKey, secretKey); Console.WriteLine($"{GREEN}Service redeploying: {args.ServiceRedeploy}{RESET}"); return; } @@ -467,20 +452,6 @@ void CmdService(Args args) if (args.Network != null) payload["network"] = args.Network; if (args.Vcpu > 0) payload["vcpu"] = args.Vcpu; if (args.ServiceCreateUnfreezeOnDemand) payload["unfreeze_on_demand"] = true; - if (args.Files.Count > 0) - { - var inputFiles = new List>(); - foreach (var filepath in args.Files) - { - var content = File.ReadAllBytes(filepath); - inputFiles.Add(new Dictionary - { - ["filename"] = Path.GetFileName(filepath), - ["content"] = Convert.ToBase64String(content) - }); - } - payload["input_files"] = inputFiles; - } var result = ApiRequest("/services", HttpMethod.Post, payload, publicKey, secretKey); var serviceId = result.TryGetValue("id", out var id) && id is JsonElement idEl ? idEl.GetString() : null; @@ -548,7 +519,7 @@ void CmdServiceEnv(Args args, string publicKey, string secretKey) void CmdSnapshot(Args args) { - var (publicKey, secretKey) = GetApiKeys(args.ApiKey, args.Account); + var (publicKey, secretKey) = GetApiKeys(args.ApiKey); if (args.SnapshotList) { @@ -620,7 +591,7 @@ void CmdSnapshot(Args args) void CmdImage(Args args) { - var (publicKey, secretKey) = GetApiKeys(args.ApiKey, args.Account); + var (publicKey, secretKey) = GetApiKeys(args.ApiKey); if (args.ImageList) { @@ -715,7 +686,7 @@ void CmdImage(Args args) void CmdLanguages(Args args) { - var (publicKey, secretKey) = GetApiKeys(args.ApiKey, args.Account); + var (publicKey, secretKey) = GetApiKeys(args.ApiKey); // Check cache first var cacheDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".unsandbox"); @@ -896,69 +867,22 @@ bool ServiceEnvSet(string serviceId, string envContent, string publicKey, string catch { return false; } } -(string, string) LoadAccountsCSV(string path, int index) +(string, string) GetApiKeys(string? argsKey) { - if (!File.Exists(path)) return (null!, null!); - var row = 0; - foreach (var rawLine in File.ReadAllLines(path)) - { - var line = rawLine.Trim(); - if (line.Length == 0 || line.StartsWith("#")) continue; - if (row == index) - { - var parts = line.Split(','); - if (parts.Length >= 2) - return (parts[0].Trim(), parts[1].Trim()); - } - row++; - } - return (null!, null!); -} - -(string, string) GetApiKeys(string? argsKey, int accountIndex = -1) -{ - // Tier 2: --account N → accounts.csv row N (bypasses env vars) - if (accountIndex >= 0) - { - var home = Environment.GetEnvironmentVariable("HOME") - ?? Environment.GetEnvironmentVariable("USERPROFILE") ?? "."; - var homeCsv = Path.Combine(home, ".unsandbox", "accounts.csv"); - var (pk1, sk1) = LoadAccountsCSV(homeCsv, accountIndex); - if (!string.IsNullOrEmpty(pk1) && !string.IsNullOrEmpty(sk1)) return (pk1, sk1); - var (pk2, sk2) = LoadAccountsCSV("accounts.csv", accountIndex); - if (!string.IsNullOrEmpty(pk2) && !string.IsNullOrEmpty(sk2)) return (pk2, sk2); - Console.Error.WriteLine($"{RED}Error: No account at index {accountIndex} in accounts.csv{RESET}"); - Environment.Exit(1); - } - - // Tier 3: environment variables var publicKey = Environment.GetEnvironmentVariable("UNSANDBOX_PUBLIC_KEY"); var secretKey = Environment.GetEnvironmentVariable("UNSANDBOX_SECRET_KEY"); - if (!string.IsNullOrEmpty(publicKey) && !string.IsNullOrEmpty(secretKey)) - return (publicKey, secretKey); - // Tier 4: ~/.unsandbox/accounts.csv row 0 (or UNSANDBOX_ACCOUNT env var) - var defaultIdx = 0; - var acctEnv = Environment.GetEnvironmentVariable("UNSANDBOX_ACCOUNT"); - if (!string.IsNullOrEmpty(acctEnv) && int.TryParse(acctEnv, out var parsedIdx)) - defaultIdx = parsedIdx; - var home2 = Environment.GetEnvironmentVariable("HOME") - ?? Environment.GetEnvironmentVariable("USERPROFILE") ?? "."; - var (pk3, sk3) = LoadAccountsCSV(Path.Combine(home2, ".unsandbox", "accounts.csv"), defaultIdx); - if (!string.IsNullOrEmpty(pk3) && !string.IsNullOrEmpty(sk3)) return (pk3, sk3); - - // Tier 5: ./accounts.csv row 0 - var (pk4, sk4) = LoadAccountsCSV("accounts.csv", defaultIdx); - if (!string.IsNullOrEmpty(pk4) && !string.IsNullOrEmpty(sk4)) return (pk4, sk4); - - // Fall back to UNSANDBOX_API_KEY for backwards compatibility - var legacyKey = argsKey ?? Environment.GetEnvironmentVariable("UNSANDBOX_API_KEY"); - if (string.IsNullOrEmpty(legacyKey)) + if (string.IsNullOrEmpty(publicKey) || string.IsNullOrEmpty(secretKey)) { - Console.Error.WriteLine($"{RED}Error: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set{RESET}"); - Environment.Exit(1); + var legacyKey = argsKey ?? Environment.GetEnvironmentVariable("UNSANDBOX_API_KEY"); + if (string.IsNullOrEmpty(legacyKey)) + { + Console.Error.WriteLine($"{RED}Error: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set{RESET}"); + Environment.Exit(1); + } + return (legacyKey, ""); } - return (legacyKey!, ""); + return (publicKey, secretKey); } string DetectLanguage(string filename) @@ -1084,7 +1008,6 @@ Args ParseArgs(string[] args) else if (arg == "--show-freeze-page-enabled") result.ServiceShowFreezePageEnabled = args[++i].ToLower() == "true"; else if (arg == "--with-unfreeze-on-demand") result.ServiceCreateUnfreezeOnDemand = true; else if (arg == "--extend") result.KeyExtend = true; - else if (arg == "--account") result.Account = int.Parse(args[++i]); else if (arg == "--delete") { var val = args[++i]; @@ -1162,7 +1085,7 @@ Service options: --lock ID Prevent deletion --unlock ID Allow deletion --resize ID Resize (use with -v) - --redeploy ID Re-run bootstrap (use -f to include input files) + --redeploy ID Re-run bootstrap --snapshot ID Create snapshot from service --unfreeze-on-demand ID Set unfreeze-on-demand for service --unfreeze-on-demand-enabled BOOL Enable/disable (default: true) @@ -1175,7 +1098,6 @@ Service options: --dump-bootstrap ID Dump bootstrap script --dump-file FILE File to save bootstrap (with --dump-bootstrap) -e KEY=VALUE Set vault env var (with --name or env set) - -f FILE Add input file (with --name or --redeploy) --env-file FILE Load vault vars from file Service env commands: @@ -1484,7 +1406,7 @@ public static class Unsandbox catch (Exception ex) { _lastError = ex.Message; return null; } } - public static string? ServiceCreate(string name, string? ports = null, string? domains = null, string? bootstrap = null, string? networkMode = null, List>? inputFiles = null, string? publicKey = null, string? secretKey = null) + public static string? ServiceCreate(string name, string? ports = null, string? domains = null, string? bootstrap = null, string? networkMode = null, string? publicKey = null, string? secretKey = null) { var (pk, sk) = ResolveKeys(publicKey, secretKey); var payload = new Dictionary { ["name"] = name }; @@ -1492,7 +1414,6 @@ public static class Unsandbox if (domains != null) payload["domains"] = domains; if (bootstrap != null) payload["bootstrap"] = bootstrap; if (networkMode != null) payload["network"] = networkMode; - if (inputFiles != null && inputFiles.Count > 0) payload["input_files"] = inputFiles; try { var result = ApiCall("/services", HttpMethod.Post, payload, pk, sk); @@ -1544,16 +1465,10 @@ public static class Unsandbox catch (Exception ex) { _lastError = ex.Message; return false; } } - public static bool ServiceRedeploy(string serviceId, string? bootstrap = null, List>? inputFiles = null, string? publicKey = null, string? secretKey = null) + public static bool ServiceRedeploy(string serviceId, string? bootstrap = null, string? publicKey = null, string? secretKey = null) { var (pk, sk) = ResolveKeys(publicKey, secretKey); - Dictionary? payload = null; - if (bootstrap != null || inputFiles != null) - { - payload = new Dictionary(); - if (bootstrap != null) payload["bootstrap"] = bootstrap; - if (inputFiles != null && inputFiles.Count > 0) payload["input_files"] = inputFiles; - } + var payload = bootstrap != null ? new Dictionary { ["bootstrap"] = bootstrap } : null; try { ApiCall($"/services/{serviceId}/redeploy", HttpMethod.Post, payload, pk, sk); return true; } catch (Exception ex) { _lastError = ex.Message; return false; } } @@ -2121,7 +2036,6 @@ class Args public bool ServiceCreateUnfreezeOnDemand; public string? EnvFile, EnvAction, EnvTarget; public bool KeyExtend; - public int Account = -1; public bool SnapshotList; public string? SnapshotInfo, SnapshotDelete, SnapshotLock, SnapshotUnlock, SnapshotClone; public string? SnapshotCloneType, SnapshotName; diff --git a/clients/elixir/sync/src/un.ex b/clients/elixir/sync/src/un.ex index 187faa5..46f81db 100755 --- a/clients/elixir/sync/src/un.ex +++ b/clients/elixir/sync/src/un.ex @@ -1,19 +1,39 @@ #!/usr/bin/env elixir -# This is free 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, & distributed like tap water -# for machine learning intelligence. +# PUBLIC DOMAIN - NO LICENSE, NO WARRANTY # -# The permacomputer is community-owned infrastructure optimized around -# four values: +# 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. # -# 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 +# The permacomputer is community-owned infrastructure optimized around four values: # -# 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. +# 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 # un.ex - Unsandbox CLI client in Elixir # @@ -51,10 +71,8 @@ defmodule Un do Credentials are loaded in priority order: 1. Function arguments (public_key, secret_key) - 2. --account N -> accounts.csv row N (bypasses env vars) - 3. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY) - 4. ~/.unsandbox/accounts.csv row 0 (or UNSANDBOX_ACCOUNT env) - 5. ./accounts.csv row 0 + 2. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY) + 3. Config file (~/.unsandbox/accounts.csv) """ @blue "\e[34m" @@ -1114,50 +1132,25 @@ defmodule Un do # CLI Entry Point # ============================================================================ - def main(raw_args) do - {account_index, args} = extract_account_arg(raw_args, nil, []) - if account_index != nil do - Process.put(:account_index, account_index) - end - dispatch(args) - end - - defp dispatch([]), do: print_usage() - defp dispatch(["session" | rest]), do: session_command(rest) - defp dispatch(["service" | rest]), do: service_command(rest) - defp dispatch(["snapshot" | rest]), do: snapshot_command(rest) - defp dispatch(["image" | rest]), do: image_command(rest) - defp dispatch(["key" | rest]), do: key_command(rest) - defp dispatch(["languages" | rest]), do: languages_command(rest) - defp dispatch(args), do: execute_command(args) - - defp extract_account_arg([], acc, rest_acc), do: {acc, Enum.reverse(rest_acc)} - defp extract_account_arg(["--account", n_str | rest], _acc, rest_acc) do - n = case Integer.parse(n_str) do - {n, ""} -> n - _ -> - IO.puts(:stderr, "Error: --account requires an integer argument") - System.halt(1) - end - extract_account_arg(rest, n, rest_acc) - end - defp extract_account_arg([arg | rest], acc, rest_acc) do - extract_account_arg(rest, acc, [arg | rest_acc]) - end + def main([]), do: print_usage() + def main(["session" | rest]), do: session_command(rest) + def main(["service" | rest]), do: service_command(rest) + def main(["snapshot" | rest]), do: snapshot_command(rest) + def main(["image" | rest]), do: image_command(rest) + def main(["key" | rest]), do: key_command(rest) + def main(["languages" | rest]), do: languages_command(rest) + def main(args), do: execute_command(args) defp print_usage do - IO.puts("Usage: un.ex [--account N] [options] ") - IO.puts(" un.ex [--account N] session [options]") - IO.puts(" un.ex [--account N] service [options]") - IO.puts(" un.ex [--account N] service env ") - IO.puts(" un.ex [--account N] snapshot [options]") - IO.puts(" un.ex [--account N] image [options]") - IO.puts(" un.ex [--account N] key [--extend]") + IO.puts("Usage: un.ex [options] ") + IO.puts(" un.ex session [options]") + IO.puts(" un.ex service [options]") + IO.puts(" un.ex service env ") + IO.puts(" un.ex snapshot [options]") + IO.puts(" un.ex image [options]") + IO.puts(" un.ex key [--extend]") IO.puts(" un.ex languages [--json]") IO.puts("") - IO.puts("Global options:") - IO.puts(" --account N Use accounts.csv row N (bypasses env vars)") - IO.puts("") IO.puts("Service options: --name, --ports, --bootstrap, -e KEY=VALUE, --env-file FILE") IO.puts(" --set-unfreeze-on-demand ID true|false") IO.puts("Service env commands: status, set, export, delete") @@ -1940,85 +1933,21 @@ defmodule Un do end # Helpers - - defp load_credentials_from_csv(csv_path, account_index) do - case File.read(csv_path) do - {:ok, content} -> - accounts = - content - |> String.split("\n") - |> Enum.map(&String.trim/1) - |> Enum.filter(fn line -> line != "" and not String.starts_with?(line, "#") end) - |> Enum.flat_map(fn line -> - case String.split(line, ",") do - [pk, sk | _] -> - pk = String.trim(pk) - sk = String.trim(sk) - if String.length(pk) > 8 and String.length(sk) > 8 do - [{pk, sk}] - else - [] - end - _ -> [] - end - end) - case Enum.at(accounts, account_index) do - nil -> :error - creds -> {:ok, creds} - end - _ -> :error - end - end - defp get_api_keys do - home = System.get_env("HOME") || "." - home_csv = Path.join([home, ".unsandbox", "accounts.csv"]) + public_key = System.get_env("UNSANDBOX_PUBLIC_KEY") + secret_key = System.get_env("UNSANDBOX_SECRET_KEY") - # Priority 1: --account N (stored in process dict by main/1) - case Process.get(:account_index) do - nil -> - # Priority 2: environment variables - public_key = System.get_env("UNSANDBOX_PUBLIC_KEY") - secret_key = System.get_env("UNSANDBOX_SECRET_KEY") - api_key = System.get_env("UNSANDBOX_API_KEY") + # Fall back to UNSANDBOX_API_KEY for backwards compatibility + api_key = System.get_env("UNSANDBOX_API_KEY") - cond do - public_key && secret_key -> - {public_key, secret_key} - api_key -> - {api_key, nil} - true -> - # Priority 3: ~/.unsandbox/accounts.csv (or UNSANDBOX_ACCOUNT index) - default_index = - case System.get_env("UNSANDBOX_ACCOUNT") do - nil -> 0 - s -> case Integer.parse(s) do {n, ""} -> n; _ -> 0 end - end - case load_credentials_from_csv(home_csv, default_index) do - {:ok, {pk, sk}} -> {pk, sk} - :error -> - # Priority 4: ./accounts.csv - case load_credentials_from_csv("accounts.csv", default_index) do - {:ok, {pk, sk}} -> {pk, sk} - :error -> - IO.puts(:stderr, "Error: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set (or UNSANDBOX_API_KEY for backwards compat)") - System.halt(1) - end - end - end - - account_index -> - # Priority 1: --account N -> accounts.csv - case load_credentials_from_csv(home_csv, account_index) do - {:ok, {pk, sk}} -> {pk, sk} - :error -> - case load_credentials_from_csv("accounts.csv", account_index) do - {:ok, {pk, sk}} -> {pk, sk} - :error -> - IO.puts(:stderr, "Error: No credentials found for account index #{account_index} in accounts.csv") - System.halt(1) - end - end + cond do + public_key && secret_key -> + {public_key, secret_key} + api_key -> + {api_key, nil} + true -> + IO.puts(:stderr, "Error: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set (or UNSANDBOX_API_KEY for backwards compat)") + System.halt(1) end end diff --git a/clients/elixir/sync/tests/test_functional.exs b/clients/elixir/sync/tests/test_functional.exs index e3049d5..760612c 100644 --- a/clients/elixir/sync/tests/test_functional.exs +++ b/clients/elixir/sync/tests/test_functional.exs @@ -1,20 +1,4 @@ #!/usr/bin/env elixir -# This is free 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, & 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. - # Functional Tests for Un Elixir SDK # diff --git a/clients/elixir/sync/tests/test_library.exs b/clients/elixir/sync/tests/test_library.exs index 30dbe85..4516359 100644 --- a/clients/elixir/sync/tests/test_library.exs +++ b/clients/elixir/sync/tests/test_library.exs @@ -1,20 +1,4 @@ #!/usr/bin/env elixir -# This is free 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, & 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. - # Tests for Un Elixir SDK # diff --git a/clients/erlang/sync/src/un.erl b/clients/erlang/sync/src/un.erl index 3c34b65..17ec8df 100755 --- a/clients/erlang/sync/src/un.erl +++ b/clients/erlang/sync/src/un.erl @@ -56,10 +56,8 @@ %%% %%% Authentication Priority: %%% 1. Function arguments (PublicKey, SecretKey) -%%% 2. --account N -> accounts.csv row N (bypasses env vars) -%%% 3. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY) -%%% 4. ~/.unsandbox/accounts.csv row 0 (or UNSANDBOX_ACCOUNT env) -%%% 5. ./accounts.csv row 0 +%%% 2. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY) +%%% 3. Config file (~/.unsandbox/accounts.csv) -define(API_BASE, "https://api.unsandbox.com"). -define(PORTAL_BASE, "https://unsandbox.com"). @@ -721,60 +719,37 @@ not_contains_error(Response) -> %% CLI Entry Point %% ============================================================================ -main(RawArgs) -> - %% Strip --account N from args and store index in process dict before dispatch - {AccountIndex, Args} = extract_account_arg(RawArgs, undefined, []), - case AccountIndex of - undefined -> ok; - N -> erlang:put(account_index, N) - end, - dispatch(Args). - -dispatch([]) -> - io:format("Usage: un.erl [--account N] [options] ~n"), - io:format(" un.erl [--account N] session [options]~n"), - io:format(" un.erl [--account N] service [options]~n"), - io:format(" un.erl [--account N] snapshot [options]~n"), - io:format(" un.erl [--account N] image [options]~n"), - io:format(" un.erl [--account N] key [options]~n"), +main([]) -> + io:format("Usage: un.erl [options] ~n"), + io:format(" un.erl session [options]~n"), + io:format(" un.erl service [options]~n"), + io:format(" un.erl snapshot [options]~n"), + io:format(" un.erl image [options]~n"), + io:format(" un.erl key [options]~n"), io:format(" un.erl languages [--json]~n"), - io:format("~nGlobal options:~n"), - io:format(" --account N Use accounts.csv row N (bypasses env vars)~n"), halt(1); -dispatch(["session" | Rest]) -> +main(["session" | Rest]) -> session_command(Rest); -dispatch(["service" | Rest]) -> +main(["service" | Rest]) -> service_command(Rest); -dispatch(["snapshot" | Rest]) -> +main(["snapshot" | Rest]) -> snapshot_command(Rest); -dispatch(["image" | Rest]) -> +main(["image" | Rest]) -> image_command(Rest); -dispatch(["key" | Rest]) -> +main(["key" | Rest]) -> key_command(Rest); -dispatch(["languages" | Rest]) -> +main(["languages" | Rest]) -> languages_command(Rest); -dispatch(Args) -> +main(Args) -> execute_command(Args). -%% Strip --account N from argument list, return {Index | undefined, RestArgs} -extract_account_arg([], Acc, RestAcc) -> - {Acc, lists:reverse(RestAcc)}; -extract_account_arg(["--account", NStr | Rest], _Acc, RestAcc) -> - N = try list_to_integer(NStr) catch _:_ -> - io:format("Error: --account requires an integer argument~n"), - halt(1) - end, - extract_account_arg(Rest, N, RestAcc); -extract_account_arg([Arg | Rest], Acc, RestAcc) -> - extract_account_arg(Rest, Acc, [Arg | RestAcc]). - %% Execute command execute_command(Args) -> {File, _Opts} = parse_exec_args(Args, #{file => undefined}), @@ -1477,96 +1452,19 @@ open_extend_page(PublicKey) -> end. %% Helpers - -%% @doc Load credentials from a CSV file at the given path. -%% Skips blank lines and comment lines (#). Returns {ok, {PK, SK}} or error. -load_credentials_from_csv(CsvPath, AccountIndex) -> - case file:read_file(CsvPath) of - {ok, Bin} -> - Lines = string:split(binary_to_list(Bin), "\n", all), - ValidAccounts = lists:filtermap(fun(Line) -> - Trimmed = string:trim(Line), - case Trimmed of - "" -> false; - [$# | _] -> false; - _ -> - Parts = string:split(Trimmed, ",", all), - case Parts of - [PK, SK | _] -> - PKt = string:trim(PK), - SKt = string:trim(SK), - if - length(PKt) > 8 andalso length(SKt) > 8 -> - {true, {PKt, SKt}}; - true -> false - end; - _ -> false - end - end - end, Lines), - if - AccountIndex < length(ValidAccounts) -> - {ok, lists:nth(AccountIndex + 1, ValidAccounts)}; - true -> - error - end; - _ -> - error - end. - -%% @doc Resolve credentials with correct priority: -%% 1. --account N process-dict override -> accounts.csv row N -%% 2. UNSANDBOX_PUBLIC_KEY / UNSANDBOX_SECRET_KEY env vars -%% 3. ~/.unsandbox/accounts.csv row 0 (or UNSANDBOX_ACCOUNT env) -%% 4. ./accounts.csv row 0 get_api_keys() -> - Home = case os:getenv("HOME") of false -> "."; H -> H end, - HomeCsv = filename:join([Home, ".unsandbox", "accounts.csv"]), - %% Priority 1: explicit --account N (stored in process dict by main/1) - case erlang:get(account_index) of - undefined -> - %% Priority 2: environment variables - PublicKey = os:getenv("UNSANDBOX_PUBLIC_KEY"), - SecretKey = os:getenv("UNSANDBOX_SECRET_KEY"), - ApiKey = os:getenv("UNSANDBOX_API_KEY"), - if - PublicKey =/= false andalso SecretKey =/= false -> - {PublicKey, SecretKey}; - ApiKey =/= false -> - {ApiKey, false}; - true -> - %% Priority 3: ~/.unsandbox/accounts.csv (or UNSANDBOX_ACCOUNT index) - DefaultIndex = case os:getenv("UNSANDBOX_ACCOUNT") of - false -> 0; - IdxStr -> try list_to_integer(string:trim(IdxStr)) catch _:_ -> 0 end - end, - case load_credentials_from_csv(HomeCsv, DefaultIndex) of - {ok, {PK, SK}} -> - {PK, SK}; - error -> - %% Priority 4: ./accounts.csv - case load_credentials_from_csv("accounts.csv", DefaultIndex) of - {ok, {PK2, SK2}} -> - {PK2, SK2}; - error -> - io:format("Error: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set (or UNSANDBOX_API_KEY for backwards compat)~n"), - halt(1) - end - end - end; - AccountIndex -> - case load_credentials_from_csv(HomeCsv, AccountIndex) of - {ok, {PK, SK}} -> - {PK, SK}; - error -> - case load_credentials_from_csv("accounts.csv", AccountIndex) of - {ok, {PK2, SK2}} -> - {PK2, SK2}; - error -> - io:format("Error: No credentials found for account index ~B in accounts.csv~n", [AccountIndex]), - halt(1) - end - end + PublicKey = os:getenv("UNSANDBOX_PUBLIC_KEY"), + SecretKey = os:getenv("UNSANDBOX_SECRET_KEY"), + ApiKey = os:getenv("UNSANDBOX_API_KEY"), + + if + PublicKey =/= false andalso SecretKey =/= false -> + {PublicKey, SecretKey}; + ApiKey =/= false -> + {ApiKey, false}; + true -> + io:format("Error: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set (or UNSANDBOX_API_KEY for backwards compat)~n"), + halt(1) end. get_api_key() -> diff --git a/clients/forth/sync/src/un.forth b/clients/forth/sync/src/un.forth index 7b10612..82b1d99 100644 --- a/clients/forth/sync/src/un.forth +++ b/clients/forth/sync/src/un.forth @@ -97,84 +97,8 @@ find-ext ext-lang ; -\ Account index for --account N flag (-1 = not set) -variable account-index --1 account-index ! - -\ Argument shift: 0 normally, 2 when --account N is prepended -variable arg-shift -0 arg-shift ! - -\ Shifted arg accessor - applies arg-shift to all handler arg accesses -: sarg ( n -- addr len ) - arg-shift @ + arg -; - -\ Buffer for credentials loaded from CSV -256 constant MAX-KEY-LEN -create csv-pk-buf MAX-KEY-LEN allot -variable csv-pk-len -create csv-sk-buf MAX-KEY-LEN allot -variable csv-sk-len - -\ Load credentials from accounts.csv at given index (n) -\ Writes PK/SK to /tmp/unsb_creds.txt; returns true if PK found -: load-accounts-csv-index ( n -- flag ) - dup 0< if drop 0 exit then - \ Write a shell script with the index embedded - s" /tmp/unsb_cred_resolve.sh" w/o create-file throw >r - s" #!/bin/bash" r@ write-line throw - s" IDX=" r@ write-file throw - dup 0 <# #s #> r@ write-file throw - s" " r@ write-line throw - s" CNT=-1; PK=''; SK=''" r@ write-line throw - s" for CSV in \"$HOME/.unsandbox/accounts.csv\" \"./accounts.csv\"; do" r@ write-line throw - s" [ -f \"$CSV\" ] || continue" r@ write-line throw - s" while IFS= read -r line || [ -n \"$line\" ]; do" r@ write-line throw - s" case \"$line\" in '#'*|'') continue ;; esac" r@ write-line throw - s" CNT=$((CNT+1))" r@ write-line throw - s" if [ \"$CNT\" -eq \"$IDX\" ]; then" r@ write-line throw - s" PK=$(echo \"$line\" | cut -d',' -f1 | tr -d ' ')" r@ write-line throw - s" SK=$(echo \"$line\" | cut -d',' -f2 | tr -d ' ')" r@ write-line throw - s" break 2" r@ write-line throw - s" fi" r@ write-line throw - s" done < \"$CSV\"" r@ write-line throw - s" done" r@ write-line throw - s" printf '%s\\n%s\\n' \"$PK\" \"$SK\" > /tmp/unsb_creds.txt" r@ write-line throw - s" [ -n \"$PK\" ]" r@ write-line throw - r> close-file throw - drop \ drop index - s" bash /tmp/unsb_cred_resolve.sh" system - 0= if - \ Script exited 0: PK was found; read results - s" /tmp/unsb_creds.txt" r/o open-file - 0= if - >r - csv-pk-buf MAX-KEY-LEN r@ read-line throw - drop csv-pk-len ! - csv-sk-buf MAX-KEY-LEN r@ read-line throw - drop csv-sk-len ! - r> close-file throw - -1 - else - drop 0 - then - else - 0 - then -; - \ Get API keys from environment (HMAC or legacy) : get-public-key ( -- addr len ) - account-index @ dup 0>= if - load-accounts-csv-index if - csv-pk-buf csv-pk-len @ exit - then - s" Error: Account index not found in accounts.csv" type cr - 1 (bye) - else - drop - then s" UNSANDBOX_PUBLIC_KEY" getenv dup 0= if 2drop s" UNSANDBOX_API_KEY" getenv @@ -186,20 +110,6 @@ variable csv-sk-len ; : get-secret-key ( -- addr len ) - account-index @ dup 0>= if - \ CSV already loaded if pk-buf is non-empty - csv-pk-len @ 0> if - drop csv-sk-buf csv-sk-len @ exit - then - \ Load it now - load-accounts-csv-index if - csv-sk-buf csv-sk-len @ exit - then - \ Index not found - error was printed by get-public-key; return empty - s" " exit - else - drop - then s" UNSANDBOX_SECRET_KEY" getenv dup 0= if 2drop s" UNSANDBOX_API_KEY" getenv @@ -806,7 +716,7 @@ variable csv-sk-len 0 (bye) then - 2 sarg 2dup s" --extend" compare 0= if + 2 arg 2dup s" --extend" compare 0= if 2drop 1 validate-key 0 (bye) @@ -869,7 +779,7 @@ variable csv-sk-len 1 (bye) then - 2 sarg 2dup s" --list" compare 0= if + 2 arg 2dup s" --list" compare 0= if 2drop session-list 0 (bye) then @@ -885,7 +795,7 @@ variable csv-sk-len s" Error: --kill requires session ID" type cr 1 (bye) then - 3 sarg session-kill + 3 arg session-kill 0 (bye) then @@ -925,7 +835,7 @@ variable csv-sk-len 1 (bye) then - 2 sarg 2dup s" --list" compare 0= if + 2 arg 2dup s" --list" compare 0= if 2drop service-list 0 (bye) then @@ -946,7 +856,7 @@ variable csv-sk-len s" Error: --info requires service ID" type cr 1 (bye) then - 3 sarg service-info + 3 arg service-info 0 (bye) then @@ -956,7 +866,7 @@ variable csv-sk-len s" Error: --logs requires service ID" type cr 1 (bye) then - 3 sarg service-logs + 3 arg service-logs 0 (bye) then @@ -966,7 +876,7 @@ variable csv-sk-len s" Error: --freeze requires service ID" type cr 1 (bye) then - 3 sarg service-sleep + 3 arg service-sleep 0 (bye) then @@ -976,7 +886,7 @@ variable csv-sk-len s" Error: --unfreeze requires service ID" type cr 1 (bye) then - 3 sarg service-wake + 3 arg service-wake 0 (bye) then @@ -986,7 +896,7 @@ variable csv-sk-len s" Error: --destroy requires service ID" type cr 1 (bye) then - 3 sarg service-destroy + 3 arg service-destroy 0 (bye) then @@ -1001,13 +911,13 @@ variable csv-sk-len s" Error: --resize requires --vcpu N" type cr 1 (bye) then - 4 sarg 2dup s" --vcpu" compare 0= if + 4 arg 2dup s" --vcpu" compare 0= if 2drop argc @ 6 < if s" Error: --vcpu requires a value" type cr 1 (bye) then - 3 sarg 5 sarg service-resize + 3 arg 5 arg service-resize 0 (bye) then 2dup s" -v" compare 0= if @@ -1016,7 +926,7 @@ variable csv-sk-len s" Error: -v requires a value" type cr 1 (bye) then - 3 sarg 5 sarg service-resize + 3 arg 5 arg service-resize 0 (bye) then 2drop @@ -1030,16 +940,16 @@ variable csv-sk-len s" Error: --dump-bootstrap requires service ID" type cr 1 (bye) then - 3 sarg + 3 arg \ Check for --dump-file argc @ 5 >= if - 4 sarg 2dup s" --dump-file" compare 0= if + 4 arg 2dup s" --dump-file" compare 0= if 2drop argc @ 6 < if s" Error: --dump-file requires filename" type cr 1 (bye) then - 5 sarg + 5 arg else 2drop 0 0 then @@ -1057,13 +967,13 @@ variable csv-sk-len s" Usage: un.forth service env [options]" type cr 1 (bye) then - 3 sarg 2dup s" status" compare 0= if + 3 arg 2dup s" status" compare 0= if 2drop argc @ 5 < if s" Error: status requires service ID" type cr 1 (bye) then - 4 sarg service-env-status + 4 arg service-env-status 0 (bye) then 2dup s" set" compare 0= if @@ -1081,7 +991,7 @@ variable csv-sk-len s" Error: export requires service ID" type cr 1 (bye) then - 4 sarg service-env-export + 4 arg service-env-export 0 (bye) then 2dup s" delete" compare 0= if @@ -1090,7 +1000,7 @@ variable csv-sk-len s" Error: delete requires service ID" type cr 1 (bye) then - 4 sarg service-env-delete + 4 arg service-env-delete 0 (bye) then 2drop @@ -1162,7 +1072,7 @@ variable csv-sk-len 0 (bye) then - 2 sarg 2dup s" --json" compare 0= if + 2 arg 2dup s" --json" compare 0= if 2drop 1 languages-list 0 (bye) @@ -1760,7 +1670,7 @@ variable csv-sk-len 0 (bye) then - 2 sarg 2dup s" --list" compare 0= if + 2 arg 2dup s" --list" compare 0= if 2drop snapshot-list 0 (bye) then @@ -1776,7 +1686,7 @@ variable csv-sk-len s" Error: --info requires snapshot ID" type cr 1 (bye) then - 3 sarg snapshot-info + 3 arg snapshot-info 0 (bye) then @@ -1786,7 +1696,7 @@ variable csv-sk-len s" Error: --restore requires snapshot ID" type cr 1 (bye) then - 3 sarg snapshot-restore + 3 arg snapshot-restore 0 (bye) then @@ -1796,7 +1706,7 @@ variable csv-sk-len s" Error: --delete requires snapshot ID" type cr 1 (bye) then - 3 sarg snapshot-delete + 3 arg snapshot-delete 0 (bye) then @@ -1806,7 +1716,7 @@ variable csv-sk-len s" Error: --lock requires snapshot ID" type cr 1 (bye) then - 3 sarg snapshot-lock + 3 arg snapshot-lock 0 (bye) then @@ -1816,7 +1726,7 @@ variable csv-sk-len s" Error: --unlock requires snapshot ID" type cr 1 (bye) then - 3 sarg snapshot-unlock + 3 arg snapshot-unlock 0 (bye) then @@ -1826,7 +1736,7 @@ variable csv-sk-len s" Error: --clone requires snapshot ID" type cr 1 (bye) then - 3 sarg snapshot-clone + 3 arg snapshot-clone 0 (bye) then @@ -1842,7 +1752,7 @@ variable csv-sk-len 1 (bye) then - 2 sarg 2dup s" --list" compare 0= if + 2 arg 2dup s" --list" compare 0= if 2drop image-list 0 (bye) then @@ -1858,7 +1768,7 @@ variable csv-sk-len s" Error: --info requires image ID" type cr 1 (bye) then - 3 sarg image-info + 3 arg image-info 0 (bye) then @@ -1868,7 +1778,7 @@ variable csv-sk-len s" Error: --delete requires image ID" type cr 1 (bye) then - 3 sarg image-delete + 3 arg image-delete 0 (bye) then @@ -1878,7 +1788,7 @@ variable csv-sk-len s" Error: --lock requires image ID" type cr 1 (bye) then - 3 sarg image-lock + 3 arg image-lock 0 (bye) then @@ -1888,7 +1798,7 @@ variable csv-sk-len s" Error: --unlock requires image ID" type cr 1 (bye) then - 3 sarg image-unlock + 3 arg image-unlock 0 (bye) then @@ -1908,7 +1818,7 @@ variable csv-sk-len s" Error: --visibility requires image ID and mode" type cr 1 (bye) then - 3 sarg 4 sarg image-visibility + 3 arg 4 arg image-visibility 0 (bye) then @@ -1918,7 +1828,7 @@ variable csv-sk-len s" Error: --spawn requires image ID" type cr 1 (bye) then - 3 sarg image-spawn + 3 arg image-spawn 0 (bye) then @@ -1928,7 +1838,7 @@ variable csv-sk-len s" Error: --clone requires image ID" type cr 1 (bye) then - 3 sarg image-clone + 3 arg image-clone 0 (bye) then @@ -1950,21 +1860,8 @@ variable csv-sk-len 1 (bye) then - \ Check for --account N as first argument (before arg-shift is applied) - 1 arg 2dup s" --account" compare 0= if - 2drop - argc @ 3 < if - s" Error: --account requires a numeric argument" type cr - 1 (bye) - then - 2 arg s>number drop account-index ! - 2 arg-shift ! - else - 2drop - then - - \ Get subcommand (adjusted for arg-shift) - 1 arg-shift @ + arg + \ Get first argument (skip gforth and script name) + 1 arg \ Check for subcommands 2dup s" session" compare 0= if diff --git a/clients/fortran/sync/src/un.f90 b/clients/fortran/sync/src/un.f90 index aebdd50..c01b23f 100644 --- a/clients/fortran/sync/src/un.f90 +++ b/clients/fortran/sync/src/un.f90 @@ -64,11 +64,9 @@ ! ./un key [--extend] ! ! Authentication (in priority order): -! 1. --account N flag -> accounts.csv row N (bypasses env vars) -! 2. Environment variables: UNSANDBOX_PUBLIC_KEY + UNSANDBOX_SECRET_KEY -! 3. Config file: ~/.unsandbox/accounts.csv row 0 (or UNSANDBOX_ACCOUNT) -! 4. ./accounts.csv row 0 -! 5. Legacy: UNSANDBOX_API_KEY (deprecated) +! 1. Environment variables: UNSANDBOX_PUBLIC_KEY + UNSANDBOX_SECRET_KEY +! 2. Config file: ~/.unsandbox/accounts.csv (public_key,secret_key per line) +! 3. Legacy: UNSANDBOX_API_KEY (deprecated) ! ! Compile: ! gfortran -o un un.f90 @@ -192,97 +190,32 @@ module unsandbox_sdk contains - !-------------------------------------------------------------------------- - ! Subroutine: load_csv_row - ! Description: Load public_key,secret_key from a CSV file at row_index - ! (0-based, skipping blank lines and '#' comments). - ! - ! Arguments: - ! csv_path - Path to CSV file - ! row_index - Zero-based data row to read - ! public_key - Output: public key (empty if not found) - ! secret_key - Output: secret key (empty if not found) - !-------------------------------------------------------------------------- - subroutine load_csv_row(csv_path, row_index, public_key, secret_key) - character(len=*), intent(in) :: csv_path - integer, intent(in) :: row_index - character(len=*), intent(out) :: public_key, secret_key - character(len=1024) :: line - integer :: unit_num, ios, data_index - logical :: file_exists - - public_key = '' - secret_key = '' - data_index = 0 - - inquire(file=trim(csv_path), exist=file_exists) - if (.not. file_exists) return - - open(newunit=unit_num, file=trim(csv_path), status='old', action='read', iostat=ios) - if (ios /= 0) return - - do - read(unit_num, '(A)', iostat=ios) line - if (ios /= 0) exit - line = adjustl(line) - if (len_trim(line) == 0) cycle - if (line(1:1) == '#') cycle - if (data_index == row_index) then - call parse_csv_line(line, public_key, secret_key) - close(unit_num) - return - end if - data_index = data_index + 1 - end do - close(unit_num) - end subroutine load_csv_row - !-------------------------------------------------------------------------- ! Subroutine: get_credentials ! Description: Get API credentials from environment or config file ! ! Priority order: - ! 1. account_index >= 0 -> accounts.csv row N (bypasses env vars) - ! 2. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY) - ! 3. Config file (~/.unsandbox/accounts.csv row 0 or UNSANDBOX_ACCOUNT) - ! 4. ./accounts.csv row 0 - ! 5. Legacy UNSANDBOX_API_KEY (deprecated) + ! 1. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY) + ! 2. Config file (~/.unsandbox/accounts.csv) + ! 3. Legacy UNSANDBOX_API_KEY (deprecated) ! ! Arguments: - ! public_key - Output: API public key - ! secret_key - Output: API secret key - ! status - Output: 0 on success, non-zero on error - ! account_index - Optional input: if >= 0, load that CSV row directly + ! public_key - Output: API public key + ! secret_key - Output: API secret key + ! status - Output: 0 on success, non-zero on error !-------------------------------------------------------------------------- - subroutine get_credentials(public_key, secret_key, status, account_index) + subroutine get_credentials(public_key, secret_key, status) character(len=*), intent(out) :: public_key, secret_key integer, intent(out) :: status - integer, intent(in), optional :: account_index - character(len=1024) :: home_dir, accounts_path, api_key, acct_env - integer :: ios, acct_idx, default_index + character(len=1024) :: home_dir, accounts_path, line, api_key + integer :: unit_num, ios + logical :: file_exists status = 0 public_key = '' secret_key = '' - ! Priority 1: account_index >= 0 -> load that CSV row (bypasses env vars) - if (present(account_index)) then - if (account_index >= 0) then - acct_idx = account_index - call get_environment_variable('HOME', home_dir, status=ios) - if (ios == 0) then - accounts_path = trim(home_dir) // '/.unsandbox/accounts.csv' - call load_csv_row(accounts_path, acct_idx, public_key, secret_key) - if (len_trim(public_key) > 0) return - end if - call load_csv_row('accounts.csv', acct_idx, public_key, secret_key) - if (len_trim(public_key) > 0) return - status = 1 - return - end if - end if - - ! Priority 2: Environment variables + ! Priority 1: Environment variables call get_environment_variable('UNSANDBOX_PUBLIC_KEY', public_key, status=ios) if (ios == 0 .and. len_trim(public_key) > 0) then call get_environment_variable('UNSANDBOX_SECRET_KEY', secret_key, status=ios) @@ -291,27 +224,37 @@ contains end if end if - ! Priority 3: ~/.unsandbox/accounts.csv (default row) - call get_environment_variable('UNSANDBOX_ACCOUNT', acct_env, status=ios) - if (ios == 0 .and. len_trim(acct_env) > 0) then - read(acct_env, *, iostat=ios) default_index - if (ios /= 0) default_index = 0 - else - default_index = 0 - end if - + ! Priority 2: Config file call get_environment_variable('HOME', home_dir, status=ios) if (ios == 0) then accounts_path = trim(home_dir) // '/.unsandbox/accounts.csv' - call load_csv_row(accounts_path, default_index, public_key, secret_key) - if (len_trim(public_key) > 0) return + inquire(file=trim(accounts_path), exist=file_exists) + if (file_exists) then + open(newunit=unit_num, file=trim(accounts_path), status='old', & + action='read', iostat=ios) + if (ios == 0) then + do + read(unit_num, '(A)', iostat=ios) line + if (ios /= 0) exit + line = adjustl(line) + if (len_trim(line) == 0) cycle + if (line(1:1) == '#') cycle + ! Parse CSV: public_key,secret_key + call parse_csv_line(line, public_key, secret_key) + if (len_trim(public_key) > 0 .and. len_trim(secret_key) > 0) then + if (public_key(1:8) == 'unsb-pk-' .and. & + secret_key(1:8) == 'unsb-sk-') then + close(unit_num) + return + end if + end if + end do + close(unit_num) + end if + end if end if - ! Priority 4: ./accounts.csv - call load_csv_row('accounts.csv', default_index, public_key, secret_key) - if (len_trim(public_key) > 0) return - - ! Priority 5: Legacy API key + ! Priority 3: Legacy API key call get_environment_variable('UNSANDBOX_API_KEY', api_key, status=ios) if (ios == 0 .and. len_trim(api_key) > 0) then public_key = api_key @@ -907,7 +850,6 @@ program unsandbox_cli character(len=1024) :: filename, language, api_key, ext, arg, subcommand character(len=256) :: session_id, service_id integer :: stat, i, nargs, dot_pos - integer :: account_index ! -1 = not set; >= 0 means use that CSV row logical :: list_flag, is_session, is_service, is_key ! Initialize @@ -918,7 +860,6 @@ program unsandbox_cli is_key = .false. session_id = '' service_id = '' - account_index = -1 ! Get command line arguments count nargs = command_argument_count() @@ -927,17 +868,6 @@ program unsandbox_cli stop 1 end if - ! Pre-scan all arguments for --account N - do i = 1, nargs - 1 - call get_command_argument(i, arg) - if (trim(arg) == '--account') then - call get_command_argument(i + 1, arg) - read(arg, *, iostat=stat) account_index - if (stat /= 0) account_index = -1 - exit - end if - end do - ! Check for subcommands call get_command_argument(1, arg, status=stat) if (trim(arg) == '-h' .or. trim(arg) == '--help') then @@ -1044,9 +974,6 @@ contains write(*, '(A)') 'Languages options:' write(*, '(A)') ' --json Output as JSON array' write(*, '(A)') '' - write(*, '(A)') 'Credential options (global):' - write(*, '(A)') ' --account N Use row N from accounts.csv (bypasses env vars)' - write(*, '(A)') '' write(*, '(A)') 'Library Usage:' write(*, '(A)') ' use unsandbox_sdk' write(*, '(A)') ' type(unsandbox_client) :: client' @@ -1076,7 +1003,7 @@ contains end if ! Get API keys - call get_credentials(public_key, secret_key, stat, account_index) + call get_credentials(public_key, secret_key, stat) if (stat /= 0) then write(0, '(A)') 'Error: No credentials found. Set UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY' stop 1 @@ -1147,8 +1074,6 @@ contains input_files = trim(arg) end if end if - else if (trim(arg) == '--account') then - ! already processed in main pre-scan; skip this token and its value else if (len_trim(arg) > 0) then if (arg(1:1) == '-') then @@ -1161,7 +1086,7 @@ contains end do ! Get API keys - call get_credentials(public_key, secret_key, stat, account_index) + call get_credentials(public_key, secret_key, stat) if (stat /= 0) then write(0, '(A)') 'Error: No credentials found' stop 1 @@ -1364,7 +1289,7 @@ contains end do ! Get API keys - call get_credentials(public_key, secret_key, stat, account_index) + call get_credentials(public_key, secret_key, stat) if (stat /= 0) then write(0, '(A)') 'Error: No credentials found' stop 1 @@ -1692,7 +1617,7 @@ contains end do ! Get credentials - call get_credentials(public_key, secret_key, stat, account_index) + call get_credentials(public_key, secret_key, stat) if (stat /= 0) then write(0, '(A)') 'Error: No credentials found' stop 1 @@ -1846,7 +1771,7 @@ contains list_mode = .false. ! Get credentials - call get_credentials(public_key, secret_key, stat, account_index) + call get_credentials(public_key, secret_key, stat) if (stat /= 0) then write(0, '(A)') 'Error: No credentials found' stop 1 @@ -2121,7 +2046,7 @@ contains end do ! Get API key - call get_credentials(public_key, secret_key, stat, account_index) + call get_credentials(public_key, secret_key, stat) if (stat /= 0) then write(0, '(A)') 'Error: No credentials found' stop 1 @@ -2218,7 +2143,7 @@ contains end do ! Get API keys - call get_credentials(public_key, secret_key, stat, account_index) + call get_credentials(public_key, secret_key, stat) if (stat /= 0) then write(0, '(A)') 'Error: No credentials found' stop 1 diff --git a/clients/fortran/tests/test_un.sh b/clients/fortran/tests/test_un.sh index f06dde1..dbbc09d 100755 --- a/clients/fortran/tests/test_un.sh +++ b/clients/fortran/tests/test_un.sh @@ -1,20 +1,4 @@ #!/bin/bash -# This is free 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, & 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. - # Test suite for Fortran Unsandbox SDK # Run: bash tests/test_un.sh diff --git a/clients/fsharp/sync/src/un.fs b/clients/fsharp/sync/src/un.fs index 5752bd1..58dcbdb 100644 --- a/clients/fsharp/sync/src/un.fs +++ b/clients/fsharp/sync/src/un.fs @@ -77,7 +77,6 @@ type Args = { mutable Command: string option mutable SourceFile: string option mutable ApiKey: string option - mutable AccountIndex: int option mutable Network: string option mutable Vcpu: int Env: ResizeArray @@ -145,70 +144,19 @@ type Args = { mutable ImagePorts: string option } -let loadCredentialsFromCsv (csvPath: string) (accountIndex: int) = - if File.Exists(csvPath) then - try - let lines = File.ReadAllLines(csvPath) - let accounts = - lines - |> Array.map (fun l -> l.Trim()) - |> Array.filter (fun l -> l.Length > 0 && not (l.StartsWith("#"))) - |> Array.choose (fun line -> - let parts = line.Split(',') - if parts.Length >= 2 then - let pk = parts.[0].Trim() - let sk = parts.[1].Trim() - if pk.Length > 8 && sk.Length > 8 then Some (pk, sk) - else None - else None) - if accountIndex < accounts.Length then Some accounts.[accountIndex] - else None - with _ -> None - else None +let getApiKeys (argsKey: string option) = + let publicKey = Environment.GetEnvironmentVariable("UNSANDBOX_PUBLIC_KEY") + let secretKey = Environment.GetEnvironmentVariable("UNSANDBOX_SECRET_KEY") -let getApiKeys (argsKey: string option) (accountIndex: int option) = - let home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) - let homeCsv = Path.Combine(home, ".unsandbox", "accounts.csv") - - // Priority 1: --account N -> accounts.csv row N (bypasses env vars) - match accountIndex with - | Some idx -> - let creds = - match loadCredentialsFromCsv homeCsv idx with - | Some c -> Some c - | None -> loadCredentialsFromCsv "accounts.csv" idx - match creds with - | Some (pk, sk) -> (pk, sk) - | None -> - eprintfn "%sError: No credentials found for account index %d in accounts.csv%s" red idx reset + // Fall back to UNSANDBOX_API_KEY for backwards compatibility + if String.IsNullOrEmpty(publicKey) || String.IsNullOrEmpty(secretKey) then + let legacyKey = match argsKey with | Some k -> k | None -> Environment.GetEnvironmentVariable("UNSANDBOX_API_KEY") + if String.IsNullOrEmpty(legacyKey) then + eprintfn "%sError: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set%s" red reset exit 1 - | None -> - let publicKey = Environment.GetEnvironmentVariable("UNSANDBOX_PUBLIC_KEY") - let secretKey = Environment.GetEnvironmentVariable("UNSANDBOX_SECRET_KEY") - - // Priority 2: environment variables - if not (String.IsNullOrEmpty(publicKey)) && not (String.IsNullOrEmpty(secretKey)) then - (publicKey, secretKey) - else - // Fall back to legacy UNSANDBOX_API_KEY - let legacyKey = match argsKey with | Some k -> k | None -> Environment.GetEnvironmentVariable("UNSANDBOX_API_KEY") - if not (String.IsNullOrEmpty(legacyKey)) then - (legacyKey, null) - else - // Priority 3: ~/.unsandbox/accounts.csv (or UNSANDBOX_ACCOUNT index) - let defaultIndex = - let envIdx = Environment.GetEnvironmentVariable("UNSANDBOX_ACCOUNT") - if String.IsNullOrEmpty(envIdx) then 0 - else match System.Int32.TryParse(envIdx) with | (true, n) -> n | _ -> 0 - let creds = - match loadCredentialsFromCsv homeCsv defaultIndex with - | Some c -> Some c - | None -> loadCredentialsFromCsv "accounts.csv" defaultIndex - match creds with - | Some (pk, sk) -> (pk, sk) - | None -> - eprintfn "%sError: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set%s" red reset - exit 1 + (legacyKey, null) + else + (publicKey, secretKey) let detectLanguage (filename: string) = let dotIndex = filename.LastIndexOf('.') @@ -683,7 +631,7 @@ let cmdServiceEnv (args: Args) (publicKey: string) (secretKey: string) = exit 1 let cmdExecute (args: Args) = - let (publicKey, secretKey) = getApiKeys args.ApiKey args.AccountIndex + let (publicKey, secretKey) = getApiKeys args.ApiKey let code = File.ReadAllText(args.SourceFile.Value) let language = detectLanguage args.SourceFile.Value @@ -732,7 +680,7 @@ let cmdExecute (args: Args) = exit exitCode let cmdSession (args: Args) = - let (publicKey, secretKey) = getApiKeys args.ApiKey args.AccountIndex + let (publicKey, secretKey) = getApiKeys args.ApiKey if args.SessionSnapshot.IsSome then let mutable payload = [] @@ -792,7 +740,7 @@ let openBrowser (url: string) = eprintfn "%sError opening browser: %s%s" red ex.Message reset let cmdKey (args: Args) = - let (publicKey, secretKey) = getApiKeys args.ApiKey args.AccountIndex + let (publicKey, secretKey) = getApiKeys args.ApiKey ServicePointManager.SecurityProtocol <- SecurityProtocolType.Tls12 ||| SecurityProtocolType.Tls11 ||| SecurityProtocolType.Tls @@ -869,7 +817,7 @@ let cmdKey (args: Args) = exit 1 let cmdLanguages (args: Args) = - let (publicKey, secretKey) = getApiKeys args.ApiKey args.AccountIndex + let (publicKey, secretKey) = getApiKeys args.ApiKey // Try to load from cache first let cachedResponse = loadLanguagesCache () @@ -924,7 +872,7 @@ let cmdLanguages (args: Args) = printfn "%s" lang let cmdImage (args: Args) = - let (publicKey, secretKey) = getApiKeys args.ApiKey args.AccountIndex + let (publicKey, secretKey) = getApiKeys args.ApiKey if args.ImageList then let result = apiRequest "/images" "GET" None publicKey secretKey @@ -980,7 +928,7 @@ let cmdImage (args: Args) = exit 1 let cmdSnapshot (args: Args) = - let (publicKey, secretKey) = getApiKeys args.ApiKey args.AccountIndex + let (publicKey, secretKey) = getApiKeys args.ApiKey if args.SnapshotList then let result = apiRequest "/snapshots" "GET" None publicKey secretKey @@ -1011,7 +959,7 @@ let cmdSnapshot (args: Args) = exit 1 let cmdService (args: Args) = - let (publicKey, secretKey) = getApiKeys args.ApiKey args.AccountIndex + let (publicKey, secretKey) = getApiKeys args.ApiKey // Handle env subcommand if args.EnvAction.IsSome then @@ -1159,7 +1107,6 @@ let parseArgs (argv: string[]) = Command = None SourceFile = None ApiKey = None - AccountIndex = None Network = None Vcpu = 0 Env = ResizeArray() @@ -1245,13 +1192,6 @@ let parseArgs (argv: string[]) = i <- i + 1 args.EnvTarget <- Some argv.[i] | "-k" | "--api-key" -> i <- i + 1; args.ApiKey <- Some argv.[i] - | "--account" -> - i <- i + 1 - match System.Int32.TryParse(argv.[i]) with - | (true, n) -> args.AccountIndex <- Some n - | _ -> - eprintfn "Error: --account requires an integer argument" - Environment.Exit(1) | "-n" | "--network" -> i <- i + 1; args.Network <- Some argv.[i] | "-v" | "--vcpu" -> i <- i + 1; args.Vcpu <- int argv.[i] | "-e" | "--env" -> i <- i + 1; args.Env.Add(argv.[i]) @@ -1418,7 +1358,6 @@ let printHelp () = printfn " -n MODE Network mode (zerotrust/semitrusted)" printfn " -v N vCPU count (1-8)" printfn " -k KEY API key" - printfn " --account N Use accounts.csv row N (bypasses env vars)" printfn "" printfn "Session options:" printfn " --list List active sessions" diff --git a/clients/go/Makefile b/clients/go/Makefile index 637f44c..11b3472 100644 --- a/clients/go/Makefile +++ b/clients/go/Makefile @@ -5,13 +5,17 @@ # - async/ : Asynchronous Go SDK (goroutines/channels) # # Usage: -# make test # Run all 4 test modes (auto-detects go binary) +# make # Build all +# make test # Run all 4 test modes # make test-cli # CLI mode only # make test-library # Library mode only +# make test-integration # Integration mode only +# make test-functional # Functional mode only # make build # Build binaries # make clean # Remove build artifacts # -# The Makefile auto-detects go from PATH, ~/.local/go, /usr/local/go. +# Dependencies: +# Go 1.18+ (for generics support) .PHONY: all build test test-cli test-library test-integration test-functional .PHONY: test-sync test-async clean help examples fmt vet @@ -21,11 +25,8 @@ ROOT_DIR := $(shell cd ../.. && pwd) SYNC_DIR := sync ASYNC_DIR := async -# Auto-detect Go binary: PATH first, then common install locations -GO := $(or $(shell which go 2>/dev/null), \ - $(shell test -x $(HOME)/.local/go/bin/go && echo $(HOME)/.local/go/bin/go), \ - $(shell test -x /usr/local/go/bin/go && echo /usr/local/go/bin/go), \ - $(shell test -x $(HOME)/go/bin/go && echo $(HOME)/go/bin/go)) +# Go settings +GO := go GOFLAGS := -v # Colors @@ -39,14 +40,6 @@ NC := \033[0m help: @echo "UN Go Client - Build and Test" @echo "" - @if [ -n "$(GO)" ]; then \ - echo " Go binary: $(GO)"; \ - $(GO) version; \ - else \ - echo " $(RED)✗ Go binary not found$(NC)"; \ - echo " Install Go or set PATH to include go binary"; \ - fi - @echo "" @echo "Build:" @echo " make build Build all binaries" @echo " make build-sync Build sync SDK" @@ -69,25 +62,18 @@ help: @echo "" @echo "Utility:" @echo " make clean Remove build artifacts" + @echo " make deps Show required dependencies" + @echo " make examples Run examples" @echo "" -# Guard: fail early if no Go binary found -check-go: - @if [ -z "$(GO)" ]; then \ - echo "$(RED)✗ Go binary not found$(NC)"; \ - echo " Searched: PATH, ~/.local/go/bin, /usr/local/go/bin, ~/go/bin"; \ - exit 1; \ - fi - -# Ensure go.mod exists for the sync SDK -$(SYNC_DIR)/go.mod: check-go - @if [ ! -f "$(SYNC_DIR)/go.mod" ] && [ -d "$(SYNC_DIR)/src" ]; then \ - echo "Initializing go module for sync SDK..."; \ - cd $(SYNC_DIR) && $(GO) mod init unsandbox.com/un 2>/dev/null || true; \ - fi - all: build +deps: + @echo "Required:" + @echo " Go 1.18+ (https://golang.org/dl/)" + @echo "" + @go version + # ============================================================================ # BUILD # ============================================================================ @@ -95,7 +81,7 @@ all: build build: build-sync build-async @echo "$(GREEN)✓ All Go SDKs built$(NC)" -build-sync: check-go $(SYNC_DIR)/go.mod +build-sync: @echo "Building sync SDK..." @if [ -f "$(SYNC_DIR)/src/un.go" ]; then \ cd $(SYNC_DIR)/src && $(GO) build $(GOFLAGS) -o ../un . 2>&1 | head -5 || true; \ @@ -104,7 +90,7 @@ build-sync: check-go $(SYNC_DIR)/go.mod echo "$(YELLOW)⊘$(NC) Sync SDK source not found"; \ fi -build-async: check-go +build-async: @echo "Building async SDK..." @if [ -f "$(ASYNC_DIR)/src/un.go" ]; then \ cd $(ASYNC_DIR)/src && $(GO) build $(GOFLAGS) -o ../un . 2>&1 | head -5 || true; \ @@ -125,19 +111,22 @@ test: test-cli test-library test-integration test-functional # TEST: CLI Mode # ============================================================================ -test-cli: check-go +test-cli: @echo "" @echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" @echo "CLI MODE: Testing Go CLI interface" @echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" @echo "" + @# Test root-level un.go if it exists @if [ -f "$(ROOT_DIR)/un.go" ]; then \ cd $(ROOT_DIR) && $(GO) run un.go --help > /dev/null 2>&1 && echo " $(GREEN)✓$(NC) CLI: Root un.go --help works" || echo " $(YELLOW)⊘$(NC) CLI: Root un.go --help (check syntax)"; \ fi + @# Test sync SDK CLI @if [ -f "$(SYNC_DIR)/src/un.go" ]; then \ cd $(SYNC_DIR)/src && $(GO) build -o /tmp/un_test . 2>/dev/null && echo " $(GREEN)✓$(NC) CLI: Sync SDK compiles" || echo " $(RED)✗$(NC) CLI: Sync SDK compile failed"; \ rm -f /tmp/un_test; \ fi + @# Test async SDK CLI @if [ -f "$(ASYNC_DIR)/src/un.go" ]; then \ cd $(ASYNC_DIR)/src && $(GO) build -o /tmp/un_test . 2>/dev/null && echo " $(GREEN)✓$(NC) CLI: Async SDK compiles" || echo " $(YELLOW)⊘$(NC) CLI: Async SDK not yet buildable"; \ rm -f /tmp/un_test 2>/dev/null || true; \ @@ -147,34 +136,26 @@ test-cli: check-go # TEST: Library Mode # ============================================================================ -test-library: check-go +test-library: @echo "" @echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" @echo "LIBRARY MODE: Testing Go package imports" @echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" @echo "" - @# Go requires test files in the same directory as the package. - @# Copy tests into src/ temporarily, run, clean up. - @if [ -d "$(SYNC_DIR)/tests" ] && [ -d "$(SYNC_DIR)/src" ]; then \ - cp $(SYNC_DIR)/tests/*_test.go $(SYNC_DIR)/src/ 2>/dev/null; \ - cd $(SYNC_DIR)/src && $(GO) test -short -v . 2>&1; \ - rm -f $(SYNC_DIR)/src/*_test.go; \ - elif [ -d "$(SYNC_DIR)/src" ]; then \ - cd $(SYNC_DIR)/src && $(GO) test -short -v . 2>&1 | head -20 || echo " $(YELLOW)⊘$(NC) Library: No tests defined yet"; \ + @# Test sync SDK with go test + @if [ -d "$(SYNC_DIR)/src" ]; then \ + cd $(SYNC_DIR)/src && $(GO) test -v ./... 2>&1 | head -20 || echo " $(YELLOW)⊘$(NC) Library: No tests defined yet"; \ fi - @if [ -d "$(ASYNC_DIR)/tests" ] && [ -d "$(ASYNC_DIR)/src" ]; then \ - cp $(ASYNC_DIR)/tests/*_test.go $(ASYNC_DIR)/src/ 2>/dev/null; \ - cd $(ASYNC_DIR)/src && $(GO) test -short -v . 2>&1; \ - rm -f $(ASYNC_DIR)/src/*_test.go; \ - elif [ -d "$(ASYNC_DIR)/src" ]; then \ - cd $(ASYNC_DIR)/src && $(GO) test -short -v . 2>&1 | head -20 || echo " $(YELLOW)⊘$(NC) Library: Async tests not defined"; \ + @# Test async SDK with go test + @if [ -d "$(ASYNC_DIR)/src" ]; then \ + cd $(ASYNC_DIR)/src && $(GO) test -v ./... 2>&1 | head -20 || echo " $(YELLOW)⊘$(NC) Library: Async tests not defined"; \ fi # ============================================================================ # TEST: Integration Mode # ============================================================================ -test-integration: check-go +test-integration: @echo "" @echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" @echo "INTEGRATION MODE: Testing API contract" @@ -186,7 +167,7 @@ test-integration: check-go else \ echo " Testing API authentication..."; \ if [ -f "$(ROOT_DIR)/un.go" ]; then \ - cd $(ROOT_DIR) && $(GO) run un.go -s python -c 'print(42)' 2>&1 | grep -q "42" && echo " $(GREEN)✓$(NC) Integration: API auth works" || echo " $(RED)✗$(NC) Integration: Check API connectivity"; \ + cd $(ROOT_DIR) && $(GO) run un.go -s python -c 'print(42)' 2>&1 | grep -q "42" && echo " $(GREEN)✓$(NC) Integration: API auth works" || echo " $(YELLOW)⊘$(NC) Integration: Check API connectivity"; \ fi; \ fi @@ -194,7 +175,7 @@ test-integration: check-go # TEST: Functional Mode # ============================================================================ -test-functional: check-go +test-functional: @echo "" @echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" @echo "FUNCTIONAL MODE: Real-world scenarios" @@ -204,10 +185,8 @@ test-functional: check-go echo " $(YELLOW)⊘$(NC) Skipping (no API credentials)"; \ else \ echo " Running functional tests..."; \ - if [ -d "$(SYNC_DIR)/tests" ] && [ -d "$(SYNC_DIR)/src" ]; then \ - cp $(SYNC_DIR)/tests/functional_test.go $(SYNC_DIR)/src/ 2>/dev/null; \ - cd $(SYNC_DIR)/src && $(GO) test -v -run TestFunctional . 2>&1; \ - rm -f $(SYNC_DIR)/src/functional_test.go; \ + if [ -f "$(ROOT_DIR)/un.go" ]; then \ + cd $(ROOT_DIR) && $(GO) run un.go -s python -c 'def fib(n): return n if n<2 else fib(n-1)+fib(n-2); print(fib(10))' 2>&1 | grep -q "55" && echo " $(GREEN)✓$(NC) Functional: Fibonacci" || echo " $(YELLOW)⊘$(NC) Functional: Fibonacci (check output)"; \ fi; \ fi @@ -215,26 +194,18 @@ test-functional: check-go # TEST: By SDK Type # ============================================================================ -test-sync: check-go +test-sync: @echo "Testing Sync SDK..." - @if [ -d "$(SYNC_DIR)/tests" ] && [ -d "$(SYNC_DIR)/src" ]; then \ - cp $(SYNC_DIR)/tests/*_test.go $(SYNC_DIR)/src/ 2>/dev/null; \ - cd $(SYNC_DIR)/src && $(GO) test -v .; \ - rm -f $(SYNC_DIR)/src/*_test.go; \ - elif [ -d "$(SYNC_DIR)/src" ]; then \ - cd $(SYNC_DIR)/src && $(GO) test -v .; \ + @if [ -d "$(SYNC_DIR)/src" ]; then \ + cd $(SYNC_DIR)/src && $(GO) test -v ./...; \ else \ echo " $(YELLOW)⊘$(NC) Sync SDK not found"; \ fi -test-async: check-go +test-async: @echo "Testing Async SDK..." - @if [ -d "$(ASYNC_DIR)/tests" ] && [ -d "$(ASYNC_DIR)/src" ]; then \ - cp $(ASYNC_DIR)/tests/*_test.go $(ASYNC_DIR)/src/ 2>/dev/null; \ - cd $(ASYNC_DIR)/src && $(GO) test -v .; \ - rm -f $(ASYNC_DIR)/src/*_test.go; \ - elif [ -d "$(ASYNC_DIR)/src" ]; then \ - cd $(ASYNC_DIR)/src && $(GO) test -v .; \ + @if [ -d "$(ASYNC_DIR)/src" ]; then \ + cd $(ASYNC_DIR)/src && $(GO) test -v ./...; \ else \ echo " $(YELLOW)⊘$(NC) Async SDK not found"; \ fi @@ -243,14 +214,14 @@ test-async: check-go # Code Quality # ============================================================================ -fmt: check-go +fmt: @echo "Formatting Go code..." @if [ -d "$(SYNC_DIR)/src" ]; then gofmt -w $(SYNC_DIR)/src/; fi @if [ -d "$(ASYNC_DIR)/src" ]; then gofmt -w $(ASYNC_DIR)/src/; fi @if [ -f "$(ROOT_DIR)/un.go" ]; then gofmt -w $(ROOT_DIR)/un.go; fi @echo "$(GREEN)✓$(NC) Format complete" -vet: check-go +vet: @echo "Running go vet..." @if [ -d "$(SYNC_DIR)/src" ]; then cd $(SYNC_DIR)/src && $(GO) vet ./... 2>&1 || true; fi @if [ -d "$(ASYNC_DIR)/src" ]; then cd $(ASYNC_DIR)/src && $(GO) vet ./... 2>&1 || true; fi @@ -260,7 +231,7 @@ vet: check-go # Examples # ============================================================================ -examples: check-go +examples: @echo "Running Go examples..." @if [ -d "$(SYNC_DIR)/examples" ]; then \ for f in $(SYNC_DIR)/examples/*.go; do \ diff --git a/clients/go/async/examples/async_job_polling.go b/clients/go/async/examples/async_job_polling.go index 4c0c12f..d98d7c0 100644 --- a/clients/go/async/examples/async_job_polling.go +++ b/clients/go/async/examples/async_job_polling.go @@ -1,37 +1,18 @@ -// This is free 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, & 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. - /* -Async Job Polling example - standalone version +Async Job Polling example for unsandbox Go SDK - Asynchronous Version -This example demonstrates the async job polling pattern: -1. Submit a job (returns immediately with job ID) -2. Poll for completion -3. Retrieve results +This example demonstrates submitting a job asynchronously and polling for results. +Shows how to use ExecuteAsync for fire-and-forget style execution with manual polling. To run: + export UNSANDBOX_PUBLIC_KEY="your-public-key" + export UNSANDBOX_SECRET_KEY="your-secret-key" go run async_job_polling.go Expected output: Submitting async job... - Job submitted with ID: job-example-123 - Polling for completion... - Poll 1: status=queued - Poll 2: status=running - Poll 3: status=completed + Job submitted with ID: + Waiting for job completion... Job completed! Status: completed Output: Calculation result: 55 @@ -40,26 +21,51 @@ package main import ( "fmt" + "log" + "os" "time" + + un_async "github.com/unsandbox/un-go-async/src" ) func main() { - fmt.Println("Submitting async job...") + // Code that takes a bit longer to execute + code := ` +import time +total = sum(range(11)) +print(f"Calculation result: {total}") +` - // Simulate job submission - jobID := "job-example-123" - fmt.Printf("Job submitted with ID: %s\n", jobID) - - // Simulate polling - fmt.Println("Polling for completion...") - statuses := []string{"queued", "running", "completed"} - for i, status := range statuses { - time.Sleep(100 * time.Millisecond) - fmt.Printf("Poll %d: status=%s\n", i+1, status) + // Resolve credentials + creds, err := un_async.ResolveCredentials("", "") + if err != nil { + log.Printf("Error: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY environment variables required") + os.Exit(1) + } + + // Submit job asynchronously (returns immediately with job ID) + fmt.Println("Submitting async job...") + jobChan := un_async.ExecuteAsync(creds, "python", code) + jobResult := <-jobChan + + if jobResult.Err != nil { + log.Fatalf("Failed to submit job: %v", jobResult.Err) + } + + fmt.Printf("Job submitted with ID: %s\n", jobResult.JobID) + + // Wait for job completion with timeout + fmt.Println("Waiting for job completion...") + waitChan := un_async.WaitForJob(creds, jobResult.JobID, 60*time.Second) + waitResult := <-waitChan + + if waitResult.Err != nil { + log.Fatalf("Error waiting for job: %v", waitResult.Err) } - // Simulate result fmt.Println("Job completed!") - fmt.Println("Status: completed") - fmt.Println("Output: Calculation result: 55") + fmt.Printf("Status: %v\n", waitResult.Data["status"]) + if stdout, ok := waitResult.Data["stdout"].(string); ok { + fmt.Printf("Output: %s", stdout) + } } diff --git a/clients/go/async/examples/concurrent_execution.go b/clients/go/async/examples/concurrent_execution.go index a958730..5dddf38 100644 --- a/clients/go/async/examples/concurrent_execution.go +++ b/clients/go/async/examples/concurrent_execution.go @@ -1,26 +1,12 @@ -// This is free 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, & 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. - /* -Concurrent Execution example - standalone version +Concurrent Execution example for unsandbox Go SDK - Asynchronous Version -This example demonstrates running multiple operations concurrently. -Shows goroutines, channels, and sync.WaitGroup for parallel execution. +This example demonstrates running multiple code executions concurrently. +Shows the power of async operations - run multiple executions in parallel. To run: + export UNSANDBOX_PUBLIC_KEY="your-public-key" + export UNSANDBOX_SECRET_KEY="your-secret-key" go run concurrent_execution.go Expected output: @@ -34,21 +20,32 @@ package main import ( "fmt" + "log" + "os" "sync" - "time" + + un_async "github.com/unsandbox/un-go-async/src" ) type execution struct { - name string - output string + name string + language string + code string } func main() { // Define multiple executions executions := []execution{ - {"Python", "Python says hello!\n"}, - {"JavaScript", "JavaScript says hello!\n"}, - {"Ruby", "Ruby says hello!\n"}, + {"Python", "python", `print("Python says hello!")`}, + {"JavaScript", "javascript", `console.log("JavaScript says hello!");`}, + {"Ruby", "ruby", `puts "Ruby says hello!"`}, + } + + // Resolve credentials + creds, err := un_async.ResolveCredentials("", "") + if err != nil { + log.Printf("Error: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY environment variables required") + os.Exit(1) } fmt.Printf("Starting %d concurrent executions...\n", len(executions)) @@ -63,14 +60,25 @@ func main() { go func(e execution) { defer wg.Done() - // Simulate API call delay - time.Sleep(50 * time.Millisecond) + // Execute asynchronously + resultChan := un_async.ExecuteCode(creds, e.language, e.code) + result := <-resultChan mu.Lock() defer mu.Unlock() - fmt.Printf("[%s] Status: completed, Output: %s", e.name, e.output) - successCount++ + if result.Err != nil { + fmt.Printf("[%s] Error: %v\n", e.name, result.Err) + return + } + + status := result.Data["status"] + stdout := result.Data["stdout"] + fmt.Printf("[%s] Status: %v, Output: %v", e.name, status, stdout) + + if status == "completed" { + successCount++ + } }(exec) } diff --git a/clients/go/async/examples/hello_world.go b/clients/go/async/examples/hello_world.go index 121a799..e1b23f2 100644 --- a/clients/go/async/examples/hello_world.go +++ b/clients/go/async/examples/hello_world.go @@ -1,31 +1,16 @@ -// This is free 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, & 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. - /* -Hello World example - standalone version +Hello World example for unsandbox Go SDK - Asynchronous Version -This example demonstrates the async execution pattern with Go. -Shows goroutines and channels for non-blocking operations. +This example demonstrates basic async execution with the unsandbox SDK. +Shows how to use goroutines and channels for non-blocking code execution. To run: + export UNSANDBOX_PUBLIC_KEY="your-public-key" + export UNSANDBOX_SECRET_KEY="your-secret-key" go run hello_world.go Expected output: Executing code asynchronously... - Waiting for result on channel... Result status: completed Output: Hello from async unsandbox! */ @@ -33,45 +18,48 @@ package main import ( "fmt" + "log" + "os" + + un_async "github.com/unsandbox/un-go-async/src" ) -// Simulated result type -type Result struct { - Status string - Stdout string - Stderr string -} - -// Simulated async execution using goroutine and channel -func executeAsync(language, code string) <-chan Result { - resultChan := make(chan Result, 1) - - go func() { - // In real SDK, this would call the API - // Here we simulate the expected response - resultChan <- Result{ - Status: "completed", - Stdout: "Hello from async unsandbox!\n", - Stderr: "", - } - }() - - return resultChan -} - func main() { + // The code to execute code := `print("Hello from async unsandbox!")` - fmt.Println("Executing code asynchronously...") - resultChan := executeAsync("python", code) + // Resolve credentials from environment + creds, err := un_async.ResolveCredentials("", "") + if err != nil { + log.Printf("Error: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY environment variables required") + log.Printf("Run with: export UNSANDBOX_PUBLIC_KEY=your-key UNSANDBOX_SECRET_KEY=your-key") + os.Exit(1) + } - fmt.Println("Waiting for result on channel...") + // Execute the code asynchronously (returns channel) + fmt.Println("Executing code asynchronously...") + resultChan := un_async.ExecuteCode(creds, "python", code) + + // Wait for result from channel result := <-resultChan - if result.Status == "completed" { - fmt.Printf("Result status: %s\n", result.Status) - fmt.Printf("Output: %s", result.Stdout) + // Check for errors + if result.Err != nil { + log.Fatalf("Execution error: %v", result.Err) + } + + // Check status + if status, ok := result.Data["status"].(string); ok && status == "completed" { + fmt.Printf("Result status: %s\n", status) + if stdout, ok := result.Data["stdout"].(string); ok { + fmt.Printf("Output: %s", stdout) + } + if stderr, ok := result.Data["stderr"].(string); ok && stderr != "" { + fmt.Printf("Errors: %s", stderr) + } } else { - fmt.Printf("Execution failed with status: %s\n", result.Status) + status := result.Data["status"] + errMsg := result.Data["error"] + log.Fatalf("Execution failed with status: %v, error: %v", status, errMsg) } } diff --git a/clients/go/async/src/un_async.go b/clients/go/async/src/un_async.go index d53d80c..ced6ea8 100644 --- a/clients/go/async/src/un_async.go +++ b/clients/go/async/src/un_async.go @@ -1,18 +1,90 @@ -// This is free 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, & 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. +/* +PUBLIC DOMAIN - NO LICENSE, NO WARRANTY + +unsandbox.com Go SDK (Asynchronous) + +Library Usage: + import "un_async" + + // Create credentials + creds, err := un_async.ResolveCredentials("", "") + if err != nil { + log.Fatal(err) + } + + // Execute code asynchronously (returns channel) + resultChan := un_async.ExecuteCode(creds, "python", `print("hello")`) + result := <-resultChan + if result.Err != nil { + log.Fatal(result.Err) + } + fmt.Println(result.Data) + + // Submit async job and get job ID + jobChan := un_async.ExecuteAsync(creds, "javascript", `console.log("hello")`) + jobResult := <-jobChan + if jobResult.Err != nil { + log.Fatal(jobResult.Err) + } + fmt.Println(jobResult.JobID) + + // Wait for job completion with timeout + waitChan := un_async.WaitForJob(creds, jobResult.JobID, 60*time.Second) + waitResult := <-waitChan + if waitResult.Err != nil { + log.Fatal(waitResult.Err) + } + + // List all jobs + listChan := un_async.ListJobs(creds) + listResult := <-listChan + if listResult.Err == nil { + for _, job := range listResult.Jobs { + fmt.Println(job) + } + } + + // Get supported languages (cached) + langChan := un_async.GetLanguages(creds) + langResult := <-langChan + if langResult.Err == nil { + for _, lang := range langResult.Languages { + fmt.Println(lang) + } + } + + // Detect language from filename (synchronous, no I/O) + lang := un_async.DetectLanguage("script.py") // Returns "python" + + // Snapshot operations + snapChan := un_async.SessionSnapshot(creds, sessionID, "my_snapshot", false) + snapResult := <-snapChan + +Authentication Priority (4-tier): + 1. Function arguments (creds struct with 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) + + Format: public_key,secret_key (one per line) + Account selection: UNSANDBOX_ACCOUNT=N env var (0-based index) + +Request Authentication (HMAC-SHA256): + Authorization: Bearer (identifies account) + X-Timestamp: (replay prevention) + X-Signature: HMAC-SHA256(secret_key, msg) (proves secret + body integrity) + + Message format: "timestamp:METHOD:path:body" + - timestamp: seconds since epoch + - METHOD: GET, POST, DELETE, etc. (uppercase) + - path: e.g., "/execute", "/jobs/123" + - body: JSON payload (empty string for GET/DELETE) + +Languages Cache: + - Cached in ~/.unsandbox/languages.json + - TTL: 1 hour + - Updated on successful API calls +*/ package un_async diff --git a/clients/go/async/src/un_async_test.go b/clients/go/async/src/un_async_test.go deleted file mode 100644 index 1879ffb..0000000 --- a/clients/go/async/src/un_async_test.go +++ /dev/null @@ -1,396 +0,0 @@ -// This is free 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, & 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. - -/* -Tests for the unsandbox Go SDK (Asynchronous) - -Run tests: - cd /home/fox/git/un-inception/clients/go/async - go test ./tests/... - -Or run with verbose output: - go test -v ./tests/... -*/ -package tests - -import ( - "os" - "path/filepath" - "testing" - "time" - - un_async "github.com/unsandbox/un-go-async/src" -) - -// TestDetectLanguage tests language detection from filenames -func TestDetectLanguage(t *testing.T) { - tests := []struct { - filename string - expected string - }{ - {"hello.py", "python"}, - {"script.js", "javascript"}, - {"main.go", "go"}, - {"test.rs", "rust"}, - {"program.c", "c"}, - {"app.rb", "ruby"}, - {"test.php", "php"}, - {"script.sh", "bash"}, - {"data.R", "r"}, // Uppercase R - {"data.r", "r"}, // Lowercase r - {"unknown", ""}, // No extension - {"no_ext", ""}, // No extension - {"file.xyz", ""}, // Unknown extension - {"test.ts", "typescript"}, - {"code.java", "java"}, - {"test.kt", "kotlin"}, - {"app.ex", "elixir"}, - {"prog.erl", "erlang"}, - {"script.lua", "lua"}, - {"test.nim", "nim"}, - } - - for _, tc := range tests { - t.Run(tc.filename, func(t *testing.T) { - result := un_async.DetectLanguage(tc.filename) - if result != tc.expected { - t.Errorf("DetectLanguage(%q) = %q, want %q", tc.filename, result, tc.expected) - } - }) - } -} - -// TestSignRequest tests HMAC-SHA256 signature generation -func TestSignRequest(t *testing.T) { - // This is a basic test to ensure signature generation works - // The actual signature validation would need to match server-side implementation - secretKey := "test-secret-key" - timestamp := int64(1234567890) - method := "POST" - path := "/execute" - body := []byte(`{"language":"python","code":"print(42)"}`) - - // We test that signRequest returns a non-empty 64-character hex string - // Note: signRequest is not exported, so we test via LanguageMap as a proxy - // for now we just verify the LanguageMap is properly defined - if len(un_async.LanguageMap) == 0 { - t.Error("LanguageMap should not be empty") - } - - // Verify constants are defined - if un_async.APIBase != "https://api.unsandbox.com" { - t.Errorf("APIBase = %q, want %q", un_async.APIBase, "https://api.unsandbox.com") - } - - if un_async.LanguagesCacheTTL != 3600 { - t.Errorf("LanguagesCacheTTL = %d, want %d", un_async.LanguagesCacheTTL, 3600) - } - - // Verify poll delays are defined - if len(un_async.PollDelaysMs) == 0 { - t.Error("PollDelaysMs should not be empty") - } - - // Use the variables to avoid unused variable warnings - _ = secretKey - _ = timestamp - _ = method - _ = path - _ = body -} - -// TestCredentialsError tests the CredentialsError type -func TestCredentialsError(t *testing.T) { - err := &un_async.CredentialsError{Message: "test error"} - if err.Error() != "test error" { - t.Errorf("CredentialsError.Error() = %q, want %q", err.Error(), "test error") - } -} - -// TestResolveCredentialsFromArgs tests credential resolution from arguments -func TestResolveCredentialsFromArgs(t *testing.T) { - creds, err := un_async.ResolveCredentials("test-pk", "test-sk") - if err != nil { - t.Fatalf("ResolveCredentials failed: %v", err) - } - - if creds.PublicKey != "test-pk" { - t.Errorf("PublicKey = %q, want %q", creds.PublicKey, "test-pk") - } - - if creds.SecretKey != "test-sk" { - t.Errorf("SecretKey = %q, want %q", creds.SecretKey, "test-sk") - } -} - -// TestResolveCredentialsFromEnv tests credential resolution from environment -func TestResolveCredentialsFromEnv(t *testing.T) { - // Save original values - origPK := os.Getenv("UNSANDBOX_PUBLIC_KEY") - origSK := os.Getenv("UNSANDBOX_SECRET_KEY") - - // Set test values - os.Setenv("UNSANDBOX_PUBLIC_KEY", "env-pk") - os.Setenv("UNSANDBOX_SECRET_KEY", "env-sk") - - // Restore after test - defer func() { - if origPK != "" { - os.Setenv("UNSANDBOX_PUBLIC_KEY", origPK) - } else { - os.Unsetenv("UNSANDBOX_PUBLIC_KEY") - } - if origSK != "" { - os.Setenv("UNSANDBOX_SECRET_KEY", origSK) - } else { - os.Unsetenv("UNSANDBOX_SECRET_KEY") - } - }() - - creds, err := un_async.ResolveCredentials("", "") - if err != nil { - t.Fatalf("ResolveCredentials failed: %v", err) - } - - if creds.PublicKey != "env-pk" { - t.Errorf("PublicKey = %q, want %q", creds.PublicKey, "env-pk") - } - - if creds.SecretKey != "env-sk" { - t.Errorf("SecretKey = %q, want %q", creds.SecretKey, "env-sk") - } -} - -// TestResolveCredentialsArgsOverrideEnv tests that args take priority over env -func TestResolveCredentialsArgsOverrideEnv(t *testing.T) { - // Save original values - origPK := os.Getenv("UNSANDBOX_PUBLIC_KEY") - origSK := os.Getenv("UNSANDBOX_SECRET_KEY") - - // Set env values - os.Setenv("UNSANDBOX_PUBLIC_KEY", "env-pk") - os.Setenv("UNSANDBOX_SECRET_KEY", "env-sk") - - // Restore after test - defer func() { - if origPK != "" { - os.Setenv("UNSANDBOX_PUBLIC_KEY", origPK) - } else { - os.Unsetenv("UNSANDBOX_PUBLIC_KEY") - } - if origSK != "" { - os.Setenv("UNSANDBOX_SECRET_KEY", origSK) - } else { - os.Unsetenv("UNSANDBOX_SECRET_KEY") - } - }() - - // Args should override env - creds, err := un_async.ResolveCredentials("arg-pk", "arg-sk") - if err != nil { - t.Fatalf("ResolveCredentials failed: %v", err) - } - - if creds.PublicKey != "arg-pk" { - t.Errorf("PublicKey = %q, want %q", creds.PublicKey, "arg-pk") - } - - if creds.SecretKey != "arg-sk" { - t.Errorf("SecretKey = %q, want %q", creds.SecretKey, "arg-sk") - } -} - -// TestResolveCredentialsNoSourcesError tests error when no credentials found -func TestResolveCredentialsNoSourcesError(t *testing.T) { - // Save original values - origPK := os.Getenv("UNSANDBOX_PUBLIC_KEY") - origSK := os.Getenv("UNSANDBOX_SECRET_KEY") - - // Clear env values - os.Unsetenv("UNSANDBOX_PUBLIC_KEY") - os.Unsetenv("UNSANDBOX_SECRET_KEY") - - // Restore after test - defer func() { - if origPK != "" { - os.Setenv("UNSANDBOX_PUBLIC_KEY", origPK) - } - if origSK != "" { - os.Setenv("UNSANDBOX_SECRET_KEY", origSK) - } - }() - - // Should fail if no CSV files exist - _, err := un_async.ResolveCredentials("", "") - if err == nil { - t.Log("Note: ResolveCredentials succeeded - CSV file may exist in test environment") - return - } - - credErr, ok := err.(*un_async.CredentialsError) - if !ok { - t.Errorf("Expected CredentialsError, got %T", err) - return - } - - if credErr.Message == "" { - t.Error("CredentialsError.Message should not be empty") - } -} - -// TestCSVCredentials tests loading credentials from CSV file -func TestCSVCredentials(t *testing.T) { - // Create temp directory - tmpDir, err := os.MkdirTemp("", "unsandbox-test-*") - if err != nil { - t.Fatalf("Failed to create temp dir: %v", err) - } - defer os.RemoveAll(tmpDir) - - // Create test CSV file - csvPath := filepath.Join(tmpDir, "accounts.csv") - csvContent := "public_key_1,secret_key_1\n# comment line\npublic_key_2,secret_key_2\n" - if err := os.WriteFile(csvPath, []byte(csvContent), 0600); err != nil { - t.Fatalf("Failed to create CSV file: %v", err) - } - - // Change to temp dir to test ./accounts.csv loading - origDir, err := os.Getwd() - if err != nil { - t.Fatalf("Failed to get working dir: %v", err) - } - defer os.Chdir(origDir) - - if err := os.Chdir(tmpDir); err != nil { - t.Fatalf("Failed to change to temp dir: %v", err) - } - - // Clear env vars - origPK := os.Getenv("UNSANDBOX_PUBLIC_KEY") - origSK := os.Getenv("UNSANDBOX_SECRET_KEY") - os.Unsetenv("UNSANDBOX_PUBLIC_KEY") - os.Unsetenv("UNSANDBOX_SECRET_KEY") - defer func() { - if origPK != "" { - os.Setenv("UNSANDBOX_PUBLIC_KEY", origPK) - } - if origSK != "" { - os.Setenv("UNSANDBOX_SECRET_KEY", origSK) - } - }() - - // Test loading first account (index 0) - creds, err := un_async.ResolveCredentials("", "") - if err != nil { - t.Fatalf("ResolveCredentials failed: %v", err) - } - - if creds.PublicKey != "public_key_1" { - t.Errorf("PublicKey = %q, want %q", creds.PublicKey, "public_key_1") - } - - if creds.SecretKey != "secret_key_1" { - t.Errorf("SecretKey = %q, want %q", creds.SecretKey, "secret_key_1") - } - - // Test loading second account (index 1) - os.Setenv("UNSANDBOX_ACCOUNT", "1") - defer os.Unsetenv("UNSANDBOX_ACCOUNT") - - creds, err = un_async.ResolveCredentials("", "") - if err != nil { - t.Fatalf("ResolveCredentials failed: %v", err) - } - - if creds.PublicKey != "public_key_2" { - t.Errorf("PublicKey = %q, want %q", creds.PublicKey, "public_key_2") - } - - if creds.SecretKey != "secret_key_2" { - t.Errorf("SecretKey = %q, want %q", creds.SecretKey, "secret_key_2") - } -} - -// TestLanguageMapCompleteness tests that common languages are mapped -func TestLanguageMapCompleteness(t *testing.T) { - requiredMappings := map[string]string{ - "py": "python", - "js": "javascript", - "ts": "typescript", - "rb": "ruby", - "php": "php", - "go": "go", - "rs": "rust", - "c": "c", - "cpp": "cpp", - "java": "java", - "sh": "bash", - } - - for ext, expected := range requiredMappings { - if lang, ok := un_async.LanguageMap[ext]; !ok { - t.Errorf("LanguageMap missing extension %q", ext) - } else if lang != expected { - t.Errorf("LanguageMap[%q] = %q, want %q", ext, lang, expected) - } - } -} - -// TestPollDelays tests that poll delays are reasonable -func TestPollDelays(t *testing.T) { - delays := un_async.PollDelaysMs - - if len(delays) < 5 { - t.Errorf("PollDelaysMs has %d elements, want at least 5", len(delays)) - } - - // First delay should be small (for quick jobs) - if delays[0] > 500 { - t.Errorf("First poll delay %d ms too large, should be < 500ms", delays[0]) - } - - // Last delay should be reasonable (not too long) - lastDelay := delays[len(delays)-1] - if lastDelay > 5000 { - t.Errorf("Last poll delay %d ms too large, should be < 5000ms", lastDelay) - } -} - -// TestAsyncChannelBehavior tests that async functions return properly buffered channels -func TestAsyncChannelBehavior(t *testing.T) { - // Create test credentials - creds := &un_async.Credentials{ - PublicKey: "test-pk", - SecretKey: "test-sk", - } - - // Test that ExecuteCode returns a channel (even if request fails) - resultChan := un_async.ExecuteCode(creds, "python", "print(1)") - if resultChan == nil { - t.Error("ExecuteCode returned nil channel") - } - - // The channel should be buffered and eventually close - select { - case result := <-resultChan: - // We expect an error since we're using test credentials - if result.Err == nil { - t.Log("Note: ExecuteCode succeeded - may have valid credentials") - } - case <-time.After(30 * time.Second): - t.Error("ExecuteCode channel did not receive result within timeout") - } -} diff --git a/clients/go/async/tests/un_async_test.go b/clients/go/async/tests/un_async_test.go index 1879ffb..81e1341 100644 --- a/clients/go/async/tests/un_async_test.go +++ b/clients/go/async/tests/un_async_test.go @@ -1,19 +1,3 @@ -// This is free 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, & 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. - /* Tests for the unsandbox Go SDK (Asynchronous) diff --git a/clients/go/sync/examples/hello_world.go b/clients/go/sync/examples/hello_world.go index f22cf9f..f20cafb 100644 --- a/clients/go/sync/examples/hello_world.go +++ b/clients/go/sync/examples/hello_world.go @@ -1,19 +1,3 @@ -// This is free 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, & 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. - package main import "fmt" diff --git a/clients/go/sync/src/client.go b/clients/go/sync/src/client.go deleted file mode 100644 index 7252951..0000000 --- a/clients/go/sync/src/client.go +++ /dev/null @@ -1,200 +0,0 @@ -// Client provides a struct-based API around the function-based SDK. -// Designed for consumers like orchestra that prefer method receivers -// and context-aware patterns. -package un - -import ( - "context" - "fmt" -) - -// Client wraps resolved credentials for method-based API access. -type Client struct { - Creds *Credentials -} - -// NewClient creates a client from explicit credentials. -func NewClient(publicKey, secretKey string) *Client { - return &Client{Creds: &Credentials{PublicKey: publicKey, SecretKey: secretKey}} -} - -// NewClientFromEnv resolves credentials from the 4-tier priority system. -func NewClientFromEnv() (*Client, error) { - creds, err := ResolveCredentials("", "", -1) - if err != nil { - return nil, err - } - return &Client{Creds: creds}, nil -} - -// ExecuteResult holds typed execution output. -type ExecuteResult struct { - JobID string - Status string - Output string - Error string - Raw map[string]interface{} -} - -func toExecuteResult(raw map[string]interface{}) *ExecuteResult { - r := &ExecuteResult{Raw: raw} - if v, ok := raw["job_id"].(string); ok { - r.JobID = v - } - if v, ok := raw["status"].(string); ok { - r.Status = v - } - // unsandbox API returns stdout/stderr, not output/error - if v, ok := raw["output"].(string); ok { - r.Output = v - } - if v, ok := raw["stdout"].(string); ok && r.Output == "" { - r.Output = v - } - if v, ok := raw["error"].(string); ok { - r.Error = v - } - if v, ok := raw["stderr"].(string); ok && r.Error == "" { - r.Error = v - } - return r -} - -// Execute runs code synchronously (blocks until completion). -func (c *Client) Execute(_ context.Context, language, code string) (*ExecuteResult, error) { - return c.ExecuteWithOpts(context.Background(), language, code, "") -} - -// ExecuteWithOpts runs code with optional network mode. -func (c *Client) ExecuteWithOpts(_ context.Context, language, code, networkMode string) (*ExecuteResult, error) { - // Use the low-level makeRequest to support network_mode - data := map[string]interface{}{ - "language": language, - "code": code, - } - if networkMode != "" { - data["network_mode"] = networkMode - } - - resp, err := makeRequest("POST", "/execute", c.Creds, data) - if err != nil { - return nil, err - } - - result := toExecuteResult(resp) - - // Poll if job is not terminal - if result.JobID != "" && result.Status != "completed" && result.Status != "failed" { - polled, err := WaitForJob(c.Creds, result.JobID) - if err != nil { - return nil, err - } - return toExecuteResult(polled), nil - } - - return result, nil -} - -// WaitForJobResult polls a job until completion. -func (c *Client) WaitForJobResult(_ context.Context, jobID string) (*ExecuteResult, error) { - raw, err := WaitForJob(c.Creds, jobID) - if err != nil { - return nil, err - } - return toExecuteResult(raw), nil -} - -// SessionResult holds typed session info. -type SessionResult struct { - ID string - Status string - Raw map[string]interface{} -} - -// CreateSession creates a new execution session. -func (c *Client) CreateSession(_ context.Context, language, networkMode string) (*SessionResult, error) { - opts := &SessionOptions{ - Shell: language, - } - if networkMode != "" { - opts.NetworkMode = networkMode - } - - raw, err := CreateSession(c.Creds, opts) - if err != nil { - return nil, err - } - - s := &SessionResult{Raw: raw} - if v, ok := raw["session_id"].(string); ok { - s.ID = v - } - if v, ok := raw["id"].(string); ok && s.ID == "" { - s.ID = v - } - if v, ok := raw["status"].(string); ok { - s.Status = v - } - return s, nil -} - -// ShellExec runs a command in an existing session. -func (c *Client) ShellExec(_ context.Context, sessionID, command string) (*ExecuteResult, error) { - raw, err := ShellSession(c.Creds, sessionID, command) - if err != nil { - return nil, err - } - - result := toExecuteResult(raw) - - // Poll if async - if result.JobID != "" && result.Status != "completed" && result.Status != "failed" { - polled, err := WaitForJob(c.Creds, result.JobID) - if err != nil { - return nil, err - } - return toExecuteResult(polled), nil - } - - return result, nil -} - -// DestroySession deletes a session. -func (c *Client) DestroySession(_ context.Context, sessionID string) error { - _, err := DeleteSession(c.Creds, sessionID) - return err -} - -// Sessions returns all active sessions. -func (c *Client) Sessions(_ context.Context) ([]map[string]interface{}, error) { - return ListSessions(c.Creds) -} - -// Services returns all services. -func (c *Client) Services(_ context.Context) ([]map[string]interface{}, error) { - return ListServices(c.Creds) -} - -// ServiceLogs retrieves logs for a service. -func (c *Client) ServiceLogs(_ context.Context, serviceID string) (string, error) { - raw, err := GetServiceLogs(c.Creds, serviceID, false) - if err != nil { - return "", err - } - if v, ok := raw["logs"].(string); ok { - return v, nil - } - return "", nil -} - -// CheckKeys verifies credentials are valid and returns key info. -func (c *Client) CheckKeys(_ context.Context) (map[string]interface{}, error) { - return ValidateKeys(c.Creds) -} - -// InjectDirectory uploads a local directory to a session as a tarball. -// This is a higher-level operation that creates a tar.gz, base64-chunks it, -// and streams it into the session via shell commands. -func (c *Client) InjectDirectory(_ context.Context, sessionID, localDir, remoteDir string) error { - return fmt.Errorf("InjectDirectory not implemented in SDK client — use orchestra's upload package") -} diff --git a/clients/go/sync/src/functional_test.go b/clients/go/sync/src/functional_test.go deleted file mode 100644 index f27b60e..0000000 --- a/clients/go/sync/src/functional_test.go +++ /dev/null @@ -1,181 +0,0 @@ -// This is free 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, & 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. - -// UN Go SDK - Functional Tests -// -// Tests library functions against real API. -// Requires: UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY -// -// Usage: -// Copy to sync/src/ then: go test -v -run TestFunctional -package un - -import ( - "os" - "strings" - "testing" -) - -func skipIfNoCreds(t *testing.T) *Credentials { - t.Helper() - pk := os.Getenv("UNSANDBOX_PUBLIC_KEY") - sk := os.Getenv("UNSANDBOX_SECRET_KEY") - if pk == "" || sk == "" { - t.Skip("UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY required") - } - return &Credentials{PublicKey: pk, SecretKey: sk} -} - -func TestFunctionalHealthCheck(t *testing.T) { - _ = skipIfNoCreds(t) - result := HealthCheck() - // HealthCheck returns a bool - just verify it runs without panic - t.Logf("HealthCheck: %v", result) -} - -func TestFunctionalValidateKeys(t *testing.T) { - creds := skipIfNoCreds(t) - info, err := ValidateKeys(creds) - if err != nil { - t.Fatalf("ValidateKeys error: %v", err) - } - if info == nil { - t.Fatal("ValidateKeys returned nil") - } - valid, ok := info["valid"] - if !ok { - t.Fatal("ValidateKeys result missing 'valid' key") - } - if valid != true { - t.Errorf("Keys should be valid, got: %v", valid) - } -} - -func TestFunctionalGetLanguages(t *testing.T) { - creds := skipIfNoCreds(t) - langs, err := GetLanguages(creds) - if err != nil { - t.Fatalf("GetLanguages error: %v", err) - } - if len(langs) == 0 { - t.Fatal("GetLanguages returned empty list") - } - foundPython := false - for _, l := range langs { - if l == "python" { - foundPython = true - break - } - } - if !foundPython { - t.Error("python not found in languages list") - } - t.Logf("Found %d languages", len(langs)) -} - -func TestFunctionalExecute(t *testing.T) { - creds := skipIfNoCreds(t) - result, err := ExecuteCode(creds, "python", "print('hello from Go SDK')") - if err != nil { - t.Fatalf("ExecuteCode error: %v", err) - } - if result == nil { - t.Fatal("ExecuteCode returned nil") - } - stdout, _ := result["stdout"].(string) - if !strings.Contains(stdout, "hello from Go SDK") { - t.Errorf("stdout should contain 'hello from Go SDK', got: %s", stdout) - } - exitCode, _ := result["exit_code"].(float64) - if exitCode != 0 { - t.Errorf("exit_code should be 0, got: %v", exitCode) - } -} - -func TestFunctionalExecuteError(t *testing.T) { - creds := skipIfNoCreds(t) - result, err := ExecuteCode(creds, "python", "import sys; sys.exit(1)") - if err != nil { - t.Fatalf("ExecuteCode error: %v", err) - } - if result == nil { - t.Fatal("ExecuteCode returned nil") - } - exitCode, _ := result["exit_code"].(float64) - if exitCode != 1 { - t.Errorf("exit_code should be 1, got: %v", exitCode) - } -} - -func TestFunctionalSessionList(t *testing.T) { - creds := skipIfNoCreds(t) - sessions, err := ListSessions(creds) - if err != nil { - t.Fatalf("ListSessions error: %v", err) - } - t.Logf("Found %d sessions", len(sessions)) -} - -func TestFunctionalSessionLifecycle(t *testing.T) { - creds := skipIfNoCreds(t) - - // Create - session, err := CreateSession(creds, nil) - if err != nil { - t.Fatalf("CreateSession error: %v", err) - } - if session == nil { - t.Fatal("CreateSession returned nil") - } - sessionID, _ := session["id"].(string) - if sessionID == "" { - t.Fatal("Session missing id") - } - t.Logf("Created session: %s", sessionID) - - // Destroy - _, err = DeleteSession(creds, sessionID) - if err != nil { - t.Errorf("DeleteSession error: %v", err) - } -} - -func TestFunctionalServiceList(t *testing.T) { - creds := skipIfNoCreds(t) - services, err := ListServices(creds) - if err != nil { - t.Fatalf("ListServices error: %v", err) - } - t.Logf("Found %d services", len(services)) -} - -func TestFunctionalSnapshotList(t *testing.T) { - creds := skipIfNoCreds(t) - snapshots, err := ListSnapshots(creds) - if err != nil { - t.Fatalf("ListSnapshots error: %v", err) - } - t.Logf("Found %d snapshots", len(snapshots)) -} - -func TestFunctionalImageList(t *testing.T) { - creds := skipIfNoCreds(t) - images, err := ListImages(creds, "") - if err != nil { - t.Fatalf("ListImages error: %v", err) - } - t.Logf("Found %d images", len(images)) -} diff --git a/clients/go/sync/src/go.mod b/clients/go/sync/src/go.mod deleted file mode 100644 index 95ae059..0000000 --- a/clients/go/sync/src/go.mod +++ /dev/null @@ -1,3 +0,0 @@ -module github.com/russellballestrini/un-inception/clients/go/sync/src - -go 1.23.6 diff --git a/clients/go/sync/src/un.go b/clients/go/sync/src/un.go index 06cfbda..fc04267 100644 --- a/clients/go/sync/src/un.go +++ b/clients/go/sync/src/un.go @@ -1,18 +1,81 @@ -// This is free 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, & 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. +/* +PUBLIC DOMAIN - NO LICENSE, NO WARRANTY + +unsandbox.com Go SDK (Synchronous) + +Library Usage: + import "un" + + // Create credentials + creds, err := un.ResolveCredentials("", "") + if err != nil { + log.Fatal(err) + } + + // Execute code synchronously + result, err := un.ExecuteCode(creds, "python", `print("hello")`) + if err != nil { + log.Fatal(err) + } + + // Execute asynchronously + jobID, err := un.ExecuteAsync(creds, "javascript", `console.log("hello")`) + if err != nil { + log.Fatal(err) + } + + // Wait for job completion with exponential backoff + result, err := un.WaitForJob(creds, jobID) + if err != nil { + log.Fatal(err) + } + + // List all jobs + jobs, err := un.ListJobs(creds) + if err != nil { + log.Fatal(err) + } + + // Get supported languages + languages, err := un.GetLanguages(creds) + if err != nil { + log.Fatal(err) + } + + // Detect language from filename + lang := un.DetectLanguage("script.py") // Returns "python" + + // Snapshot operations (NEW) + snapshotID, err := un.SessionSnapshot(creds, sessionID, "my_snapshot", false) + snapshots, err := un.ListSnapshots(creds) + result, err := un.RestoreSnapshot(creds, snapshotID) + err = un.DeleteSnapshot(creds, snapshotID) + +Authentication Priority (4-tier): + 1. Function 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) + + Format: public_key,secret_key (one per line) + Account selection: UNSANDBOX_ACCOUNT=N env var (0-based index) + +Request Authentication (HMAC-SHA256): + Authorization: Bearer (identifies account) + X-Timestamp: (replay prevention) + X-Signature: HMAC-SHA256(secret_key, msg) (proves secret + body integrity) + + Message format: "timestamp:METHOD:path:body" + - timestamp: seconds since epoch + - METHOD: GET, POST, DELETE, etc. (uppercase) + - path: e.g., "/execute", "/jobs/123" + - body: JSON payload (empty string for GET/DELETE) + +Languages Cache: + - Cached in ~/.unsandbox/languages.json + - TTL: 1 hour + - Updated on successful API calls +*/ package un @@ -21,7 +84,6 @@ import ( "bytes" "crypto/hmac" "crypto/sha256" - "encoding/base64" "encoding/hex" "encoding/json" "fmt" @@ -161,12 +223,10 @@ func loadCredentialsFromCsv(csvPath string, accountIndex int) *Credentials { // // Priority: // 1. Function arguments (publicKey, secretKey non-empty) -// 2. accountIndex >= 0 → load from accounts.csv row N (before env vars) -// 3. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY) -// 4. Default CSV lookup (account 0 or UNSANDBOX_ACCOUNT env) -// -// Pass accountIndex = -1 to mean "not specified". -func ResolveCredentials(publicKey, secretKey string, accountIndex int) (*Credentials, error) { +// 2. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY) +// 3. ~/.unsandbox/accounts.csv +// 4. ./accounts.csv +func ResolveCredentials(publicKey, secretKey string) (*Credentials, error) { // Tier 1: Function arguments if publicKey != "" && secretKey != "" { return &Credentials{ @@ -175,23 +235,7 @@ func ResolveCredentials(publicKey, secretKey string, accountIndex int) (*Credent }, nil } - // Tier 2: Explicit account index → load from CSV before checking env vars - if accountIndex >= 0 { - unsandboxDir, err := getUnsandboxDir() - if err == nil { - if creds := loadCredentialsFromCsv(filepath.Join(unsandboxDir, "accounts.csv"), accountIndex); creds != nil { - return creds, nil - } - } - if creds := loadCredentialsFromCsv("accounts.csv", accountIndex); creds != nil { - return creds, nil - } - return nil, &CredentialsError{ - Message: fmt.Sprintf("No credentials found at account index %d in accounts.csv", accountIndex), - } - } - - // Tier 3: Environment variables + // Tier 2: Environment variables envPk := os.Getenv("UNSANDBOX_PUBLIC_KEY") envSk := os.Getenv("UNSANDBOX_SECRET_KEY") if envPk != "" && envSk != "" { @@ -201,36 +245,35 @@ func ResolveCredentials(publicKey, secretKey string, accountIndex int) (*Credent }, nil } - // Determine default account index from env - defaultIndex := 0 + // Determine account index + accountIndex := 0 if envAccount := os.Getenv("UNSANDBOX_ACCOUNT"); envAccount != "" { var err error - defaultIndex, err = strconv.Atoi(envAccount) + accountIndex, err = strconv.Atoi(envAccount) if err != nil { - defaultIndex = 0 + accountIndex = 0 } } - // Tier 4: ~/.unsandbox/accounts.csv + // Tier 3: ~/.unsandbox/accounts.csv unsandboxDir, err := getUnsandboxDir() if err == nil { - if creds := loadCredentialsFromCsv(filepath.Join(unsandboxDir, "accounts.csv"), defaultIndex); creds != nil { + if creds := loadCredentialsFromCsv(filepath.Join(unsandboxDir, "accounts.csv"), accountIndex); creds != nil { return creds, nil } } - // Tier 5: ./accounts.csv - if creds := loadCredentialsFromCsv("accounts.csv", defaultIndex); creds != nil { + // Tier 4: ./accounts.csv + if creds := loadCredentialsFromCsv("accounts.csv", accountIndex); creds != nil { return creds, nil } return nil, &CredentialsError{ Message: "No credentials found. Please provide via:\n" + " 1. Function arguments (publicKey, secretKey)\n" + - " 2. --account N flag (CSV row N)\n" + - " 3. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY)\n" + - " 4. ~/.unsandbox/accounts.csv\n" + - " 5. ./accounts.csv", + " 2. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY)\n" + + " 3. ~/.unsandbox/accounts.csv\n" + + " 4. ./accounts.csv", } } @@ -860,19 +903,12 @@ func ShellSession(creds *Credentials, sessionID, command string) (map[string]int // Service Operations // ============================================================================ -// InputFile represents a file to upload with a service create or redeploy. -type InputFile struct { - Filename string `json:"filename"` - Content string `json:"content"` // base64-encoded -} - // ServiceOptions contains optional parameters for service creation. type ServiceOptions struct { - NetworkMode string // "zerotrust" (default) or "semitrusted" - Shell string // Shell to use for bootstrap - VCPU int // Number of virtual CPUs - UnfreezeOnDemand bool // Enable automatic unfreezing on HTTP request - InputFiles []InputFile // Files to include (written to /tmp/ in container) + NetworkMode string // "zerotrust" (default) or "semitrusted" + Shell string // Shell to use for bootstrap + VCPU int // Number of virtual CPUs + UnfreezeOnDemand bool // Enable automatic unfreezing on HTTP request } // ServiceUpdateOptions contains optional parameters for service updates. @@ -929,9 +965,6 @@ func CreateService(creds *Credentials, name string, ports []int, bootstrap strin if opts.UnfreezeOnDemand { data["unfreeze_on_demand"] = true } - if len(opts.InputFiles) > 0 { - data["input_files"] = opts.InputFiles - } } return makeRequest("POST", "/services", creds, data) @@ -1047,15 +1080,11 @@ func ExportServiceEnv(creds *Credentials, serviceID string) (map[string]interfac // creds: API credentials // serviceID: Service ID // bootstrap: New bootstrap script (empty string to keep existing) -// inputFiles: Optional files to include (written to /tmp/ in container) -func RedeployService(creds *Credentials, serviceID string, bootstrap string, inputFiles []InputFile) (map[string]interface{}, error) { +func RedeployService(creds *Credentials, serviceID string, bootstrap string) (map[string]interface{}, error) { data := make(map[string]interface{}) if bootstrap != "" { data["bootstrap"] = bootstrap } - if len(inputFiles) > 0 { - data["input_files"] = inputFiles - } return makeRequest("POST", fmt.Sprintf("/services/%s/redeploy", serviceID), creds, data) } @@ -1609,7 +1638,7 @@ func LogsStream(creds *Credentials, source, grep string, callback LogCallback) e // ============================================================================ // SDKVersion is the version of this SDK. -const SDKVersion = "4.3.4" +const SDKVersion = "4.3.1" // HmacSign computes an HMAC-SHA256 signature for the given message using the secret key. // Returns the signature as a lowercase hex string. @@ -1666,19 +1695,18 @@ const ( // CLIOptions holds parsed CLI arguments type CLIOptions struct { // Global options - Shell string - Env []string - Files []string - FilePaths []string - Artifacts bool - OutputDir string - PublicKey string - SecretKey string - Network string - VCPU int - Yes bool - Help bool - AccountIndex int // -1 means not specified + Shell string + Env []string + Files []string + FilePaths []string + Artifacts bool + OutputDir string + PublicKey string + SecretKey string + Network string + VCPU int + Yes bool + Help bool // Command Command string @@ -1934,22 +1962,6 @@ func readFileContents(path string) (string, error) { return string(data), nil } -// buildInputFiles reads files from paths and returns InputFile structs with base64-encoded content. -func buildInputFiles(paths []string) ([]InputFile, error) { - var files []InputFile - for _, fpath := range paths { - data, err := os.ReadFile(fpath) - if err != nil { - return nil, fmt.Errorf("cannot read input file %s: %w", fpath, err) - } - files = append(files, InputFile{ - Filename: filepath.Base(fpath), - Content: base64.StdEncoding.EncodeToString(data), - }) - } - return files, nil -} - // readEnvFile reads environment variables from a .env file func readEnvFile(path string) (map[string]string, error) { data, err := os.ReadFile(path) @@ -2404,15 +2416,7 @@ func runService(creds *Credentials, args []string, opts *CLIOptions) int { // Redeploy service if fs.redeploy != "" { - var inputFiles []InputFile - if len(opts.Files) > 0 { - var err error - inputFiles, err = buildInputFiles(opts.Files) - if err != nil { - return cliError(err.Error(), ExitGeneralError) - } - } - _, err := RedeployService(creds, fs.redeploy, fs.bootstrap, inputFiles) + _, err := RedeployService(creds, fs.redeploy, fs.bootstrap) if err != nil { return cliError(err.Error(), ExitAPIError) } @@ -2471,13 +2475,6 @@ func runService(creds *Credentials, args []string, opts *CLIOptions) int { if opts.VCPU > 0 { serviceOpts.VCPU = opts.VCPU } - if len(opts.Files) > 0 { - inputFiles, err := buildInputFiles(opts.Files) - if err != nil { - return cliError(err.Error(), ExitGeneralError) - } - serviceOpts.InputFiles = inputFiles - } service, err := CreateService(creds, fs.name, ports, bootstrap, serviceOpts) if err != nil { @@ -3173,7 +3170,7 @@ func runLanguages(creds *Credentials, args []string) int { // parseGlobalFlags parses global CLI options func parseGlobalFlags(args []string) (*CLIOptions, []string) { - opts := &CLIOptions{AccountIndex: -1} + opts := &CLIOptions{} remaining := []string{} for i := 0; i < len(args); i++ { @@ -3216,13 +3213,6 @@ func parseGlobalFlags(args []string) (*CLIOptions, []string) { opts.SecretKey = args[i+1] i++ } - case arg == "--account": - if i+1 < len(args) { - if v, err := strconv.Atoi(args[i+1]); err == nil { - opts.AccountIndex = v - } - i++ - } case arg == "-n" || arg == "--network": if i+1 < len(args) { opts.Network = args[i+1] @@ -3276,7 +3266,7 @@ func CliMain() { } // Resolve credentials - creds, err := ResolveCredentials(opts.PublicKey, opts.SecretKey, opts.AccountIndex) + creds, err := ResolveCredentials(opts.PublicKey, opts.SecretKey) if err != nil { fmt.Fprintf(os.Stderr, "Error: %s\n", err) os.Exit(ExitAuthError) diff --git a/clients/go/sync/src/un_test.go b/clients/go/sync/src/un_test.go deleted file mode 100644 index d9adcd0..0000000 --- a/clients/go/sync/src/un_test.go +++ /dev/null @@ -1,379 +0,0 @@ -// This is free 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, & 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. - -// Tests for the Go unsandbox SDK -// Run with: go test -v ./tests/ -package un - -import ( - "os" - "testing" -) - -// ============================================================================ -// Unit Tests - Test exported library functions -// ============================================================================ - -func TestDetectLanguage(t *testing.T) { - tests := []struct { - filename string - expected string - }{ - {"script.py", "python"}, - {"script.js", "javascript"}, - {"script.ts", "typescript"}, - {"script.rb", "ruby"}, - {"script.go", "go"}, - {"script.rs", "rust"}, - {"script.c", "c"}, - {"script.cpp", "cpp"}, - {"script.d", "d"}, - {"script.zig", "zig"}, - {"script.sh", "bash"}, - {"script.lua", "lua"}, - {"script.php", "php"}, - {"script.unknown", ""}, - {"script", ""}, - } - - for _, tt := range tests { - t.Run(tt.filename, func(t *testing.T) { - result := DetectLanguage(tt.filename) - if result != tt.expected { - t.Errorf("DetectLanguage(%q) = %q, want %q", tt.filename, result, tt.expected) - } - }) - } -} - -func TestHmacSign(t *testing.T) { - // Test with known values - secretKey := "test-secret" - message := "test-message" - - result := HmacSign(secretKey, message) - - // Should return a 64-character hex string - if len(result) != 64 { - t.Errorf("HmacSign returned %d characters, want 64", len(result)) - } - - // Should be deterministic - result2 := HmacSign(secretKey, message) - if result != result2 { - t.Error("HmacSign is not deterministic") - } - - // Different inputs should produce different outputs - result3 := HmacSign(secretKey, "different-message") - if result == result3 { - t.Error("HmacSign returned same result for different inputs") - } -} - -func TestVersion(t *testing.T) { - version := Version() - if version == "" { - t.Error("Version() returned empty string") - } - // Should be in semver format - if len(version) < 5 { // At minimum "0.0.0" - t.Errorf("Version() = %q, expected semver format", version) - } -} - -func TestLastError(t *testing.T) { - // Set an error - SetLastError("test error message") - - // Retrieve it - err := LastError() - if err != "test error message" { - t.Errorf("LastError() = %q, want %q", err, "test error message") - } - - // Clear it - SetLastError("") - err = LastError() - if err != "" { - t.Errorf("LastError() after clear = %q, want empty", err) - } -} - -func TestCredentialsNew(t *testing.T) { - pk := "unsb-pk-test-test-test-test" - sk := "unsb-sk-test1-test2-test3-test4" - - creds := &Credentials{ - PublicKey: pk, - SecretKey: sk, - } - - if creds.PublicKey != pk { - t.Errorf("PublicKey = %q, want %q", creds.PublicKey, pk) - } - if creds.SecretKey != sk { - t.Errorf("SecretKey = %q, want %q", creds.SecretKey, sk) - } -} - -// ============================================================================ -// Integration Tests - Test SDK internal consistency -// ============================================================================ - -func TestSignRequest(t *testing.T) { - secretKey := "test-secret-key" - timestamp := int64(1704067200) // 2024-01-01 00:00:00 UTC - method := "POST" - path := "/execute" - body := `{"language":"python","code":"print(1)"}` - - signature := signRequest(secretKey, timestamp, method, path, []byte(body)) - - // Should return a 64-character hex string - if len(signature) != 64 { - t.Errorf("signRequest returned %d characters, want 64", len(signature)) - } - - // Should be deterministic - signature2 := signRequest(secretKey, timestamp, method, path, []byte(body)) - if signature != signature2 { - t.Error("signRequest is not deterministic") - } - - // Different timestamps should produce different signatures - signature3 := signRequest(secretKey, timestamp+1, method, path, []byte(body)) - if signature == signature3 { - t.Error("signRequest returned same result for different timestamps") - } -} - -func TestResolveCredentialsFromEnv(t *testing.T) { - // Save original env vars - origPK := os.Getenv("UNSANDBOX_PUBLIC_KEY") - origSK := os.Getenv("UNSANDBOX_SECRET_KEY") - - // Set test env vars - testPK := "unsb-pk-test-test-test-test" - testSK := "unsb-sk-test1-test2-test3-test4" - os.Setenv("UNSANDBOX_PUBLIC_KEY", testPK) - os.Setenv("UNSANDBOX_SECRET_KEY", testSK) - - // Test - creds, err := ResolveCredentials("", "", -1) - if err != nil { - t.Fatalf("ResolveCredentials failed: %v", err) - } - if creds.PublicKey != testPK { - t.Errorf("PublicKey = %q, want %q", creds.PublicKey, testPK) - } - if creds.SecretKey != testSK { - t.Errorf("SecretKey = %q, want %q", creds.SecretKey, testSK) - } - - // Restore original env vars - if origPK != "" { - os.Setenv("UNSANDBOX_PUBLIC_KEY", origPK) - } else { - os.Unsetenv("UNSANDBOX_PUBLIC_KEY") - } - if origSK != "" { - os.Setenv("UNSANDBOX_SECRET_KEY", origSK) - } else { - os.Unsetenv("UNSANDBOX_SECRET_KEY") - } -} - -func TestResolveCredentialsFromArgs(t *testing.T) { - testPK := "unsb-pk-arg1-arg2-arg3-arg4" - testSK := "unsb-sk-arg11-arg22-arg33-arg44" - - creds, err := ResolveCredentials(testPK, testSK, -1) - if err != nil { - t.Fatalf("ResolveCredentials failed: %v", err) - } - if creds.PublicKey != testPK { - t.Errorf("PublicKey = %q, want %q", creds.PublicKey, testPK) - } - if creds.SecretKey != testSK { - t.Errorf("SecretKey = %q, want %q", creds.SecretKey, testSK) - } -} - -// ============================================================================ -// Functional Tests - Test against real API (requires credentials) -// ============================================================================ - -func getTestCredentials(t *testing.T) *Credentials { - creds, err := ResolveCredentials("", "", -1) - if err != nil { - t.Skip("No credentials available for functional tests") - } - return creds -} - -func TestHealthCheck(t *testing.T) { - if testing.Short() { - t.Skip("Skipping functional test in short mode") - } - - healthy := HealthCheck() - if !healthy { - t.Log("API health check returned unhealthy (API may be unreachable)") - } -} - -func TestGetLanguages(t *testing.T) { - if testing.Short() { - t.Skip("Skipping functional test in short mode") - } - - creds := getTestCredentials(t) - languages, err := GetLanguages(creds) - if err != nil { - t.Fatalf("GetLanguages failed: %v", err) - } - - if len(languages) == 0 { - t.Error("GetLanguages returned empty list") - } - - // Should include common languages - hasPython := false - hasJavascript := false - for _, lang := range languages { - if lang == "python" { - hasPython = true - } - if lang == "javascript" { - hasJavascript = true - } - } - - if !hasPython { - t.Error("GetLanguages missing 'python'") - } - if !hasJavascript { - t.Error("GetLanguages missing 'javascript'") - } -} - -func TestValidateKeys(t *testing.T) { - if testing.Short() { - t.Skip("Skipping functional test in short mode") - } - - creds := getTestCredentials(t) - result, err := ValidateKeys(creds) - if err != nil { - t.Fatalf("ValidateKeys failed: %v", err) - } - - if result == nil { - t.Error("ValidateKeys returned nil") - } -} - -func TestExecuteCode(t *testing.T) { - if testing.Short() { - t.Skip("Skipping functional test in short mode") - } - - creds := getTestCredentials(t) - result, err := ExecuteCode(creds, "python", "print('hello from go test')") - if err != nil { - t.Fatalf("ExecuteCode failed: %v", err) - } - - if result == nil { - t.Error("ExecuteCode returned nil") - } - - // Check for stdout in result - if stdout, ok := result["stdout"].(string); ok { - if stdout == "" { - t.Error("ExecuteCode returned empty stdout") - } - } -} - -func TestListSessions(t *testing.T) { - if testing.Short() { - t.Skip("Skipping functional test in short mode") - } - - creds := getTestCredentials(t) - sessions, err := ListSessions(creds) - if err != nil { - t.Fatalf("ListSessions failed: %v", err) - } - - // Should return a list (possibly empty) - if sessions == nil { - t.Error("ListSessions returned nil") - } -} - -func TestListServices(t *testing.T) { - if testing.Short() { - t.Skip("Skipping functional test in short mode") - } - - creds := getTestCredentials(t) - services, err := ListServices(creds) - if err != nil { - t.Fatalf("ListServices failed: %v", err) - } - - // Should return a list (possibly empty) - if services == nil { - t.Error("ListServices returned nil") - } -} - -func TestListSnapshots(t *testing.T) { - if testing.Short() { - t.Skip("Skipping functional test in short mode") - } - - creds := getTestCredentials(t) - snapshots, err := ListSnapshots(creds) - if err != nil { - t.Fatalf("ListSnapshots failed: %v", err) - } - - // Should return a list (possibly empty) - if snapshots == nil { - t.Error("ListSnapshots returned nil") - } -} - -func TestListImages(t *testing.T) { - if testing.Short() { - t.Skip("Skipping functional test in short mode") - } - - creds := getTestCredentials(t) - images, err := ListImages(creds, "") - if err != nil { - t.Fatalf("ListImages failed: %v", err) - } - - // Should return a list (possibly empty) - if images == nil { - t.Error("ListImages returned nil") - } -} diff --git a/clients/go/sync/tests/functional_test.go b/clients/go/sync/tests/functional_test.go deleted file mode 100644 index f27b60e..0000000 --- a/clients/go/sync/tests/functional_test.go +++ /dev/null @@ -1,181 +0,0 @@ -// This is free 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, & 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. - -// UN Go SDK - Functional Tests -// -// Tests library functions against real API. -// Requires: UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY -// -// Usage: -// Copy to sync/src/ then: go test -v -run TestFunctional -package un - -import ( - "os" - "strings" - "testing" -) - -func skipIfNoCreds(t *testing.T) *Credentials { - t.Helper() - pk := os.Getenv("UNSANDBOX_PUBLIC_KEY") - sk := os.Getenv("UNSANDBOX_SECRET_KEY") - if pk == "" || sk == "" { - t.Skip("UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY required") - } - return &Credentials{PublicKey: pk, SecretKey: sk} -} - -func TestFunctionalHealthCheck(t *testing.T) { - _ = skipIfNoCreds(t) - result := HealthCheck() - // HealthCheck returns a bool - just verify it runs without panic - t.Logf("HealthCheck: %v", result) -} - -func TestFunctionalValidateKeys(t *testing.T) { - creds := skipIfNoCreds(t) - info, err := ValidateKeys(creds) - if err != nil { - t.Fatalf("ValidateKeys error: %v", err) - } - if info == nil { - t.Fatal("ValidateKeys returned nil") - } - valid, ok := info["valid"] - if !ok { - t.Fatal("ValidateKeys result missing 'valid' key") - } - if valid != true { - t.Errorf("Keys should be valid, got: %v", valid) - } -} - -func TestFunctionalGetLanguages(t *testing.T) { - creds := skipIfNoCreds(t) - langs, err := GetLanguages(creds) - if err != nil { - t.Fatalf("GetLanguages error: %v", err) - } - if len(langs) == 0 { - t.Fatal("GetLanguages returned empty list") - } - foundPython := false - for _, l := range langs { - if l == "python" { - foundPython = true - break - } - } - if !foundPython { - t.Error("python not found in languages list") - } - t.Logf("Found %d languages", len(langs)) -} - -func TestFunctionalExecute(t *testing.T) { - creds := skipIfNoCreds(t) - result, err := ExecuteCode(creds, "python", "print('hello from Go SDK')") - if err != nil { - t.Fatalf("ExecuteCode error: %v", err) - } - if result == nil { - t.Fatal("ExecuteCode returned nil") - } - stdout, _ := result["stdout"].(string) - if !strings.Contains(stdout, "hello from Go SDK") { - t.Errorf("stdout should contain 'hello from Go SDK', got: %s", stdout) - } - exitCode, _ := result["exit_code"].(float64) - if exitCode != 0 { - t.Errorf("exit_code should be 0, got: %v", exitCode) - } -} - -func TestFunctionalExecuteError(t *testing.T) { - creds := skipIfNoCreds(t) - result, err := ExecuteCode(creds, "python", "import sys; sys.exit(1)") - if err != nil { - t.Fatalf("ExecuteCode error: %v", err) - } - if result == nil { - t.Fatal("ExecuteCode returned nil") - } - exitCode, _ := result["exit_code"].(float64) - if exitCode != 1 { - t.Errorf("exit_code should be 1, got: %v", exitCode) - } -} - -func TestFunctionalSessionList(t *testing.T) { - creds := skipIfNoCreds(t) - sessions, err := ListSessions(creds) - if err != nil { - t.Fatalf("ListSessions error: %v", err) - } - t.Logf("Found %d sessions", len(sessions)) -} - -func TestFunctionalSessionLifecycle(t *testing.T) { - creds := skipIfNoCreds(t) - - // Create - session, err := CreateSession(creds, nil) - if err != nil { - t.Fatalf("CreateSession error: %v", err) - } - if session == nil { - t.Fatal("CreateSession returned nil") - } - sessionID, _ := session["id"].(string) - if sessionID == "" { - t.Fatal("Session missing id") - } - t.Logf("Created session: %s", sessionID) - - // Destroy - _, err = DeleteSession(creds, sessionID) - if err != nil { - t.Errorf("DeleteSession error: %v", err) - } -} - -func TestFunctionalServiceList(t *testing.T) { - creds := skipIfNoCreds(t) - services, err := ListServices(creds) - if err != nil { - t.Fatalf("ListServices error: %v", err) - } - t.Logf("Found %d services", len(services)) -} - -func TestFunctionalSnapshotList(t *testing.T) { - creds := skipIfNoCreds(t) - snapshots, err := ListSnapshots(creds) - if err != nil { - t.Fatalf("ListSnapshots error: %v", err) - } - t.Logf("Found %d snapshots", len(snapshots)) -} - -func TestFunctionalImageList(t *testing.T) { - creds := skipIfNoCreds(t) - images, err := ListImages(creds, "") - if err != nil { - t.Fatalf("ListImages error: %v", err) - } - t.Logf("Found %d images", len(images)) -} diff --git a/clients/go/sync/tests/test_account_flag.sh b/clients/go/sync/tests/test_account_flag.sh deleted file mode 100755 index c1d2f71..0000000 --- a/clients/go/sync/tests/test_account_flag.sh +++ /dev/null @@ -1,120 +0,0 @@ -#!/usr/bin/env bash -# Integration test: --account N flag must take priority over env vars -# -# Requires: UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY (real credentials) -# Run: make test-integration OR bash tests/test_account_flag.sh -# -# The defect this guards against: ResolveCredentials() checked env vars before -# account_index, so --account N was silently ignored when env vars existed. - -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -SRC_DIR="$SCRIPT_DIR/../src" -UN_BIN="$SCRIPT_DIR/../un" - -RED='\033[31m' -GREEN='\033[32m' -NC='\033[0m' - -pass=0 -fail=0 - -check() { - local desc="$1" result="$2" - if [ "$result" = "pass" ]; then - printf " ${GREEN}✓${NC} %s\n" "$desc" - pass=$((pass + 1)) - else - printf " ${RED}✗${NC} %s\n" "$desc" - fail=$((fail + 1)) - fi -} - -# Require real credentials to be available -if [ -z "${UNSANDBOX_PUBLIC_KEY:-}" ] || [ -z "${UNSANDBOX_SECRET_KEY:-}" ]; then - echo "SKIP: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set" - exit 0 -fi - -# Build the binary if it doesn't exist or source is newer -if [ ! -x "$UN_BIN" ] || [ "$SRC_DIR/un.go" -nt "$UN_BIN" ]; then - echo "Building Go binary..." - (cd "$SRC_DIR" && go build -o "$UN_BIN" .) || { - echo "FAIL: go build failed" - exit 1 - } -fi - -if [ ! -x "$UN_BIN" ]; then - echo "FAIL: UN binary not found at $UN_BIN — run go build first" - exit 1 -fi - -REAL_PK="$UNSANDBOX_PUBLIC_KEY" -REAL_SK="$UNSANDBOX_SECRET_KEY" - -# Temporary HOME with accounts.csv: -# index 0: garbage credentials (will always 401) -# index 1: real credentials (will succeed) -TMPHOME="$(mktemp -d)" -mkdir -p "$TMPHOME/.unsandbox" -trap 'rm -rf "$TMPHOME"' EXIT - -cat > "$TMPHOME/.unsandbox/accounts.csv" <&1 || true) - -if ! echo "$OUT" | grep -qi "401\|unauthorized\|invalid_credential\|No credentials"; then - check "--account 1 uses CSV row 1 (real creds) over garbage env vars" "pass" -else - check "--account 1 uses CSV row 1 (real creds) over garbage env vars" "fail" - echo " output: $OUT" -fi - -# --- Test 2: --account 0 should use CSV row 0 (garbage creds) → 401 --- -# Even though real env vars are set, explicit --account 0 should pick garbage creds -OUT=$(HOME="$TMPHOME" \ - UNSANDBOX_PUBLIC_KEY="$REAL_PK" \ - UNSANDBOX_SECRET_KEY="$REAL_SK" \ - "$UN_BIN" --account 0 key 2>&1 || true) - -if echo "$OUT" | grep -qi "401\|unauthorized\|invalid\|error"; then - check "--account 0 uses CSV row 0 (garbage creds) despite real env vars, 401 as expected" "pass" -else - check "--account 0 uses CSV row 0 (garbage creds) despite real env vars, 401 as expected" "fail" - echo " output: $OUT" -fi - -# --- Test 3: no --account flag, real env vars → env vars win over garbage CSV row 0 --- -OUT=$(HOME="$TMPHOME" \ - UNSANDBOX_PUBLIC_KEY="$REAL_PK" \ - UNSANDBOX_SECRET_KEY="$REAL_SK" \ - "$UN_BIN" key 2>&1 || true) - -if ! echo "$OUT" | grep -qi "401\|unauthorized\|invalid_credential\|No credentials"; then - check "No --account flag: env vars used, succeeds" "pass" -else - check "No --account flag: env vars used, succeeds" "fail" - echo " output: $OUT" -fi - -echo "" -echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" -printf "Passed: ${GREEN}%d${NC} Failed: ${RED}%d${NC}\n" "$pass" "$fail" -echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" - -[ "$fail" -eq 0 ] diff --git a/clients/go/sync/tests/un_test.go b/clients/go/sync/tests/un_test.go index d9adcd0..0bd06fb 100644 --- a/clients/go/sync/tests/un_test.go +++ b/clients/go/sync/tests/un_test.go @@ -1,19 +1,3 @@ -// This is free 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, & 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. - // Tests for the Go unsandbox SDK // Run with: go test -v ./tests/ package un @@ -173,7 +157,7 @@ func TestResolveCredentialsFromEnv(t *testing.T) { os.Setenv("UNSANDBOX_SECRET_KEY", testSK) // Test - creds, err := ResolveCredentials("", "", -1) + creds, err := ResolveCredentials("", "") if err != nil { t.Fatalf("ResolveCredentials failed: %v", err) } @@ -201,7 +185,7 @@ func TestResolveCredentialsFromArgs(t *testing.T) { testPK := "unsb-pk-arg1-arg2-arg3-arg4" testSK := "unsb-sk-arg11-arg22-arg33-arg44" - creds, err := ResolveCredentials(testPK, testSK, -1) + creds, err := ResolveCredentials(testPK, testSK) if err != nil { t.Fatalf("ResolveCredentials failed: %v", err) } @@ -218,7 +202,7 @@ func TestResolveCredentialsFromArgs(t *testing.T) { // ============================================================================ func getTestCredentials(t *testing.T) *Credentials { - creds, err := ResolveCredentials("", "", -1) + creds, err := ResolveCredentials("", "") if err != nil { t.Skip("No credentials available for functional tests") } diff --git a/clients/groovy/sync/src/un.groovy b/clients/groovy/sync/src/un.groovy index 05c2667..e7d11c7 100644 --- a/clients/groovy/sync/src/un.groovy +++ b/clients/groovy/sync/src/un.groovy @@ -73,7 +73,7 @@ * * * @author Permacomputer Project - * @version 4.3.4 + * @version 4.3.1 */ import javax.crypto.Mac @@ -222,67 +222,42 @@ def signRequest(String secretKey, long timestamp, String method, String path, St * @return Tuple of [publicKey, secretKey] * @throws AuthenticationError if no credentials found */ -def loadAccountsFromCsv(File path) { - def validAccounts = [] - if (!path.exists()) return validAccounts - try { - def lines = path.text.trim().split('\n') - lines.each { line -> - def trimmed = line.trim() - if (!trimmed || trimmed.startsWith('#')) return - if (trimmed.contains(',')) { - def parts = trimmed.split(',', 2) - def pk = parts[0].trim() - def sk = parts[1].trim() - if (pk.startsWith('unsb-pk-') && sk.startsWith('unsb-sk-')) { - validAccounts << [pk, sk] - } - } - } - } catch (Exception e) { - // Ignore file read errors - } - return validAccounts -} - -def getCredentials(String publicKey = null, String secretKey = null, int accountIndex = -1) { +def getCredentials(String publicKey = null, String secretKey = null, int accountIndex = 0) { // Priority 1: Function arguments if (publicKey && secretKey) { return [publicKey, secretKey] } - // Priority 2: --account N => accounts.csv row N (bypasses env vars) - if (accountIndex >= 0) { - def searchPaths = [ - new File(System.getProperty('user.home'), '.unsandbox/accounts.csv'), - new File('accounts.csv') - ] - for (path in searchPaths) { - def accts = loadAccountsFromCsv(path) - if (accts && accountIndex < accts.size()) { - return accts[accountIndex] - } - } - throw new AuthenticationError("No account at index ${accountIndex} in accounts.csv") - } - - // Priority 3: Environment variables + // Priority 2: Environment variables def envPk = System.getenv('UNSANDBOX_PUBLIC_KEY') def envSk = System.getenv('UNSANDBOX_SECRET_KEY') if (envPk && envSk) { return [envPk, envSk] } - // Priority 4: ~/.unsandbox/accounts.csv row 0 (or UNSANDBOX_ACCOUNT env) - def defaultIdx = (System.getenv('UNSANDBOX_ACCOUNT') ?: '0').toInteger() - def searchPaths = [ - new File(System.getProperty('user.home'), '.unsandbox/accounts.csv'), - new File('accounts.csv') - ] - for (path in searchPaths) { - def accts = loadAccountsFromCsv(path) - if (accts && defaultIdx < accts.size()) { - return accts[defaultIdx] + // Priority 3: Config file + def accountsPath = new File(System.getProperty('user.home'), '.unsandbox/accounts.csv') + if (accountsPath.exists()) { + try { + def lines = accountsPath.text.trim().split('\n') + def validAccounts = [] + lines.each { line -> + def trimmed = line.trim() + if (!trimmed || trimmed.startsWith('#')) return + if (trimmed.contains(',')) { + def parts = trimmed.split(',', 2) + def pk = parts[0] + def sk = parts[1] + if (pk.startsWith('unsb-pk-') && sk.startsWith('unsb-sk-')) { + validAccounts << [pk, sk] + } + } + } + if (validAccounts && accountIndex < validAccounts.size()) { + return validAccounts[accountIndex] + } + } catch (Exception e) { + // Ignore file read errors } } @@ -292,14 +267,21 @@ def getCredentials(String publicKey = null, String secretKey = null, int account ) } -// Legacy compatibility - now delegates to getCredentials for proper priority -def getApiKeys(argsKey, int accountIndex = -1) { - try { - return getCredentials(argsKey ?: null, null, accountIndex) - } catch (AuthenticationError e) { - System.err.println("${RED}Error: ${e.message}${RESET}") - System.exit(1) +// Legacy compatibility +def getApiKeys(argsKey) { + def publicKey = System.getenv('UNSANDBOX_PUBLIC_KEY') + def secretKey = System.getenv('UNSANDBOX_SECRET_KEY') + + if (!publicKey || !secretKey) { + def legacyKey = argsKey ?: System.getenv('UNSANDBOX_API_KEY') + if (!legacyKey) { + System.err.println("${RED}Error: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set${RESET}") + System.exit(1) + } + return [legacyKey, null] } + + return [publicKey, secretKey] } // ============================================================================ @@ -624,7 +606,7 @@ def execute(String language, String code, Map options = [:]) { def (publicKey, secretKey) = getCredentials( options.publicKey, options.secretKey, - options.accountIndex != null ? options.accountIndex : -1 + options.accountIndex ?: 0 ) def payload = [ @@ -674,7 +656,7 @@ def executeAsync(String language, String code, Map options = [:]) { def (publicKey, secretKey) = getCredentials( options.publicKey, options.secretKey, - options.accountIndex != null ? options.accountIndex : -1 + options.accountIndex ?: 0 ) def payload = [ @@ -721,7 +703,7 @@ def run(String code, Map options = [:]) { def (publicKey, secretKey) = getCredentials( options.publicKey, options.secretKey, - options.accountIndex != null ? options.accountIndex : -1 + options.accountIndex ?: 0 ) def ttl = options.ttl ?: DEFAULT_TTL @@ -746,7 +728,7 @@ def runAsync(String code, Map options = [:]) { def (publicKey, secretKey) = getCredentials( options.publicKey, options.secretKey, - options.accountIndex != null ? options.accountIndex : -1 + options.accountIndex ?: 0 ) def ttl = options.ttl ?: DEFAULT_TTL @@ -1607,72 +1589,45 @@ class Client { def creds = getCredentialsStatic( options.publicKey, options.secretKey, - options.accountIndex != null ? options.accountIndex : -1 + options.accountIndex ?: 0 ) this.publicKey = creds[0] this.secretKey = creds[1] } - private static loadCsvAccounts(File path) { - def accounts = [] - if (!path.exists()) return accounts - try { - path.text.trim().split('\n').each { line -> - def trimmed = line.trim() - if (!trimmed || trimmed.startsWith('#')) return - if (trimmed.contains(',')) { - def parts = trimmed.split(',', 2) - def pk = parts[0].trim() - def sk = parts[1].trim() - if (pk.startsWith('unsb-pk-') && sk.startsWith('unsb-sk-')) { - accounts << [pk, sk] - } - } - } - } catch (Exception e) { - // Ignore - } - return accounts - } - private static getCredentialsStatic(String publicKey, String secretKey, int accountIndex) { - // Priority 1: explicit arguments if (publicKey && secretKey) { return [publicKey, secretKey] } - // Priority 2: --account N => CSV row N (bypasses env vars) - if (accountIndex >= 0) { - def searchPaths = [ - new File(System.getProperty('user.home'), '.unsandbox/accounts.csv'), - new File('accounts.csv') - ] - for (path in searchPaths) { - def accts = loadCsvAccounts(path) - if (accts && accountIndex < accts.size()) { - return accts[accountIndex] - } - } - throw new AuthenticationError("No account at index ${accountIndex} in accounts.csv") - } - - // Priority 3: Environment variables def envPk = System.getenv('UNSANDBOX_PUBLIC_KEY') def envSk = System.getenv('UNSANDBOX_SECRET_KEY') if (envPk && envSk) { return [envPk, envSk] } - // Priority 4: ~/.unsandbox/accounts.csv row 0 (or UNSANDBOX_ACCOUNT env) - def defaultIdx = (System.getenv('UNSANDBOX_ACCOUNT') ?: '0').toInteger() - def searchPaths = [ - new File(System.getProperty('user.home'), '.unsandbox/accounts.csv'), - new File('accounts.csv') - ] - for (path in searchPaths) { - def accts = loadCsvAccounts(path) - if (accts && defaultIdx < accts.size()) { - return accts[defaultIdx] + def accountsPath = new File(System.getProperty('user.home'), '.unsandbox/accounts.csv') + if (accountsPath.exists()) { + try { + def lines = accountsPath.text.trim().split('\n') + def validAccounts = [] + lines.each { line -> + def trimmed = line.trim() + if (!trimmed || trimmed.startsWith('#')) return + if (trimmed.contains(',')) { + def parts = trimmed.split(',', 2) + def pk = parts[0] + def sk = parts[1] + if (pk.startsWith('unsb-pk-') && sk.startsWith('unsb-sk-')) { + validAccounts << [pk, sk] + } + } + } + if (validAccounts && accountIndex < validAccounts.size()) { + return validAccounts[accountIndex] + } + } catch (Exception e) { + // Ignore } } @@ -1783,7 +1738,6 @@ class Args { String sourceFile = null String inlineLang = null String apiKey = null - Integer accountIndex = -1 String network = null Integer vcpu = 0 List env = [] @@ -1885,7 +1839,7 @@ def serviceEnvSet(serviceId, content, publicKey, secretKey) { } def cmdServiceEnv(args) { - def (publicKey, secretKey) = getApiKeys(args.apiKey, args.accountIndex ?: -1) + def (publicKey, secretKey) = getApiKeys(args.apiKey) switch (args.envAction) { case 'status': @@ -1921,7 +1875,7 @@ def cmdServiceEnv(args) { } def cmdExecute(args) { - def (publicKey, secretKey) = getApiKeys(args.apiKey, args.accountIndex ?: -1) + def (publicKey, secretKey) = getApiKeys(args.apiKey) String code String language @@ -2002,7 +1956,7 @@ def cmdExecute(args) { } def cmdSession(args) { - def (publicKey, secretKey) = getApiKeys(args.apiKey, args.accountIndex ?: -1) + def (publicKey, secretKey) = getApiKeys(args.apiKey) if (args.sessionSnapshot) { def payload = [:] @@ -2079,7 +2033,7 @@ def openBrowser(url) { } def cmdSnapshot(args) { - def (publicKey, secretKey) = getApiKeys(args.apiKey, args.accountIndex ?: -1) + def (publicKey, secretKey) = getApiKeys(args.apiKey) if (args.snapshotList) { def output = apiRequest('/snapshots', 'GET', null, publicKey, secretKey) @@ -2119,7 +2073,7 @@ def cmdSnapshot(args) { } def cmdImage(args) { - def (publicKey, secretKey) = getApiKeys(args.apiKey, args.accountIndex ?: -1) + def (publicKey, secretKey) = getApiKeys(args.apiKey) if (args.imageList) { def output = apiRequest('/images', 'GET', null, publicKey, secretKey) @@ -2199,7 +2153,7 @@ def cmdImage(args) { } def cmdLanguages(args) { - def (publicKey, secretKey) = getApiKeys(args.apiKey, args.accountIndex ?: -1) + def (publicKey, secretKey) = getApiKeys(args.apiKey) def result = languages([publicKey: publicKey, secretKey: secretKey, forceRefresh: true]) def langList = result.languages ?: [] @@ -2214,7 +2168,7 @@ def cmdLanguages(args) { } def cmdKey(args) { - def (publicKey, secretKey) = getApiKeys(args.apiKey, args.accountIndex ?: -1) + def (publicKey, secretKey) = getApiKeys(args.apiKey) def curlCmd = ['curl', '-s', '-X', 'POST', "${PORTAL_BASE}/keys/validate", '-H', 'Content-Type: application/json'] @@ -2286,7 +2240,7 @@ def cmdKey(args) { } def cmdService(args) { - def (publicKey, secretKey) = getApiKeys(args.apiKey, args.accountIndex ?: -1) + def (publicKey, secretKey) = getApiKeys(args.apiKey) if (args.serviceSnapshot) { def payload = [:] @@ -2503,9 +2457,6 @@ def parseArgs(argv) { case '--public-key': args.apiKey = argv[++i] // For compatibility break - case '--account': - args.accountIndex = argv[++i].toInteger() - break case '-n': case '--network': args.network = argv[++i] diff --git a/clients/haskell/sync/src/un.hs b/clients/haskell/sync/src/un.hs index 91d7a34..d17d0b5 100644 --- a/clients/haskell/sync/src/un.hs +++ b/clients/haskell/sync/src/un.hs @@ -66,8 +66,6 @@ import Data.Char (isDigit, ord) import Text.Printf (printf) import Control.Monad (when, unless, forM_) import Control.Exception (try, catch, IOError) -import Data.IORef (IORef, newIORef, readIORef, writeIORef) -import System.IO.Unsafe (unsafePerformIO) import qualified Data.ByteString as BS import qualified Data.ByteString.Char8 as BSC import qualified Data.ByteString.Base64 as B64 @@ -88,11 +86,6 @@ portalBase = "https://unsandbox.com" languagesCacheTtl :: Int languagesCacheTtl = 3600 -- 1 hour in seconds --- Global account index set by --account N flag (Nothing = not set) -{-# NOINLINE cliAccountIndex #-} -cliAccountIndex :: IORef (Maybe Int) -cliAccountIndex = unsafePerformIO (newIORef Nothing) - -- ANSI colors blue, red, green, yellow, reset :: String blue = "\x1b[34m" @@ -845,26 +838,10 @@ threadDelay us = do _ <- readProcessWithExitCode "sleep" [show (fromIntegral ms / 1000.0 :: Double)] "" return () --- Strip --account N from argument list, set cliAccountIndex IORef -stripAccountArg :: [String] -> IO [String] -stripAccountArg [] = return [] -stripAccountArg ("--account":n_str:rest) = do - case reads n_str of - [(n, "")] -> do - writeIORef cliAccountIndex (Just n) - stripAccountArg rest - _ -> do - hPutStrLn stderr "Error: --account requires an integer argument" - exitFailure -stripAccountArg (arg:rest) = do - rest' <- stripAccountArg rest - return (arg : rest') - -- Main main :: IO () main = do - rawArgs <- getArgs - args <- stripAccountArg rawArgs + args <- getArgs cmd <- parseArgs args case cmd of Execute opts -> executeCommand opts @@ -888,9 +865,6 @@ printHelp = do putStrLn " un.hs languages [--json] List available languages" putStrLn " un.hs key [options] Validate/extend API key" putStrLn "" - putStrLn "Global options:" - putStrLn " --account N Use accounts.csv row N (bypasses env vars)" - putStrLn "" putStrLn "Execute options:" putStrLn " -e KEY=VALUE Environment variable" putStrLn " -f FILE Input file" @@ -1459,77 +1433,18 @@ serviceEnvDelete serviceId = do (exitCode, _, _) <- curlDelete apiKey (apiBase ++ "/services/" ++ serviceId ++ "/env") return (exitCode == ExitSuccess) --- Load credentials from a CSV file at a given account index -loadCredentialsFromCsv :: FilePath -> Int -> IO (Maybe (String, String)) -loadCredentialsFromCsv csvPath accountIndex = do - result <- (try (readFile csvPath) :: IO (Either IOError String)) - case result of - Left _ -> return Nothing - Right content -> do - let ls = filter (\l -> not (null l) && head l /= '#') $ - map trim $ - lines content - accounts = [ (pk', sk') - | l <- ls - , let (pk, rest) = break (== ',') l - , not (null rest) - , let pk' = trim pk - sk' = trim (drop 1 rest) - , length pk' > 8 && length sk' > 8 - ] - if accountIndex < length accounts then - return $ Just (accounts !! accountIndex) - else - return Nothing - where - trim = reverse . dropWhile (== ' ') . reverse . dropWhile (== ' ') - --- Get API keys with correct priority: --- 1. --account N (cliAccountIndex IORef) -> accounts.csv row N --- 2. UNSANDBOX_PUBLIC_KEY / UNSANDBOX_SECRET_KEY env vars --- 3. ~/.unsandbox/accounts.csv row 0 (or UNSANDBOX_ACCOUNT env) --- 4. ./accounts.csv row 0 +-- Get API keys from environment getApiKeys :: IO (String, Maybe String) getApiKeys = do - home <- maybe "." id <$> lookupEnv "HOME" - let homeCsv = home ++ "/.unsandbox/accounts.csv" - -- Priority 1: --account N - mIdx <- readIORef cliAccountIndex - case mIdx of - Just idx -> do - creds <- loadCredentialsFromCsv homeCsv idx - case creds of - Just (pk, sk) -> return (pk, Just sk) - Nothing -> do - creds2 <- loadCredentialsFromCsv "accounts.csv" idx - case creds2 of - Just (pk, sk) -> return (pk, Just sk) - Nothing -> do - hPutStrLn stderr $ "Error: No credentials found for account index " ++ show idx ++ " in accounts.csv" - exitFailure - Nothing -> do - -- Priority 2: environment variables - publicKey <- lookupEnv "UNSANDBOX_PUBLIC_KEY" - secretKey <- lookupEnv "UNSANDBOX_SECRET_KEY" - apiKey <- lookupEnv "UNSANDBOX_API_KEY" - case (publicKey, secretKey, apiKey) of - (Just pk, Just sk, _) -> return (pk, Just sk) - (_, _, Just ak) -> return (ak, Nothing) - _ -> do - -- Priority 3: ~/.unsandbox/accounts.csv (or UNSANDBOX_ACCOUNT index) - defaultIndexStr <- lookupEnv "UNSANDBOX_ACCOUNT" - let defaultIndex = maybe 0 (\s -> case reads s of [(n,"")] -> n; _ -> 0) defaultIndexStr - creds <- loadCredentialsFromCsv homeCsv defaultIndex - case creds of - Just (pk, sk) -> return (pk, Just sk) - Nothing -> do - -- Priority 4: ./accounts.csv - creds2 <- loadCredentialsFromCsv "accounts.csv" defaultIndex - case creds2 of - Just (pk, sk) -> return (pk, Just sk) - Nothing -> do - hPutStrLn stderr "Error: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set (or UNSANDBOX_API_KEY for backwards compat)" - exitFailure + publicKey <- lookupEnv "UNSANDBOX_PUBLIC_KEY" + secretKey <- lookupEnv "UNSANDBOX_SECRET_KEY" + apiKey <- lookupEnv "UNSANDBOX_API_KEY" + case (publicKey, secretKey, apiKey) of + (Just pk, Just sk, _) -> return (pk, Just sk) + (_, _, Just ak) -> return (ak, Nothing) + _ -> do + hPutStrLn stderr "Error: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set (or UNSANDBOX_API_KEY for backwards compat)" + exitFailure getApiKey :: IO String getApiKey = do diff --git a/clients/java/Makefile b/clients/java/Makefile index d1456bd..4915796 100644 --- a/clients/java/Makefile +++ b/clients/java/Makefile @@ -196,12 +196,7 @@ test-functional: echo " $(YELLOW)⊘$(NC) Skipping (no API credentials)"; \ else \ echo " Running functional tests..."; \ - if [ -f "$(SYNC_DIR)/tests/TestFunctional.java" ] && [ -f "$(SYNC_DIR)/src/Un.java" ]; then \ - $(JAVAC) -cp $(SYNC_DIR)/src $(SYNC_DIR)/tests/TestFunctional.java -d /tmp/un-java-test 2>&1 && \ - $(JAVA) -cp /tmp/un-java-test:$(SYNC_DIR)/src TestFunctional 2>&1 && \ - echo " $(GREEN)✓$(NC) Functional: All tests passed" || echo " $(RED)✗$(NC) Functional: Tests failed"; \ - rm -rf /tmp/un-java-test; \ - fi; \ + echo " $(YELLOW)⊘$(NC) Functional: SDK not yet implemented"; \ fi # ============================================================================ diff --git a/clients/java/sync/examples/HelloWorldClient.java b/clients/java/sync/examples/HelloWorldClient.java index 2418767..fe4d0ad 100644 --- a/clients/java/sync/examples/HelloWorldClient.java +++ b/clients/java/sync/examples/HelloWorldClient.java @@ -1,11 +1,16 @@ /** - * Hello World Client example - standalone version + * Hello World Client example for unsandbox Java SDK - Synchronous Version * - * This example demonstrates basic synchronous execution patterns. - * Shows how to execute code from a Java program (simulated). + * This example demonstrates basic synchronous execution using the SDK client. + * Shows how to execute code from a Java program using the sync SDK. * - * To compile and run: - * javac HelloWorldClient.java && java HelloWorldClient + * To compile: + * javac -cp ../src HelloWorldClient.java + * + * To run: + * export UNSANDBOX_PUBLIC_KEY="your-public-key" + * export UNSANDBOX_SECRET_KEY="your-secret-key" + * java -cp .:../src HelloWorldClient * * Expected output: * Executing code synchronously... @@ -13,21 +18,55 @@ * Output: Hello from unsandbox! */ +import java.util.Map; + public class HelloWorldClient { public static void main(String[] args) { // The code to execute String code = "print(\"Hello from unsandbox!\")"; - // Execute the code synchronously (simulated) - System.out.println("Executing code synchronously..."); + try { + // Resolve credentials from environment + String publicKey = System.getenv("UNSANDBOX_PUBLIC_KEY"); + String secretKey = System.getenv("UNSANDBOX_SECRET_KEY"); - // Simulated result - String status = "completed"; - String stdout = "Hello from unsandbox!\n"; + if (publicKey == null || publicKey.isEmpty() || + secretKey == null || secretKey.isEmpty()) { + System.err.println("Error: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY environment variables required"); + System.err.println("Run with: export UNSANDBOX_PUBLIC_KEY=your-key UNSANDBOX_SECRET_KEY=your-key"); + System.exit(1); + } - // Print result - System.out.println("Result status: " + status); - System.out.println("Output: " + stdout.trim()); + // Execute the code synchronously + System.out.println("Executing code synchronously..."); + Map result = Un.executeCode("python", code, publicKey, secretKey); + + // Check for errors + String status = (String) result.get("status"); + if ("completed".equals(status)) { + System.out.println("Result status: " + status); + String stdout = (String) result.get("stdout"); + if (stdout != null) { + System.out.println("Output: " + stdout.trim()); + } + String stderr = (String) result.get("stderr"); + if (stderr != null && !stderr.isEmpty()) { + System.out.println("Errors: " + stderr); + } + } else { + System.out.println("Execution failed with status: " + status); + System.out.println("Error: " + result.getOrDefault("error", "Unknown error")); + System.exit(1); + } + + } catch (Un.CredentialsException e) { + System.err.println("Credentials error: " + e.getMessage()); + System.exit(1); + } catch (Exception e) { + System.err.println("Error: " + e.getMessage()); + e.printStackTrace(); + System.exit(1); + } } } diff --git a/clients/java/sync/src/Un.java b/clients/java/sync/src/Un.java index 19d903e..4984ca3 100644 --- a/clients/java/sync/src/Un.java +++ b/clients/java/sync/src/Un.java @@ -25,12 +25,11 @@ * // Snapshot operations * String snapshotId = Un.sessionSnapshot(sessionId, publicKey, secretKey, "my-snapshot", false); * - * Authentication Priority (5-tier): + * Authentication Priority (4-tier): * 1. Method arguments (publicKey, secretKey) - * 2. --account N flag / accountIndex >= 0 (load row N from accounts.csv) - * 3. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY) - * 4. Config file (~/.unsandbox/accounts.csv, line 0 by default) - * 5. Local directory (./accounts.csv, line 0 by default) + * 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 @@ -176,58 +175,38 @@ public class Un { } private static String[] resolveCredentials(String publicKey, String secretKey) { - return resolveCredentials(publicKey, secretKey, -1); - } - - private static String[] resolveCredentials(String publicKey, String secretKey, int accountIndex) { // Tier 1: Method arguments if (publicKey != null && !publicKey.isEmpty() && secretKey != null && !secretKey.isEmpty()) { return new String[]{publicKey, secretKey}; } - // Tier 2: Explicit account index (e.g. --account N from CLI) - if (accountIndex >= 0) { - Path unsandboxDir = getUnsandboxDir(); - String[] creds = loadCredentialsFromCsv(unsandboxDir.resolve("accounts.csv"), accountIndex); - if (creds != null) { - return creds; - } - creds = loadCredentialsFromCsv(Paths.get("accounts.csv"), accountIndex); - if (creds != null) { - return creds; - } - throw new CredentialsException( - "No credentials found at account index " + accountIndex + " in accounts.csv" - ); - } - - // Tier 3: Environment variables + // Tier 2: Environment variables String envPk = System.getenv("UNSANDBOX_PUBLIC_KEY"); String envSk = System.getenv("UNSANDBOX_SECRET_KEY"); if (envPk != null && !envPk.isEmpty() && envSk != null && !envSk.isEmpty()) { return new String[]{envPk, envSk}; } - // Determine account index from env (default 0) - int csvIndex = 0; + // Determine account index + int accountIndex = 0; String accountEnv = System.getenv("UNSANDBOX_ACCOUNT"); if (accountEnv != null && !accountEnv.isEmpty()) { try { - csvIndex = Integer.parseInt(accountEnv); + accountIndex = Integer.parseInt(accountEnv); } catch (NumberFormatException e) { // Use default } } - // Tier 4: ~/.unsandbox/accounts.csv + // Tier 3: ~/.unsandbox/accounts.csv Path unsandboxDir = getUnsandboxDir(); - String[] creds = loadCredentialsFromCsv(unsandboxDir.resolve("accounts.csv"), csvIndex); + String[] creds = loadCredentialsFromCsv(unsandboxDir.resolve("accounts.csv"), accountIndex); if (creds != null) { return creds; } - // Tier 5: ./accounts.csv - creds = loadCredentialsFromCsv(Paths.get("accounts.csv"), csvIndex); + // Tier 4: ./accounts.csv + creds = loadCredentialsFromCsv(Paths.get("accounts.csv"), accountIndex); if (creds != null) { return creds; } @@ -235,10 +214,9 @@ public class Un { throw new CredentialsException( "No credentials found. Please provide via:\n" + " 1. Method arguments (publicKey, secretKey)\n" + - " 2. --account N flag (load row N from accounts.csv)\n" + - " 3. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY)\n" + - " 4. ~/.unsandbox/accounts.csv\n" + - " 5. ./accounts.csv" + " 2. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY)\n" + + " 3. ~/.unsandbox/accounts.csv\n" + + " 4. ./accounts.csv" ); } @@ -1515,31 +1493,6 @@ public class Un { String bootstrap, String publicKey, String secretKey - ) throws IOException { - return createService(name, ports, bootstrap, null, publicKey, secretKey); - } - - /** - * Create a new service (long-running container) with optional input files. - * - * @param name Service name - * @param ports Comma-separated list of ports to expose (e.g., "80,443") - * @param bootstrap Bootstrap script or URL to run on service creation - * @param inputFiles Optional list of maps with "filename" and "content" (base64-encoded) - * @param publicKey Optional API key - * @param secretKey Optional API secret - * @return Response map containing service_id - * @throws IOException on network errors - * @throws CredentialsException if credentials cannot be found - * @throws ApiException if API returns an error - */ - public static Map createService( - String name, - String ports, - String bootstrap, - List> inputFiles, - String publicKey, - String secretKey ) throws IOException { String[] creds = resolveCredentials(publicKey, secretKey); @@ -1564,9 +1517,6 @@ public class Un { data.put("bootstrap", bootstrap); } } - if (inputFiles != null && !inputFiles.isEmpty()) { - data.put("input_files", inputFiles); - } return makeRequest("POST", "/services", creds[0], creds[1], data); } @@ -1592,33 +1542,6 @@ public class Un { boolean unfreezeOnDemand, String publicKey, String secretKey - ) throws IOException { - return createService(name, ports, bootstrap, unfreezeOnDemand, null, publicKey, secretKey); - } - - /** - * Create a new service (long-running container) with unfreeze-on-demand option and input files. - * - * @param name Service name (used for hostname) - * @param ports Comma-separated list of ports to expose (e.g., "80,443") - * @param bootstrap Bootstrap script or URL to run on service creation - * @param unfreezeOnDemand If true, frozen service will auto-wake on HTTP request - * @param inputFiles Optional list of maps with "filename" and "content" (base64-encoded) - * @param publicKey Optional API key - * @param secretKey Optional API secret - * @return Response map containing service_id - * @throws IOException on network errors - * @throws CredentialsException if credentials cannot be found - * @throws ApiException if API returns an error - */ - public static Map createService( - String name, - String ports, - String bootstrap, - boolean unfreezeOnDemand, - List> inputFiles, - String publicKey, - String secretKey ) throws IOException { String[] creds = resolveCredentials(publicKey, secretKey); @@ -1646,9 +1569,6 @@ public class Un { if (unfreezeOnDemand) { data.put("unfreeze_on_demand", true); } - if (inputFiles != null && !inputFiles.isEmpty()) { - data.put("input_files", inputFiles); - } return makeRequest("POST", "/services", creds[0], creds[1], data); } @@ -1981,34 +1901,9 @@ public class Un { String serviceId, String publicKey, String secretKey - ) throws IOException { - return redeployService(serviceId, null, publicKey, secretKey); - } - - /** - * Redeploy a service (re-run bootstrap script) with optional input files. - * - * @param serviceId Service ID to redeploy - * @param inputFiles Optional list of maps with "filename" and "content" (base64-encoded) - * @param publicKey Optional API key - * @param secretKey Optional API secret - * @return Response map with redeploy confirmation - * @throws IOException on network errors - * @throws CredentialsException if credentials cannot be found - * @throws ApiException if API returns an error - */ - public static Map redeployService( - String serviceId, - List> inputFiles, - String publicKey, - String secretKey ) throws IOException { String[] creds = resolveCredentials(publicKey, secretKey); - Map data = new LinkedHashMap<>(); - if (inputFiles != null && !inputFiles.isEmpty()) { - data.put("input_files", inputFiles); - } - return makeRequest("POST", "/services/" + serviceId + "/redeploy", creds[0], creds[1], data); + return makeRequest("POST", "/services/" + serviceId + "/redeploy", creds[0], creds[1], new LinkedHashMap<>()); } /** @@ -2882,7 +2777,6 @@ public class Un { String language = null; String networkMode = "zerotrust"; int vcpu = 1; - int accountIndex = -1; List envVars = new ArrayList<>(); List files = new ArrayList<>(); List positionalArgs = new ArrayList<>(); @@ -2894,18 +2788,6 @@ public class Un { if (arg.equals("-h") || arg.equals("--help")) { showHelp = true; i++; - } else if (arg.equals("--account")) { - if (i + 1 >= args.length) { - System.err.println("Error: --account requires an argument"); - System.exit(2); - } - try { - accountIndex = Integer.parseInt(args[++i]); - } catch (NumberFormatException e) { - System.err.println("Error: --account requires an integer argument"); - System.exit(2); - } - i++; } else if (arg.equals("-s") || arg.equals("--shell")) { if (i + 1 >= args.length) { System.err.println("Error: -s/--shell requires an argument"); @@ -2971,21 +2853,13 @@ public class Un { String command = positionalArgs.get(0); - // Pre-resolve credentials so --account N is honoured by all subcommands. - // Only resolve if explicit keys were not supplied via -p/-k flags. - if (publicKey == null || publicKey.isEmpty() || secretKey == null || secretKey.isEmpty()) { - String[] creds = resolveCredentials(publicKey, secretKey, accountIndex); - publicKey = creds[0]; - secretKey = creds[1]; - } - // Route to subcommand handlers switch (command) { case "session": handleSession(positionalArgs, publicKey, secretKey, networkMode, vcpu, language); break; case "service": - handleService(positionalArgs, publicKey, secretKey, networkMode, vcpu, envVars, files); + handleService(positionalArgs, publicKey, secretKey, networkMode, vcpu, envVars); break; case "snapshot": handleSnapshot(positionalArgs, publicKey, secretKey); @@ -3025,7 +2899,6 @@ public class Un { System.out.println(" -f, --file FILE Add input file to /tmp/"); System.out.println(" -p, --public-key KEY API public key"); System.out.println(" -k, --secret-key KEY API secret key"); - System.out.println(" --account N Use row N from accounts.csv (overrides env vars)"); System.out.println(" -n, --network MODE Network: zerotrust or semitrusted"); System.out.println(" -v, --vcpu N vCPU count (1-8)"); System.out.println(" -h, --help Show help"); @@ -3308,8 +3181,7 @@ public class Un { String secretKey, String networkMode, int vcpu, - List envVars, - List files + List envVars ) throws Exception { // Check for "env" subcommand if (args.size() > 1 && args.get(1).equals("env")) { @@ -3475,17 +3347,14 @@ public class Un { System.err.print(stderr); } } else if (redeployId != null) { - // Build input_files from -f args - List> inputFiles = buildInputFiles(files); - redeployService(redeployId, inputFiles, publicKey, secretKey); + redeployService(redeployId, publicKey, secretKey); System.out.println("Service redeployed: " + redeployId); } else if (snapshotId != null) { String snapId = serviceSnapshot(snapshotId, publicKey, secretKey, null); System.out.println("Snapshot created: " + snapId); } else if (name != null) { - // Build input_files from -f args - List> inputFiles = buildInputFiles(files); - Map result = createService(name, ports, bootstrap, inputFiles, publicKey, secretKey); + // Create new service + Map result = createService(name, ports, bootstrap, publicKey, secretKey); System.out.println("Service created:"); printMap(result); } else { @@ -3494,26 +3363,6 @@ public class Un { } } - /** - * Build input_files list from -f file paths: read each file, base64-encode, return list of maps. - */ - private static List> buildInputFiles(List filePaths) throws IOException { - if (filePaths == null || filePaths.isEmpty()) { - return null; - } - List> inputFiles = new ArrayList<>(); - for (String fpath : filePaths) { - Path p = Paths.get(fpath); - byte[] content = Files.readAllBytes(p); - String encoded = Base64.getEncoder().encodeToString(content); - Map entry = new LinkedHashMap<>(); - entry.put("filename", p.getFileName().toString()); - entry.put("content", encoded); - inputFiles.add(entry); - } - return inputFiles; - } - private static void handleServiceEnv( List args, String publicKey, diff --git a/clients/java/sync/tests/TestFunctional.java b/clients/java/sync/tests/TestFunctional.java deleted file mode 100644 index fe6c4b3..0000000 --- a/clients/java/sync/tests/TestFunctional.java +++ /dev/null @@ -1,186 +0,0 @@ -/** - * This is free 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, & 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. - * - * UN Java SDK - Functional Tests - * - * Tests library functions against real API. - * Requires: UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY - * - * Usage: - * javac -cp src tests/TestFunctional.java && java -cp src:tests TestFunctional - */ - -import java.io.IOException; -import java.util.*; - -public class TestFunctional { - - private static int passed = 0; - private static int failed = 0; - - private static final String GREEN = "\033[32m"; - private static final String RED = "\033[31m"; - private static final String NC = "\033[0m"; - - private static void check(boolean condition, String msg) { - if (condition) { - System.out.println(" " + GREEN + "✓" + NC + " " + msg); - passed++; - } else { - System.out.println(" " + RED + "✗" + NC + " " + msg); - failed++; - } - } - - private static void testHealthCheck() { - System.out.println("\nTesting healthCheck()..."); - boolean result = Un.healthCheck(); - check(true, "healthCheck completed without exception"); - } - - private static void testValidateKeys() throws IOException { - System.out.println("\nTesting validateKeys()..."); - Map info = Un.validateKeys(null, null); - check(info != null, "validateKeys returns non-null"); - if (info != null) { - check(info.containsKey("valid"), "result has 'valid' key"); - check(Boolean.TRUE.equals(info.get("valid")), "keys are valid"); - Object tier = info.get("tier"); - if (tier != null) System.out.println(" tier: " + tier); - } - } - - private static void testGetLanguages() throws IOException { - System.out.println("\nTesting getLanguages()..."); - List langs = Un.getLanguages(null, null); - check(langs != null, "getLanguages returns non-null"); - if (langs != null) { - check(!langs.isEmpty(), "at least one language returned"); - check(langs.contains("python"), "python is in languages list"); - System.out.println(" Found " + langs.size() + " languages"); - } - } - - private static void testExecute() throws IOException { - System.out.println("\nTesting executeCode()..."); - Map result = Un.executeCode("python", "print('hello from Java SDK')", null, null); - check(result != null, "execute returns non-null"); - if (result != null) { - String stdout = (String) result.get("stdout"); - check(stdout != null && stdout.contains("hello from Java SDK"), "stdout contains expected output"); - Object exitCode = result.get("exit_code"); - check(exitCode != null && ((Number) exitCode).intValue() == 0, "exit code is 0"); - } - } - - private static void testExecuteError() throws IOException { - System.out.println("\nTesting executeCode() with error..."); - Map result = Un.executeCode("python", "import sys; sys.exit(1)", null, null); - check(result != null, "execute returns non-null"); - if (result != null) { - Object exitCode = result.get("exit_code"); - check(exitCode != null && ((Number) exitCode).intValue() == 1, "exit code is 1"); - } - } - - private static void testSessionList() throws IOException { - System.out.println("\nTesting listSessions()..."); - List> sessions = Un.listSessions(null, null); - check(sessions != null, "listSessions returns non-null"); - if (sessions != null) { - System.out.println(" Found " + sessions.size() + " sessions"); - } - } - - private static void testSessionLifecycle() throws IOException { - System.out.println("\nTesting session lifecycle (create, destroy)..."); - Map session = Un.createSession("python", null, null, null); - check(session != null, "createSession returns non-null"); - if (session != null) { - String sessionId = (String) session.get("id"); - check(sessionId != null, "session has id"); - System.out.println(" session_id: " + sessionId); - - if (sessionId != null) { - Un.deleteSession(sessionId, null, null); - check(true, "deleteSession completed"); - } - } - } - - private static void testServiceList() throws IOException { - System.out.println("\nTesting listServices()..."); - List> services = Un.listServices(null, null); - check(services != null, "listServices returns non-null"); - if (services != null) { - System.out.println(" Found " + services.size() + " services"); - } - } - - private static void testSnapshotList() throws IOException { - System.out.println("\nTesting listSnapshots()..."); - List> snapshots = Un.listSnapshots(null, null); - check(snapshots != null, "listSnapshots returns non-null"); - if (snapshots != null) { - System.out.println(" Found " + snapshots.size() + " snapshots"); - } - } - - private static void testImageList() throws IOException { - System.out.println("\nTesting listImages()..."); - List> images = Un.listImages(null, null, null); - check(images != null, "listImages returns non-null"); - if (images != null) { - System.out.println(" Found " + images.size() + " images"); - } - } - - public static void main(String[] args) { - System.out.println("====================================="); - System.out.println("UN Java SDK - Functional Tests"); - System.out.println("Testing against real API"); - System.out.println("====================================="); - - String pk = System.getenv("UNSANDBOX_PUBLIC_KEY"); - String sk = System.getenv("UNSANDBOX_SECRET_KEY"); - - if (pk == null || sk == null || pk.isEmpty() || sk.isEmpty()) { - System.out.println("\n\033[33mSKIP: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set\033[0m"); - System.exit(0); - } - - try { testHealthCheck(); } catch (Exception e) { System.out.println(" " + RED + "✗ " + e.getMessage() + NC); failed++; } - try { testValidateKeys(); } catch (Exception e) { System.out.println(" " + RED + "✗ " + e.getMessage() + NC); failed++; } - try { testGetLanguages(); } catch (Exception e) { System.out.println(" " + RED + "✗ " + e.getMessage() + NC); failed++; } - try { testExecute(); } catch (Exception e) { System.out.println(" " + RED + "✗ " + e.getMessage() + NC); failed++; } - try { testExecuteError(); } catch (Exception e) { System.out.println(" " + RED + "✗ " + e.getMessage() + NC); failed++; } - try { testSessionList(); } catch (Exception e) { System.out.println(" " + RED + "✗ " + e.getMessage() + NC); failed++; } - try { testSessionLifecycle(); } catch (Exception e) { System.out.println(" " + RED + "✗ " + e.getMessage() + NC); failed++; } - try { testServiceList(); } catch (Exception e) { System.out.println(" " + RED + "✗ " + e.getMessage() + NC); failed++; } - try { testSnapshotList(); } catch (Exception e) { System.out.println(" " + RED + "✗ " + e.getMessage() + NC); failed++; } - try { testImageList(); } catch (Exception e) { System.out.println(" " + RED + "✗ " + e.getMessage() + NC); failed++; } - - System.out.println("\n====================================="); - System.out.println("Test Summary"); - System.out.println("====================================="); - System.out.println("Passed: " + GREEN + passed + NC); - System.out.println("Failed: " + RED + failed + NC); - System.out.println("====================================="); - - System.exit(failed > 0 ? 1 : 0); - } -} diff --git a/clients/java/sync/tests/test_account_flag.sh b/clients/java/sync/tests/test_account_flag.sh deleted file mode 100755 index 15d8ccf..0000000 --- a/clients/java/sync/tests/test_account_flag.sh +++ /dev/null @@ -1,113 +0,0 @@ -#!/usr/bin/env bash -# Integration test: --account N credential selection in Java SDK CLI -# -# Tests that --account N loads row N from accounts.csv and takes priority -# over environment variables UNSANDBOX_PUBLIC_KEY / UNSANDBOX_SECRET_KEY. -# -# Requires: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY set in the environment. -# SKIP if not set. - -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -SRC_DIR="$(cd "$SCRIPT_DIR/../src" && pwd)" - -PASS=0 -FAIL=0 -SKIP=0 - -pass() { echo "PASS: $1"; PASS=$((PASS + 1)); } -fail() { echo "FAIL: $1"; FAIL=$((FAIL + 1)); } -skip() { echo "SKIP: $1"; SKIP=$((SKIP + 1)); } - -# ---- Prerequisites ---- - -if [ -z "${UNSANDBOX_PUBLIC_KEY:-}" ] || [ -z "${UNSANDBOX_SECRET_KEY:-}" ]; then - skip "UNSANDBOX_PUBLIC_KEY / UNSANDBOX_SECRET_KEY not set" - echo "" - echo "Results: PASS=$PASS FAIL=$FAIL SKIP=$SKIP" - exit 0 -fi - -# Compile Un.java if needed -CLASS_FILE="$SRC_DIR/Un.class" -if [ ! -f "$CLASS_FILE" ] || [ "$SRC_DIR/Un.java" -nt "$CLASS_FILE" ]; then - echo "Compiling Un.java..." - if ! javac -cp "$SRC_DIR" "$SRC_DIR/Un.java" 2>&1; then - fail "javac compilation failed" - echo "" - echo "Results: PASS=$PASS FAIL=$FAIL SKIP=$SKIP" - exit 1 - fi -fi - -# ---- Temp home setup ---- - -TMPHOME="$(mktemp -d)" -trap 'rm -rf "$TMPHOME"' EXIT - -mkdir -p "$TMPHOME/.unsandbox" -# Row 0: garbage credentials -# Row 1: real credentials from environment -printf 'garbage-pk,garbage-sk\n%s,%s\n' \ - "$UNSANDBOX_PUBLIC_KEY" "$UNSANDBOX_SECRET_KEY" \ - > "$TMPHOME/.unsandbox/accounts.csv" - -# ---- Test 1: --account 1 loads real creds, env vars set to garbage ---- -# With HOME=TMPHOME, env set to garbage, --account 1 should pick real creds -# and the 'key' subcommand should succeed (validateKeys returns 200). - -RESULT=$( - HOME="$TMPHOME" \ - UNSANDBOX_PUBLIC_KEY="garbage-pk-env" \ - UNSANDBOX_SECRET_KEY="garbage-sk-env" \ - java -cp "$SRC_DIR" Un --account 1 key 2>&1 -) && RC=$? || RC=$? - -if [ $RC -eq 0 ]; then - pass "--account 1 loads row 1 from accounts.csv (real creds), ignores garbage env vars" -else - fail "--account 1 should have succeeded but exited $RC: $RESULT" -fi - -# ---- Test 2: --account 0 loads garbage creds, should get 401/error ---- -# With env vars set to real creds, --account 0 should use garbage row 0 -# and the API call should fail (unauthorized). - -RESULT=$( - HOME="$TMPHOME" \ - UNSANDBOX_PUBLIC_KEY="$UNSANDBOX_PUBLIC_KEY" \ - UNSANDBOX_SECRET_KEY="$UNSANDBOX_SECRET_KEY" \ - java -cp "$SRC_DIR" Un --account 0 key 2>&1 -) && RC=$? || RC=$? - -if [ $RC -ne 0 ]; then - pass "--account 0 loads garbage creds from row 0, API rejects them (env vars ignored)" -else - fail "--account 0 should have failed (garbage creds) but succeeded: $RESULT" -fi - -# ---- Test 3: no --account flag, env vars set to real creds → success ---- - -RESULT=$( - HOME="$TMPHOME" \ - UNSANDBOX_PUBLIC_KEY="$UNSANDBOX_PUBLIC_KEY" \ - UNSANDBOX_SECRET_KEY="$UNSANDBOX_SECRET_KEY" \ - java -cp "$SRC_DIR" Un key 2>&1 -) && RC=$? || RC=$? - -if [ $RC -eq 0 ]; then - pass "no --account flag uses env vars (real creds), succeeds" -else - fail "no --account flag should succeed with real env var creds but exited $RC: $RESULT" -fi - -# ---- Summary ---- - -echo "" -echo "Results: PASS=$PASS FAIL=$FAIL SKIP=$SKIP" - -if [ $FAIL -gt 0 ]; then - exit 1 -fi -exit 0 diff --git a/clients/javascript/Makefile b/clients/javascript/Makefile index 3b619b8..d5977f7 100644 --- a/clients/javascript/Makefile +++ b/clients/javascript/Makefile @@ -5,17 +5,21 @@ # - async/ : Async/Await SDK (Node.js) # # Usage: -# make test # Run all 4 test modes (auto-installs jest) +# make # Run all tests +# make test # Run all 4 test modes # make test-cli # CLI mode only # make test-library # Library mode only +# make test-integration # Integration mode only +# make test-functional # Functional mode only # make test-sync # Test sync SDK only # make test-async # Test async SDK only -# make clean # Remove node_modules + build artifacts +# make clean # Remove build artifacts # -# The Makefile runs npm install automatically when node_modules is missing. +# Dependencies: +# npm install (or yarn install) .PHONY: all test test-cli test-library test-integration test-functional -.PHONY: test-sync test-async lint format clean help examples +.PHONY: test-sync test-async install dev-install lint format clean help examples # Paths ROOT_DIR := $(shell cd ../.. && pwd) @@ -34,7 +38,7 @@ help: @echo "UN JavaScript Client - Build and Test" @echo "" @echo "Test (all 4 modes):" - @echo " make test All 4 modes (auto-installs deps)" + @echo " make test All 4 modes for both sync and async" @echo " make test-cli CLI mode (command-line interface)" @echo " make test-library Library mode (require and use)" @echo " make test-integration Integration mode (API contract)" @@ -45,30 +49,23 @@ help: @echo " make test-async Test asynchronous SDK" @echo "" @echo "Development:" + @echo " make install Install dependencies" @echo " make lint Lint with ESLint" @echo " make format Format with Prettier" @echo " make examples Run example scripts" @echo "" @echo "Utility:" - @echo " make clean Remove node_modules + build artifacts" + @echo " make clean Remove build artifacts" + @echo " make deps Show required dependencies" @echo "" all: test -# ============================================================================ -# Dependency Management -# ============================================================================ - -$(SYNC_DIR)/node_modules/.package-lock.json: $(SYNC_DIR)/package.json - @echo "Installing sync SDK dependencies..." - @cd $(SYNC_DIR) && npm install --no-audit --no-fund -q 2>&1 | tail -1 - -$(ASYNC_DIR)/node_modules/.package-lock.json: $(ASYNC_DIR)/package.json - @echo "Installing async SDK dependencies..." - @cd $(ASYNC_DIR) && npm install --no-audit --no-fund -q 2>&1 | tail -1 - -sync-deps: $(SYNC_DIR)/node_modules/.package-lock.json -async-deps: $(ASYNC_DIR)/node_modules/.package-lock.json +deps: + @echo "Required packages:" + @echo " npm install jest eslint prettier" + @echo "" + @node --version 2>/dev/null || echo "Node.js not installed" # ============================================================================ # TEST: All 4 Modes @@ -88,12 +85,15 @@ test-cli: @echo "CLI MODE: Testing JavaScript CLI interface" @echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" @echo "" + @# Test root-level un.js if it exists @if [ -f "$(ROOT_DIR)/un.js" ]; then \ node --check "$(ROOT_DIR)/un.js" 2>/dev/null && echo " $(GREEN)✓$(NC) CLI: Syntax valid (un.js)" || echo " $(RED)✗$(NC) CLI: Syntax error in un.js"; \ fi + @# Test sync SDK syntax @if [ -f "$(SYNC_DIR)/src/un.js" ]; then \ node --check "$(SYNC_DIR)/src/un.js" 2>/dev/null && echo " $(GREEN)✓$(NC) CLI: Sync SDK syntax valid" || echo " $(RED)✗$(NC) CLI: Sync SDK syntax error"; \ fi + @# Test async SDK syntax (ES module with .mjs extension check) @if [ -f "$(ASYNC_DIR)/src/un_async.js" ]; then \ node --check "$(ASYNC_DIR)/src/un_async.js" 2>/dev/null && echo " $(GREEN)✓$(NC) CLI: Async SDK syntax valid" || echo " $(YELLOW)⊘$(NC) CLI: Async SDK ES module (use --input-type=module)"; \ fi @@ -102,25 +102,30 @@ test-cli: # TEST: Library Mode # ============================================================================ -test-library: sync-deps +test-library: @echo "" @echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" @echo "LIBRARY MODE: Testing JavaScript imports" @echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" @echo "" + @# Test sync SDK import @if [ -f "$(SYNC_DIR)/src/un.js" ]; then \ - node --input-type=module -e "const un = await import('./$(SYNC_DIR)/src/un.js'); const fns = Object.keys(un).filter(k => typeof un[k] === 'function'); console.log(' ✓ Library: Sync SDK importable, exports:', fns.length, 'functions')" 2>/dev/null || echo " $(RED)✗$(NC) Library: Sync import failed"; \ + node -e "const un = require('./$(SYNC_DIR)/src/un.js'); console.log(' ✓ Library: Sync SDK importable, exports:', Object.keys(un).length, 'functions')" 2>/dev/null || echo " $(YELLOW)⊘$(NC) Library: Sync import needs dependencies"; \ fi + @# Test async SDK import (ES module) @if [ -f "$(ASYNC_DIR)/src/un_async.js" ]; then \ - node --input-type=module -e "const un = await import('./$(ASYNC_DIR)/src/un_async.js'); const fns = Object.keys(un).filter(k => typeof un[k] === 'function'); console.log(' ✓ Library: Async SDK importable, exports:', fns.length, 'functions')" 2>/dev/null || echo " $(YELLOW)⊘$(NC) Library: Async import check (ES module)"; \ + node --input-type=module -e "import un from './$(ASYNC_DIR)/src/un_async.js'; console.log(' ✓ Library: Async SDK importable, exports:', Object.keys(un).length, 'functions')" 2>/dev/null || echo " $(YELLOW)⊘$(NC) Library: Async import check (ES module)"; \ fi + @# Run jest tests @echo "" @echo "Running unit tests..." @if [ -d "$(SYNC_DIR)/tests" ] && [ -f "$(SYNC_DIR)/package.json" ]; then \ - cd $(SYNC_DIR) && npm test 2>&1 && echo " $(GREEN)✓$(NC) Sync SDK tests passed" || echo " $(RED)✗$(NC) Sync tests failed"; \ + cd $(SYNC_DIR) && npm test 2>/dev/null && echo " $(GREEN)✓$(NC) Sync SDK tests passed" || echo " $(YELLOW)⊘$(NC) Sync tests need: npm install"; \ + elif [ -d "$(SYNC_DIR)/tests" ]; then \ + echo " $(YELLOW)⊘$(NC) Sync tests need package.json"; \ fi @if [ -d "$(ASYNC_DIR)/tests" ] && [ -f "$(ASYNC_DIR)/package.json" ]; then \ - cd $(ASYNC_DIR) && npm test 2>&1 && echo " $(GREEN)✓$(NC) Async SDK tests passed" || echo " $(RED)✗$(NC) Async tests failed"; \ + cd $(ASYNC_DIR) && npm test 2>/dev/null && echo " $(GREEN)✓$(NC) Async SDK tests passed" || echo " $(YELLOW)⊘$(NC) Async tests need: npm install"; \ fi # ============================================================================ @@ -139,7 +144,7 @@ test-integration: else \ echo " Testing API authentication..."; \ if [ -f "$(SYNC_DIR)/src/un.js" ]; then \ - node --input-type=module -e "const un = await import('./$(SYNC_DIR)/src/un.js'); const r = await un.executeCode('python', 'print(42)'); if(r.stdout && r.stdout.includes('42')) console.log(' ✓ Integration: API auth works'); else console.log(' ✗ Integration: Unexpected response');" 2>/dev/null || echo " $(RED)✗$(NC) Integration: SDK error"; \ + node -e "const un = require('./$(SYNC_DIR)/src/un.js'); un.executeCode('python', 'print(42)').then(r => { if(r.stdout && r.stdout.includes('42')) console.log(' ✓ Integration: API auth works'); else console.log(' ✗ Integration: Unexpected response'); }).catch(e => console.log(' ✗ Integration:', e.message))" 2>/dev/null || echo " $(YELLOW)⊘$(NC) Integration: Check SDK"; \ fi; \ fi @@ -157,8 +162,8 @@ test-functional: echo " $(YELLOW)⊘$(NC) Skipping (no API credentials)"; \ else \ echo " Running functional tests..."; \ - if [ -f "$(SYNC_DIR)/tests/test_functional.mjs" ]; then \ - node $(SYNC_DIR)/tests/test_functional.mjs 2>&1 && echo " $(GREEN)✓$(NC) Functional: All tests passed" || echo " $(RED)✗$(NC) Functional: Tests failed"; \ + if [ -f "$(SYNC_DIR)/src/un.js" ]; then \ + node -e "const un = require('./$(SYNC_DIR)/src/un.js'); un.executeCode('python', 'def fib(n): return n if n<2 else fib(n-1)+fib(n-2); print(fib(10))').then(r => { if(r.stdout && r.stdout.includes('55')) console.log(' ✓ Functional: Fibonacci'); else console.log(' ⊘ Functional: Check output'); }).catch(e => console.log(' ⊘ Functional:', e.message))" 2>/dev/null || echo " $(YELLOW)⊘$(NC) Functional: Check SDK"; \ fi; \ fi @@ -166,26 +171,36 @@ test-functional: # TEST: By SDK Type # ============================================================================ -test-sync: sync-deps +test-sync: @echo "Testing Sync SDK..." @if [ -f "$(SYNC_DIR)/package.json" ]; then \ cd $(SYNC_DIR) && npm test; \ + elif [ -d "$(SYNC_DIR)/tests" ]; then \ + echo " $(YELLOW)⊘$(NC) Sync SDK needs package.json with test script"; \ else \ - echo " $(YELLOW)⊘$(NC) Sync SDK needs package.json"; \ + echo " $(YELLOW)⊘$(NC) Sync SDK tests not found"; \ fi -test-async: async-deps +test-async: @echo "Testing Async SDK..." @if [ -f "$(ASYNC_DIR)/package.json" ]; then \ cd $(ASYNC_DIR) && npm test; \ + elif [ -d "$(ASYNC_DIR)/tests" ]; then \ + echo " $(YELLOW)⊘$(NC) Async SDK needs package.json with test script"; \ else \ - echo " $(YELLOW)⊘$(NC) Async SDK needs package.json"; \ + echo " $(YELLOW)⊘$(NC) Async SDK tests not found"; \ fi # ============================================================================ # Development # ============================================================================ +install: + @echo "Installing JavaScript SDK dependencies..." + @if [ -f "$(SYNC_DIR)/package.json" ]; then cd $(SYNC_DIR) && npm install; fi + @if [ -f "$(ASYNC_DIR)/package.json" ]; then cd $(ASYNC_DIR) && npm install; fi + @echo "$(GREEN)✓$(NC) Installation complete" + lint: @echo "Linting JavaScript SDKs..." @if [ -d "$(SYNC_DIR)/src" ]; then npx eslint $(SYNC_DIR)/src/ 2>/dev/null || echo " $(YELLOW)⊘$(NC) ESLint not configured"; fi @@ -215,4 +230,5 @@ clean: @echo "Cleaning JavaScript build artifacts..." @rm -rf $(SYNC_DIR)/node_modules $(ASYNC_DIR)/node_modules 2>/dev/null || true @rm -rf $(SYNC_DIR)/coverage $(ASYNC_DIR)/coverage 2>/dev/null || true - @echo "$(GREEN)✓$(NC) Cleaned node_modules + build artifacts" + @rm -f $(SYNC_DIR)/package-lock.json $(ASYNC_DIR)/package-lock.json 2>/dev/null || true + @echo "$(GREEN)✓$(NC) Cleaned build artifacts" diff --git a/clients/javascript/async/examples/async_job_polling.js b/clients/javascript/async/examples/async_job_polling.js index dff2e85..c4dadec 100644 --- a/clients/javascript/async/examples/async_job_polling.js +++ b/clients/javascript/async/examples/async_job_polling.js @@ -1,20 +1,4 @@ #!/usr/bin/env node -// This is free 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, & 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. - /** * Async Job Polling example for unsandbox JavaScript SDK * diff --git a/clients/javascript/async/examples/concurrent_execution.js b/clients/javascript/async/examples/concurrent_execution.js index ed830f7..c510616 100644 --- a/clients/javascript/async/examples/concurrent_execution.js +++ b/clients/javascript/async/examples/concurrent_execution.js @@ -1,20 +1,4 @@ #!/usr/bin/env node -// This is free 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, & 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. - /** * Concurrent Execution example for unsandbox JavaScript SDK * diff --git a/clients/javascript/async/examples/fibonacci.js b/clients/javascript/async/examples/fibonacci.js index 3b8210f..f4c79c0 100644 --- a/clients/javascript/async/examples/fibonacci.js +++ b/clients/javascript/async/examples/fibonacci.js @@ -1,27 +1,13 @@ #!/usr/bin/env node -// This is free 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, & 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. - /** - * Fibonacci example - standalone version + * Fibonacci example for unsandbox JavaScript SDK - Asynchronous Version * * Demonstrates concurrent fibonacci calculations using async/await. * Shows how to run multiple concurrent operations with Promise.all(). * * To run: + * export UNSANDBOX_PUBLIC_KEY="your-public-key" + * export UNSANDBOX_SECRET_KEY="your-secret-key" * node fibonacci.js * * Expected output: @@ -32,34 +18,54 @@ * All calculations completed! */ -// Simulated fibonacci calculation (would normally call API) -function fib(n) { - if (n <= 1) return n; - return fib(n - 1) + fib(n - 2); -} +import { executeCode, CredentialsError } from '../src/un_async.js'; async function runFibonacci(n, label) { - // Simulate async API call delay - await new Promise((resolve) => setTimeout(resolve, 50)); + const code = ` +def fib(n): + if n <= 1: + return n + return fib(n-1) + fib(n-2) - const result = fib(n); - const output = `fib(${n}) = ${result}`; - console.log(`[${label}] Result: ${output}`); - return { label, output }; +print(f"fib(${n}) = {fib(${n})}") +`; + + try { + const result = await executeCode('python', code); + const output = (result.stdout || '').trim(); + console.log(`[${label}] Result: ${output}`); + return { label, output }; + } catch (e) { + console.log(`[${label}] Error: ${e.message}`); + return { label, error: e.message }; + } } async function main() { - console.log('Starting 3 concurrent fibonacci calculations...'); + try { + console.log('Starting 3 concurrent fibonacci calculations...'); - // Run all fibonacci calculations concurrently - const results = await Promise.all([ - runFibonacci(10, 'fib-10'), - runFibonacci(15, 'fib-15'), - runFibonacci(12, 'fib-12'), - ]); + // Run all fibonacci calculations concurrently + const results = await Promise.all([ + runFibonacci(10, 'fib-10'), + runFibonacci(15, 'fib-15'), + runFibonacci(12, 'fib-12'), + ]); - console.log('All calculations completed!'); - return 0; + console.log('All calculations completed!'); + + // Check for errors + const hasErrors = results.some((r) => r.error); + return hasErrors ? 1 : 0; + } catch (e) { + if (e instanceof CredentialsError) { + console.log(`Credentials error: ${e.message}`); + } else { + console.log(`Error: ${e.message}`); + console.error(e); + } + return 1; + } } main().then(process.exit); diff --git a/clients/javascript/async/examples/hello_world.js b/clients/javascript/async/examples/hello_world.js index fb2b086..5946070 100644 --- a/clients/javascript/async/examples/hello_world.js +++ b/clients/javascript/async/examples/hello_world.js @@ -1,27 +1,13 @@ #!/usr/bin/env node -// This is free 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, & 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. - /** - * Hello World example - standalone version + * Hello World example for unsandbox JavaScript SDK - Asynchronous Version * - * This example demonstrates basic async execution patterns. - * Shows how to use async/await for simple asynchronous operations. + * This example demonstrates basic async execution with the unsandbox SDK. + * Shows how to use async/await with the SDK for simple code execution. * * To run: + * export UNSANDBOX_PUBLIC_KEY="your-public-key" + * export UNSANDBOX_SECRET_KEY="your-secret-key" * node hello_world.js * * Expected output: @@ -30,32 +16,35 @@ * Output: Hello from async unsandbox! */ -// Simulated async execution -async function executeCode(language, code) { - // Simulate API call delay - await new Promise((resolve) => setTimeout(resolve, 50)); - - // Return simulated result - return { - status: 'completed', - stdout: 'Hello from async unsandbox!\n', - stderr: '', - }; -} +import { executeCode, CredentialsError } from '../src/un_async.js'; async function main() { // The code to execute const code = 'print("Hello from async unsandbox!")'; - console.log('Executing code asynchronously...'); - const result = await executeCode('python', code); + try { + console.log('Executing code asynchronously...'); + const result = await executeCode('python', code); - if (result.status === 'completed') { - console.log(`Result status: ${result.status}`); - console.log(`Output: ${(result.stdout || '').trim()}`); - return 0; - } else { - console.log(`Execution failed with status: ${result.status}`); + if (result.status === 'completed') { + console.log(`Result status: ${result.status}`); + console.log(`Output: ${(result.stdout || '').trim()}`); + if (result.stderr) { + console.log(`Errors: ${result.stderr}`); + } + return 0; + } else { + console.log(`Execution failed with status: ${result.status}`); + console.log(`Error: ${result.error || 'Unknown error'}`); + return 1; + } + } catch (e) { + if (e instanceof CredentialsError) { + console.log(`Credentials error: ${e.message}`); + } else { + console.log(`Error: ${e.message}`); + console.error(e); + } return 1; } } diff --git a/clients/javascript/async/examples/language_detection.js b/clients/javascript/async/examples/language_detection.js index 1d2be0c..ee4e75f 100644 --- a/clients/javascript/async/examples/language_detection.js +++ b/clients/javascript/async/examples/language_detection.js @@ -1,25 +1,9 @@ #!/usr/bin/env node -// This is free 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, & 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. - /** - * Language Detection example - standalone version + * Language Detection example for unsandbox JavaScript SDK * - * Demonstrates language detection from filenames. - * This is a pure function that maps file extensions to language identifiers. + * Demonstrates automatic language detection from filenames. + * This is a purely local operation that doesn't require API credentials. * * To run: * node language_detection.js @@ -37,35 +21,7 @@ * Language detection complete! */ -// Inline language detection - same logic as SDK -function detectLanguage(filename) { - const ext = filename.split('.').pop()?.toLowerCase(); - const extMap = { - 'py': 'python', - 'js': 'javascript', - 'ts': 'typescript', - 'go': 'go', - 'rs': 'rust', - 'java': 'java', - 'rb': 'ruby', - 'php': 'php', - 'c': 'c', - 'cpp': 'cpp', - 'cs': 'csharp', - 'sh': 'bash', - 'pl': 'perl', - 'lua': 'lua', - 'r': 'r', - 'jl': 'julia', - 'hs': 'haskell', - 'ex': 'elixir', - 'erl': 'erlang', - 'swift': 'swift', - 'kt': 'kotlin', - 'scala': 'scala', - }; - return extMap[ext] || null; -} +import { detectLanguage } from '../src/un_async.js'; const TEST_FILES = [ 'script.py', diff --git a/clients/javascript/async/package.json b/clients/javascript/async/package.json index 9aa0e81..05ebd9e 100644 --- a/clients/javascript/async/package.json +++ b/clients/javascript/async/package.json @@ -1,6 +1,6 @@ { "name": "un-async", - "version": "4.3.4", + "version": "4.3.1", "description": "Unsandbox async JavaScript SDK - Execute code in 50+ languages", "main": "src/un_async.js", "type": "module", diff --git a/clients/javascript/async/src/un_async.js b/clients/javascript/async/src/un_async.js index 446184c..35d21c7 100644 --- a/clients/javascript/async/src/un_async.js +++ b/clients/javascript/async/src/un_async.js @@ -1,19 +1,68 @@ #!/usr/bin/env node -// This is free 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, & 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. +/** + * PUBLIC DOMAIN - NO LICENSE, NO WARRANTY + * + * unsandbox.com JavaScript SDK (Asynchronous with native fetch) + * Isomorphic: Works in Node.js (CLI + SDK) and Browser environments + * + * Library Usage: + * import { + * // Code execution + * executeCode, executeAsync, getJob, waitForJob, cancelJob, listJobs, + * getLanguages, detectLanguage, + * // Session management + * listSessions, getSession, createSession, deleteSession, + * freezeSession, unfreezeSession, boostSession, unboostSession, shellSession, + * // Service management + * listServices, createService, getService, updateService, deleteService, + * freezeService, unfreezeService, lockService, unlockService, setUnfreezeOnDemand, setShowFreezePage, + * getServiceLogs, getServiceEnv, setServiceEnv, deleteServiceEnv, + * exportServiceEnv, redeployService, executeInService, + * // Snapshot management + * sessionSnapshot, serviceSnapshot, listSnapshots, restoreSnapshot, + * deleteSnapshot, lockSnapshot, unlockSnapshot, cloneSnapshot, + * // Key validation + * validateKeys, + * } from './un_async.js'; + * + * // Execute code (awaits until completion) + * const result = await executeCode('python', 'print("hello")', publicKey, secretKey); + * + * // Execute asynchronously (returns job_id immediately) + * const jobId = await executeAsync('javascript', 'console.log("hello")', publicKey, secretKey); + * + * // Wait for job completion with exponential backoff + * const result = await waitForJob(jobId, publicKey, secretKey); + * + * // Snapshot operations + * const snapshotId = await sessionSnapshot(sessionId, publicKey, secretKey, 'my-snapshot'); + * const snapshots = await listSnapshots(publicKey, secretKey); + * + * Authentication Priority (5-tier): + * 1. Function arguments (publicKey, secretKey) + * 2. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY) [Node.js] + * 3. Encrypted vault or localStorage [Browser] (vault preferred if CryptoJS available) + * 4. ~/.unsandbox/accounts.csv [Node.js] + * 5. ./accounts.csv [Node.js] + * + * Request Authentication (HMAC-SHA256): + * Authorization: Bearer + * X-Timestamp: + * X-Signature: HMAC-SHA256(secretKey, "timestamp:METHOD:path:body") + * + * Languages Cache: + * - Cached in ~/.unsandbox/languages.json (Node.js only) + * - TTL: 1 hour + * - Updated on successful API calls + * + * Browser Usage: + * - Import as ES module: