Compare commits
27 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| dfb6b7747d | |||
| 84b9222ad8 | |||
| a9f1d62889 | |||
| b1b2c14d86 | |||
| 7dd16bf796 | |||
| 422985c6db | |||
| 5373da4108 | |||
| 13f15c8abc | |||
| a839ffcd12 | |||
| 6860871128 | |||
| 59eb474995 | |||
| ce327b35e9 | |||
| ed6c52b001 | |||
| 8093386568 | |||
| 8d17b2babc | |||
| 369f3aa39d | |||
| ac50544ac3 | |||
| de8c816b27 | |||
| 8fae03d9fd | |||
| 1c5a35f2b5 | |||
| 57fa1008ee | |||
| 2e787cb9e4 | |||
| e1fbfb3884 | |||
| b3ed0f808d | |||
| 710eb5d9ac | |||
|
|
2fd5b5617f | ||
|
|
9c91d8bbf7 |
215 changed files with 22869 additions and 2189 deletions
5
.gitignore
vendored
5
.gitignore
vendored
|
|
@ -15,6 +15,8 @@
|
||||||
/Un.class
|
/Un.class
|
||||||
|
|
||||||
__pycache__/
|
__pycache__/
|
||||||
|
.venv/
|
||||||
|
*.egg-info/
|
||||||
|
|
||||||
# Build directories
|
# Build directories
|
||||||
_build/
|
_build/
|
||||||
|
|
@ -40,8 +42,11 @@ Thumbs.db
|
||||||
/output/
|
/output/
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.pyc
|
*.pyc
|
||||||
|
science-results/
|
||||||
|
science-results.xml
|
||||||
clients/c/un
|
clients/c/un
|
||||||
clients/c/examples/fibonacci
|
clients/c/examples/fibonacci
|
||||||
clients/c/examples/hello_world
|
clients/c/examples/hello_world
|
||||||
clients/c/tests/test_library
|
clients/c/tests/test_library
|
||||||
build/
|
build/
|
||||||
|
.claude/
|
||||||
|
|
|
||||||
|
|
@ -168,7 +168,6 @@ science-validate-examples:
|
||||||
paths:
|
paths:
|
||||||
- science-results/
|
- science-results/
|
||||||
expire_in: 30 days
|
expire_in: 30 days
|
||||||
allow_failure: true
|
|
||||||
only:
|
only:
|
||||||
- main
|
- main
|
||||||
- /^\d+\.\d+\.\d+$/
|
- /^\d+\.\d+\.\d+$/
|
||||||
|
|
@ -231,7 +230,6 @@ validate-examples:
|
||||||
paths:
|
paths:
|
||||||
- science-results/
|
- science-results/
|
||||||
expire_in: 30 days
|
expire_in: 30 days
|
||||||
allow_failure: true
|
|
||||||
only:
|
only:
|
||||||
- main
|
- main
|
||||||
- /^\d+\.\d+\.\d+$/
|
- /^\d+\.\d+\.\d+$/
|
||||||
|
|
|
||||||
|
|
@ -1,31 +1,13 @@
|
||||||
# UN Inception: Aggregated Performance Analysis
|
# UN Inception: Aggregated Performance Analysis
|
||||||
|
|
||||||
<<<<<<< Updated upstream
|
**Analysis Date:** 1771084652.659423
|
||||||
<<<<<<< Updated upstream
|
**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:** 1770551195.2485962
|
|
||||||
**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
|
|
||||||
=======
|
|
||||||
**Analysis Date:** 1770575731.1513839
|
|
||||||
**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
|
|
||||||
>>>>>>> Stashed changes
|
|
||||||
=======
|
|
||||||
**Analysis Date:** 1770578949.9558537
|
|
||||||
**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
|
|
||||||
>>>>>>> Stashed changes
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Executive Summary
|
## Executive Summary
|
||||||
|
|
||||||
<<<<<<< Updated upstream
|
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:
|
||||||
<<<<<<< Updated upstream
|
|
||||||
Analysis of 40 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 41 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:
|
|
||||||
>>>>>>> Stashed changes
|
|
||||||
=======
|
|
||||||
Analysis of 42 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:
|
|
||||||
>>>>>>> Stashed changes
|
|
||||||
|
|
||||||
1. **Orchestrator placement on CPU-bound pool** (not an SRE best practice)
|
1. **Orchestrator placement on CPU-bound pool** (not an SRE best practice)
|
||||||
2. **Resource contention** between the orchestrator & test jobs
|
2. **Resource contention** between the orchestrator & test jobs
|
||||||
|
|
@ -80,15 +62,9 @@ Analysis of 42 performance reports reveals **significant variance** in execution
|
||||||
| 4.2.9 | 107s | ruby (279s) | d (19s) | -4s (-3.6%) |
|
| 4.2.9 | 107s | ruby (279s) | d (19s) | -4s (-3.6%) |
|
||||||
| 4.3.0 | 376s | typescript (829s) | c (47s) | +269s (+251.4%) |
|
| 4.3.0 | 376s | typescript (829s) | c (47s) | +269s (+251.4%) |
|
||||||
| 4.3.1 | 238s | go (414s) | prolog (58s) | -138s (-36.7%) |
|
| 4.3.1 | 238s | go (414s) | prolog (58s) | -138s (-36.7%) |
|
||||||
<<<<<<< Updated upstream
|
|
||||||
<<<<<<< Updated upstream
|
|
||||||
=======
|
|
||||||
| 4.3.2 | 132s | crystal (314s) | c (66s) | -106s (-44.5%) |
|
|
||||||
>>>>>>> Stashed changes
|
|
||||||
=======
|
|
||||||
| 4.3.2 | 132s | crystal (314s) | c (66s) | -106s (-44.5%) |
|
| 4.3.2 | 132s | crystal (314s) | c (66s) | -106s (-44.5%) |
|
||||||
| 4.3.3 | 175s | go (447s) | v (65s) | +43s (+32.6%) |
|
| 4.3.3 | 175s | go (447s) | v (65s) | +43s (+32.6%) |
|
||||||
>>>>>>> Stashed changes
|
| 4.3.4 | 204s | perl (456s) | php (49s) | +29s (+16.6%) |
|
||||||
|
|
||||||
**Observation:** Average duration increased **0.0%** from 0s to 0s.
|
**Observation:** Average duration increased **0.0%** from 0s to 0s.
|
||||||
|
|
||||||
|
|
@ -145,15 +121,9 @@ The same language changes dramatically in rank between runs:
|
||||||
- 4.2.9: 166s
|
- 4.2.9: 166s
|
||||||
- 4.3.0: 196s
|
- 4.3.0: 196s
|
||||||
- 4.3.1: 164s
|
- 4.3.1: 164s
|
||||||
<<<<<<< Updated upstream
|
|
||||||
<<<<<<< Updated upstream
|
|
||||||
=======
|
|
||||||
- 4.3.2: 193s
|
|
||||||
>>>>>>> Stashed changes
|
|
||||||
=======
|
|
||||||
- 4.3.2: 193s
|
- 4.3.2: 193s
|
||||||
- 4.3.3: 202s
|
- 4.3.3: 202s
|
||||||
>>>>>>> Stashed changes
|
- 4.3.4: 198s
|
||||||
- **Range:** 32s → 2173s (6690.6% variance)
|
- **Range:** 32s → 2173s (6690.6% variance)
|
||||||
|
|
||||||
**R:**
|
**R:**
|
||||||
|
|
@ -197,15 +167,9 @@ The same language changes dramatically in rank between runs:
|
||||||
- 4.2.9: 54s
|
- 4.2.9: 54s
|
||||||
- 4.3.0: 453s
|
- 4.3.0: 453s
|
||||||
- 4.3.1: 163s
|
- 4.3.1: 163s
|
||||||
<<<<<<< Updated upstream
|
|
||||||
<<<<<<< Updated upstream
|
|
||||||
=======
|
|
||||||
- 4.3.2: 179s
|
|
||||||
>>>>>>> Stashed changes
|
|
||||||
=======
|
|
||||||
- 4.3.2: 179s
|
- 4.3.2: 179s
|
||||||
- 4.3.3: 81s
|
- 4.3.3: 81s
|
||||||
>>>>>>> Stashed changes
|
- 4.3.4: 106s
|
||||||
- **Range:** 9s → 1834s (20277.8% variance)
|
- **Range:** 9s → 1834s (20277.8% variance)
|
||||||
|
|
||||||
**SCHEME:**
|
**SCHEME:**
|
||||||
|
|
@ -249,15 +213,9 @@ The same language changes dramatically in rank between runs:
|
||||||
- 4.2.9: 153s
|
- 4.2.9: 153s
|
||||||
- 4.3.0: 171s
|
- 4.3.0: 171s
|
||||||
- 4.3.1: 276s
|
- 4.3.1: 276s
|
||||||
<<<<<<< Updated upstream
|
|
||||||
<<<<<<< Updated upstream
|
|
||||||
=======
|
|
||||||
- 4.3.2: 132s
|
|
||||||
>>>>>>> Stashed changes
|
|
||||||
=======
|
|
||||||
- 4.3.2: 132s
|
- 4.3.2: 132s
|
||||||
- 4.3.3: 196s
|
- 4.3.3: 196s
|
||||||
>>>>>>> Stashed changes
|
- 4.3.4: 148s
|
||||||
- **Range:** 15s → 1574s (10393.3% variance)
|
- **Range:** 15s → 1574s (10393.3% variance)
|
||||||
|
|
||||||
**PYTHON:**
|
**PYTHON:**
|
||||||
|
|
@ -301,15 +259,9 @@ The same language changes dramatically in rank between runs:
|
||||||
- 4.2.9: 165s
|
- 4.2.9: 165s
|
||||||
- 4.3.0: 671s
|
- 4.3.0: 671s
|
||||||
- 4.3.1: 148s
|
- 4.3.1: 148s
|
||||||
<<<<<<< Updated upstream
|
|
||||||
<<<<<<< Updated upstream
|
|
||||||
=======
|
|
||||||
- 4.3.2: 85s
|
|
||||||
>>>>>>> Stashed changes
|
|
||||||
=======
|
|
||||||
- 4.3.2: 85s
|
- 4.3.2: 85s
|
||||||
- 4.3.3: 158s
|
- 4.3.3: 158s
|
||||||
>>>>>>> Stashed changes
|
- 4.3.4: 307s
|
||||||
- **Range:** 19s → 1574s (8184.2% variance)
|
- **Range:** 19s → 1574s (8184.2% variance)
|
||||||
|
|
||||||
**TCL:**
|
**TCL:**
|
||||||
|
|
@ -353,15 +305,9 @@ The same language changes dramatically in rank between runs:
|
||||||
- 4.2.9: 54s
|
- 4.2.9: 54s
|
||||||
- 4.3.0: 476s
|
- 4.3.0: 476s
|
||||||
- 4.3.1: 132s
|
- 4.3.1: 132s
|
||||||
<<<<<<< Updated upstream
|
|
||||||
<<<<<<< Updated upstream
|
|
||||||
=======
|
|
||||||
- 4.3.2: 80s
|
|
||||||
>>>>>>> Stashed changes
|
|
||||||
=======
|
|
||||||
- 4.3.2: 80s
|
- 4.3.2: 80s
|
||||||
- 4.3.3: 186s
|
- 4.3.3: 186s
|
||||||
>>>>>>> Stashed changes
|
- 4.3.4: 247s
|
||||||
- **Range:** 20s → 1572s (7760.0% variance)
|
- **Range:** 20s → 1572s (7760.0% variance)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -411,15 +357,9 @@ The same language changes dramatically in rank between runs:
|
||||||
4.2.9: d, julia, csharp, v, objc
|
4.2.9: d, julia, csharp, v, objc
|
||||||
4.3.0: c, bash, php, fortran, ruby
|
4.3.0: c, bash, php, fortran, ruby
|
||||||
4.3.1: prolog, awk, powershell, typescript, dart
|
4.3.1: prolog, awk, powershell, typescript, dart
|
||||||
<<<<<<< Updated upstream
|
|
||||||
<<<<<<< Updated upstream
|
|
||||||
=======
|
|
||||||
4.3.2: c, cpp, fsharp, perl, csharp
|
|
||||||
>>>>>>> Stashed changes
|
|
||||||
=======
|
|
||||||
4.3.2: c, cpp, fsharp, perl, csharp
|
4.3.2: c, cpp, fsharp, perl, csharp
|
||||||
4.3.3: v, r, dart, perl, rust
|
4.3.3: v, r, dart, perl, rust
|
||||||
>>>>>>> Stashed changes
|
4.3.4: php, powershell, v, bash, fortran
|
||||||
|
|
||||||
**Slowest Languages by Run:**
|
**Slowest Languages by Run:**
|
||||||
|
|
||||||
|
|
@ -463,15 +403,9 @@ The same language changes dramatically in rank between runs:
|
||||||
4.2.9: ruby, deno, rust, crystal, java
|
4.2.9: ruby, deno, rust, crystal, java
|
||||||
4.3.0: typescript, go, python, java, objc
|
4.3.0: typescript, go, python, java, objc
|
||||||
4.3.1: go, groovy, perl, deno, objc
|
4.3.1: go, groovy, perl, deno, objc
|
||||||
<<<<<<< Updated upstream
|
|
||||||
<<<<<<< Updated upstream
|
|
||||||
=======
|
|
||||||
4.3.2: crystal, erlang, elixir, rust, groovy
|
|
||||||
>>>>>>> Stashed changes
|
|
||||||
=======
|
|
||||||
4.3.2: crystal, erlang, elixir, rust, groovy
|
4.3.2: crystal, erlang, elixir, rust, groovy
|
||||||
4.3.3: go, crystal, raku, typescript, kotlin
|
4.3.3: go, crystal, raku, typescript, kotlin
|
||||||
>>>>>>> Stashed changes
|
4.3.4: perl, ruby, go, cobol, kotlin
|
||||||
|
|
||||||
**Conclusion:** No consistent "fast" or "slow" languages across runs. This proves:
|
**Conclusion:** No consistent "fast" or "slow" languages across runs. This proves:
|
||||||
- Execution order is random or system-dependent
|
- Execution order is random or system-dependent
|
||||||
|
|
@ -482,21 +416,9 @@ The same language changes dramatically in rank between runs:
|
||||||
|
|
||||||
### 4. API Health Trends
|
### 4. API Health Trends
|
||||||
|
|
||||||
<<<<<<< Updated upstream
|
**Overall API Health:** 5.2/100 (avg across 12 releases)
|
||||||
<<<<<<< Updated upstream
|
|
||||||
**Overall API Health:** 6.9/100 (avg across 9 releases)
|
|
||||||
**Trend:** STABLE
|
**Trend:** STABLE
|
||||||
**Total Retries (all releases):** 4293
|
**Total Retries (all releases):** 5332
|
||||||
=======
|
|
||||||
**Overall API Health:** 6.2/100 (avg across 10 releases)
|
|
||||||
**Trend:** STABLE
|
|
||||||
**Total Retries (all releases):** 4510
|
|
||||||
>>>>>>> Stashed changes
|
|
||||||
=======
|
|
||||||
**Overall API Health:** 5.6/100 (avg across 11 releases)
|
|
||||||
**Trend:** STABLE
|
|
||||||
**Total Retries (all releases):** 4840
|
|
||||||
>>>>>>> Stashed changes
|
|
||||||
|
|
||||||
| Release | Health Score | Total Retries | 429 (Rate Limit) | 5xx (Server) | Timeout | Connection |
|
| Release | Health Score | Total Retries | 429 (Rate Limit) | 5xx (Server) | Timeout | Connection |
|
||||||
|---------|--------------|---------------|------------------|--------------|---------|------------|
|
|---------|--------------|---------------|------------------|--------------|---------|------------|
|
||||||
|
|
@ -509,15 +431,9 @@ The same language changes dramatically in rank between runs:
|
||||||
| 4.2.52 | 34/100 | 33 | 0 | 33 | 0 | 0 |
|
| 4.2.52 | 34/100 | 33 | 0 | 33 | 0 | 0 |
|
||||||
| 4.3.0 | 0/100 | 876 | 839 | 27 | 10 | 0 |
|
| 4.3.0 | 0/100 | 876 | 839 | 27 | 10 | 0 |
|
||||||
| 4.3.1 | 0/100 | 649 | 634 | 5 | 10 | 0 |
|
| 4.3.1 | 0/100 | 649 | 634 | 5 | 10 | 0 |
|
||||||
<<<<<<< Updated upstream
|
|
||||||
<<<<<<< Updated upstream
|
|
||||||
=======
|
|
||||||
| 4.3.2 | 0/100 | 217 | 207 | 0 | 10 | 0 |
|
|
||||||
>>>>>>> Stashed changes
|
|
||||||
=======
|
|
||||||
| 4.3.2 | 0/100 | 217 | 207 | 0 | 10 | 0 |
|
| 4.3.2 | 0/100 | 217 | 207 | 0 | 10 | 0 |
|
||||||
| 4.3.3 | 0/100 | 330 | 320 | 0 | 10 | 0 |
|
| 4.3.3 | 0/100 | 330 | 320 | 0 | 10 | 0 |
|
||||||
>>>>>>> Stashed changes
|
| 4.3.4 | 0/100 | 492 | 482 | 0 | 10 | 0 |
|
||||||
|
|
||||||
**Interpretation:**
|
**Interpretation:**
|
||||||
- **Score 95-100:** API healthy, tests pass on first attempt
|
- **Score 95-100:** API healthy, tests pass on first attempt
|
||||||
|
|
@ -673,72 +589,26 @@ Keep it as-is for stress testing, but in separate test environment.
|
||||||
|
|
||||||
| Language | Min (s) | Max (s) | Avg (s) | Range (s) | Variance % |
|
| Language | Min (s) | Max (s) | Avg (s) | Range (s) | Variance % |
|
||||||
|----------|---------|---------|---------|-----------|------------|
|
|----------|---------|---------|---------|-----------|------------|
|
||||||
<<<<<<< Updated upstream
|
| DOTNET | 5 | 1539 | 171.3 | 1534 | 30680.0% |
|
||||||
<<<<<<< Updated upstream
|
| R | 9 | 1834 | 219.7 | 1825 | 20277.8% |
|
||||||
| DOTNET | 5 | 1539 | 173.6 | 1534 | 30680.0% |
|
| NIM | 8 | 1557 | 173.6 | 1549 | 19362.5% |
|
||||||
| R | 9 | 1834 | 227.0 | 1825 | 20277.8% |
|
| CLOJURE | 8 | 1549 | 190.6 | 1541 | 19262.5% |
|
||||||
| NIM | 8 | 1557 | 175.9 | 1549 | 19362.5% |
|
|
||||||
| CLOJURE | 8 | 1549 | 188.4 | 1541 | 19262.5% |
|
|
||||||
| FORTRAN | 8 | 1547 | 139.2 | 1539 | 19237.5% |
|
|
||||||
| PERL | 8 | 1546 | 182.1 | 1538 | 19225.0% |
|
|
||||||
| D | 8 | 1545 | 141.9 | 1537 | 19212.5% |
|
|
||||||
| ZIG | 8 | 1542 | 218.8 | 1534 | 19175.0% |
|
|
||||||
| FORTH | 8 | 1540 | 172.7 | 1532 | 19150.0% |
|
|
||||||
| KOTLIN | 8 | 1536 | 161.9 | 1528 | 19100.0% |
|
|
||||||
| CSHARP | 8 | 1534 | 162.4 | 1526 | 19075.0% |
|
|
||||||
| LUA | 9 | 1548 | 209.3 | 1539 | 17100.0% |
|
|
||||||
| PROLOG | 9 | 1540 | 124.2 | 1531 | 17011.1% |
|
|
||||||
| RUST | 9 | 1537 | 191.5 | 1528 | 16977.8% |
|
|
||||||
| SCHEME | 15 | 1574 | 168.0 | 1559 | 10393.3% |
|
|
||||||
| OBJC | 17 | 1559 | 168.4 | 1542 | 9070.6% |
|
|
||||||
| POWERSHELL | 14 | 1269 | 128.0 | 1255 | 8964.3% |
|
|
||||||
| PYTHON | 19 | 1574 | 174.8 | 1555 | 8184.2% |
|
|
||||||
| OCAML | 19 | 1537 | 168.4 | 1518 | 7989.5% |
|
|
||||||
| TCL | 20 | 1572 | 187.1 | 1552 | 7760.0% |
|
|
||||||
=======
|
|
||||||
| DOTNET | 5 | 1539 | 171.1 | 1534 | 30680.0% |
|
|
||||||
| R | 9 | 1834 | 225.8 | 1825 | 20277.8% |
|
|
||||||
| NIM | 8 | 1557 | 174.4 | 1549 | 19362.5% |
|
|
||||||
| CLOJURE | 8 | 1549 | 186.8 | 1541 | 19262.5% |
|
|
||||||
| FORTRAN | 8 | 1547 | 138.9 | 1539 | 19237.5% |
|
| FORTRAN | 8 | 1547 | 138.9 | 1539 | 19237.5% |
|
||||||
| PERL | 8 | 1546 | 179.5 | 1538 | 19225.0% |
|
| PERL | 8 | 1546 | 183.8 | 1538 | 19225.0% |
|
||||||
| D | 8 | 1545 | 142.1 | 1537 | 19212.5% |
|
| D | 8 | 1545 | 142.7 | 1537 | 19212.5% |
|
||||||
| ZIG | 8 | 1542 | 216.1 | 1534 | 19175.0% |
|
| ZIG | 8 | 1542 | 215.1 | 1534 | 19175.0% |
|
||||||
| FORTH | 8 | 1540 | 172.7 | 1532 | 19150.0% |
|
|
||||||
| KOTLIN | 8 | 1536 | 161.9 | 1528 | 19100.0% |
|
|
||||||
| CSHARP | 8 | 1534 | 160.4 | 1526 | 19075.0% |
|
|
||||||
| LUA | 9 | 1548 | 206.3 | 1539 | 17100.0% |
|
|
||||||
| PROLOG | 9 | 1540 | 123.6 | 1531 | 17011.1% |
|
|
||||||
| RUST | 9 | 1537 | 192.1 | 1528 | 16977.8% |
|
|
||||||
| SCHEME | 15 | 1574 | 167.1 | 1559 | 10393.3% |
|
|
||||||
| OBJC | 17 | 1559 | 168.6 | 1542 | 9070.6% |
|
|
||||||
| POWERSHELL | 14 | 1269 | 127.1 | 1255 | 8964.3% |
|
|
||||||
| PYTHON | 19 | 1574 | 172.6 | 1555 | 8184.2% |
|
|
||||||
| OCAML | 19 | 1537 | 166.4 | 1518 | 7989.5% |
|
|
||||||
| TCL | 20 | 1572 | 184.5 | 1552 | 7760.0% |
|
|
||||||
>>>>>>> Stashed changes
|
|
||||||
=======
|
|
||||||
| DOTNET | 5 | 1539 | 169.9 | 1534 | 30680.0% |
|
|
||||||
| R | 9 | 1834 | 222.4 | 1825 | 20277.8% |
|
|
||||||
| NIM | 8 | 1557 | 172.4 | 1549 | 19362.5% |
|
|
||||||
| CLOJURE | 8 | 1549 | 188.9 | 1541 | 19262.5% |
|
|
||||||
| FORTRAN | 8 | 1547 | 140.0 | 1539 | 19237.5% |
|
|
||||||
| PERL | 8 | 1546 | 177.3 | 1538 | 19225.0% |
|
|
||||||
| D | 8 | 1545 | 141.9 | 1537 | 19212.5% |
|
|
||||||
| ZIG | 8 | 1542 | 213.6 | 1534 | 19175.0% |
|
|
||||||
| FORTH | 8 | 1540 | 172.4 | 1532 | 19150.0% |
|
| FORTH | 8 | 1540 | 172.4 | 1532 | 19150.0% |
|
||||||
| KOTLIN | 8 | 1536 | 164.7 | 1528 | 19100.0% |
|
| KOTLIN | 8 | 1536 | 168.5 | 1528 | 19100.0% |
|
||||||
| CSHARP | 8 | 1534 | 159.8 | 1526 | 19075.0% |
|
| CSHARP | 8 | 1534 | 159.8 | 1526 | 19075.0% |
|
||||||
| LUA | 9 | 1548 | 206.1 | 1539 | 17100.0% |
|
| LUA | 9 | 1548 | 208.0 | 1539 | 17100.0% |
|
||||||
| PROLOG | 9 | 1540 | 125.1 | 1531 | 17011.1% |
|
| PROLOG | 9 | 1540 | 127.8 | 1531 | 17011.1% |
|
||||||
| RUST | 9 | 1537 | 189.6 | 1528 | 16977.8% |
|
| RUST | 9 | 1537 | 189.3 | 1528 | 16977.8% |
|
||||||
| SCHEME | 15 | 1574 | 167.8 | 1559 | 10393.3% |
|
| SCHEME | 15 | 1574 | 167.3 | 1559 | 10393.3% |
|
||||||
| OBJC | 17 | 1559 | 167.3 | 1542 | 9070.6% |
|
| OBJC | 17 | 1559 | 165.9 | 1542 | 9070.6% |
|
||||||
| POWERSHELL | 14 | 1269 | 126.6 | 1255 | 8964.3% |
|
| POWERSHELL | 14 | 1269 | 124.9 | 1255 | 8964.3% |
|
||||||
| PYTHON | 19 | 1574 | 172.2 | 1555 | 8184.2% |
|
| PYTHON | 19 | 1574 | 175.4 | 1555 | 8184.2% |
|
||||||
| OCAML | 19 | 1537 | 166.6 | 1518 | 7989.5% |
|
| OCAML | 19 | 1537 | 168.0 | 1518 | 7989.5% |
|
||||||
| TCL | 20 | 1572 | 184.5 | 1552 | 7760.0% |
|
| TCL | 20 | 1572 | 186.0 | 1552 | 7760.0% |
|
||||||
>>>>>>> Stashed changes
|
|
||||||
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -833,15 +703,9 @@ Individual Reports → Aggregation Script → Chart Generation (via UN) → Fina
|
||||||
- `reports/4.2.9/perf.json` - 645 tests, generated 2026-01-23T10:05:34Z
|
- `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.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.1/perf.json` - 824 tests, generated 2026-02-08T11:44:58Z
|
||||||
<<<<<<< Updated upstream
|
|
||||||
<<<<<<< Updated upstream
|
|
||||||
=======
|
|
||||||
- `reports/4.3.2/perf.json` - 860 tests, generated 2026-02-08T18:34:37Z
|
|
||||||
>>>>>>> Stashed changes
|
|
||||||
=======
|
|
||||||
- `reports/4.3.2/perf.json` - 860 tests, generated 2026-02-08T18:34:37Z
|
- `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.3/perf.json` - 848 tests, generated 2026-02-08T19:28:06Z
|
||||||
>>>>>>> Stashed changes
|
- `reports/4.3.4/perf.json` - 844 tests, generated 2026-02-14T15:56:41Z
|
||||||
|
|
||||||
|
|
||||||
Each `perf.json` contains:
|
Each `perf.json` contains:
|
||||||
|
|
@ -1038,13 +902,5 @@ For questions about this methodology or to report issues:
|
||||||
---
|
---
|
||||||
|
|
||||||
**Generated by UN Inception Performance Analysis Pipeline**
|
**Generated by UN Inception Performance Analysis Pipeline**
|
||||||
<<<<<<< Updated upstream
|
**Analysis Date:** 2026-02-14T10:57:32.791573
|
||||||
<<<<<<< Updated upstream
|
|
||||||
**Analysis Date:** 2026-02-08T06:46:35.375632
|
|
||||||
=======
|
|
||||||
**Analysis Date:** 2026-02-08T13:35:31.282306
|
|
||||||
>>>>>>> Stashed changes
|
|
||||||
=======
|
|
||||||
**Analysis Date:** 2026-02-08T14:29:10.036207
|
|
||||||
>>>>>>> Stashed changes
|
|
||||||
**Report Version:** 1.0.0
|
**Report Version:** 1.0.0
|
||||||
|
|
|
||||||
59
CLAUDE.md
59
CLAUDE.md
|
|
@ -2,9 +2,9 @@
|
||||||
|
|
||||||
## ⚠️ CRITICAL: QR TEST FILES USE NATIVE LIBRARIES - NEVER SHELL OUT
|
## ⚠️ CRITICAL: QR TEST FILES USE NATIVE LIBRARIES - NEVER SHELL OUT
|
||||||
|
|
||||||
**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.
|
**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.
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# ❌ FORBIDDEN - shelling out defeats the test
|
# ❌ FORBIDDEN - shelling out defeats the test
|
||||||
|
|
@ -18,9 +18,9 @@ q.add_data("unsandbox-qr-ok")
|
||||||
|
|
||||||
## ⚠️ CRITICAL: SCIENTIFIC INTEGRITY - TESTS MUST NEVER LIE
|
## ⚠️ CRITICAL: SCIENTIFIC INTEGRITY - TESTS MUST NEVER LIE
|
||||||
|
|
||||||
**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.
|
**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.
|
||||||
|
|
||||||
### The Cardinal Rule
|
### Our Cardinal Rule
|
||||||
|
|
||||||
**If a test cannot verify its assertion, it MUST FAIL or RETRY - never silently pass.**
|
**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:
|
On 2026-01-28, we discovered our "100% pass rate" was a lie:
|
||||||
- **780 tests "passed"** across 42 languages
|
- **780 tests "passed"** across 42 languages
|
||||||
- **270 were soft passes** (35%) - masked failures
|
- **270 were soft passes** (35%) - masked failures
|
||||||
- The test matrix was telling us everything worked when it didn't
|
- Our test matrix was telling us everything worked when it didn't
|
||||||
|
|
||||||
**Soft passes are scientific fraud.** They:
|
**Soft passes are scientific fraud.** They:
|
||||||
- Hide real bugs in SDKs
|
- 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
|
1. **Retry ALL transient errors** - HTTP 429, 500, 502, 503, 504, timeouts
|
||||||
2. **Use exponential backoff** - Start at 2s, cap at 60s
|
2. **Use exponential backoff** - Start at 2s, cap at 60s
|
||||||
3. **Max retries = 10** - Then FAIL, don't fake pass
|
3. **Max retries = 10** - Then FAIL, don't fake pass
|
||||||
4. **No soft passes** - If the expected output isn't there, it's a FAIL
|
4. **No soft passes** - If our expected output isn't there, it's a FAIL
|
||||||
5. **Track retry stats** - So we can see API health over time
|
5. **Track retry stats** - So we can see API health over time
|
||||||
|
|
||||||
### Acceptable Test Outcomes
|
### Acceptable Test Outcomes
|
||||||
|
|
@ -77,16 +77,16 @@ On 2026-01-28, we discovered our "100% pass rate" was a lie:
|
||||||
|
|
||||||
**Every failure stays visible until fixed.** No skipping. No `allow_failure: true` to hide problems. No workarounds.
|
**Every failure stays visible until fixed.** No skipping. No `allow_failure: true` to hide problems. No workarounds.
|
||||||
|
|
||||||
The goal is a **green christmas tree** - all tests passing, all examples validating, all lights green. Until then:
|
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
|
1. **Failures are features** - They tell us what to fix next
|
||||||
2. **Red stays red** - Don't mask failures to make CI "pass"
|
2. **Red stays red** - Don't mask failures to make CI "pass"
|
||||||
3. **Iterate until green** - Keep fixing until everything works
|
3. **Iterate until green** - Keep fixing until everything works
|
||||||
4. **Log errors loudly** - Show stderr, stdout, API errors on every failure
|
4. **Log errors loudly** - Show stderr, stdout, API errors on every failure
|
||||||
|
|
||||||
When CI fails, the response is:
|
When CI fails, our response is:
|
||||||
- ❌ NOT: "Let's skip this test" or "Let's allow this to fail"
|
- ❌ NOT: "Let's skip this test" or "Let's allow this to fail"
|
||||||
- ✅ YES: "Let's fix this test" or "Let's fix the code"
|
- ✅ YES: "Let's fix this test" or "Let's fix our code"
|
||||||
|
|
||||||
**Current known issues to fix (2026-02-13):**
|
**Current known issues to fix (2026-02-13):**
|
||||||
- SDK client examples fail in sandbox (no credentials, no SDK installed)
|
- SDK client examples fail in sandbox (no credentials, no SDK installed)
|
||||||
|
|
@ -115,11 +115,11 @@ On 2026-01-11, raw `lxc delete` destroyed 8 production services causing complete
|
||||||
|
|
||||||
## Commit Messages
|
## Commit Messages
|
||||||
|
|
||||||
**NEVER add Claude attribution to commit messages.** No robot emoji, no "Generated with Claude Code", no "Co-Authored-By: Claude". Just write the commit message like a human wrote it.
|
**NEVER add Claude attribution to commit messages.** No robot emoji, no "Generated with Claude Code", no "Co-Authored-By: Claude". Just write our commit message like a human wrote it.
|
||||||
|
|
||||||
## Project Overview
|
## Project Overview
|
||||||
|
|
||||||
UN CLI Inception - The UN CLI written in every language it can execute. 42+ implementations, one unified interface.
|
UN CLI Inception - Our UN CLI written in every language it can execute. 42+ implementations, one unified interface.
|
||||||
|
|
||||||
### SDK Architecture
|
### SDK Architecture
|
||||||
|
|
||||||
|
|
@ -184,7 +184,7 @@ export UNSANDBOX_PUBLIC_KEY="unsb-pk-xxxx-xxxx-xxxx-xxxx"
|
||||||
export UNSANDBOX_SECRET_KEY="unsb-sk-xxxxx-xxxxx-xxxxx-xxxxx"
|
export UNSANDBOX_SECRET_KEY="unsb-sk-xxxxx-xxxxx-xxxxx-xxxxx"
|
||||||
```
|
```
|
||||||
|
|
||||||
The auth pattern for all implementations:
|
Our auth pattern for all implementations:
|
||||||
- `Authorization: Bearer {public_key}`
|
- `Authorization: Bearer {public_key}`
|
||||||
- `X-Timestamp: {unix_seconds}`
|
- `X-Timestamp: {unix_seconds}`
|
||||||
- `X-Signature: HMAC-SHA256(secret_key, "timestamp:METHOD:path:body")`
|
- `X-Signature: HMAC-SHA256(secret_key, "timestamp:METHOD:path:body")`
|
||||||
|
|
@ -217,11 +217,11 @@ Each implementation needs HMAC-SHA256 capability:
|
||||||
|
|
||||||
**Note**: Languages without native HMAC (Lua, Bash, AWK, Forth) shell out to `openssl dgst -sha256 -hmac`.
|
**Note**: Languages without native HMAC (Lua, Bash, AWK, Forth) shell out to `openssl dgst -sha256 -hmac`.
|
||||||
|
|
||||||
## The Inception Matrix - Testing Languages Without Local Interpreters
|
## Our Inception Matrix - Testing Languages Without Local Interpreters
|
||||||
|
|
||||||
**CRITICAL INSIGHT**: Use `un` (the C implementation) to run tests for languages not installed locally!
|
**CRITICAL INSIGHT**: Use `un` (our C implementation) to run tests for languages not installed locally!
|
||||||
|
|
||||||
If a language isn't available on the local machine (e.g., PHP, Julia, Haskell), run the UN implementation through unsandbox itself:
|
If a language isn't available on our local machine (e.g., PHP, Julia, Haskell), run our UN implementation through unsandbox itself:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Key flags:
|
# Key flags:
|
||||||
|
|
@ -235,7 +235,7 @@ un -n semitrusted -e UNSANDBOX_PUBLIC_KEY=$UNSANDBOX_PUBLIC_KEY -e UNSANDBOX_SEC
|
||||||
un -n semitrusted -e UNSANDBOX_PUBLIC_KEY=$UNSANDBOX_PUBLIC_KEY -e UNSANDBOX_SECRET_KEY=$UNSANDBOX_SECRET_KEY un.jl test/fib.py
|
un -n semitrusted -e UNSANDBOX_PUBLIC_KEY=$UNSANDBOX_PUBLIC_KEY -e UNSANDBOX_SECRET_KEY=$UNSANDBOX_SECRET_KEY un.jl test/fib.py
|
||||||
```
|
```
|
||||||
|
|
||||||
This is the **inception** - using un to run un to run code. Each layer executes through unsandbox's remote execution API.
|
This is our **inception** - using un to run un to run code. Each layer executes through unsandbox's remote execution API.
|
||||||
|
|
||||||
### Inception Test Matrix
|
### Inception Test Matrix
|
||||||
|
|
||||||
|
|
@ -254,7 +254,7 @@ done
|
||||||
|
|
||||||
### CI Test Status
|
### CI Test Status
|
||||||
|
|
||||||
The CI runs the full inception test matrix on every tag release and smart change detection on regular pushes.
|
Our CI runs our full inception test matrix on every tag release and smart change detection on regular pushes.
|
||||||
|
|
||||||
**Test results must be truthful.** Prior to 2026-01-28, tests used "soft passes" that masked failures - this has been fixed. Tests now retry transient errors and fail honestly if they can't verify.
|
**Test results must be truthful.** Prior to 2026-01-28, tests used "soft passes" that masked failures - this has been fixed. Tests now retry transient errors and fail honestly if they can't verify.
|
||||||
|
|
||||||
|
|
@ -311,7 +311,7 @@ See **TESTING-STRATEGY.md** for complete testing matrix.
|
||||||
## Common Test Fixes
|
## Common Test Fixes
|
||||||
|
|
||||||
### Bash arithmetic in `set -e` mode
|
### Bash arithmetic in `set -e` mode
|
||||||
The pattern `((VAR++))` returns exit code 1 when VAR is 0. Use `VAR=$((VAR + 1))` instead.
|
Our pattern `((VAR++))` returns exit code 1 when VAR is 0. Use `VAR=$((VAR + 1))` instead.
|
||||||
|
|
||||||
### Script directory detection
|
### Script directory detection
|
||||||
- **Lua**: `arg[0]:match("(.*/)") or "./"`
|
- **Lua**: `arg[0]:match("(.*/)") or "./"`
|
||||||
|
|
@ -325,10 +325,10 @@ Shebang MUST be on line 1, not buried in license headers.
|
||||||
|
|
||||||
**CRITICAL: ALL 38 implementations must have feature parity.**
|
**CRITICAL: ALL 38 implementations must have feature parity.**
|
||||||
|
|
||||||
When adding a new feature to the CLI (e.g., new flag, new command):
|
When adding a new feature to our CLI (e.g., new flag, new command):
|
||||||
1. Update the canonical C implementation at `~/git/unsandbox.com/cli/un.c`
|
1. Update our canonical C implementation at `~/git/unsandbox.com/cli/un.c`
|
||||||
2. Update ALL 38 implementations in this repo - not just "main" ones, ALL of them
|
2. Update ALL 38 implementations in this repo - not just "main" ones, ALL of them
|
||||||
3. Use the Task agent to batch update if needed
|
3. Use our Task agent to batch update if needed
|
||||||
|
|
||||||
Current implementations (ALL must be updated):
|
Current implementations (ALL must be updated):
|
||||||
```
|
```
|
||||||
|
|
@ -345,7 +345,7 @@ Each implementation must support:
|
||||||
- **Session**: `un session` - interactive shell with `-f FILE`, `--tmux`, `--screen`, `--list`, `--attach`, `--kill`
|
- **Session**: `un session` - interactive shell with `-f FILE`, `--tmux`, `--screen`, `--list`, `--attach`, `--kill`
|
||||||
- **Service**: `un service` - persistent services with `-f FILE`, `--name`, `--ports`, `--bootstrap`, `--bootstrap-file`, `--list`, `--info`, `--logs`, `--destroy`
|
- **Service**: `un service` - persistent services with `-f FILE`, `--name`, `--ports`, `--bootstrap`, `--bootstrap-file`, `--list`, `--info`, `--logs`, `--destroy`
|
||||||
|
|
||||||
The `-f FILE` flag must work for ALL three commands (execute, session, service) - files go to `/tmp/` in the container.
|
Our `-f FILE` flag must work for ALL three commands (execute, session, service) - files go to `/tmp/` in our container.
|
||||||
|
|
||||||
## Git Remotes & Mirroring
|
## Git Remotes & Mirroring
|
||||||
|
|
||||||
|
|
@ -377,9 +377,9 @@ git remote set-url --add --push origin git@github.com:russellballestrini/un-ince
|
||||||
|
|
||||||
## Releases & Versioning
|
## Releases & Versioning
|
||||||
|
|
||||||
**ALWAYS update the VERSION file BEFORE creating a release tag.**
|
**ALWAYS update our VERSION file BEFORE creating a release tag.**
|
||||||
|
|
||||||
The `VERSION` file in the repo root contains the current semantic version (e.g., `0.0.2`). This file is the source of truth for the release version.
|
Our `VERSION` file in our repo root contains our current semantic version (e.g., `0.0.2`). This file is our source of truth for our release version.
|
||||||
|
|
||||||
### Release Process
|
### Release Process
|
||||||
|
|
||||||
|
|
@ -398,7 +398,7 @@ git push origin main 4.2.0
|
||||||
|
|
||||||
### Tag Triggers
|
### Tag Triggers
|
||||||
|
|
||||||
- **Tag push** (`X.Y.Z` format, no v prefix) triggers the **full test matrix** with all 42 languages
|
- **Tag push** (`X.Y.Z` format, no v prefix) triggers our **full test matrix** with all 42 languages
|
||||||
- **Regular push to main** only tests changed SDKs (smart detection)
|
- **Regular push to main** only tests changed SDKs (smart detection)
|
||||||
|
|
||||||
### Version Format
|
### Version Format
|
||||||
|
|
@ -442,7 +442,7 @@ void test_sha256() {
|
||||||
### SDK Export Requirements
|
### SDK Export Requirements
|
||||||
|
|
||||||
Each SDK MUST export functions that can be:
|
Each SDK MUST export functions that can be:
|
||||||
1. **Imported** - Other code can `import`/`require`/`use` the SDK
|
1. **Imported** - Other code can `import`/`require`/`use` our SDK
|
||||||
2. **Tested** - Unit tests can call exported functions directly
|
2. **Tested** - Unit tests can call exported functions directly
|
||||||
3. **Documented** - Public API is clear and documented
|
3. **Documented** - Public API is clear and documented
|
||||||
|
|
||||||
|
|
@ -490,7 +490,7 @@ See **docs/TESTING.md** for complete testing guidelines.
|
||||||
|
|
||||||
### Overview
|
### Overview
|
||||||
|
|
||||||
All 42 language implementations have been migrated to `clients/`. The C implementation (`clients/c/src/un.c`) is our **north star** - all other SDKs should match its CLI and library API.
|
All 42 language implementations have been migrated to `clients/`. Our C implementation (`clients/c/src/un.c`) is our **north star** - all other SDKs should match its CLI and library API.
|
||||||
|
|
||||||
### Migration Progress
|
### Migration Progress
|
||||||
|
|
||||||
|
|
@ -511,7 +511,7 @@ All 42 language implementations have been migrated to `clients/`. The C implemen
|
||||||
|
|
||||||
### C SDK Library API Status (un.h) - COMPLETE
|
### C SDK Library API Status (un.h) - COMPLETE
|
||||||
|
|
||||||
The C SDK implements **43+ library functions** with full JSON parsing:
|
Our C SDK implements **43+ library functions** with full JSON parsing:
|
||||||
|
|
||||||
**Execution (7 functions):**
|
**Execution (7 functions):**
|
||||||
- ✅ `unsandbox_execute()` - Synchronous code execution
|
- ✅ `unsandbox_execute()` - Synchronous code execution
|
||||||
|
|
@ -590,7 +590,7 @@ clients/
|
||||||
2. **Add functional tests** - Each SDK needs functional test coverage
|
2. **Add functional tests** - Each SDK needs functional test coverage
|
||||||
3. **Create scala SDK** - Only missing language
|
3. **Create scala SDK** - Only missing language
|
||||||
|
|
||||||
2. **Add Images API to un.h** - The CLI supports images but library API doesn't expose them
|
2. **Add Images API to un.h** - Our CLI supports images but library API doesn't expose them
|
||||||
|
|
||||||
3. **Migrate next language** - Use this template:
|
3. **Migrate next language** - Use this template:
|
||||||
```bash
|
```bash
|
||||||
|
|
@ -618,4 +618,5 @@ clients/
|
||||||
|
|
||||||
## Style
|
## Style
|
||||||
|
|
||||||
|
- Never use "the" — use "our" instead.
|
||||||
- **Never use "AI" — always say "machine learning."** We grow machine learning, not "AI." This term is forbidden in all permacomputer discourse, marketing, & documentation.
|
- **Never use "AI" — always say "machine learning."** We grow machine learning, not "AI." This term is forbidden in all permacomputer discourse, marketing, & documentation.
|
||||||
|
|
|
||||||
26
LICENSE
26
LICENSE
|
|
@ -1,16 +1,16 @@
|
||||||
PUBLIC DOMAIN - NO LICENSE, NO WARRANTY
|
PUBLIC DOMAIN, NO LICENSE, NO WARRANTY
|
||||||
|
|
||||||
This is free public domain software for the public good of a permacomputer hosted
|
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
|
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
|
which is durable, easy to repair, & distributed like tap water for machine
|
||||||
learning intelligence.
|
learning intelligence.
|
||||||
|
|
||||||
The permacomputer is community-owned infrastructure optimized around four values:
|
Our permacomputer is community-owned infrastructure optimized around four values:
|
||||||
|
|
||||||
TRUTH - Source code must be open source & freely distributed
|
TRUTH First principles, math & science, open source code freely distributed
|
||||||
FREEDOM - Voluntary participation without corporate control
|
FREEDOM Voluntary partnerships, freedom from tyranny & corporate control
|
||||||
HARMONY - Systems operating with minimal waste that self-renew
|
HARMONY Minimal waste, self-renewing systems with diverse thriving connections
|
||||||
LOVE - Individual rights protected while fostering cooperation
|
LOVE Be yourself without hurting others, cooperation through natural law
|
||||||
|
|
||||||
This software contributes to that vision by enabling code execution across 42+
|
This software contributes to that vision by enabling code execution across 42+
|
||||||
programming languages through a unified interface, accessible to all. Code is
|
programming languages through a unified interface, accessible to all. Code is
|
||||||
|
|
@ -20,16 +20,18 @@ Learn more: https://www.permacomputer.com
|
||||||
|
|
||||||
Anyone is free to copy, modify, publish, use, compile, sell, or distribute this
|
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,
|
software, either in source code form or as a compiled binary, for any purpose,
|
||||||
commercial or non-commercial, and by any means.
|
commercial or non-commercial, & by any means.
|
||||||
|
|
||||||
NO WARRANTY. THE SOFTWARE IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND.
|
NO WARRANTY. THE SOFTWARE IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND.
|
||||||
|
|
||||||
That said, our permacomputer's digital membrane stratum continuously runs unit,
|
That said, our permacomputer's digital membrane stratum continuously runs unit,
|
||||||
integration, and functional tests on all of it's own software - with our
|
integration, & functional tests on all of it's own software, with our
|
||||||
permacomputer monitoring itself, repairing itself, with minimal human in the
|
permacomputer monitoring itself, repairing itself, with minimal human in the
|
||||||
loop guidance. Our agents do their best.
|
loop guidance. Our machine learning agents do their best to leave no stone unturned.
|
||||||
|
|
||||||
Copyright 2025 TimeHexOn & foxhop & russell@unturf
|
Copyright (C) 2025-2026 TimeHexOn & foxhop & russell@unturf
|
||||||
|
https://russell.ballestrini.net
|
||||||
https://www.timehexon.com
|
https://www.timehexon.com
|
||||||
https://www.foxhop.net
|
https://www.foxhop.net
|
||||||
https://www.unturf.com/software
|
https://www.unturf.com/software
|
||||||
|
https://www.permacomputer.com
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,9 @@ BEGIN {
|
||||||
GREEN = "\033[32m"
|
GREEN = "\033[32m"
|
||||||
YELLOW = "\033[33m"
|
YELLOW = "\033[33m"
|
||||||
RESET = "\033[0m"
|
RESET = "\033[0m"
|
||||||
|
|
||||||
|
# Global credential state
|
||||||
|
GLOBAL_ACCOUNT_INDEX = -1
|
||||||
}
|
}
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
@ -106,32 +109,84 @@ function health_check( cmd, result) {
|
||||||
return (result == "200")
|
return (result == "200")
|
||||||
}
|
}
|
||||||
|
|
||||||
function get_api_keys( public_key, secret_key, cmd) {
|
function load_accounts_csv(index , home, path, line, fields, count, pk, sk) {
|
||||||
# Get public key
|
home = ENVIRON["HOME"]
|
||||||
cmd = "echo -n $UNSANDBOX_PUBLIC_KEY"
|
count = -1
|
||||||
cmd | getline public_key
|
pk = ""
|
||||||
close(cmd)
|
sk = ""
|
||||||
|
# Try ~/.unsandbox/accounts.csv first
|
||||||
# Get secret key
|
path = home "/.unsandbox/accounts.csv"
|
||||||
cmd = "echo -n $UNSANDBOX_SECRET_KEY"
|
while ((getline line < path) > 0) {
|
||||||
cmd | getline secret_key
|
if (line ~ /^[[:space:]]*$/ || line ~ /^[[:space:]]*#/) continue
|
||||||
close(cmd)
|
count++
|
||||||
|
if (count == index) {
|
||||||
# Fallback to old UNSANDBOX_API_KEY for backwards compat
|
split(line, fields, ",")
|
||||||
if (public_key == "") {
|
pk = fields[1]; sk = fields[2]
|
||||||
cmd = "echo -n $UNSANDBOX_API_KEY"
|
gsub(/^[[:space:]]+|[[:space:]]+$/, "", pk)
|
||||||
cmd | getline public_key
|
gsub(/^[[:space:]]+|[[:space:]]+$/, "", sk)
|
||||||
close(cmd)
|
close(path)
|
||||||
secret_key = ""
|
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
|
||||||
|
}
|
||||||
|
|
||||||
if (public_key == "") {
|
function get_api_keys( public_key, secret_key, cmd, default_index) {
|
||||||
print RED "Error: UNSANDBOX_PUBLIC_KEY or UNSANDBOX_API_KEY not set" RESET > "/dev/stderr"
|
# Priority 1: already set via -p/-k flags
|
||||||
|
if (GLOBAL_PUBLIC_KEY != "" && GLOBAL_SECRET_KEY != "") { return }
|
||||||
|
|
||||||
|
# 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"
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
GLOBAL_PUBLIC_KEY = public_key
|
# Priority 3: env vars UNSANDBOX_PUBLIC_KEY / UNSANDBOX_SECRET_KEY
|
||||||
GLOBAL_SECRET_KEY = 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
|
||||||
}
|
}
|
||||||
|
|
||||||
function get_extension(filename) {
|
function get_extension(filename) {
|
||||||
|
|
@ -2359,6 +2414,20 @@ END {
|
||||||
exit 0
|
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 (ARGV[1] == "session") {
|
||||||
if (ARGC >= 3 && ARGV[2] == "--list") {
|
if (ARGC >= 3 && ARGV[2] == "--list") {
|
||||||
session_list()
|
session_list()
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,19 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# PUBLIC DOMAIN - NO LICENSE, NO WARRANTY
|
# 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.
|
||||||
#
|
#
|
||||||
# unsandbox.com Bash SDK (Synchronous)
|
# The permacomputer is community-owned infrastructure optimized around
|
||||||
# Full API with execution, sessions, services, snapshots, and images.
|
# four values:
|
||||||
#
|
#
|
||||||
# Library Usage:
|
# TRUTH First principles, math & science, open source code freely distributed
|
||||||
# source un.sh
|
# FREEDOM Voluntary partnerships, freedom from tyranny & corporate control
|
||||||
# result=$(execute "python" "print(42)")
|
# HARMONY Minimal waste, self-renewing systems with diverse thriving connections
|
||||||
# echo "$result" | jq -r '.stdout'
|
# LOVE Be yourself without hurting others, cooperation through natural law
|
||||||
#
|
#
|
||||||
# CLI Usage:
|
# This software contributes to that vision by enabling code execution across 42+ programming languages through a unified interface, accessible to all.
|
||||||
# bash un.sh script.py
|
# Code is seeds to sprout on any abandoned technology.
|
||||||
# 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
|
set -euo pipefail
|
||||||
|
|
||||||
|
|
@ -23,6 +21,7 @@ VERSION="4.2.50"
|
||||||
API_BASE="https://api.unsandbox.com"
|
API_BASE="https://api.unsandbox.com"
|
||||||
PORTAL_BASE="https://unsandbox.com"
|
PORTAL_BASE="https://unsandbox.com"
|
||||||
LAST_ERROR=""
|
LAST_ERROR=""
|
||||||
|
ACCOUNT_INDEX=-1
|
||||||
|
|
||||||
# Colors
|
# Colors
|
||||||
BLUE='\033[34m'
|
BLUE='\033[34m'
|
||||||
|
|
@ -106,8 +105,19 @@ hmac_sign() {
|
||||||
|
|
||||||
load_accounts_csv() {
|
load_accounts_csv() {
|
||||||
local path="${1:-$HOME/.unsandbox/accounts.csv}"
|
local path="${1:-$HOME/.unsandbox/accounts.csv}"
|
||||||
|
local row="${2:-0}"
|
||||||
[ -f "$path" ] || return 1
|
[ -f "$path" ] || return 1
|
||||||
head -1 "$path" 2>/dev/null | grep -v '^#'
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
get_credentials() {
|
get_credentials() {
|
||||||
|
|
@ -117,7 +127,20 @@ get_credentials() {
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Tier 2: Environment
|
# 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
|
||||||
if [ -n "${UNSANDBOX_PUBLIC_KEY:-}" ] && [ -n "${UNSANDBOX_SECRET_KEY:-}" ]; then
|
if [ -n "${UNSANDBOX_PUBLIC_KEY:-}" ] && [ -n "${UNSANDBOX_SECRET_KEY:-}" ]; then
|
||||||
echo "$UNSANDBOX_PUBLIC_KEY:$UNSANDBOX_SECRET_KEY"
|
echo "$UNSANDBOX_PUBLIC_KEY:$UNSANDBOX_SECRET_KEY"
|
||||||
return
|
return
|
||||||
|
|
@ -129,7 +152,7 @@ get_credentials() {
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Tier 3: Home directory
|
# Tier 4: Home directory
|
||||||
local creds
|
local creds
|
||||||
creds=$(load_accounts_csv "$HOME/.unsandbox/accounts.csv" 2>/dev/null || true)
|
creds=$(load_accounts_csv "$HOME/.unsandbox/accounts.csv" 2>/dev/null || true)
|
||||||
if [ -n "$creds" ]; then
|
if [ -n "$creds" ]; then
|
||||||
|
|
@ -137,7 +160,7 @@ get_credentials() {
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Tier 4: Local directory
|
# Tier 5: Local directory
|
||||||
creds=$(load_accounts_csv "./accounts.csv" 2>/dev/null || true)
|
creds=$(load_accounts_csv "./accounts.csv" 2>/dev/null || true)
|
||||||
if [ -n "$creds" ]; then
|
if [ -n "$creds" ]; then
|
||||||
echo "$creds"
|
echo "$creds"
|
||||||
|
|
@ -426,6 +449,7 @@ service_create() {
|
||||||
local name="$1"
|
local name="$1"
|
||||||
local ports="${2:-}"
|
local ports="${2:-}"
|
||||||
local bootstrap="${3:-}"
|
local bootstrap="${3:-}"
|
||||||
|
local input_files_json="${4:-}"
|
||||||
|
|
||||||
local body
|
local body
|
||||||
body=$(jq -n --arg name "$name" '{name: $name}')
|
body=$(jq -n --arg name "$name" '{name: $name}')
|
||||||
|
|
@ -436,6 +460,9 @@ service_create() {
|
||||||
if [ -n "$bootstrap" ]; then
|
if [ -n "$bootstrap" ]; then
|
||||||
body=$(echo "$body" | jq --arg boot "$bootstrap" '. + {bootstrap: $boot}')
|
body=$(echo "$body" | jq --arg boot "$bootstrap" '. + {bootstrap: $boot}')
|
||||||
fi
|
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"
|
api_request "POST" "/services" "$body"
|
||||||
}
|
}
|
||||||
|
|
@ -474,10 +501,14 @@ service_set_unfreeze_on_demand() {
|
||||||
service_redeploy() {
|
service_redeploy() {
|
||||||
local service_id="$1"
|
local service_id="$1"
|
||||||
local bootstrap="${2:-}"
|
local bootstrap="${2:-}"
|
||||||
|
local input_files_json="${3:-}"
|
||||||
local body="{}"
|
local body="{}"
|
||||||
if [ -n "$bootstrap" ]; then
|
if [ -n "$bootstrap" ]; then
|
||||||
body=$(jq -n --arg boot "$bootstrap" '{bootstrap: $boot}')
|
body=$(jq -n --arg boot "$bootstrap" '{bootstrap: $boot}')
|
||||||
fi
|
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"
|
api_request "POST" "/services/$service_id/redeploy" "$body"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -904,6 +935,9 @@ cmd_service() {
|
||||||
local target=""
|
local target=""
|
||||||
local name=""
|
local name=""
|
||||||
local ports=""
|
local ports=""
|
||||||
|
local bootstrap=""
|
||||||
|
local bootstrap_file=""
|
||||||
|
local -a files=()
|
||||||
|
|
||||||
while [ $# -gt 0 ]; do
|
while [ $# -gt 0 ]; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
|
|
@ -915,13 +949,47 @@ cmd_service() {
|
||||||
--lock) action="lock"; target="$2"; shift ;;
|
--lock) action="lock"; target="$2"; shift ;;
|
||||||
--unlock) action="unlock"; target="$2"; shift ;;
|
--unlock) action="unlock"; target="$2"; shift ;;
|
||||||
--logs) action="logs"; target="$2"; shift ;;
|
--logs) action="logs"; target="$2"; shift ;;
|
||||||
|
--redeploy) action="redeploy"; target="$2"; shift ;;
|
||||||
--name) name="$2"; shift ;;
|
--name) name="$2"; shift ;;
|
||||||
--ports) ports="$2"; shift ;;
|
--ports) ports="$2"; shift ;;
|
||||||
|
--bootstrap) bootstrap="$2"; shift ;;
|
||||||
|
--bootstrap-file) bootstrap_file="$2"; shift ;;
|
||||||
|
-f|--file) files+=("$2"); shift ;;
|
||||||
*) ;;
|
*) ;;
|
||||||
esac
|
esac
|
||||||
shift
|
shift
|
||||||
done
|
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
|
case "$action" in
|
||||||
list)
|
list)
|
||||||
local result
|
local result
|
||||||
|
|
@ -956,14 +1024,19 @@ cmd_service() {
|
||||||
result=$(service_logs "$target")
|
result=$(service_logs "$target")
|
||||||
echo "$result" | jq -r '.logs // empty'
|
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
|
if [ -n "$name" ]; then
|
||||||
local result
|
local result
|
||||||
result=$(service_create "$name" "$ports" "")
|
result=$(service_create "$name" "$ports" "$bootstrap" "$input_files_json")
|
||||||
echo -e "${GREEN}Service created${RESET}"
|
echo -e "${GREEN}Service created${RESET}"
|
||||||
echo "$result" | jq -r '"ID: \(.id)\nName: \(.name)"'
|
echo "$result" | jq -r '"ID: \(.id)\nName: \(.name)"'
|
||||||
else
|
else
|
||||||
echo "Usage: bash un.sh service --list|--info ID|--destroy ID|--name NAME" >&2
|
echo "Usage: bash un.sh service --list|--info ID|--destroy ID|--redeploy ID|--name NAME" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
|
|
@ -1140,8 +1213,12 @@ Service options:
|
||||||
--lock ID Lock service
|
--lock ID Lock service
|
||||||
--unlock ID Unlock service
|
--unlock ID Unlock service
|
||||||
--logs ID Get service logs
|
--logs ID Get service logs
|
||||||
|
--redeploy ID Re-run bootstrap (supports -f, --bootstrap)
|
||||||
--name NAME Create service with name
|
--name NAME Create service with name
|
||||||
--ports PORTS Service ports (comma-separated)
|
--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:
|
Snapshot options:
|
||||||
--list List all snapshots
|
--list List all snapshots
|
||||||
|
|
@ -1178,6 +1255,21 @@ if [ "${BASH_SOURCE[0]}" = "$0" ]; then
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
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
|
case "$1" in
|
||||||
languages)
|
languages)
|
||||||
shift
|
shift
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,20 @@
|
||||||
#!/bin/bash
|
#!/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
|
# Unit Tests for un.sh Library Functions
|
||||||
#
|
#
|
||||||
# Tests the ACTUAL exported functions from Un module.
|
# Tests the ACTUAL exported functions from Un module.
|
||||||
|
|
|
||||||
|
|
@ -108,6 +108,14 @@ test: build $(TEST_DIR)/test_library
|
||||||
|
|
||||||
test-library: test
|
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
|
test-functional: build $(TEST_DIR)/test_functional
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
@echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
|
|
||||||
|
|
@ -396,7 +396,14 @@ static UnsandboxCredentials* get_credentials(const char *cli_pk, const char *cli
|
||||||
return creds;
|
return creds;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Priority 2: Environment variables (keys)
|
// 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)
|
||||||
const char *env_pk = getenv("UNSANDBOX_PUBLIC_KEY");
|
const char *env_pk = getenv("UNSANDBOX_PUBLIC_KEY");
|
||||||
const char *env_sk = getenv("UNSANDBOX_SECRET_KEY");
|
const char *env_sk = getenv("UNSANDBOX_SECRET_KEY");
|
||||||
|
|
||||||
|
|
@ -416,16 +423,12 @@ static UnsandboxCredentials* get_credentials(const char *cli_pk, const char *cli
|
||||||
return creds;
|
return creds;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Priority 3: Config file (~/.unsandbox/accounts.csv)
|
// Priority 4: Config file (~/.unsandbox/accounts.csv)
|
||||||
// Use account_index from --account flag, or UNSANDBOX_ACCOUNT env var, or default to 0
|
// Use UNSANDBOX_ACCOUNT env var, or default to account 0
|
||||||
int csv_index = account_index;
|
int csv_index = 0;
|
||||||
if (csv_index < 0) {
|
const char *env_account = getenv("UNSANDBOX_ACCOUNT");
|
||||||
const char *env_account = getenv("UNSANDBOX_ACCOUNT");
|
if (env_account && strlen(env_account) > 0) {
|
||||||
if (env_account && strlen(env_account) > 0) {
|
csv_index = atoi(env_account);
|
||||||
csv_index = atoi(env_account);
|
|
||||||
} else {
|
|
||||||
csv_index = 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return load_credentials_from_csv(csv_index);
|
return load_credentials_from_csv(csv_index);
|
||||||
}
|
}
|
||||||
|
|
@ -4643,7 +4646,7 @@ static char* read_env_stdin(void) {
|
||||||
|
|
||||||
// Redeploy a service (re-run bootstrap script)
|
// Redeploy a service (re-run bootstrap script)
|
||||||
// Bootstrap scripts should be idempotent for proper upgrade behavior
|
// Bootstrap scripts should be idempotent for proper upgrade behavior
|
||||||
static int redeploy_service(const UnsandboxCredentials *creds, const char *service_id, const char *bootstrap) {
|
static int redeploy_service(const UnsandboxCredentials *creds, const char *service_id, const char *bootstrap, struct InputFile *input_files, int input_file_count) {
|
||||||
CURL *curl = curl_easy_init();
|
CURL *curl = curl_easy_init();
|
||||||
if (!curl) return 1;
|
if (!curl) return 1;
|
||||||
|
|
||||||
|
|
@ -4692,6 +4695,9 @@ static int redeploy_service(const UnsandboxCredentials *creds, const char *servi
|
||||||
} else if (bootstrap_url) {
|
} else if (bootstrap_url) {
|
||||||
payload_size += strlen(bootstrap_url) * 2 + 100;
|
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)
|
// Build JSON payload manually (matching create_service pattern)
|
||||||
char *payload = malloc(payload_size);
|
char *payload = malloc(payload_size);
|
||||||
|
|
@ -4704,15 +4710,31 @@ static int redeploy_service(const UnsandboxCredentials *creds, const char *servi
|
||||||
char *p = payload;
|
char *p = payload;
|
||||||
p += sprintf(p, "{");
|
p += sprintf(p, "{");
|
||||||
|
|
||||||
|
int has_field = 0;
|
||||||
if (bootstrap_content) {
|
if (bootstrap_content) {
|
||||||
char *esc_content = escape_json_string(bootstrap_content);
|
char *esc_content = escape_json_string(bootstrap_content);
|
||||||
p += sprintf(p, "\"bootstrap_content\":\"%s\"", esc_content);
|
p += sprintf(p, "\"bootstrap_content\":\"%s\"", esc_content);
|
||||||
free(esc_content);
|
free(esc_content);
|
||||||
free(bootstrap_content);
|
free(bootstrap_content);
|
||||||
|
has_field = 1;
|
||||||
} else if (bootstrap_url) {
|
} else if (bootstrap_url) {
|
||||||
char *esc_url = escape_json_string(bootstrap_url);
|
char *esc_url = escape_json_string(bootstrap_url);
|
||||||
p += sprintf(p, "\"bootstrap\":\"%s\"", esc_url);
|
p += sprintf(p, "\"bootstrap\":\"%s\"", esc_url);
|
||||||
free(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, "}");
|
p += sprintf(p, "}");
|
||||||
|
|
@ -7100,7 +7122,7 @@ int unsandbox_service_redeploy(const char *service_id, const char *bootstrap,
|
||||||
const char *public_key, const char *secret_key) {
|
const char *public_key, const char *secret_key) {
|
||||||
UnsandboxCredentials *creds = get_credentials(public_key, secret_key, -1);
|
UnsandboxCredentials *creds = get_credentials(public_key, secret_key, -1);
|
||||||
if (!creds) return -1;
|
if (!creds) return -1;
|
||||||
int result = redeploy_service(creds, service_id, bootstrap);
|
int result = redeploy_service(creds, service_id, bootstrap, NULL, 0);
|
||||||
free_credentials(creds);
|
free_credentials(creds);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
@ -10489,7 +10511,12 @@ int main(int argc, char *argv[]) {
|
||||||
// - If provided via --bootstrap or --bootstrap-file, use it
|
// - If provided via --bootstrap or --bootstrap-file, use it
|
||||||
// - If omitted, API will use the stored encrypted bootstrap
|
// - If omitted, API will use the stored encrypted bootstrap
|
||||||
const char *bootstrap_to_use = bootstrap_file ? bootstrap_file : service_bootstrap;
|
const char *bootstrap_to_use = bootstrap_file ? bootstrap_file : service_bootstrap;
|
||||||
ret = redeploy_service(creds, service_id, bootstrap_to_use);
|
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);
|
||||||
|
}
|
||||||
} else if (do_execute) {
|
} else if (do_execute) {
|
||||||
// Pass input files if provided (written to /tmp/input/ before command runs)
|
// 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);
|
ret = execute_service(creds, service_id, execute_command, execute_timeout, service_input_files, service_input_file_count);
|
||||||
|
|
|
||||||
111
clients/c/tests/test_account_flag.sh
Executable file
111
clients/c/tests/test_account_flag.sh
Executable file
|
|
@ -0,0 +1,111 @@
|
||||||
|
#!/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" <<CSV
|
||||||
|
unsb-pk-fake-0000-0000-0000,unsb-sk-fake0-00000-00000-00000
|
||||||
|
${REAL_PK},${REAL_SK}
|
||||||
|
CSV
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
echo "INTEGRATION: --account flag priority test"
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
|
||||||
|
# --- Test 1: --account 1 should use CSV row 1 (real creds), ignoring env vars ---
|
||||||
|
# Set env vars to GARBAGE so the test fails if env vars win.
|
||||||
|
# Auth success = no 401/unauthorized (429 rate-limit means creds passed auth, just hit concurrency cap).
|
||||||
|
OUT=$(HOME="$TMPHOME" \
|
||||||
|
UNSANDBOX_PUBLIC_KEY=unsb-pk-fake-0000-0000-0000 \
|
||||||
|
UNSANDBOX_SECRET_KEY=unsb-sk-fake0-00000-00000-00000 \
|
||||||
|
"$UN_BIN" --account 1 key 2>&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 ]
|
||||||
|
|
@ -118,17 +118,51 @@
|
||||||
(or (second (re-find pattern-str json-str))
|
(or (second (re-find pattern-str json-str))
|
||||||
(second (re-find pattern-num 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 []
|
(defn get-api-keys []
|
||||||
(let [public-key (System/getenv "UNSANDBOX_PUBLIC_KEY")
|
(let [public-key (System/getenv "UNSANDBOX_PUBLIC_KEY")
|
||||||
secret-key (System/getenv "UNSANDBOX_SECRET_KEY")
|
secret-key (System/getenv "UNSANDBOX_SECRET_KEY")
|
||||||
api-key (System/getenv "UNSANDBOX_API_KEY")]
|
api-key (System/getenv "UNSANDBOX_API_KEY")
|
||||||
|
home (System/getenv "HOME")
|
||||||
|
account-idx (or *account-index* 0)]
|
||||||
(cond
|
(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]
|
(and public-key secret-key) [public-key secret-key]
|
||||||
api-key [api-key nil]
|
api-key [api-key nil]
|
||||||
:else (do
|
;; 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
|
||||||
(binding [*out* *err*]
|
(binding [*out* *err*]
|
||||||
(println "Error: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set (or UNSANDBOX_API_KEY for backwards compat)"))
|
(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 []
|
(defn get-api-key []
|
||||||
(first (get-api-keys)))
|
(first (get-api-keys)))
|
||||||
|
|
@ -1216,4 +1250,14 @@
|
||||||
(recur (rest args) file env-vars artifacts out-dir network vcpu session-action session-id session-shell session-input-files
|
(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))))
|
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))))
|
||||||
|
|
||||||
(parse-args *command-line-args*)
|
(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)))
|
||||||
|
|
|
||||||
|
|
@ -45,15 +45,21 @@
|
||||||
SELECT SOURCE-FILE ASSIGN TO WS-FILENAME
|
SELECT SOURCE-FILE ASSIGN TO WS-FILENAME
|
||||||
ORGANIZATION IS LINE SEQUENTIAL
|
ORGANIZATION IS LINE SEQUENTIAL
|
||||||
FILE STATUS IS WS-FILE-STATUS.
|
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.
|
DATA DIVISION.
|
||||||
FILE SECTION.
|
FILE SECTION.
|
||||||
FD SOURCE-FILE.
|
FD SOURCE-FILE.
|
||||||
01 SOURCE-LINE PIC X(1024).
|
01 SOURCE-LINE PIC X(1024).
|
||||||
|
FD CRED-FILE.
|
||||||
|
01 CRED-LINE PIC X(512).
|
||||||
|
|
||||||
WORKING-STORAGE SECTION.
|
WORKING-STORAGE SECTION.
|
||||||
01 WS-FILENAME PIC X(256).
|
01 WS-FILENAME PIC X(256).
|
||||||
01 WS-FILE-STATUS PIC XX.
|
01 WS-FILE-STATUS PIC XX.
|
||||||
|
01 WS-CRED-STATUS PIC XX.
|
||||||
01 WS-API-KEY PIC X(256).
|
01 WS-API-KEY PIC X(256).
|
||||||
01 WS-PUBLIC-KEY PIC X(256).
|
01 WS-PUBLIC-KEY PIC X(256).
|
||||||
01 WS-SECRET-KEY PIC X(256).
|
01 WS-SECRET-KEY PIC X(256).
|
||||||
|
|
@ -97,12 +103,23 @@
|
||||||
01 WS-UOD-ENABLED PIC X(8).
|
01 WS-UOD-ENABLED PIC X(8).
|
||||||
01 WS-TYPE PIC X(32).
|
01 WS-TYPE PIC X(32).
|
||||||
01 WS-SHELL 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.
|
PROCEDURE DIVISION.
|
||||||
MAIN-PROCEDURE.
|
MAIN-PROCEDURE.
|
||||||
* Get command line argument (first argument)
|
* Get first command line argument
|
||||||
ACCEPT WS-ARG1 FROM COMMAND-LINE.
|
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
|
IF WS-ARG1 = SPACES
|
||||||
DISPLAY "Usage: un.cob <source_file>" UPON SYSERR
|
DISPLAY "Usage: un.cob <source_file>" UPON SYSERR
|
||||||
DISPLAY " un.cob session [options]" UPON SYSERR
|
DISPLAY " un.cob session [options]" UPON SYSERR
|
||||||
|
|
@ -147,7 +164,94 @@
|
||||||
PERFORM HANDLE-EXECUTE.
|
PERFORM HANDLE-EXECUTE.
|
||||||
STOP RUN.
|
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.
|
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
|
* Check if file exists
|
||||||
OPEN INPUT SOURCE-FILE.
|
OPEN INPUT SOURCE-FILE.
|
||||||
IF WS-FILE-STATUS NOT = "00"
|
IF WS-FILE-STATUS NOT = "00"
|
||||||
|
|
@ -168,25 +272,14 @@
|
||||||
STOP RUN
|
STOP RUN
|
||||||
END-IF.
|
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
|
* Use curl to make request
|
||||||
PERFORM MAKE-EXECUTE-REQUEST.
|
PERFORM MAKE-EXECUTE-REQUEST.
|
||||||
|
|
||||||
HANDLE-SESSION.
|
HANDLE-SESSION.
|
||||||
* Get API key
|
* Get credentials
|
||||||
ACCEPT WS-API-KEY FROM ENVIRONMENT "UNSANDBOX_API_KEY".
|
PERFORM GET-CREDENTIALS.
|
||||||
IF WS-API-KEY = SPACES
|
IF WS-API-KEY = SPACES
|
||||||
DISPLAY "Error: UNSANDBOX_API_KEY not set" UPON SYSERR
|
MOVE WS-PUBLIC-KEY TO WS-API-KEY
|
||||||
MOVE 1 TO RETURN-CODE
|
|
||||||
STOP RUN
|
|
||||||
END-IF.
|
END-IF.
|
||||||
|
|
||||||
* Initialize session parameters
|
* Initialize session parameters
|
||||||
|
|
@ -209,27 +302,8 @@
|
||||||
END-IF.
|
END-IF.
|
||||||
|
|
||||||
HANDLE-SERVICE.
|
HANDLE-SERVICE.
|
||||||
* Get API keys (try new format first, fall back to old)
|
* Get credentials
|
||||||
ACCEPT WS-PUBLIC-KEY FROM ENVIRONMENT "UNSANDBOX_PUBLIC_KEY".
|
PERFORM GET-CREDENTIALS.
|
||||||
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
|
* Initialize service parameters
|
||||||
MOVE SPACES TO WS-NAME.
|
MOVE SPACES TO WS-NAME.
|
||||||
|
|
@ -332,26 +406,7 @@
|
||||||
END-IF.
|
END-IF.
|
||||||
|
|
||||||
MAKE-EXECUTE-REQUEST.
|
MAKE-EXECUTE-REQUEST.
|
||||||
* Get public/secret keys with fallback
|
* Credentials already resolved by caller (GET-CREDENTIALS)
|
||||||
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
|
* Build curl command using shell with HMAC signature
|
||||||
STRING "TS=$(date +%s); "
|
STRING "TS=$(date +%s); "
|
||||||
|
|
@ -943,12 +998,10 @@
|
||||||
CALL "SYSTEM" USING WS-CURL-CMD.
|
CALL "SYSTEM" USING WS-CURL-CMD.
|
||||||
|
|
||||||
HANDLE-KEY.
|
HANDLE-KEY.
|
||||||
* Get API key
|
* Get credentials
|
||||||
ACCEPT WS-API-KEY FROM ENVIRONMENT "UNSANDBOX_API_KEY".
|
PERFORM GET-CREDENTIALS.
|
||||||
IF WS-API-KEY = SPACES
|
IF WS-API-KEY = SPACES
|
||||||
DISPLAY "Error: UNSANDBOX_API_KEY not set" UPON SYSERR
|
MOVE WS-PUBLIC-KEY TO WS-API-KEY
|
||||||
MOVE 1 TO RETURN-CODE
|
|
||||||
STOP RUN
|
|
||||||
END-IF.
|
END-IF.
|
||||||
|
|
||||||
* Parse key arguments
|
* Parse key arguments
|
||||||
|
|
@ -1124,27 +1177,8 @@
|
||||||
CALL "SYSTEM" USING WS-CURL-CMD.
|
CALL "SYSTEM" USING WS-CURL-CMD.
|
||||||
|
|
||||||
HANDLE-LANGUAGES.
|
HANDLE-LANGUAGES.
|
||||||
* Get API keys
|
* Get credentials
|
||||||
ACCEPT WS-PUBLIC-KEY FROM ENVIRONMENT "UNSANDBOX_PUBLIC_KEY".
|
PERFORM GET-CREDENTIALS.
|
||||||
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
|
* Parse --json flag
|
||||||
MOVE SPACES TO WS-JSON-OUTPUT.
|
MOVE SPACES TO WS-JSON-OUTPUT.
|
||||||
|
|
@ -1219,27 +1253,8 @@
|
||||||
CALL "SYSTEM" USING WS-CURL-CMD.
|
CALL "SYSTEM" USING WS-CURL-CMD.
|
||||||
|
|
||||||
HANDLE-IMAGE.
|
HANDLE-IMAGE.
|
||||||
* Get API keys (try new format first, fall back to old)
|
* Get credentials
|
||||||
ACCEPT WS-PUBLIC-KEY FROM ENVIRONMENT "UNSANDBOX_PUBLIC_KEY".
|
PERFORM GET-CREDENTIALS.
|
||||||
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
|
* Initialize image parameters
|
||||||
MOVE SPACES TO WS-ID.
|
MOVE SPACES TO WS-ID.
|
||||||
|
|
@ -1676,15 +1691,7 @@
|
||||||
|
|
||||||
HANDLE-SNAPSHOT.
|
HANDLE-SNAPSHOT.
|
||||||
* Get credentials
|
* Get credentials
|
||||||
ACCEPT WS-PUBLIC-KEY FROM ENVIRONMENT
|
PERFORM GET-CREDENTIALS.
|
||||||
"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)
|
* Get second argument (operation or --list)
|
||||||
ACCEPT WS-ARG2 FROM ARGUMENT-VALUE.
|
ACCEPT WS-ARG2 FROM ARGUMENT-VALUE.
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,20 @@
|
||||||
#!/bin/bash
|
#!/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
|
# Test suite for COBOL Unsandbox SDK
|
||||||
# Run: bash tests/test_un.sh
|
# Run: bash tests/test_un.sh
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -103,6 +103,27 @@ string read_file(const string& filename) {
|
||||||
return buf.str();
|
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<string,string> 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) {
|
string escape_json(const string& s) {
|
||||||
ostringstream o;
|
ostringstream o;
|
||||||
for (char c : s) {
|
for (char c : s) {
|
||||||
|
|
@ -705,9 +726,32 @@ string service_unlock(const string& service_id, const string& public_key, const
|
||||||
return exec_curl(cmd);
|
return exec_curl(cmd);
|
||||||
}
|
}
|
||||||
|
|
||||||
string service_redeploy(const string& service_id, const string& bootstrap, const string& public_key, const string& secret_key) {
|
string service_redeploy(const string& service_id, const string& bootstrap, const vector<string>& input_files, const string& public_key, const string& secret_key) {
|
||||||
string path = "/services/" + service_id + "/redeploy";
|
string path = "/services/" + service_id + "/redeploy";
|
||||||
string body = bootstrap.empty() ? "{}" : "{\"bootstrap\":\"" + escape_json(bootstrap) + "\"}";
|
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 auth_headers = build_auth_headers("POST", path, body, public_key, secret_key);
|
string auth_headers = build_auth_headers("POST", path, body, public_key, secret_key);
|
||||||
string cmd = "curl -s -X POST '" + API_BASE + path + "' "
|
string cmd = "curl -s -X POST '" + API_BASE + path + "' "
|
||||||
"-H 'Content-Type: application/json' "
|
"-H 'Content-Type: application/json' "
|
||||||
|
|
@ -1202,7 +1246,7 @@ void cmd_session(bool list, const string& kill, const string& shell, const strin
|
||||||
cout << exec_curl(cmd) << endl;
|
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<string>& 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<string>& 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<string>& 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<string>& 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
|
// Handle service env subcommand
|
||||||
if (!env_action.empty()) {
|
if (!env_action.empty()) {
|
||||||
cmd_service_env(env_action, env_target, envs, env_file, public_key, secret_key);
|
cmd_service_env(env_action, env_target, envs, env_file, public_key, secret_key);
|
||||||
|
|
@ -1328,6 +1372,62 @@ void cmd_service(const string& name, const string& ports, const string& type, co
|
||||||
return;
|
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()) {
|
if (!dump_bootstrap.empty()) {
|
||||||
cerr << "Fetching bootstrap script from " << dump_bootstrap << "..." << endl;
|
cerr << "Fetching bootstrap script from " << dump_bootstrap << "..." << endl;
|
||||||
string json_body = "{\"command\":\"cat /tmp/bootstrap.sh\"}";
|
string json_body = "{\"command\":\"cat /tmp/bootstrap.sh\"}";
|
||||||
|
|
@ -1702,12 +1802,62 @@ void cmd_validate_key(bool extend, const string& public_key, const string& secre
|
||||||
}
|
}
|
||||||
|
|
||||||
int main(int argc, char* argv[]) {
|
int main(int argc, char* argv[]) {
|
||||||
string public_key = getenv("UNSANDBOX_PUBLIC_KEY") ? getenv("UNSANDBOX_PUBLIC_KEY") : "";
|
string public_key;
|
||||||
string secret_key = getenv("UNSANDBOX_SECRET_KEY") ? getenv("UNSANDBOX_SECRET_KEY") : "";
|
string secret_key;
|
||||||
|
int account_index = -1; // -1 = not set
|
||||||
|
|
||||||
// Fall back to UNSANDBOX_API_KEY for backwards compatibility
|
// First pass: scan for --account N and -p/-k flags before full arg parsing
|
||||||
if (public_key.empty()) {
|
for (int i = 1; i < argc; i++) {
|
||||||
public_key = getenv("UNSANDBOX_API_KEY") ? getenv("UNSANDBOX_API_KEY") : "";
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (argc < 2) {
|
if (argc < 2) {
|
||||||
|
|
@ -1745,6 +1895,7 @@ int main(int argc, char* argv[]) {
|
||||||
else if (arg == "--name" && i+1 < argc) name = argv[++i];
|
else if (arg == "--name" && i+1 < argc) name = argv[++i];
|
||||||
else if (arg == "--ports" && i+1 < argc) ports = 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 == "-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);
|
cmd_image(list, info, del, lock, unlock, publish, source_type, visibility_id, visibility, spawn, clone, name, ports, public_key, secret_key);
|
||||||
|
|
@ -1769,6 +1920,7 @@ int main(int argc, char* argv[]) {
|
||||||
else if (arg == "--tmux") tmux = true;
|
else if (arg == "--tmux") tmux = true;
|
||||||
else if (arg == "--screen") screen = true;
|
else if (arg == "--screen") screen = true;
|
||||||
else if (arg == "-k" && i+1 < argc) public_key = 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_session(list, kill, shell, network, vcpu, tmux, screen, files, public_key, secret_key);
|
cmd_session(list, kill, shell, network, vcpu, tmux, screen, files, public_key, secret_key);
|
||||||
|
|
@ -1778,7 +1930,7 @@ int main(int argc, char* argv[]) {
|
||||||
if (cmd_type == "service") {
|
if (cmd_type == "service") {
|
||||||
string name, ports, type, bootstrap, bootstrap_file;
|
string name, ports, type, bootstrap, bootstrap_file;
|
||||||
bool list = false;
|
bool list = false;
|
||||||
string info, logs, tail, sleep, wake, destroy, resize, execute, command, dump_bootstrap, dump_file, network;
|
string info, logs, tail, sleep, wake, destroy, resize, execute, command, dump_bootstrap, dump_file, network, redeploy;
|
||||||
int vcpu = 0;
|
int vcpu = 0;
|
||||||
vector<string> files;
|
vector<string> files;
|
||||||
vector<string> envs;
|
vector<string> envs;
|
||||||
|
|
@ -1816,6 +1968,7 @@ int main(int argc, char* argv[]) {
|
||||||
else if (arg == "--resize" && i+1 < argc) resize = argv[++i];
|
else if (arg == "--resize" && i+1 < argc) resize = argv[++i];
|
||||||
else if (arg == "--execute" && i+1 < argc) execute = 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 == "--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-bootstrap" && i+1 < argc) dump_bootstrap = argv[++i];
|
||||||
else if (arg == "--dump-file" && i+1 < argc) dump_file = argv[++i];
|
else if (arg == "--dump-file" && i+1 < argc) dump_file = argv[++i];
|
||||||
else if (arg == "-n" && i+1 < argc) network = argv[++i];
|
else if (arg == "-n" && i+1 < argc) network = argv[++i];
|
||||||
|
|
@ -1830,9 +1983,10 @@ int main(int argc, char* argv[]) {
|
||||||
unfreeze_on_demand = (val == "true") ? 1 : 0;
|
unfreeze_on_demand = (val == "true") ? 1 : 0;
|
||||||
}
|
}
|
||||||
else if (arg == "-k" && i+1 < argc) public_key = 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_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);
|
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);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1843,6 +1997,7 @@ int main(int argc, char* argv[]) {
|
||||||
string arg = argv[i];
|
string arg = argv[i];
|
||||||
if (arg == "--extend") extend = true;
|
if (arg == "--extend") extend = true;
|
||||||
else if (arg == "-k" && i+1 < argc) public_key = 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_validate_key(extend, public_key, secret_key);
|
cmd_validate_key(extend, public_key, secret_key);
|
||||||
|
|
@ -1856,6 +2011,7 @@ int main(int argc, char* argv[]) {
|
||||||
string arg = argv[i];
|
string arg = argv[i];
|
||||||
if (arg == "--json") json_output = true;
|
if (arg == "--json") json_output = true;
|
||||||
else if (arg == "-k" && i+1 < argc) public_key = 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_languages(json_output, public_key, secret_key);
|
cmd_languages(json_output, public_key, secret_key);
|
||||||
|
|
@ -1876,6 +2032,7 @@ int main(int argc, char* argv[]) {
|
||||||
else if (arg == "-n" && i+1 < argc) network = argv[++i];
|
else if (arg == "-n" && i+1 < argc) network = argv[++i];
|
||||||
else if (arg == "-v" && i+1 < argc) vcpu = stoi(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 == "-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] == '-') {
|
else if (arg[0] == '-') {
|
||||||
cerr << RED << "Unknown option: " << arg << RESET << endl;
|
cerr << RED << "Unknown option: " << arg << RESET << endl;
|
||||||
return 1;
|
return 1;
|
||||||
|
|
|
||||||
|
|
@ -132,21 +132,72 @@ def save_languages_cache(response : JSON::Any)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def get_api_keys(args_key : String?) : {String, String?}
|
def load_accounts_csv(path : String, index : Int32) : {String, String}?
|
||||||
public_key = ENV["UNSANDBOX_PUBLIC_KEY"]?
|
return nil unless File.exists?(path)
|
||||||
secret_key = ENV["UNSANDBOX_SECRET_KEY"]?
|
begin
|
||||||
|
lines = File.read(path).split('\n').select do |l|
|
||||||
# Fall back to UNSANDBOX_API_KEY for backwards compatibility
|
t = l.strip
|
||||||
if public_key.nil? || public_key.empty? || secret_key.nil? || secret_key.empty?
|
!t.empty? && !t.starts_with?('#')
|
||||||
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
|
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}
|
return {legacy_key, nil}
|
||||||
end
|
end
|
||||||
|
|
||||||
{public_key, secret_key}
|
STDERR.puts "#{RED}Error: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set#{RESET}"
|
||||||
|
exit 1
|
||||||
end
|
end
|
||||||
|
|
||||||
def extract_challenge_id(response_body : String) : String?
|
def extract_challenge_id(response_body : String) : String?
|
||||||
|
|
@ -394,7 +445,7 @@ def build_env_content(envs : Array(String), env_file : String?) : String
|
||||||
end
|
end
|
||||||
|
|
||||||
def cmd_service_env(args)
|
def cmd_service_env(args)
|
||||||
public_key, secret_key = get_api_keys(args[:api_key]?.as?(String))
|
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))
|
||||||
|
|
||||||
action = args[:env_action]?.as?(String) || ""
|
action = args[:env_action]?.as?(String) || ""
|
||||||
target = args[:env_target]?.as?(String) || ""
|
target = args[:env_target]?.as?(String) || ""
|
||||||
|
|
@ -463,7 +514,7 @@ def cmd_service_env(args)
|
||||||
end
|
end
|
||||||
|
|
||||||
def cmd_execute(args)
|
def cmd_execute(args)
|
||||||
public_key, secret_key = get_api_keys(args[:api_key]?)
|
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))
|
||||||
|
|
||||||
filename = args[:source_file].as(String)
|
filename = args[:source_file].as(String)
|
||||||
unless File.exists?(filename)
|
unless File.exists?(filename)
|
||||||
|
|
@ -553,7 +604,7 @@ def cmd_execute(args)
|
||||||
end
|
end
|
||||||
|
|
||||||
def cmd_session(args)
|
def cmd_session(args)
|
||||||
public_key, secret_key = get_api_keys(args[:api_key]?)
|
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))
|
||||||
|
|
||||||
if args[:list]?.as?(Bool)
|
if args[:list]?.as?(Bool)
|
||||||
result = api_request("/sessions", public_key, secret_key)
|
result = api_request("/sessions", public_key, secret_key)
|
||||||
|
|
@ -661,7 +712,7 @@ def cmd_session(args)
|
||||||
end
|
end
|
||||||
|
|
||||||
def cmd_languages(args)
|
def cmd_languages(args)
|
||||||
public_key, secret_key = get_api_keys(args[:api_key]?)
|
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))
|
||||||
|
|
||||||
# Try to load from cache first
|
# Try to load from cache first
|
||||||
cached_response = load_languages_cache
|
cached_response = load_languages_cache
|
||||||
|
|
@ -694,7 +745,7 @@ def cmd_languages(args)
|
||||||
end
|
end
|
||||||
|
|
||||||
def cmd_key(args)
|
def cmd_key(args)
|
||||||
public_key, secret_key = get_api_keys(args[:api_key]?)
|
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))
|
||||||
|
|
||||||
# Validate key
|
# Validate key
|
||||||
url = URI.parse(PORTAL_BASE + "/keys/validate")
|
url = URI.parse(PORTAL_BASE + "/keys/validate")
|
||||||
|
|
@ -788,7 +839,7 @@ def cmd_key(args)
|
||||||
end
|
end
|
||||||
|
|
||||||
def cmd_image(args)
|
def cmd_image(args)
|
||||||
public_key, secret_key = get_api_keys(args[:api_key]?)
|
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))
|
||||||
|
|
||||||
if args[:list]?.as?(Bool)
|
if args[:list]?.as?(Bool)
|
||||||
result = api_request("/images", public_key, secret_key)
|
result = api_request("/images", public_key, secret_key)
|
||||||
|
|
@ -949,7 +1000,7 @@ def cmd_image(args)
|
||||||
end
|
end
|
||||||
|
|
||||||
def cmd_snapshot(args)
|
def cmd_snapshot(args)
|
||||||
public_key, secret_key = get_api_keys(args[:api_key]?)
|
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))
|
||||||
|
|
||||||
if args[:list]?.as?(Bool)
|
if args[:list]?.as?(Bool)
|
||||||
result = api_request("/snapshots", public_key, secret_key)
|
result = api_request("/snapshots", public_key, secret_key)
|
||||||
|
|
@ -1072,7 +1123,7 @@ def cmd_snapshot(args)
|
||||||
end
|
end
|
||||||
|
|
||||||
def cmd_logs(args)
|
def cmd_logs(args)
|
||||||
public_key, secret_key = get_api_keys(args[:api_key]?)
|
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))
|
||||||
|
|
||||||
source = args[:logs_source]?.as?(String) || "all"
|
source = args[:logs_source]?.as?(String) || "all"
|
||||||
lines = args[:logs_lines]?.as?(Int32) || 100
|
lines = args[:logs_lines]?.as?(Int32) || 100
|
||||||
|
|
@ -1187,7 +1238,7 @@ def cmd_service(args)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
public_key, secret_key = get_api_keys(args[:api_key]?)
|
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))
|
||||||
|
|
||||||
if args[:list]?.as?(Bool)
|
if args[:list]?.as?(Bool)
|
||||||
result = api_request("/services", public_key, secret_key)
|
result = api_request("/services", public_key, secret_key)
|
||||||
|
|
@ -1441,6 +1492,8 @@ def main
|
||||||
args = {
|
args = {
|
||||||
source_file: nil,
|
source_file: nil,
|
||||||
api_key: nil,
|
api_key: nil,
|
||||||
|
public_key: nil,
|
||||||
|
account: nil,
|
||||||
network: nil,
|
network: nil,
|
||||||
env: [] of String,
|
env: [] of String,
|
||||||
files: [] of String,
|
files: [] of String,
|
||||||
|
|
@ -1529,7 +1582,9 @@ def main
|
||||||
parser = OptionParser.new do |opts|
|
parser = OptionParser.new do |opts|
|
||||||
opts.banner = "Usage: un.cr [options] <source_file>\n un.cr languages [--json]\n un.cr session [options]\n un.cr service [options]\n un.cr service env <action> <service_id> [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 <id> Show vault status\n env set <id> Set vault (-e KEY=VALUE or --env-file FILE)\n env export <id> Export vault contents\n env delete <id> Delete vault"
|
opts.banner = "Usage: un.cr [options] <source_file>\n un.cr languages [--json]\n un.cr session [options]\n un.cr service [options]\n un.cr service env <action> <service_id> [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 <id> Show vault status\n env set <id> Set vault (-e KEY=VALUE or --env-file FILE)\n env export <id> Export vault contents\n env delete <id> Delete vault"
|
||||||
|
|
||||||
opts.on("-k API_KEY", "--api-key=API_KEY", "API key") { |k| args[:api_key] = k }
|
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("-n NETWORK", "--network=NETWORK", "Network mode") { |n| args[:network] = n }
|
opts.on("-n NETWORK", "--network=NETWORK", "Network mode") { |n| args[:network] = n }
|
||||||
opts.on("-e ENV", "--env=ENV", "Environment variable (KEY=VALUE)") { |e|
|
opts.on("-e ENV", "--env=ENV", "Environment variable (KEY=VALUE)") { |e|
|
||||||
args[:env].as(Array(String)) << e
|
args[:env].as(Array(String)) << e
|
||||||
|
|
|
||||||
|
|
@ -92,6 +92,10 @@ class Un
|
||||||
{
|
{
|
||||||
CmdKey(parsedArgs);
|
CmdKey(parsedArgs);
|
||||||
}
|
}
|
||||||
|
else if (parsedArgs.Command == "languages")
|
||||||
|
{
|
||||||
|
CmdLanguages(parsedArgs);
|
||||||
|
}
|
||||||
else if (parsedArgs.SourceFile != null)
|
else if (parsedArgs.SourceFile != null)
|
||||||
{
|
{
|
||||||
CmdExecute(parsedArgs);
|
CmdExecute(parsedArgs);
|
||||||
|
|
@ -111,7 +115,7 @@ class Un
|
||||||
|
|
||||||
static void CmdExecute(Args args)
|
static void CmdExecute(Args args)
|
||||||
{
|
{
|
||||||
var (publicKey, secretKey) = GetApiKeys(args.ApiKey);
|
var (publicKey, secretKey) = GetApiKeys(args.ApiKey, args.Account);
|
||||||
string code = File.ReadAllText(args.SourceFile);
|
string code = File.ReadAllText(args.SourceFile);
|
||||||
string language = DetectLanguage(args.SourceFile);
|
string language = DetectLanguage(args.SourceFile);
|
||||||
|
|
||||||
|
|
@ -198,7 +202,7 @@ class Un
|
||||||
|
|
||||||
static void CmdSession(Args args)
|
static void CmdSession(Args args)
|
||||||
{
|
{
|
||||||
var (publicKey, secretKey) = GetApiKeys(args.ApiKey);
|
var (publicKey, secretKey) = GetApiKeys(args.ApiKey, args.Account);
|
||||||
|
|
||||||
if (args.SessionList)
|
if (args.SessionList)
|
||||||
{
|
{
|
||||||
|
|
@ -251,7 +255,7 @@ class Un
|
||||||
|
|
||||||
static void CmdKey(Args args)
|
static void CmdKey(Args args)
|
||||||
{
|
{
|
||||||
var (publicKey, secretKey) = GetApiKeys(args.ApiKey);
|
var (publicKey, secretKey) = GetApiKeys(args.ApiKey, args.Account);
|
||||||
|
|
||||||
var result = ApiRequest("/keys/validate", "POST", null, publicKey, secretKey);
|
var result = ApiRequest("/keys/validate", "POST", null, publicKey, secretKey);
|
||||||
|
|
||||||
|
|
@ -336,7 +340,7 @@ class Un
|
||||||
|
|
||||||
static void CmdService(Args args)
|
static void CmdService(Args args)
|
||||||
{
|
{
|
||||||
var (publicKey, secretKey) = GetApiKeys(args.ApiKey);
|
var (publicKey, secretKey) = GetApiKeys(args.ApiKey, args.Account);
|
||||||
|
|
||||||
// Handle env subcommand
|
// Handle env subcommand
|
||||||
if (!string.IsNullOrEmpty(args.EnvAction))
|
if (!string.IsNullOrEmpty(args.EnvAction))
|
||||||
|
|
@ -438,6 +442,32 @@ class Un
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (args.ServiceRedeploy != null)
|
||||||
|
{
|
||||||
|
var payload = new Dictionary<string, object>();
|
||||||
|
if (args.ServiceBootstrap != null)
|
||||||
|
{
|
||||||
|
payload["bootstrap"] = args.ServiceBootstrap;
|
||||||
|
}
|
||||||
|
if (args.Files.Count > 0)
|
||||||
|
{
|
||||||
|
var inputFiles = new List<Dictionary<string, string>>();
|
||||||
|
foreach (var filepath in args.Files)
|
||||||
|
{
|
||||||
|
var content = File.ReadAllBytes(filepath);
|
||||||
|
inputFiles.Add(new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
["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)
|
if (args.ServiceExecute != null)
|
||||||
{
|
{
|
||||||
var payload = new Dictionary<string, object>
|
var payload = new Dictionary<string, object>
|
||||||
|
|
@ -529,6 +559,20 @@ class Un
|
||||||
{
|
{
|
||||||
payload["unfreeze_on_demand"] = true;
|
payload["unfreeze_on_demand"] = true;
|
||||||
}
|
}
|
||||||
|
if (args.Files.Count > 0)
|
||||||
|
{
|
||||||
|
var inputFiles = new List<Dictionary<string, string>>();
|
||||||
|
foreach (var filepath in args.Files)
|
||||||
|
{
|
||||||
|
var content = File.ReadAllBytes(filepath);
|
||||||
|
inputFiles.Add(new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
["filename"] = Path.GetFileName(filepath),
|
||||||
|
["content"] = Convert.ToBase64String(content)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
payload["input_files"] = inputFiles;
|
||||||
|
}
|
||||||
|
|
||||||
var result = ApiRequest("/services", "POST", payload, publicKey, secretKey);
|
var result = ApiRequest("/services", "POST", payload, publicKey, secretKey);
|
||||||
string serviceId = result.ContainsKey("id") ? (string)result["id"] : null;
|
string serviceId = result.ContainsKey("id") ? (string)result["id"] : null;
|
||||||
|
|
@ -562,24 +606,77 @@ class Un
|
||||||
Environment.Exit(1);
|
Environment.Exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
static (string, string) GetApiKeys(string argsKey)
|
static (string, string) LoadAccountsCSV(string path, int index)
|
||||||
{
|
{
|
||||||
string publicKey = Environment.GetEnvironmentVariable("UNSANDBOX_PUBLIC_KEY");
|
if (!File.Exists(path)) return (null, null);
|
||||||
string secretKey = Environment.GetEnvironmentVariable("UNSANDBOX_SECRET_KEY");
|
int row = 0;
|
||||||
|
foreach (string rawLine in File.ReadAllLines(path))
|
||||||
// Fall back to UNSANDBOX_API_KEY for backwards compatibility
|
|
||||||
if (string.IsNullOrEmpty(publicKey) || string.IsNullOrEmpty(secretKey))
|
|
||||||
{
|
{
|
||||||
string legacyKey = argsKey ?? Environment.GetEnvironmentVariable("UNSANDBOX_API_KEY");
|
string line = rawLine.Trim();
|
||||||
if (string.IsNullOrEmpty(legacyKey))
|
if (line.Length == 0 || line.StartsWith("#")) continue;
|
||||||
|
if (row == index)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine($"{RED}Error: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set{RESET}");
|
string[] parts = line.Split(',');
|
||||||
Environment.Exit(1);
|
if (parts.Length >= 2)
|
||||||
|
return (parts[0].Trim(), parts[1].Trim());
|
||||||
}
|
}
|
||||||
return (legacyKey, null);
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (publicKey, secretKey);
|
// 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))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"{RED}Error: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set{RESET}");
|
||||||
|
Environment.Exit(1);
|
||||||
|
}
|
||||||
|
return (legacyKey, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
static string DetectLanguage(string filename)
|
static string DetectLanguage(string filename)
|
||||||
|
|
@ -1234,10 +1331,13 @@ class Un
|
||||||
public string ServiceShowFreezePage = null;
|
public string ServiceShowFreezePage = null;
|
||||||
public bool ServiceShowFreezePageEnabled = true;
|
public bool ServiceShowFreezePageEnabled = true;
|
||||||
public bool ServiceCreateUnfreezeOnDemand = false;
|
public bool ServiceCreateUnfreezeOnDemand = false;
|
||||||
|
public string ServiceRedeploy = null;
|
||||||
public string EnvFile = null;
|
public string EnvFile = null;
|
||||||
public string EnvAction = null;
|
public string EnvAction = null;
|
||||||
public string EnvTarget = null;
|
public string EnvTarget = null;
|
||||||
public bool KeyExtend = false;
|
public bool KeyExtend = false;
|
||||||
|
public bool LanguagesJson = false;
|
||||||
|
public int Account = -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
static Args ParseArgs(string[] args)
|
static Args ParseArgs(string[] args)
|
||||||
|
|
@ -1249,6 +1349,7 @@ class Un
|
||||||
if (arg == "session") result.Command = "session";
|
if (arg == "session") result.Command = "session";
|
||||||
else if (arg == "service") result.Command = "service";
|
else if (arg == "service") result.Command = "service";
|
||||||
else if (arg == "key") result.Command = "key";
|
else if (arg == "key") result.Command = "key";
|
||||||
|
else if (arg == "languages") result.Command = "languages";
|
||||||
else if (arg == "env" && result.Command == "service")
|
else if (arg == "env" && result.Command == "service")
|
||||||
{
|
{
|
||||||
// Parse: service env <action> <target>
|
// Parse: service env <action> <target>
|
||||||
|
|
@ -1295,12 +1396,99 @@ class Un
|
||||||
else if (arg == "--show-freeze-page") result.ServiceShowFreezePage = args[++i];
|
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 == "--show-freeze-page-enabled") result.ServiceShowFreezePageEnabled = args[++i].ToLower() == "true";
|
||||||
else if (arg == "--with-unfreeze-on-demand") result.ServiceCreateUnfreezeOnDemand = 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 == "--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;
|
else if (!arg.StartsWith("-")) result.SourceFile = arg;
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static string GetLanguagesCachePath()
|
||||||
|
{
|
||||||
|
string home = Environment.GetEnvironmentVariable("HOME")
|
||||||
|
?? Environment.GetEnvironmentVariable("USERPROFILE")
|
||||||
|
?? ".";
|
||||||
|
return Path.Combine(home, ".unsandbox", "languages.json");
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<string> 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<object> langs)
|
||||||
|
return langs.ConvertAll(x => x.ToString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void SaveLanguagesCache(List<string> 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<string>();
|
||||||
|
if (result.ContainsKey("languages") && result["languages"] is List<object> 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()
|
static void PrintHelp()
|
||||||
{
|
{
|
||||||
Console.WriteLine(@"Usage: Un [options] <source_file>
|
Console.WriteLine(@"Usage: Un [options] <source_file>
|
||||||
|
|
@ -1308,6 +1496,7 @@ class Un
|
||||||
Un service [options]
|
Un service [options]
|
||||||
Un service env <action> <service_id> [options]
|
Un service env <action> <service_id> [options]
|
||||||
Un key [options]
|
Un key [options]
|
||||||
|
Un languages [--json]
|
||||||
|
|
||||||
Execute options:
|
Execute options:
|
||||||
-e KEY=VALUE Set environment variable
|
-e KEY=VALUE Set environment variable
|
||||||
|
|
@ -1340,6 +1529,7 @@ Service options:
|
||||||
--show-freeze-page-enabled BOOL Enable/disable (default: true)
|
--show-freeze-page-enabled BOOL Enable/disable (default: true)
|
||||||
--with-unfreeze-on-demand Enable unfreeze-on-demand when creating service
|
--with-unfreeze-on-demand Enable unfreeze-on-demand when creating service
|
||||||
--destroy ID Destroy service
|
--destroy ID Destroy service
|
||||||
|
--redeploy ID Re-run bootstrap (with optional --bootstrap, -f)
|
||||||
--execute ID Execute command in service
|
--execute ID Execute command in service
|
||||||
--command CMD Command to execute (with --execute)
|
--command CMD Command to execute (with --execute)
|
||||||
--dump-bootstrap ID Dump bootstrap script
|
--dump-bootstrap ID Dump bootstrap script
|
||||||
|
|
@ -1354,7 +1544,10 @@ Service env commands:
|
||||||
env delete ID Delete vault
|
env delete ID Delete vault
|
||||||
|
|
||||||
Key options:
|
Key options:
|
||||||
--extend Open browser to extend expired key");
|
--extend Open browser to extend expired key
|
||||||
|
|
||||||
|
Languages options:
|
||||||
|
--json Output as JSON array");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1486,15 +1679,76 @@ public static class Unsandbox
|
||||||
catch (Exception ex) { _lastError = ex.Message; return new List<JobInfo>(); }
|
catch (Exception ex) { _lastError = ex.Message; return new List<JobInfo>(); }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Get available programming languages</summary>
|
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<string> 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<object> langs)
|
||||||
|
return langs.ConvertAll(x => x.ToString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void SaveLanguagesCache(List<string> 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 { }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Get available programming languages (cached for 1 hour)</summary>
|
||||||
public static List<string> GetLanguages(string publicKey = null, string secretKey = null)
|
public static List<string> GetLanguages(string publicKey = null, string secretKey = null)
|
||||||
{
|
{
|
||||||
|
// Try cache first
|
||||||
|
var cached = LoadLanguagesCache();
|
||||||
|
if (cached != null) return cached;
|
||||||
|
|
||||||
var (pk, sk) = ResolveKeys(publicKey, secretKey);
|
var (pk, sk) = ResolveKeys(publicKey, secretKey);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var result = ApiCall("/languages", "GET", null, pk, sk);
|
var result = ApiCall("/languages", "GET", null, pk, sk);
|
||||||
if (result.ContainsKey("languages") && result["languages"] is List<object> langs)
|
if (result.ContainsKey("languages") && result["languages"] is List<object> langs)
|
||||||
return langs.ConvertAll(x => x.ToString());
|
{
|
||||||
|
var languages = langs.ConvertAll(x => x.ToString());
|
||||||
|
SaveLanguagesCache(languages);
|
||||||
|
return languages;
|
||||||
|
}
|
||||||
return new List<string>();
|
return new List<string>();
|
||||||
}
|
}
|
||||||
catch (Exception ex) { _lastError = ex.Message; return new List<string>(); }
|
catch (Exception ex) { _lastError = ex.Message; return new List<string>(); }
|
||||||
|
|
@ -1632,7 +1886,7 @@ public static class Unsandbox
|
||||||
catch (Exception ex) { _lastError = ex.Message; return null; }
|
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, string publicKey = null, string secretKey = null)
|
public static string ServiceCreate(string name, string ports = null, string domains = null, string bootstrap = null, string networkMode = null, List<Dictionary<string, string>> inputFiles = null, string publicKey = null, string secretKey = null)
|
||||||
{
|
{
|
||||||
var (pk, sk) = ResolveKeys(publicKey, secretKey);
|
var (pk, sk) = ResolveKeys(publicKey, secretKey);
|
||||||
var payload = new Dictionary<string, object> { ["name"] = name };
|
var payload = new Dictionary<string, object> { ["name"] = name };
|
||||||
|
|
@ -1645,6 +1899,7 @@ public static class Unsandbox
|
||||||
if (domains != null) payload["domains"] = domains;
|
if (domains != null) payload["domains"] = domains;
|
||||||
if (bootstrap != null) payload["bootstrap"] = bootstrap;
|
if (bootstrap != null) payload["bootstrap"] = bootstrap;
|
||||||
if (networkMode != null) payload["network"] = networkMode;
|
if (networkMode != null) payload["network"] = networkMode;
|
||||||
|
if (inputFiles != null && inputFiles.Count > 0) payload["input_files"] = inputFiles;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var result = ApiCall("/services", "POST", payload, pk, sk);
|
var result = ApiCall("/services", "POST", payload, pk, sk);
|
||||||
|
|
@ -1696,10 +1951,16 @@ public static class Unsandbox
|
||||||
catch (Exception ex) { _lastError = ex.Message; return false; }
|
catch (Exception ex) { _lastError = ex.Message; return false; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool ServiceRedeploy(string serviceId, string bootstrap = null, string publicKey = null, string secretKey = null)
|
public static bool ServiceRedeploy(string serviceId, string bootstrap = null, List<Dictionary<string, string>> inputFiles = null, string publicKey = null, string secretKey = null)
|
||||||
{
|
{
|
||||||
var (pk, sk) = ResolveKeys(publicKey, secretKey);
|
var (pk, sk) = ResolveKeys(publicKey, secretKey);
|
||||||
var payload = bootstrap != null ? new Dictionary<string, object> { ["bootstrap"] = bootstrap } : null;
|
Dictionary<string, object> payload = null;
|
||||||
|
if (bootstrap != null || (inputFiles != null && inputFiles.Count > 0))
|
||||||
|
{
|
||||||
|
payload = new Dictionary<string, object>();
|
||||||
|
if (bootstrap != null) payload["bootstrap"] = bootstrap;
|
||||||
|
if (inputFiles != null && inputFiles.Count > 0) payload["input_files"] = inputFiles;
|
||||||
|
}
|
||||||
try { ApiCall($"/services/{serviceId}/redeploy", "POST", payload, pk, sk); return true; }
|
try { ApiCall($"/services/{serviceId}/redeploy", "POST", payload, pk, sk); return true; }
|
||||||
catch (Exception ex) { _lastError = ex.Message; return false; }
|
catch (Exception ex) { _lastError = ex.Message; return false; }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,7 @@ import std.string;
|
||||||
import std.conv;
|
import std.conv;
|
||||||
import std.array;
|
import std.array;
|
||||||
import std.algorithm;
|
import std.algorithm;
|
||||||
|
import std.typecons;
|
||||||
|
|
||||||
immutable string API_BASE = "https://api.unsandbox.com";
|
immutable string API_BASE = "https://api.unsandbox.com";
|
||||||
immutable string PORTAL_BASE = "https://unsandbox.com";
|
immutable string PORTAL_BASE = "https://unsandbox.com";
|
||||||
|
|
@ -1555,13 +1556,82 @@ void validateKey(string publicKey, string secretKey, bool extend) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
int main(string[] args) {
|
// Load a row from an accounts.csv file (format: public_key,secret_key per line).
|
||||||
string publicKey = environment.get("UNSANDBOX_PUBLIC_KEY", "");
|
// Lines starting with '#' and blank lines are skipped. Returns the Nth data row.
|
||||||
string secretKey = environment.get("UNSANDBOX_SECRET_KEY", "");
|
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("", "");
|
||||||
|
}
|
||||||
|
|
||||||
// Fall back to UNSANDBOX_API_KEY for backwards compatibility
|
int main(string[] args) {
|
||||||
if (publicKey.empty) {
|
string publicKey;
|
||||||
publicKey = environment.get("UNSANDBOX_API_KEY", "");
|
string secretKey;
|
||||||
|
int accountIndex = -1; // -1 = not set
|
||||||
|
string explicitPublicKey;
|
||||||
|
|
||||||
|
// 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];
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (args.length < 2) {
|
if (args.length < 2) {
|
||||||
|
|
@ -1598,6 +1668,7 @@ int main(string[] args) {
|
||||||
else if (args[i] == "--screen") screen = true;
|
else if (args[i] == "--screen") screen = true;
|
||||||
else if (args[i] == "-f" && i+1 < args.length) inputFiles ~= args[++i];
|
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] == "-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);
|
cmdSession(list, kill, shell, network, vcpu, tmux, screen, inputFiles, publicKey, secretKey);
|
||||||
|
|
@ -1625,6 +1696,7 @@ int main(string[] args) {
|
||||||
if (args[i] == "-e" && i+1 < args.length) svcEnvs ~= args[++i];
|
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] == "--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] == "-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);
|
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;
|
return 0;
|
||||||
|
|
@ -1658,6 +1730,7 @@ int main(string[] args) {
|
||||||
else if (args[i] == "-e" && i+1 < args.length) svcEnvs ~= args[++i];
|
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] == "--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] == "-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);
|
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);
|
||||||
|
|
@ -1670,6 +1743,7 @@ int main(string[] args) {
|
||||||
for (size_t i = 2; i < args.length; i++) {
|
for (size_t i = 2; i < args.length; i++) {
|
||||||
if (args[i] == "--extend") extend = true;
|
if (args[i] == "--extend") extend = true;
|
||||||
else if (args[i] == "-k" && i+1 < args.length) publicKey = 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) {
|
if (publicKey.empty) {
|
||||||
|
|
@ -1687,6 +1761,7 @@ int main(string[] args) {
|
||||||
for (size_t i = 2; i < args.length; i++) {
|
for (size_t i = 2; i < args.length; i++) {
|
||||||
if (args[i] == "--json") jsonOutput = true;
|
if (args[i] == "--json") jsonOutput = true;
|
||||||
else if (args[i] == "-k" && i+1 < args.length) publicKey = 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) {
|
if (publicKey.empty) {
|
||||||
|
|
@ -1720,6 +1795,7 @@ int main(string[] args) {
|
||||||
else if (args[i] == "--name" && i+1 < args.length) name = args[++i];
|
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] == "--ports" && i+1 < args.length) ports = args[++i];
|
||||||
else if (args[i] == "-k" && i+1 < args.length) publicKey = 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) {
|
if (publicKey.empty) {
|
||||||
|
|
@ -1743,6 +1819,7 @@ int main(string[] args) {
|
||||||
else if (args[i] == "-n" && i+1 < args.length) network = args[++i];
|
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] == "-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] == "-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("-")) {
|
else if (args[i].startsWith("-")) {
|
||||||
stderr.writefln("%sUnknown option: %s%s", RED, args[i], RESET);
|
stderr.writefln("%sUnknown option: %s%s", RED, args[i], RESET);
|
||||||
return 1;
|
return 1;
|
||||||
|
|
|
||||||
|
|
@ -73,6 +73,8 @@ class Args {
|
||||||
String? command;
|
String? command;
|
||||||
String? sourceFile;
|
String? sourceFile;
|
||||||
String? apiKey;
|
String? apiKey;
|
||||||
|
String? publicKey;
|
||||||
|
int? account;
|
||||||
String? network;
|
String? network;
|
||||||
int vcpu = 0;
|
int vcpu = 0;
|
||||||
List<String> env = [];
|
List<String> env = [];
|
||||||
|
|
@ -141,21 +143,73 @@ class Args {
|
||||||
bool snapshotHot = false;
|
bool snapshotHot = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<String?> getApiKeys(String? argsKey) {
|
Map<String, String>? loadAccountsCSV(String path, int index) {
|
||||||
final publicKey = Platform.environment['UNSANDBOX_PUBLIC_KEY'];
|
try {
|
||||||
final secretKey = Platform.environment['UNSANDBOX_SECRET_KEY'];
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Fall back to UNSANDBOX_API_KEY for backwards compatibility
|
List<String?> getApiKeys(String? argsKey, {String? argsPublicKey, int? account}) {
|
||||||
if (publicKey == null || publicKey.isEmpty || secretKey == null || secretKey.isEmpty) {
|
// Tier 1: explicit -p/-k flags
|
||||||
final legacyKey = argsKey ?? Platform.environment['UNSANDBOX_API_KEY'];
|
if (argsPublicKey != null && argsPublicKey.isNotEmpty && argsKey != null && argsKey.isNotEmpty) {
|
||||||
if (legacyKey == null || legacyKey.isEmpty) {
|
return [argsPublicKey, argsKey];
|
||||||
stderr.writeln('${red}Error: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set$reset');
|
}
|
||||||
exit(1);
|
|
||||||
|
// 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']];
|
||||||
}
|
}
|
||||||
|
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];
|
return [legacyKey, null];
|
||||||
}
|
}
|
||||||
|
|
||||||
return [publicKey, secretKey];
|
stderr.writeln('${red}Error: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set$reset');
|
||||||
|
exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
String detectLanguage(String filename) {
|
String detectLanguage(String filename) {
|
||||||
|
|
@ -509,7 +563,7 @@ Future<bool> serviceEnvDelete(String serviceId, String publicKey, String? secret
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> cmdServiceEnv(Args args) async {
|
Future<void> cmdServiceEnv(Args args) async {
|
||||||
final keys = getApiKeys(args.apiKey);
|
final keys = getApiKeys(args.apiKey, argsPublicKey: args.publicKey, account: args.account);
|
||||||
final publicKey = keys[0]!;
|
final publicKey = keys[0]!;
|
||||||
final secretKey = keys[1];
|
final secretKey = keys[1];
|
||||||
final action = args.envAction;
|
final action = args.envAction;
|
||||||
|
|
@ -579,7 +633,7 @@ Future<void> cmdServiceEnv(Args args) async {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> cmdExecute(Args args) async {
|
Future<void> cmdExecute(Args args) async {
|
||||||
final keys = getApiKeys(args.apiKey);
|
final keys = getApiKeys(args.apiKey, argsPublicKey: args.publicKey, account: args.account);
|
||||||
final publicKey = keys[0]!;
|
final publicKey = keys[0]!;
|
||||||
final secretKey = keys[1];
|
final secretKey = keys[1];
|
||||||
final code = await File(args.sourceFile!).readAsString();
|
final code = await File(args.sourceFile!).readAsString();
|
||||||
|
|
@ -657,7 +711,7 @@ Future<void> cmdExecute(Args args) async {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> cmdSession(Args args) async {
|
Future<void> cmdSession(Args args) async {
|
||||||
final keys = getApiKeys(args.apiKey);
|
final keys = getApiKeys(args.apiKey, argsPublicKey: args.publicKey, account: args.account);
|
||||||
final publicKey = keys[0]!;
|
final publicKey = keys[0]!;
|
||||||
final secretKey = keys[1];
|
final secretKey = keys[1];
|
||||||
|
|
||||||
|
|
@ -717,7 +771,7 @@ Future<void> cmdSession(Args args) async {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> cmdService(Args args) async {
|
Future<void> cmdService(Args args) async {
|
||||||
final keys = getApiKeys(args.apiKey);
|
final keys = getApiKeys(args.apiKey, argsPublicKey: args.publicKey, account: args.account);
|
||||||
final publicKey = keys[0]!;
|
final publicKey = keys[0]!;
|
||||||
final secretKey = keys[1];
|
final secretKey = keys[1];
|
||||||
|
|
||||||
|
|
@ -941,7 +995,7 @@ Future<void> cmdService(Args args) async {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> cmdLanguages(Args args) async {
|
Future<void> cmdLanguages(Args args) async {
|
||||||
final keys = getApiKeys(args.apiKey);
|
final keys = getApiKeys(args.apiKey, argsPublicKey: args.publicKey, account: args.account);
|
||||||
final publicKey = keys[0]!;
|
final publicKey = keys[0]!;
|
||||||
final secretKey = keys[1];
|
final secretKey = keys[1];
|
||||||
|
|
||||||
|
|
@ -973,7 +1027,7 @@ Future<void> cmdLanguages(Args args) async {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> cmdImage(Args args) async {
|
Future<void> cmdImage(Args args) async {
|
||||||
final keys = getApiKeys(args.apiKey);
|
final keys = getApiKeys(args.apiKey, argsPublicKey: args.publicKey, account: args.account);
|
||||||
final publicKey = keys[0]!;
|
final publicKey = keys[0]!;
|
||||||
final secretKey = keys[1];
|
final secretKey = keys[1];
|
||||||
|
|
||||||
|
|
@ -1115,7 +1169,7 @@ Future<void> imageTransfer(String id, String toKey, String publicKey, String? se
|
||||||
|
|
||||||
// Snapshot functions
|
// Snapshot functions
|
||||||
Future<void> cmdSnapshot(Args args) async {
|
Future<void> cmdSnapshot(Args args) async {
|
||||||
final keys = getApiKeys(args.apiKey);
|
final keys = getApiKeys(args.apiKey, argsPublicKey: args.publicKey, account: args.account);
|
||||||
final publicKey = keys[0]!;
|
final publicKey = keys[0]!;
|
||||||
final secretKey = keys[1];
|
final secretKey = keys[1];
|
||||||
|
|
||||||
|
|
@ -1319,7 +1373,7 @@ String sdkVersion() {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> cmdKey(Args args) async {
|
Future<void> cmdKey(Args args) async {
|
||||||
final keys = getApiKeys(args.apiKey);
|
final keys = getApiKeys(args.apiKey, argsPublicKey: args.publicKey, account: args.account);
|
||||||
final publicKey = keys[0]!;
|
final publicKey = keys[0]!;
|
||||||
final secretKey = keys[1];
|
final secretKey = keys[1];
|
||||||
|
|
||||||
|
|
@ -1408,6 +1462,13 @@ Args parseArgs(List<String> argv) {
|
||||||
case '--api-key':
|
case '--api-key':
|
||||||
args.apiKey = argv[++i];
|
args.apiKey = argv[++i];
|
||||||
break;
|
break;
|
||||||
|
case '-p':
|
||||||
|
case '--public-key':
|
||||||
|
args.publicKey = argv[++i];
|
||||||
|
break;
|
||||||
|
case '--account':
|
||||||
|
args.account = int.parse(argv[++i]);
|
||||||
|
break;
|
||||||
case '-n':
|
case '-n':
|
||||||
case '--network':
|
case '--network':
|
||||||
args.network = argv[++i];
|
args.network = argv[++i];
|
||||||
|
|
@ -1658,7 +1719,9 @@ Execute options:
|
||||||
-o DIR Output directory for artifacts
|
-o DIR Output directory for artifacts
|
||||||
-n MODE Network mode (zerotrust/semitrusted)
|
-n MODE Network mode (zerotrust/semitrusted)
|
||||||
-v N vCPU count (1-8)
|
-v N vCPU count (1-8)
|
||||||
-k KEY API key
|
-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)
|
||||||
|
|
||||||
Session options:
|
Session options:
|
||||||
--list List active sessions
|
--list List active sessions
|
||||||
|
|
|
||||||
|
|
@ -87,7 +87,7 @@ catch (Exception ex)
|
||||||
|
|
||||||
void CmdExecute(Args args)
|
void CmdExecute(Args args)
|
||||||
{
|
{
|
||||||
var (publicKey, secretKey) = GetApiKeys(args.ApiKey);
|
var (publicKey, secretKey) = GetApiKeys(args.ApiKey, args.Account);
|
||||||
var code = File.ReadAllText(args.SourceFile!);
|
var code = File.ReadAllText(args.SourceFile!);
|
||||||
var language = DetectLanguage(args.SourceFile!);
|
var language = DetectLanguage(args.SourceFile!);
|
||||||
|
|
||||||
|
|
@ -148,7 +148,7 @@ void CmdExecute(Args args)
|
||||||
|
|
||||||
void CmdSession(Args args)
|
void CmdSession(Args args)
|
||||||
{
|
{
|
||||||
var (publicKey, secretKey) = GetApiKeys(args.ApiKey);
|
var (publicKey, secretKey) = GetApiKeys(args.ApiKey, args.Account);
|
||||||
|
|
||||||
if (args.SessionList)
|
if (args.SessionList)
|
||||||
{
|
{
|
||||||
|
|
@ -224,7 +224,7 @@ void CmdSession(Args args)
|
||||||
|
|
||||||
void CmdKey(Args args)
|
void CmdKey(Args args)
|
||||||
{
|
{
|
||||||
var (publicKey, secretKey) = GetApiKeys(args.ApiKey);
|
var (publicKey, secretKey) = GetApiKeys(args.ApiKey, args.Account);
|
||||||
var result = ApiRequest("/keys/validate", HttpMethod.Post, null, publicKey, secretKey);
|
var result = ApiRequest("/keys/validate", HttpMethod.Post, null, publicKey, secretKey);
|
||||||
|
|
||||||
if (!result.TryGetValue("valid", out var validObj) || validObj is not JsonElement validEl)
|
if (!result.TryGetValue("valid", out var validObj) || validObj is not JsonElement validEl)
|
||||||
|
|
@ -281,7 +281,7 @@ void OpenBrowser(string url)
|
||||||
|
|
||||||
void CmdService(Args args)
|
void CmdService(Args args)
|
||||||
{
|
{
|
||||||
var (publicKey, secretKey) = GetApiKeys(args.ApiKey);
|
var (publicKey, secretKey) = GetApiKeys(args.ApiKey, args.Account);
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(args.EnvAction))
|
if (!string.IsNullOrEmpty(args.EnvAction))
|
||||||
{
|
{
|
||||||
|
|
@ -392,7 +392,22 @@ void CmdService(Args args)
|
||||||
|
|
||||||
if (args.ServiceRedeploy != null)
|
if (args.ServiceRedeploy != null)
|
||||||
{
|
{
|
||||||
ApiRequest($"/services/{args.ServiceRedeploy}/redeploy", HttpMethod.Post, null, publicKey, secretKey);
|
var payload = new Dictionary<string, object>();
|
||||||
|
if (args.Files.Count > 0)
|
||||||
|
{
|
||||||
|
var inputFiles = new List<Dictionary<string, string>>();
|
||||||
|
foreach (var filepath in args.Files)
|
||||||
|
{
|
||||||
|
var content = File.ReadAllBytes(filepath);
|
||||||
|
inputFiles.Add(new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
["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);
|
||||||
Console.WriteLine($"{GREEN}Service redeploying: {args.ServiceRedeploy}{RESET}");
|
Console.WriteLine($"{GREEN}Service redeploying: {args.ServiceRedeploy}{RESET}");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -452,6 +467,20 @@ void CmdService(Args args)
|
||||||
if (args.Network != null) payload["network"] = args.Network;
|
if (args.Network != null) payload["network"] = args.Network;
|
||||||
if (args.Vcpu > 0) payload["vcpu"] = args.Vcpu;
|
if (args.Vcpu > 0) payload["vcpu"] = args.Vcpu;
|
||||||
if (args.ServiceCreateUnfreezeOnDemand) payload["unfreeze_on_demand"] = true;
|
if (args.ServiceCreateUnfreezeOnDemand) payload["unfreeze_on_demand"] = true;
|
||||||
|
if (args.Files.Count > 0)
|
||||||
|
{
|
||||||
|
var inputFiles = new List<Dictionary<string, string>>();
|
||||||
|
foreach (var filepath in args.Files)
|
||||||
|
{
|
||||||
|
var content = File.ReadAllBytes(filepath);
|
||||||
|
inputFiles.Add(new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
["filename"] = Path.GetFileName(filepath),
|
||||||
|
["content"] = Convert.ToBase64String(content)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
payload["input_files"] = inputFiles;
|
||||||
|
}
|
||||||
|
|
||||||
var result = ApiRequest("/services", HttpMethod.Post, payload, publicKey, secretKey);
|
var result = ApiRequest("/services", HttpMethod.Post, payload, publicKey, secretKey);
|
||||||
var serviceId = result.TryGetValue("id", out var id) && id is JsonElement idEl ? idEl.GetString() : null;
|
var serviceId = result.TryGetValue("id", out var id) && id is JsonElement idEl ? idEl.GetString() : null;
|
||||||
|
|
@ -519,7 +548,7 @@ void CmdServiceEnv(Args args, string publicKey, string secretKey)
|
||||||
|
|
||||||
void CmdSnapshot(Args args)
|
void CmdSnapshot(Args args)
|
||||||
{
|
{
|
||||||
var (publicKey, secretKey) = GetApiKeys(args.ApiKey);
|
var (publicKey, secretKey) = GetApiKeys(args.ApiKey, args.Account);
|
||||||
|
|
||||||
if (args.SnapshotList)
|
if (args.SnapshotList)
|
||||||
{
|
{
|
||||||
|
|
@ -591,7 +620,7 @@ void CmdSnapshot(Args args)
|
||||||
|
|
||||||
void CmdImage(Args args)
|
void CmdImage(Args args)
|
||||||
{
|
{
|
||||||
var (publicKey, secretKey) = GetApiKeys(args.ApiKey);
|
var (publicKey, secretKey) = GetApiKeys(args.ApiKey, args.Account);
|
||||||
|
|
||||||
if (args.ImageList)
|
if (args.ImageList)
|
||||||
{
|
{
|
||||||
|
|
@ -686,7 +715,7 @@ void CmdImage(Args args)
|
||||||
|
|
||||||
void CmdLanguages(Args args)
|
void CmdLanguages(Args args)
|
||||||
{
|
{
|
||||||
var (publicKey, secretKey) = GetApiKeys(args.ApiKey);
|
var (publicKey, secretKey) = GetApiKeys(args.ApiKey, args.Account);
|
||||||
|
|
||||||
// Check cache first
|
// Check cache first
|
||||||
var cacheDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".unsandbox");
|
var cacheDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".unsandbox");
|
||||||
|
|
@ -867,22 +896,69 @@ bool ServiceEnvSet(string serviceId, string envContent, string publicKey, string
|
||||||
catch { return false; }
|
catch { return false; }
|
||||||
}
|
}
|
||||||
|
|
||||||
(string, string) GetApiKeys(string? argsKey)
|
(string, string) LoadAccountsCSV(string path, int index)
|
||||||
{
|
{
|
||||||
|
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 publicKey = Environment.GetEnvironmentVariable("UNSANDBOX_PUBLIC_KEY");
|
||||||
var secretKey = Environment.GetEnvironmentVariable("UNSANDBOX_SECRET_KEY");
|
var secretKey = Environment.GetEnvironmentVariable("UNSANDBOX_SECRET_KEY");
|
||||||
|
if (!string.IsNullOrEmpty(publicKey) && !string.IsNullOrEmpty(secretKey))
|
||||||
|
return (publicKey, secretKey);
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(publicKey) || string.IsNullOrEmpty(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))
|
||||||
{
|
{
|
||||||
var legacyKey = argsKey ?? Environment.GetEnvironmentVariable("UNSANDBOX_API_KEY");
|
Console.Error.WriteLine($"{RED}Error: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set{RESET}");
|
||||||
if (string.IsNullOrEmpty(legacyKey))
|
Environment.Exit(1);
|
||||||
{
|
|
||||||
Console.Error.WriteLine($"{RED}Error: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set{RESET}");
|
|
||||||
Environment.Exit(1);
|
|
||||||
}
|
|
||||||
return (legacyKey, "");
|
|
||||||
}
|
}
|
||||||
return (publicKey, secretKey);
|
return (legacyKey!, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
string DetectLanguage(string filename)
|
string DetectLanguage(string filename)
|
||||||
|
|
@ -1008,6 +1084,7 @@ Args ParseArgs(string[] args)
|
||||||
else if (arg == "--show-freeze-page-enabled") result.ServiceShowFreezePageEnabled = args[++i].ToLower() == "true";
|
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 == "--with-unfreeze-on-demand") result.ServiceCreateUnfreezeOnDemand = true;
|
||||||
else if (arg == "--extend") result.KeyExtend = true;
|
else if (arg == "--extend") result.KeyExtend = true;
|
||||||
|
else if (arg == "--account") result.Account = int.Parse(args[++i]);
|
||||||
else if (arg == "--delete")
|
else if (arg == "--delete")
|
||||||
{
|
{
|
||||||
var val = args[++i];
|
var val = args[++i];
|
||||||
|
|
@ -1085,7 +1162,7 @@ Service options:
|
||||||
--lock ID Prevent deletion
|
--lock ID Prevent deletion
|
||||||
--unlock ID Allow deletion
|
--unlock ID Allow deletion
|
||||||
--resize ID Resize (use with -v)
|
--resize ID Resize (use with -v)
|
||||||
--redeploy ID Re-run bootstrap
|
--redeploy ID Re-run bootstrap (use -f to include input files)
|
||||||
--snapshot ID Create snapshot from service
|
--snapshot ID Create snapshot from service
|
||||||
--unfreeze-on-demand ID Set unfreeze-on-demand for service
|
--unfreeze-on-demand ID Set unfreeze-on-demand for service
|
||||||
--unfreeze-on-demand-enabled BOOL Enable/disable (default: true)
|
--unfreeze-on-demand-enabled BOOL Enable/disable (default: true)
|
||||||
|
|
@ -1098,6 +1175,7 @@ Service options:
|
||||||
--dump-bootstrap ID Dump bootstrap script
|
--dump-bootstrap ID Dump bootstrap script
|
||||||
--dump-file FILE File to save bootstrap (with --dump-bootstrap)
|
--dump-file FILE File to save bootstrap (with --dump-bootstrap)
|
||||||
-e KEY=VALUE Set vault env var (with --name or env set)
|
-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
|
--env-file FILE Load vault vars from file
|
||||||
|
|
||||||
Service env commands:
|
Service env commands:
|
||||||
|
|
@ -1406,7 +1484,7 @@ public static class Unsandbox
|
||||||
catch (Exception ex) { _lastError = ex.Message; return null; }
|
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, string? publicKey = null, string? secretKey = null)
|
public static string? ServiceCreate(string name, string? ports = null, string? domains = null, string? bootstrap = null, string? networkMode = null, List<Dictionary<string, string>>? inputFiles = null, string? publicKey = null, string? secretKey = null)
|
||||||
{
|
{
|
||||||
var (pk, sk) = ResolveKeys(publicKey, secretKey);
|
var (pk, sk) = ResolveKeys(publicKey, secretKey);
|
||||||
var payload = new Dictionary<string, object> { ["name"] = name };
|
var payload = new Dictionary<string, object> { ["name"] = name };
|
||||||
|
|
@ -1414,6 +1492,7 @@ public static class Unsandbox
|
||||||
if (domains != null) payload["domains"] = domains;
|
if (domains != null) payload["domains"] = domains;
|
||||||
if (bootstrap != null) payload["bootstrap"] = bootstrap;
|
if (bootstrap != null) payload["bootstrap"] = bootstrap;
|
||||||
if (networkMode != null) payload["network"] = networkMode;
|
if (networkMode != null) payload["network"] = networkMode;
|
||||||
|
if (inputFiles != null && inputFiles.Count > 0) payload["input_files"] = inputFiles;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var result = ApiCall("/services", HttpMethod.Post, payload, pk, sk);
|
var result = ApiCall("/services", HttpMethod.Post, payload, pk, sk);
|
||||||
|
|
@ -1465,10 +1544,16 @@ public static class Unsandbox
|
||||||
catch (Exception ex) { _lastError = ex.Message; return false; }
|
catch (Exception ex) { _lastError = ex.Message; return false; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool ServiceRedeploy(string serviceId, string? bootstrap = null, string? publicKey = null, string? secretKey = null)
|
public static bool ServiceRedeploy(string serviceId, string? bootstrap = null, List<Dictionary<string, string>>? inputFiles = null, string? publicKey = null, string? secretKey = null)
|
||||||
{
|
{
|
||||||
var (pk, sk) = ResolveKeys(publicKey, secretKey);
|
var (pk, sk) = ResolveKeys(publicKey, secretKey);
|
||||||
var payload = bootstrap != null ? new Dictionary<string, object> { ["bootstrap"] = bootstrap } : null;
|
Dictionary<string, object>? payload = null;
|
||||||
|
if (bootstrap != null || inputFiles != null)
|
||||||
|
{
|
||||||
|
payload = new Dictionary<string, object>();
|
||||||
|
if (bootstrap != null) payload["bootstrap"] = bootstrap;
|
||||||
|
if (inputFiles != null && inputFiles.Count > 0) payload["input_files"] = inputFiles;
|
||||||
|
}
|
||||||
try { ApiCall($"/services/{serviceId}/redeploy", HttpMethod.Post, payload, pk, sk); return true; }
|
try { ApiCall($"/services/{serviceId}/redeploy", HttpMethod.Post, payload, pk, sk); return true; }
|
||||||
catch (Exception ex) { _lastError = ex.Message; return false; }
|
catch (Exception ex) { _lastError = ex.Message; return false; }
|
||||||
}
|
}
|
||||||
|
|
@ -2036,6 +2121,7 @@ class Args
|
||||||
public bool ServiceCreateUnfreezeOnDemand;
|
public bool ServiceCreateUnfreezeOnDemand;
|
||||||
public string? EnvFile, EnvAction, EnvTarget;
|
public string? EnvFile, EnvAction, EnvTarget;
|
||||||
public bool KeyExtend;
|
public bool KeyExtend;
|
||||||
|
public int Account = -1;
|
||||||
public bool SnapshotList;
|
public bool SnapshotList;
|
||||||
public string? SnapshotInfo, SnapshotDelete, SnapshotLock, SnapshotUnlock, SnapshotClone;
|
public string? SnapshotInfo, SnapshotDelete, SnapshotLock, SnapshotUnlock, SnapshotClone;
|
||||||
public string? SnapshotCloneType, SnapshotName;
|
public string? SnapshotCloneType, SnapshotName;
|
||||||
|
|
|
||||||
|
|
@ -1,39 +1,19 @@
|
||||||
#!/usr/bin/env elixir
|
#!/usr/bin/env elixir
|
||||||
# PUBLIC DOMAIN - NO LICENSE, NO WARRANTY
|
# 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.
|
||||||
#
|
#
|
||||||
# This is free public domain software for the public good of a permacomputer hosted
|
# The permacomputer is community-owned infrastructure optimized around
|
||||||
# at permacomputer.com - an always-on computer by the people, for the people. One
|
# four values:
|
||||||
# which is durable, easy to repair, and distributed like tap water for machine
|
|
||||||
# learning intelligence.
|
|
||||||
#
|
#
|
||||||
# The permacomputer is community-owned infrastructure optimized around four values:
|
# TRUTH First principles, math & science, open source code freely distributed
|
||||||
|
# FREEDOM Voluntary partnerships, freedom from tyranny & corporate control
|
||||||
|
# HARMONY Minimal waste, self-renewing systems with diverse thriving connections
|
||||||
|
# LOVE Be yourself without hurting others, cooperation through natural law
|
||||||
#
|
#
|
||||||
# TRUTH - First principles, math & science, open source code freely distributed
|
# This software contributes to that vision by enabling code execution across 42+ programming languages through a unified interface, accessible to all.
|
||||||
# FREEDOM - Voluntary partnerships, freedom from tyranny & corporate control
|
# Code is seeds to sprout on any abandoned technology.
|
||||||
# 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
|
# un.ex - Unsandbox CLI client in Elixir
|
||||||
#
|
#
|
||||||
|
|
@ -71,8 +51,10 @@ defmodule Un do
|
||||||
|
|
||||||
Credentials are loaded in priority order:
|
Credentials are loaded in priority order:
|
||||||
1. Function arguments (public_key, secret_key)
|
1. Function arguments (public_key, secret_key)
|
||||||
2. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY)
|
2. --account N -> accounts.csv row N (bypasses env vars)
|
||||||
3. Config file (~/.unsandbox/accounts.csv)
|
3. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY)
|
||||||
|
4. ~/.unsandbox/accounts.csv row 0 (or UNSANDBOX_ACCOUNT env)
|
||||||
|
5. ./accounts.csv row 0
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@blue "\e[34m"
|
@blue "\e[34m"
|
||||||
|
|
@ -1132,25 +1114,50 @@ defmodule Un do
|
||||||
# CLI Entry Point
|
# CLI Entry Point
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
def main([]), do: print_usage()
|
def main(raw_args) do
|
||||||
def main(["session" | rest]), do: session_command(rest)
|
{account_index, args} = extract_account_arg(raw_args, nil, [])
|
||||||
def main(["service" | rest]), do: service_command(rest)
|
if account_index != nil do
|
||||||
def main(["snapshot" | rest]), do: snapshot_command(rest)
|
Process.put(:account_index, account_index)
|
||||||
def main(["image" | rest]), do: image_command(rest)
|
end
|
||||||
def main(["key" | rest]), do: key_command(rest)
|
dispatch(args)
|
||||||
def main(["languages" | rest]), do: languages_command(rest)
|
end
|
||||||
def main(args), do: execute_command(args)
|
|
||||||
|
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
|
||||||
|
|
||||||
defp print_usage do
|
defp print_usage do
|
||||||
IO.puts("Usage: un.ex [options] <source_file>")
|
IO.puts("Usage: un.ex [--account N] [options] <source_file>")
|
||||||
IO.puts(" un.ex session [options]")
|
IO.puts(" un.ex [--account N] session [options]")
|
||||||
IO.puts(" un.ex service [options]")
|
IO.puts(" un.ex [--account N] service [options]")
|
||||||
IO.puts(" un.ex service env <action> <service_id>")
|
IO.puts(" un.ex [--account N] service env <action> <service_id>")
|
||||||
IO.puts(" un.ex snapshot [options]")
|
IO.puts(" un.ex [--account N] snapshot [options]")
|
||||||
IO.puts(" un.ex image [options]")
|
IO.puts(" un.ex [--account N] image [options]")
|
||||||
IO.puts(" un.ex key [--extend]")
|
IO.puts(" un.ex [--account N] key [--extend]")
|
||||||
IO.puts(" un.ex languages [--json]")
|
IO.puts(" un.ex languages [--json]")
|
||||||
IO.puts("")
|
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("Service options: --name, --ports, --bootstrap, -e KEY=VALUE, --env-file FILE")
|
||||||
IO.puts(" --set-unfreeze-on-demand ID true|false")
|
IO.puts(" --set-unfreeze-on-demand ID true|false")
|
||||||
IO.puts("Service env commands: status, set, export, delete")
|
IO.puts("Service env commands: status, set, export, delete")
|
||||||
|
|
@ -1933,21 +1940,85 @@ defmodule Un do
|
||||||
end
|
end
|
||||||
|
|
||||||
# Helpers
|
# 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
|
defp get_api_keys do
|
||||||
public_key = System.get_env("UNSANDBOX_PUBLIC_KEY")
|
home = System.get_env("HOME") || "."
|
||||||
secret_key = System.get_env("UNSANDBOX_SECRET_KEY")
|
home_csv = Path.join([home, ".unsandbox", "accounts.csv"])
|
||||||
|
|
||||||
# Fall back to UNSANDBOX_API_KEY for backwards compatibility
|
# Priority 1: --account N (stored in process dict by main/1)
|
||||||
api_key = System.get_env("UNSANDBOX_API_KEY")
|
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")
|
||||||
|
|
||||||
cond do
|
cond do
|
||||||
public_key && secret_key ->
|
public_key && secret_key ->
|
||||||
{public_key, secret_key}
|
{public_key, secret_key}
|
||||||
api_key ->
|
api_key ->
|
||||||
{api_key, nil}
|
{api_key, nil}
|
||||||
true ->
|
true ->
|
||||||
IO.puts(:stderr, "Error: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set (or UNSANDBOX_API_KEY for backwards compat)")
|
# Priority 3: ~/.unsandbox/accounts.csv (or UNSANDBOX_ACCOUNT index)
|
||||||
System.halt(1)
|
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
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,20 @@
|
||||||
#!/usr/bin/env elixir
|
#!/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
|
# Functional Tests for Un Elixir SDK
|
||||||
#
|
#
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,20 @@
|
||||||
#!/usr/bin/env elixir
|
#!/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
|
# Tests for Un Elixir SDK
|
||||||
#
|
#
|
||||||
|
|
|
||||||
|
|
@ -56,8 +56,10 @@
|
||||||
%%%
|
%%%
|
||||||
%%% Authentication Priority:
|
%%% Authentication Priority:
|
||||||
%%% 1. Function arguments (PublicKey, SecretKey)
|
%%% 1. Function arguments (PublicKey, SecretKey)
|
||||||
%%% 2. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY)
|
%%% 2. --account N -> accounts.csv row N (bypasses env vars)
|
||||||
%%% 3. Config file (~/.unsandbox/accounts.csv)
|
%%% 3. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY)
|
||||||
|
%%% 4. ~/.unsandbox/accounts.csv row 0 (or UNSANDBOX_ACCOUNT env)
|
||||||
|
%%% 5. ./accounts.csv row 0
|
||||||
|
|
||||||
-define(API_BASE, "https://api.unsandbox.com").
|
-define(API_BASE, "https://api.unsandbox.com").
|
||||||
-define(PORTAL_BASE, "https://unsandbox.com").
|
-define(PORTAL_BASE, "https://unsandbox.com").
|
||||||
|
|
@ -719,37 +721,60 @@ not_contains_error(Response) ->
|
||||||
%% CLI Entry Point
|
%% CLI Entry Point
|
||||||
%% ============================================================================
|
%% ============================================================================
|
||||||
|
|
||||||
main([]) ->
|
main(RawArgs) ->
|
||||||
io:format("Usage: un.erl [options] <source_file>~n"),
|
%% Strip --account N from args and store index in process dict before dispatch
|
||||||
io:format(" un.erl session [options]~n"),
|
{AccountIndex, Args} = extract_account_arg(RawArgs, undefined, []),
|
||||||
io:format(" un.erl service [options]~n"),
|
case AccountIndex of
|
||||||
io:format(" un.erl snapshot [options]~n"),
|
undefined -> ok;
|
||||||
io:format(" un.erl image [options]~n"),
|
N -> erlang:put(account_index, N)
|
||||||
io:format(" un.erl key [options]~n"),
|
end,
|
||||||
|
dispatch(Args).
|
||||||
|
|
||||||
|
dispatch([]) ->
|
||||||
|
io:format("Usage: un.erl [--account N] [options] <source_file>~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"),
|
||||||
io:format(" un.erl languages [--json]~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);
|
halt(1);
|
||||||
|
|
||||||
main(["session" | Rest]) ->
|
dispatch(["session" | Rest]) ->
|
||||||
session_command(Rest);
|
session_command(Rest);
|
||||||
|
|
||||||
main(["service" | Rest]) ->
|
dispatch(["service" | Rest]) ->
|
||||||
service_command(Rest);
|
service_command(Rest);
|
||||||
|
|
||||||
main(["snapshot" | Rest]) ->
|
dispatch(["snapshot" | Rest]) ->
|
||||||
snapshot_command(Rest);
|
snapshot_command(Rest);
|
||||||
|
|
||||||
main(["image" | Rest]) ->
|
dispatch(["image" | Rest]) ->
|
||||||
image_command(Rest);
|
image_command(Rest);
|
||||||
|
|
||||||
main(["key" | Rest]) ->
|
dispatch(["key" | Rest]) ->
|
||||||
key_command(Rest);
|
key_command(Rest);
|
||||||
|
|
||||||
main(["languages" | Rest]) ->
|
dispatch(["languages" | Rest]) ->
|
||||||
languages_command(Rest);
|
languages_command(Rest);
|
||||||
|
|
||||||
main(Args) ->
|
dispatch(Args) ->
|
||||||
execute_command(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
|
||||||
execute_command(Args) ->
|
execute_command(Args) ->
|
||||||
{File, _Opts} = parse_exec_args(Args, #{file => undefined}),
|
{File, _Opts} = parse_exec_args(Args, #{file => undefined}),
|
||||||
|
|
@ -1452,19 +1477,96 @@ open_extend_page(PublicKey) ->
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% Helpers
|
%% Helpers
|
||||||
get_api_keys() ->
|
|
||||||
PublicKey = os:getenv("UNSANDBOX_PUBLIC_KEY"),
|
|
||||||
SecretKey = os:getenv("UNSANDBOX_SECRET_KEY"),
|
|
||||||
ApiKey = os:getenv("UNSANDBOX_API_KEY"),
|
|
||||||
|
|
||||||
if
|
%% @doc Load credentials from a CSV file at the given path.
|
||||||
PublicKey =/= false andalso SecretKey =/= false ->
|
%% Skips blank lines and comment lines (#). Returns {ok, {PK, SK}} or error.
|
||||||
{PublicKey, SecretKey};
|
load_credentials_from_csv(CsvPath, AccountIndex) ->
|
||||||
ApiKey =/= false ->
|
case file:read_file(CsvPath) of
|
||||||
{ApiKey, false};
|
{ok, Bin} ->
|
||||||
true ->
|
Lines = string:split(binary_to_list(Bin), "\n", all),
|
||||||
io:format("Error: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set (or UNSANDBOX_API_KEY for backwards compat)~n"),
|
ValidAccounts = lists:filtermap(fun(Line) ->
|
||||||
halt(1)
|
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
|
||||||
end.
|
end.
|
||||||
|
|
||||||
get_api_key() ->
|
get_api_key() ->
|
||||||
|
|
|
||||||
|
|
@ -97,8 +97,84 @@
|
||||||
find-ext ext-lang
|
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 API keys from environment (HMAC or legacy)
|
||||||
: get-public-key ( -- addr len )
|
: 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
|
s" UNSANDBOX_PUBLIC_KEY" getenv
|
||||||
dup 0= if
|
dup 0= if
|
||||||
2drop s" UNSANDBOX_API_KEY" getenv
|
2drop s" UNSANDBOX_API_KEY" getenv
|
||||||
|
|
@ -110,6 +186,20 @@
|
||||||
;
|
;
|
||||||
|
|
||||||
: get-secret-key ( -- addr 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
|
s" UNSANDBOX_SECRET_KEY" getenv
|
||||||
dup 0= if
|
dup 0= if
|
||||||
2drop s" UNSANDBOX_API_KEY" getenv
|
2drop s" UNSANDBOX_API_KEY" getenv
|
||||||
|
|
@ -716,7 +806,7 @@
|
||||||
0 (bye)
|
0 (bye)
|
||||||
then
|
then
|
||||||
|
|
||||||
2 arg 2dup s" --extend" compare 0= if
|
2 sarg 2dup s" --extend" compare 0= if
|
||||||
2drop
|
2drop
|
||||||
1 validate-key
|
1 validate-key
|
||||||
0 (bye)
|
0 (bye)
|
||||||
|
|
@ -779,7 +869,7 @@
|
||||||
1 (bye)
|
1 (bye)
|
||||||
then
|
then
|
||||||
|
|
||||||
2 arg 2dup s" --list" compare 0= if
|
2 sarg 2dup s" --list" compare 0= if
|
||||||
2drop session-list
|
2drop session-list
|
||||||
0 (bye)
|
0 (bye)
|
||||||
then
|
then
|
||||||
|
|
@ -795,7 +885,7 @@
|
||||||
s" Error: --kill requires session ID" type cr
|
s" Error: --kill requires session ID" type cr
|
||||||
1 (bye)
|
1 (bye)
|
||||||
then
|
then
|
||||||
3 arg session-kill
|
3 sarg session-kill
|
||||||
0 (bye)
|
0 (bye)
|
||||||
then
|
then
|
||||||
|
|
||||||
|
|
@ -835,7 +925,7 @@
|
||||||
1 (bye)
|
1 (bye)
|
||||||
then
|
then
|
||||||
|
|
||||||
2 arg 2dup s" --list" compare 0= if
|
2 sarg 2dup s" --list" compare 0= if
|
||||||
2drop service-list
|
2drop service-list
|
||||||
0 (bye)
|
0 (bye)
|
||||||
then
|
then
|
||||||
|
|
@ -856,7 +946,7 @@
|
||||||
s" Error: --info requires service ID" type cr
|
s" Error: --info requires service ID" type cr
|
||||||
1 (bye)
|
1 (bye)
|
||||||
then
|
then
|
||||||
3 arg service-info
|
3 sarg service-info
|
||||||
0 (bye)
|
0 (bye)
|
||||||
then
|
then
|
||||||
|
|
||||||
|
|
@ -866,7 +956,7 @@
|
||||||
s" Error: --logs requires service ID" type cr
|
s" Error: --logs requires service ID" type cr
|
||||||
1 (bye)
|
1 (bye)
|
||||||
then
|
then
|
||||||
3 arg service-logs
|
3 sarg service-logs
|
||||||
0 (bye)
|
0 (bye)
|
||||||
then
|
then
|
||||||
|
|
||||||
|
|
@ -876,7 +966,7 @@
|
||||||
s" Error: --freeze requires service ID" type cr
|
s" Error: --freeze requires service ID" type cr
|
||||||
1 (bye)
|
1 (bye)
|
||||||
then
|
then
|
||||||
3 arg service-sleep
|
3 sarg service-sleep
|
||||||
0 (bye)
|
0 (bye)
|
||||||
then
|
then
|
||||||
|
|
||||||
|
|
@ -886,7 +976,7 @@
|
||||||
s" Error: --unfreeze requires service ID" type cr
|
s" Error: --unfreeze requires service ID" type cr
|
||||||
1 (bye)
|
1 (bye)
|
||||||
then
|
then
|
||||||
3 arg service-wake
|
3 sarg service-wake
|
||||||
0 (bye)
|
0 (bye)
|
||||||
then
|
then
|
||||||
|
|
||||||
|
|
@ -896,7 +986,7 @@
|
||||||
s" Error: --destroy requires service ID" type cr
|
s" Error: --destroy requires service ID" type cr
|
||||||
1 (bye)
|
1 (bye)
|
||||||
then
|
then
|
||||||
3 arg service-destroy
|
3 sarg service-destroy
|
||||||
0 (bye)
|
0 (bye)
|
||||||
then
|
then
|
||||||
|
|
||||||
|
|
@ -911,13 +1001,13 @@
|
||||||
s" Error: --resize requires --vcpu N" type cr
|
s" Error: --resize requires --vcpu N" type cr
|
||||||
1 (bye)
|
1 (bye)
|
||||||
then
|
then
|
||||||
4 arg 2dup s" --vcpu" compare 0= if
|
4 sarg 2dup s" --vcpu" compare 0= if
|
||||||
2drop
|
2drop
|
||||||
argc @ 6 < if
|
argc @ 6 < if
|
||||||
s" Error: --vcpu requires a value" type cr
|
s" Error: --vcpu requires a value" type cr
|
||||||
1 (bye)
|
1 (bye)
|
||||||
then
|
then
|
||||||
3 arg 5 arg service-resize
|
3 sarg 5 sarg service-resize
|
||||||
0 (bye)
|
0 (bye)
|
||||||
then
|
then
|
||||||
2dup s" -v" compare 0= if
|
2dup s" -v" compare 0= if
|
||||||
|
|
@ -926,7 +1016,7 @@
|
||||||
s" Error: -v requires a value" type cr
|
s" Error: -v requires a value" type cr
|
||||||
1 (bye)
|
1 (bye)
|
||||||
then
|
then
|
||||||
3 arg 5 arg service-resize
|
3 sarg 5 sarg service-resize
|
||||||
0 (bye)
|
0 (bye)
|
||||||
then
|
then
|
||||||
2drop
|
2drop
|
||||||
|
|
@ -940,16 +1030,16 @@
|
||||||
s" Error: --dump-bootstrap requires service ID" type cr
|
s" Error: --dump-bootstrap requires service ID" type cr
|
||||||
1 (bye)
|
1 (bye)
|
||||||
then
|
then
|
||||||
3 arg
|
3 sarg
|
||||||
\ Check for --dump-file
|
\ Check for --dump-file
|
||||||
argc @ 5 >= if
|
argc @ 5 >= if
|
||||||
4 arg 2dup s" --dump-file" compare 0= if
|
4 sarg 2dup s" --dump-file" compare 0= if
|
||||||
2drop
|
2drop
|
||||||
argc @ 6 < if
|
argc @ 6 < if
|
||||||
s" Error: --dump-file requires filename" type cr
|
s" Error: --dump-file requires filename" type cr
|
||||||
1 (bye)
|
1 (bye)
|
||||||
then
|
then
|
||||||
5 arg
|
5 sarg
|
||||||
else
|
else
|
||||||
2drop 0 0
|
2drop 0 0
|
||||||
then
|
then
|
||||||
|
|
@ -967,13 +1057,13 @@
|
||||||
s" Usage: un.forth service env <status|set|export|delete> <service_id> [options]" type cr
|
s" Usage: un.forth service env <status|set|export|delete> <service_id> [options]" type cr
|
||||||
1 (bye)
|
1 (bye)
|
||||||
then
|
then
|
||||||
3 arg 2dup s" status" compare 0= if
|
3 sarg 2dup s" status" compare 0= if
|
||||||
2drop
|
2drop
|
||||||
argc @ 5 < if
|
argc @ 5 < if
|
||||||
s" Error: status requires service ID" type cr
|
s" Error: status requires service ID" type cr
|
||||||
1 (bye)
|
1 (bye)
|
||||||
then
|
then
|
||||||
4 arg service-env-status
|
4 sarg service-env-status
|
||||||
0 (bye)
|
0 (bye)
|
||||||
then
|
then
|
||||||
2dup s" set" compare 0= if
|
2dup s" set" compare 0= if
|
||||||
|
|
@ -991,7 +1081,7 @@
|
||||||
s" Error: export requires service ID" type cr
|
s" Error: export requires service ID" type cr
|
||||||
1 (bye)
|
1 (bye)
|
||||||
then
|
then
|
||||||
4 arg service-env-export
|
4 sarg service-env-export
|
||||||
0 (bye)
|
0 (bye)
|
||||||
then
|
then
|
||||||
2dup s" delete" compare 0= if
|
2dup s" delete" compare 0= if
|
||||||
|
|
@ -1000,7 +1090,7 @@
|
||||||
s" Error: delete requires service ID" type cr
|
s" Error: delete requires service ID" type cr
|
||||||
1 (bye)
|
1 (bye)
|
||||||
then
|
then
|
||||||
4 arg service-env-delete
|
4 sarg service-env-delete
|
||||||
0 (bye)
|
0 (bye)
|
||||||
then
|
then
|
||||||
2drop
|
2drop
|
||||||
|
|
@ -1072,7 +1162,7 @@
|
||||||
0 (bye)
|
0 (bye)
|
||||||
then
|
then
|
||||||
|
|
||||||
2 arg 2dup s" --json" compare 0= if
|
2 sarg 2dup s" --json" compare 0= if
|
||||||
2drop
|
2drop
|
||||||
1 languages-list
|
1 languages-list
|
||||||
0 (bye)
|
0 (bye)
|
||||||
|
|
@ -1670,7 +1760,7 @@
|
||||||
0 (bye)
|
0 (bye)
|
||||||
then
|
then
|
||||||
|
|
||||||
2 arg 2dup s" --list" compare 0= if
|
2 sarg 2dup s" --list" compare 0= if
|
||||||
2drop snapshot-list
|
2drop snapshot-list
|
||||||
0 (bye)
|
0 (bye)
|
||||||
then
|
then
|
||||||
|
|
@ -1686,7 +1776,7 @@
|
||||||
s" Error: --info requires snapshot ID" type cr
|
s" Error: --info requires snapshot ID" type cr
|
||||||
1 (bye)
|
1 (bye)
|
||||||
then
|
then
|
||||||
3 arg snapshot-info
|
3 sarg snapshot-info
|
||||||
0 (bye)
|
0 (bye)
|
||||||
then
|
then
|
||||||
|
|
||||||
|
|
@ -1696,7 +1786,7 @@
|
||||||
s" Error: --restore requires snapshot ID" type cr
|
s" Error: --restore requires snapshot ID" type cr
|
||||||
1 (bye)
|
1 (bye)
|
||||||
then
|
then
|
||||||
3 arg snapshot-restore
|
3 sarg snapshot-restore
|
||||||
0 (bye)
|
0 (bye)
|
||||||
then
|
then
|
||||||
|
|
||||||
|
|
@ -1706,7 +1796,7 @@
|
||||||
s" Error: --delete requires snapshot ID" type cr
|
s" Error: --delete requires snapshot ID" type cr
|
||||||
1 (bye)
|
1 (bye)
|
||||||
then
|
then
|
||||||
3 arg snapshot-delete
|
3 sarg snapshot-delete
|
||||||
0 (bye)
|
0 (bye)
|
||||||
then
|
then
|
||||||
|
|
||||||
|
|
@ -1716,7 +1806,7 @@
|
||||||
s" Error: --lock requires snapshot ID" type cr
|
s" Error: --lock requires snapshot ID" type cr
|
||||||
1 (bye)
|
1 (bye)
|
||||||
then
|
then
|
||||||
3 arg snapshot-lock
|
3 sarg snapshot-lock
|
||||||
0 (bye)
|
0 (bye)
|
||||||
then
|
then
|
||||||
|
|
||||||
|
|
@ -1726,7 +1816,7 @@
|
||||||
s" Error: --unlock requires snapshot ID" type cr
|
s" Error: --unlock requires snapshot ID" type cr
|
||||||
1 (bye)
|
1 (bye)
|
||||||
then
|
then
|
||||||
3 arg snapshot-unlock
|
3 sarg snapshot-unlock
|
||||||
0 (bye)
|
0 (bye)
|
||||||
then
|
then
|
||||||
|
|
||||||
|
|
@ -1736,7 +1826,7 @@
|
||||||
s" Error: --clone requires snapshot ID" type cr
|
s" Error: --clone requires snapshot ID" type cr
|
||||||
1 (bye)
|
1 (bye)
|
||||||
then
|
then
|
||||||
3 arg snapshot-clone
|
3 sarg snapshot-clone
|
||||||
0 (bye)
|
0 (bye)
|
||||||
then
|
then
|
||||||
|
|
||||||
|
|
@ -1752,7 +1842,7 @@
|
||||||
1 (bye)
|
1 (bye)
|
||||||
then
|
then
|
||||||
|
|
||||||
2 arg 2dup s" --list" compare 0= if
|
2 sarg 2dup s" --list" compare 0= if
|
||||||
2drop image-list
|
2drop image-list
|
||||||
0 (bye)
|
0 (bye)
|
||||||
then
|
then
|
||||||
|
|
@ -1768,7 +1858,7 @@
|
||||||
s" Error: --info requires image ID" type cr
|
s" Error: --info requires image ID" type cr
|
||||||
1 (bye)
|
1 (bye)
|
||||||
then
|
then
|
||||||
3 arg image-info
|
3 sarg image-info
|
||||||
0 (bye)
|
0 (bye)
|
||||||
then
|
then
|
||||||
|
|
||||||
|
|
@ -1778,7 +1868,7 @@
|
||||||
s" Error: --delete requires image ID" type cr
|
s" Error: --delete requires image ID" type cr
|
||||||
1 (bye)
|
1 (bye)
|
||||||
then
|
then
|
||||||
3 arg image-delete
|
3 sarg image-delete
|
||||||
0 (bye)
|
0 (bye)
|
||||||
then
|
then
|
||||||
|
|
||||||
|
|
@ -1788,7 +1878,7 @@
|
||||||
s" Error: --lock requires image ID" type cr
|
s" Error: --lock requires image ID" type cr
|
||||||
1 (bye)
|
1 (bye)
|
||||||
then
|
then
|
||||||
3 arg image-lock
|
3 sarg image-lock
|
||||||
0 (bye)
|
0 (bye)
|
||||||
then
|
then
|
||||||
|
|
||||||
|
|
@ -1798,7 +1888,7 @@
|
||||||
s" Error: --unlock requires image ID" type cr
|
s" Error: --unlock requires image ID" type cr
|
||||||
1 (bye)
|
1 (bye)
|
||||||
then
|
then
|
||||||
3 arg image-unlock
|
3 sarg image-unlock
|
||||||
0 (bye)
|
0 (bye)
|
||||||
then
|
then
|
||||||
|
|
||||||
|
|
@ -1818,7 +1908,7 @@
|
||||||
s" Error: --visibility requires image ID and mode" type cr
|
s" Error: --visibility requires image ID and mode" type cr
|
||||||
1 (bye)
|
1 (bye)
|
||||||
then
|
then
|
||||||
3 arg 4 arg image-visibility
|
3 sarg 4 sarg image-visibility
|
||||||
0 (bye)
|
0 (bye)
|
||||||
then
|
then
|
||||||
|
|
||||||
|
|
@ -1828,7 +1918,7 @@
|
||||||
s" Error: --spawn requires image ID" type cr
|
s" Error: --spawn requires image ID" type cr
|
||||||
1 (bye)
|
1 (bye)
|
||||||
then
|
then
|
||||||
3 arg image-spawn
|
3 sarg image-spawn
|
||||||
0 (bye)
|
0 (bye)
|
||||||
then
|
then
|
||||||
|
|
||||||
|
|
@ -1838,7 +1928,7 @@
|
||||||
s" Error: --clone requires image ID" type cr
|
s" Error: --clone requires image ID" type cr
|
||||||
1 (bye)
|
1 (bye)
|
||||||
then
|
then
|
||||||
3 arg image-clone
|
3 sarg image-clone
|
||||||
0 (bye)
|
0 (bye)
|
||||||
then
|
then
|
||||||
|
|
||||||
|
|
@ -1860,8 +1950,21 @@
|
||||||
1 (bye)
|
1 (bye)
|
||||||
then
|
then
|
||||||
|
|
||||||
\ Get first argument (skip gforth and script name)
|
\ Check for --account N as first argument (before arg-shift is applied)
|
||||||
1 arg
|
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
|
||||||
|
|
||||||
\ Check for subcommands
|
\ Check for subcommands
|
||||||
2dup s" session" compare 0= if
|
2dup s" session" compare 0= if
|
||||||
|
|
|
||||||
|
|
@ -64,9 +64,11 @@
|
||||||
! ./un key [--extend]
|
! ./un key [--extend]
|
||||||
!
|
!
|
||||||
! Authentication (in priority order):
|
! Authentication (in priority order):
|
||||||
! 1. Environment variables: UNSANDBOX_PUBLIC_KEY + UNSANDBOX_SECRET_KEY
|
! 1. --account N flag -> accounts.csv row N (bypasses env vars)
|
||||||
! 2. Config file: ~/.unsandbox/accounts.csv (public_key,secret_key per line)
|
! 2. Environment variables: UNSANDBOX_PUBLIC_KEY + UNSANDBOX_SECRET_KEY
|
||||||
! 3. Legacy: UNSANDBOX_API_KEY (deprecated)
|
! 3. Config file: ~/.unsandbox/accounts.csv row 0 (or UNSANDBOX_ACCOUNT)
|
||||||
|
! 4. ./accounts.csv row 0
|
||||||
|
! 5. Legacy: UNSANDBOX_API_KEY (deprecated)
|
||||||
!
|
!
|
||||||
! Compile:
|
! Compile:
|
||||||
! gfortran -o un un.f90
|
! gfortran -o un un.f90
|
||||||
|
|
@ -190,32 +192,97 @@ module unsandbox_sdk
|
||||||
|
|
||||||
contains
|
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
|
! Subroutine: get_credentials
|
||||||
! Description: Get API credentials from environment or config file
|
! Description: Get API credentials from environment or config file
|
||||||
!
|
!
|
||||||
! Priority order:
|
! Priority order:
|
||||||
! 1. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY)
|
! 1. account_index >= 0 -> accounts.csv row N (bypasses env vars)
|
||||||
! 2. Config file (~/.unsandbox/accounts.csv)
|
! 2. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY)
|
||||||
! 3. Legacy UNSANDBOX_API_KEY (deprecated)
|
! 3. Config file (~/.unsandbox/accounts.csv row 0 or UNSANDBOX_ACCOUNT)
|
||||||
|
! 4. ./accounts.csv row 0
|
||||||
|
! 5. Legacy UNSANDBOX_API_KEY (deprecated)
|
||||||
!
|
!
|
||||||
! Arguments:
|
! Arguments:
|
||||||
! public_key - Output: API public key
|
! public_key - Output: API public key
|
||||||
! secret_key - Output: API secret key
|
! secret_key - Output: API secret key
|
||||||
! status - Output: 0 on success, non-zero on error
|
! status - Output: 0 on success, non-zero on error
|
||||||
|
! account_index - Optional input: if >= 0, load that CSV row directly
|
||||||
!--------------------------------------------------------------------------
|
!--------------------------------------------------------------------------
|
||||||
subroutine get_credentials(public_key, secret_key, status)
|
subroutine get_credentials(public_key, secret_key, status, account_index)
|
||||||
character(len=*), intent(out) :: public_key, secret_key
|
character(len=*), intent(out) :: public_key, secret_key
|
||||||
integer, intent(out) :: status
|
integer, intent(out) :: status
|
||||||
character(len=1024) :: home_dir, accounts_path, line, api_key
|
integer, intent(in), optional :: account_index
|
||||||
integer :: unit_num, ios
|
character(len=1024) :: home_dir, accounts_path, api_key, acct_env
|
||||||
logical :: file_exists
|
integer :: ios, acct_idx, default_index
|
||||||
|
|
||||||
status = 0
|
status = 0
|
||||||
public_key = ''
|
public_key = ''
|
||||||
secret_key = ''
|
secret_key = ''
|
||||||
|
|
||||||
! Priority 1: Environment variables
|
! 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
|
||||||
call get_environment_variable('UNSANDBOX_PUBLIC_KEY', public_key, status=ios)
|
call get_environment_variable('UNSANDBOX_PUBLIC_KEY', public_key, status=ios)
|
||||||
if (ios == 0 .and. len_trim(public_key) > 0) then
|
if (ios == 0 .and. len_trim(public_key) > 0) then
|
||||||
call get_environment_variable('UNSANDBOX_SECRET_KEY', secret_key, status=ios)
|
call get_environment_variable('UNSANDBOX_SECRET_KEY', secret_key, status=ios)
|
||||||
|
|
@ -224,37 +291,27 @@ contains
|
||||||
end if
|
end if
|
||||||
end if
|
end if
|
||||||
|
|
||||||
! Priority 2: Config file
|
! 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
|
||||||
|
|
||||||
call get_environment_variable('HOME', home_dir, status=ios)
|
call get_environment_variable('HOME', home_dir, status=ios)
|
||||||
if (ios == 0) then
|
if (ios == 0) then
|
||||||
accounts_path = trim(home_dir) // '/.unsandbox/accounts.csv'
|
accounts_path = trim(home_dir) // '/.unsandbox/accounts.csv'
|
||||||
inquire(file=trim(accounts_path), exist=file_exists)
|
call load_csv_row(accounts_path, default_index, public_key, secret_key)
|
||||||
if (file_exists) then
|
if (len_trim(public_key) > 0) return
|
||||||
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
|
end if
|
||||||
|
|
||||||
! Priority 3: Legacy API key
|
! 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
|
||||||
call get_environment_variable('UNSANDBOX_API_KEY', api_key, status=ios)
|
call get_environment_variable('UNSANDBOX_API_KEY', api_key, status=ios)
|
||||||
if (ios == 0 .and. len_trim(api_key) > 0) then
|
if (ios == 0 .and. len_trim(api_key) > 0) then
|
||||||
public_key = api_key
|
public_key = api_key
|
||||||
|
|
@ -850,6 +907,7 @@ program unsandbox_cli
|
||||||
character(len=1024) :: filename, language, api_key, ext, arg, subcommand
|
character(len=1024) :: filename, language, api_key, ext, arg, subcommand
|
||||||
character(len=256) :: session_id, service_id
|
character(len=256) :: session_id, service_id
|
||||||
integer :: stat, i, nargs, dot_pos
|
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
|
logical :: list_flag, is_session, is_service, is_key
|
||||||
|
|
||||||
! Initialize
|
! Initialize
|
||||||
|
|
@ -860,6 +918,7 @@ program unsandbox_cli
|
||||||
is_key = .false.
|
is_key = .false.
|
||||||
session_id = ''
|
session_id = ''
|
||||||
service_id = ''
|
service_id = ''
|
||||||
|
account_index = -1
|
||||||
|
|
||||||
! Get command line arguments count
|
! Get command line arguments count
|
||||||
nargs = command_argument_count()
|
nargs = command_argument_count()
|
||||||
|
|
@ -868,6 +927,17 @@ program unsandbox_cli
|
||||||
stop 1
|
stop 1
|
||||||
end if
|
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
|
! Check for subcommands
|
||||||
call get_command_argument(1, arg, status=stat)
|
call get_command_argument(1, arg, status=stat)
|
||||||
if (trim(arg) == '-h' .or. trim(arg) == '--help') then
|
if (trim(arg) == '-h' .or. trim(arg) == '--help') then
|
||||||
|
|
@ -974,6 +1044,9 @@ contains
|
||||||
write(*, '(A)') 'Languages options:'
|
write(*, '(A)') 'Languages options:'
|
||||||
write(*, '(A)') ' --json Output as JSON array'
|
write(*, '(A)') ' --json Output as JSON array'
|
||||||
write(*, '(A)') ''
|
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)') 'Library Usage:'
|
||||||
write(*, '(A)') ' use unsandbox_sdk'
|
write(*, '(A)') ' use unsandbox_sdk'
|
||||||
write(*, '(A)') ' type(unsandbox_client) :: client'
|
write(*, '(A)') ' type(unsandbox_client) :: client'
|
||||||
|
|
@ -1003,7 +1076,7 @@ contains
|
||||||
end if
|
end if
|
||||||
|
|
||||||
! Get API keys
|
! Get API keys
|
||||||
call get_credentials(public_key, secret_key, stat)
|
call get_credentials(public_key, secret_key, stat, account_index)
|
||||||
if (stat /= 0) then
|
if (stat /= 0) then
|
||||||
write(0, '(A)') 'Error: No credentials found. Set UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY'
|
write(0, '(A)') 'Error: No credentials found. Set UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY'
|
||||||
stop 1
|
stop 1
|
||||||
|
|
@ -1074,6 +1147,8 @@ contains
|
||||||
input_files = trim(arg)
|
input_files = trim(arg)
|
||||||
end if
|
end if
|
||||||
end if
|
end if
|
||||||
|
else if (trim(arg) == '--account') then
|
||||||
|
! already processed in main pre-scan; skip this token and its value
|
||||||
else
|
else
|
||||||
if (len_trim(arg) > 0) then
|
if (len_trim(arg) > 0) then
|
||||||
if (arg(1:1) == '-') then
|
if (arg(1:1) == '-') then
|
||||||
|
|
@ -1086,7 +1161,7 @@ contains
|
||||||
end do
|
end do
|
||||||
|
|
||||||
! Get API keys
|
! Get API keys
|
||||||
call get_credentials(public_key, secret_key, stat)
|
call get_credentials(public_key, secret_key, stat, account_index)
|
||||||
if (stat /= 0) then
|
if (stat /= 0) then
|
||||||
write(0, '(A)') 'Error: No credentials found'
|
write(0, '(A)') 'Error: No credentials found'
|
||||||
stop 1
|
stop 1
|
||||||
|
|
@ -1289,7 +1364,7 @@ contains
|
||||||
end do
|
end do
|
||||||
|
|
||||||
! Get API keys
|
! Get API keys
|
||||||
call get_credentials(public_key, secret_key, stat)
|
call get_credentials(public_key, secret_key, stat, account_index)
|
||||||
if (stat /= 0) then
|
if (stat /= 0) then
|
||||||
write(0, '(A)') 'Error: No credentials found'
|
write(0, '(A)') 'Error: No credentials found'
|
||||||
stop 1
|
stop 1
|
||||||
|
|
@ -1617,7 +1692,7 @@ contains
|
||||||
end do
|
end do
|
||||||
|
|
||||||
! Get credentials
|
! Get credentials
|
||||||
call get_credentials(public_key, secret_key, stat)
|
call get_credentials(public_key, secret_key, stat, account_index)
|
||||||
if (stat /= 0) then
|
if (stat /= 0) then
|
||||||
write(0, '(A)') 'Error: No credentials found'
|
write(0, '(A)') 'Error: No credentials found'
|
||||||
stop 1
|
stop 1
|
||||||
|
|
@ -1771,7 +1846,7 @@ contains
|
||||||
list_mode = .false.
|
list_mode = .false.
|
||||||
|
|
||||||
! Get credentials
|
! Get credentials
|
||||||
call get_credentials(public_key, secret_key, stat)
|
call get_credentials(public_key, secret_key, stat, account_index)
|
||||||
if (stat /= 0) then
|
if (stat /= 0) then
|
||||||
write(0, '(A)') 'Error: No credentials found'
|
write(0, '(A)') 'Error: No credentials found'
|
||||||
stop 1
|
stop 1
|
||||||
|
|
@ -2046,7 +2121,7 @@ contains
|
||||||
end do
|
end do
|
||||||
|
|
||||||
! Get API key
|
! Get API key
|
||||||
call get_credentials(public_key, secret_key, stat)
|
call get_credentials(public_key, secret_key, stat, account_index)
|
||||||
if (stat /= 0) then
|
if (stat /= 0) then
|
||||||
write(0, '(A)') 'Error: No credentials found'
|
write(0, '(A)') 'Error: No credentials found'
|
||||||
stop 1
|
stop 1
|
||||||
|
|
@ -2143,7 +2218,7 @@ contains
|
||||||
end do
|
end do
|
||||||
|
|
||||||
! Get API keys
|
! Get API keys
|
||||||
call get_credentials(public_key, secret_key, stat)
|
call get_credentials(public_key, secret_key, stat, account_index)
|
||||||
if (stat /= 0) then
|
if (stat /= 0) then
|
||||||
write(0, '(A)') 'Error: No credentials found'
|
write(0, '(A)') 'Error: No credentials found'
|
||||||
stop 1
|
stop 1
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,20 @@
|
||||||
#!/bin/bash
|
#!/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
|
# Test suite for Fortran Unsandbox SDK
|
||||||
# Run: bash tests/test_un.sh
|
# Run: bash tests/test_un.sh
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -77,6 +77,7 @@ type Args = {
|
||||||
mutable Command: string option
|
mutable Command: string option
|
||||||
mutable SourceFile: string option
|
mutable SourceFile: string option
|
||||||
mutable ApiKey: string option
|
mutable ApiKey: string option
|
||||||
|
mutable AccountIndex: int option
|
||||||
mutable Network: string option
|
mutable Network: string option
|
||||||
mutable Vcpu: int
|
mutable Vcpu: int
|
||||||
Env: ResizeArray<string>
|
Env: ResizeArray<string>
|
||||||
|
|
@ -144,19 +145,70 @@ type Args = {
|
||||||
mutable ImagePorts: string option
|
mutable ImagePorts: string option
|
||||||
}
|
}
|
||||||
|
|
||||||
let getApiKeys (argsKey: string option) =
|
let loadCredentialsFromCsv (csvPath: string) (accountIndex: int) =
|
||||||
let publicKey = Environment.GetEnvironmentVariable("UNSANDBOX_PUBLIC_KEY")
|
if File.Exists(csvPath) then
|
||||||
let secretKey = Environment.GetEnvironmentVariable("UNSANDBOX_SECRET_KEY")
|
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
|
||||||
|
|
||||||
// Fall back to UNSANDBOX_API_KEY for backwards compatibility
|
let getApiKeys (argsKey: string option) (accountIndex: int option) =
|
||||||
if String.IsNullOrEmpty(publicKey) || String.IsNullOrEmpty(secretKey) then
|
let home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile)
|
||||||
let legacyKey = match argsKey with | Some k -> k | None -> Environment.GetEnvironmentVariable("UNSANDBOX_API_KEY")
|
let homeCsv = Path.Combine(home, ".unsandbox", "accounts.csv")
|
||||||
if String.IsNullOrEmpty(legacyKey) then
|
|
||||||
eprintfn "%sError: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set%s" red reset
|
// 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
|
||||||
exit 1
|
exit 1
|
||||||
(legacyKey, null)
|
| None ->
|
||||||
else
|
let publicKey = Environment.GetEnvironmentVariable("UNSANDBOX_PUBLIC_KEY")
|
||||||
(publicKey, secretKey)
|
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
|
||||||
|
|
||||||
let detectLanguage (filename: string) =
|
let detectLanguage (filename: string) =
|
||||||
let dotIndex = filename.LastIndexOf('.')
|
let dotIndex = filename.LastIndexOf('.')
|
||||||
|
|
@ -631,7 +683,7 @@ let cmdServiceEnv (args: Args) (publicKey: string) (secretKey: string) =
|
||||||
exit 1
|
exit 1
|
||||||
|
|
||||||
let cmdExecute (args: Args) =
|
let cmdExecute (args: Args) =
|
||||||
let (publicKey, secretKey) = getApiKeys args.ApiKey
|
let (publicKey, secretKey) = getApiKeys args.ApiKey args.AccountIndex
|
||||||
let code = File.ReadAllText(args.SourceFile.Value)
|
let code = File.ReadAllText(args.SourceFile.Value)
|
||||||
let language = detectLanguage args.SourceFile.Value
|
let language = detectLanguage args.SourceFile.Value
|
||||||
|
|
||||||
|
|
@ -680,7 +732,7 @@ let cmdExecute (args: Args) =
|
||||||
exit exitCode
|
exit exitCode
|
||||||
|
|
||||||
let cmdSession (args: Args) =
|
let cmdSession (args: Args) =
|
||||||
let (publicKey, secretKey) = getApiKeys args.ApiKey
|
let (publicKey, secretKey) = getApiKeys args.ApiKey args.AccountIndex
|
||||||
|
|
||||||
if args.SessionSnapshot.IsSome then
|
if args.SessionSnapshot.IsSome then
|
||||||
let mutable payload = []
|
let mutable payload = []
|
||||||
|
|
@ -740,7 +792,7 @@ let openBrowser (url: string) =
|
||||||
eprintfn "%sError opening browser: %s%s" red ex.Message reset
|
eprintfn "%sError opening browser: %s%s" red ex.Message reset
|
||||||
|
|
||||||
let cmdKey (args: Args) =
|
let cmdKey (args: Args) =
|
||||||
let (publicKey, secretKey) = getApiKeys args.ApiKey
|
let (publicKey, secretKey) = getApiKeys args.ApiKey args.AccountIndex
|
||||||
|
|
||||||
ServicePointManager.SecurityProtocol <- SecurityProtocolType.Tls12 ||| SecurityProtocolType.Tls11 ||| SecurityProtocolType.Tls
|
ServicePointManager.SecurityProtocol <- SecurityProtocolType.Tls12 ||| SecurityProtocolType.Tls11 ||| SecurityProtocolType.Tls
|
||||||
|
|
||||||
|
|
@ -817,7 +869,7 @@ let cmdKey (args: Args) =
|
||||||
exit 1
|
exit 1
|
||||||
|
|
||||||
let cmdLanguages (args: Args) =
|
let cmdLanguages (args: Args) =
|
||||||
let (publicKey, secretKey) = getApiKeys args.ApiKey
|
let (publicKey, secretKey) = getApiKeys args.ApiKey args.AccountIndex
|
||||||
|
|
||||||
// Try to load from cache first
|
// Try to load from cache first
|
||||||
let cachedResponse = loadLanguagesCache ()
|
let cachedResponse = loadLanguagesCache ()
|
||||||
|
|
@ -872,7 +924,7 @@ let cmdLanguages (args: Args) =
|
||||||
printfn "%s" lang
|
printfn "%s" lang
|
||||||
|
|
||||||
let cmdImage (args: Args) =
|
let cmdImage (args: Args) =
|
||||||
let (publicKey, secretKey) = getApiKeys args.ApiKey
|
let (publicKey, secretKey) = getApiKeys args.ApiKey args.AccountIndex
|
||||||
|
|
||||||
if args.ImageList then
|
if args.ImageList then
|
||||||
let result = apiRequest "/images" "GET" None publicKey secretKey
|
let result = apiRequest "/images" "GET" None publicKey secretKey
|
||||||
|
|
@ -928,7 +980,7 @@ let cmdImage (args: Args) =
|
||||||
exit 1
|
exit 1
|
||||||
|
|
||||||
let cmdSnapshot (args: Args) =
|
let cmdSnapshot (args: Args) =
|
||||||
let (publicKey, secretKey) = getApiKeys args.ApiKey
|
let (publicKey, secretKey) = getApiKeys args.ApiKey args.AccountIndex
|
||||||
|
|
||||||
if args.SnapshotList then
|
if args.SnapshotList then
|
||||||
let result = apiRequest "/snapshots" "GET" None publicKey secretKey
|
let result = apiRequest "/snapshots" "GET" None publicKey secretKey
|
||||||
|
|
@ -959,7 +1011,7 @@ let cmdSnapshot (args: Args) =
|
||||||
exit 1
|
exit 1
|
||||||
|
|
||||||
let cmdService (args: Args) =
|
let cmdService (args: Args) =
|
||||||
let (publicKey, secretKey) = getApiKeys args.ApiKey
|
let (publicKey, secretKey) = getApiKeys args.ApiKey args.AccountIndex
|
||||||
|
|
||||||
// Handle env subcommand
|
// Handle env subcommand
|
||||||
if args.EnvAction.IsSome then
|
if args.EnvAction.IsSome then
|
||||||
|
|
@ -1107,6 +1159,7 @@ let parseArgs (argv: string[]) =
|
||||||
Command = None
|
Command = None
|
||||||
SourceFile = None
|
SourceFile = None
|
||||||
ApiKey = None
|
ApiKey = None
|
||||||
|
AccountIndex = None
|
||||||
Network = None
|
Network = None
|
||||||
Vcpu = 0
|
Vcpu = 0
|
||||||
Env = ResizeArray<string>()
|
Env = ResizeArray<string>()
|
||||||
|
|
@ -1192,6 +1245,13 @@ let parseArgs (argv: string[]) =
|
||||||
i <- i + 1
|
i <- i + 1
|
||||||
args.EnvTarget <- Some argv.[i]
|
args.EnvTarget <- Some argv.[i]
|
||||||
| "-k" | "--api-key" -> i <- i + 1; args.ApiKey <- 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]
|
| "-n" | "--network" -> i <- i + 1; args.Network <- Some argv.[i]
|
||||||
| "-v" | "--vcpu" -> i <- i + 1; args.Vcpu <- int argv.[i]
|
| "-v" | "--vcpu" -> i <- i + 1; args.Vcpu <- int argv.[i]
|
||||||
| "-e" | "--env" -> i <- i + 1; args.Env.Add(argv.[i])
|
| "-e" | "--env" -> i <- i + 1; args.Env.Add(argv.[i])
|
||||||
|
|
@ -1358,6 +1418,7 @@ let printHelp () =
|
||||||
printfn " -n MODE Network mode (zerotrust/semitrusted)"
|
printfn " -n MODE Network mode (zerotrust/semitrusted)"
|
||||||
printfn " -v N vCPU count (1-8)"
|
printfn " -v N vCPU count (1-8)"
|
||||||
printfn " -k KEY API key"
|
printfn " -k KEY API key"
|
||||||
|
printfn " --account N Use accounts.csv row N (bypasses env vars)"
|
||||||
printfn ""
|
printfn ""
|
||||||
printfn "Session options:"
|
printfn "Session options:"
|
||||||
printfn " --list List active sessions"
|
printfn " --list List active sessions"
|
||||||
|
|
|
||||||
|
|
@ -5,17 +5,13 @@
|
||||||
# - async/ : Asynchronous Go SDK (goroutines/channels)
|
# - async/ : Asynchronous Go SDK (goroutines/channels)
|
||||||
#
|
#
|
||||||
# Usage:
|
# Usage:
|
||||||
# make # Build all
|
# make test # Run all 4 test modes (auto-detects go binary)
|
||||||
# make test # Run all 4 test modes
|
|
||||||
# make test-cli # CLI mode only
|
# make test-cli # CLI mode only
|
||||||
# make test-library # Library 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 build # Build binaries
|
||||||
# make clean # Remove build artifacts
|
# make clean # Remove build artifacts
|
||||||
#
|
#
|
||||||
# Dependencies:
|
# The Makefile auto-detects go from PATH, ~/.local/go, /usr/local/go.
|
||||||
# Go 1.18+ (for generics support)
|
|
||||||
|
|
||||||
.PHONY: all build test test-cli test-library test-integration test-functional
|
.PHONY: all build test test-cli test-library test-integration test-functional
|
||||||
.PHONY: test-sync test-async clean help examples fmt vet
|
.PHONY: test-sync test-async clean help examples fmt vet
|
||||||
|
|
@ -25,8 +21,11 @@ ROOT_DIR := $(shell cd ../.. && pwd)
|
||||||
SYNC_DIR := sync
|
SYNC_DIR := sync
|
||||||
ASYNC_DIR := async
|
ASYNC_DIR := async
|
||||||
|
|
||||||
# Go settings
|
# Auto-detect Go binary: PATH first, then common install locations
|
||||||
GO := go
|
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))
|
||||||
GOFLAGS := -v
|
GOFLAGS := -v
|
||||||
|
|
||||||
# Colors
|
# Colors
|
||||||
|
|
@ -40,6 +39,14 @@ NC := \033[0m
|
||||||
help:
|
help:
|
||||||
@echo "UN Go Client - Build and Test"
|
@echo "UN Go Client - Build and Test"
|
||||||
@echo ""
|
@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 "Build:"
|
||||||
@echo " make build Build all binaries"
|
@echo " make build Build all binaries"
|
||||||
@echo " make build-sync Build sync SDK"
|
@echo " make build-sync Build sync SDK"
|
||||||
|
|
@ -62,18 +69,25 @@ help:
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo "Utility:"
|
@echo "Utility:"
|
||||||
@echo " make clean Remove build artifacts"
|
@echo " make clean Remove build artifacts"
|
||||||
@echo " make deps Show required dependencies"
|
|
||||||
@echo " make examples Run examples"
|
|
||||||
@echo ""
|
@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
|
all: build
|
||||||
|
|
||||||
deps:
|
|
||||||
@echo "Required:"
|
|
||||||
@echo " Go 1.18+ (https://golang.org/dl/)"
|
|
||||||
@echo ""
|
|
||||||
@go version
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# BUILD
|
# BUILD
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
@ -81,7 +95,7 @@ deps:
|
||||||
build: build-sync build-async
|
build: build-sync build-async
|
||||||
@echo "$(GREEN)✓ All Go SDKs built$(NC)"
|
@echo "$(GREEN)✓ All Go SDKs built$(NC)"
|
||||||
|
|
||||||
build-sync:
|
build-sync: check-go $(SYNC_DIR)/go.mod
|
||||||
@echo "Building sync SDK..."
|
@echo "Building sync SDK..."
|
||||||
@if [ -f "$(SYNC_DIR)/src/un.go" ]; then \
|
@if [ -f "$(SYNC_DIR)/src/un.go" ]; then \
|
||||||
cd $(SYNC_DIR)/src && $(GO) build $(GOFLAGS) -o ../un . 2>&1 | head -5 || true; \
|
cd $(SYNC_DIR)/src && $(GO) build $(GOFLAGS) -o ../un . 2>&1 | head -5 || true; \
|
||||||
|
|
@ -90,7 +104,7 @@ build-sync:
|
||||||
echo "$(YELLOW)⊘$(NC) Sync SDK source not found"; \
|
echo "$(YELLOW)⊘$(NC) Sync SDK source not found"; \
|
||||||
fi
|
fi
|
||||||
|
|
||||||
build-async:
|
build-async: check-go
|
||||||
@echo "Building async SDK..."
|
@echo "Building async SDK..."
|
||||||
@if [ -f "$(ASYNC_DIR)/src/un.go" ]; then \
|
@if [ -f "$(ASYNC_DIR)/src/un.go" ]; then \
|
||||||
cd $(ASYNC_DIR)/src && $(GO) build $(GOFLAGS) -o ../un . 2>&1 | head -5 || true; \
|
cd $(ASYNC_DIR)/src && $(GO) build $(GOFLAGS) -o ../un . 2>&1 | head -5 || true; \
|
||||||
|
|
@ -111,22 +125,19 @@ test: test-cli test-library test-integration test-functional
|
||||||
# TEST: CLI Mode
|
# TEST: CLI Mode
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
test-cli:
|
test-cli: check-go
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
@echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
@echo "CLI MODE: Testing Go CLI interface"
|
@echo "CLI MODE: Testing Go CLI interface"
|
||||||
@echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
@echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
@echo ""
|
@echo ""
|
||||||
@# Test root-level un.go if it exists
|
|
||||||
@if [ -f "$(ROOT_DIR)/un.go" ]; then \
|
@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)"; \
|
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
|
fi
|
||||||
@# Test sync SDK CLI
|
|
||||||
@if [ -f "$(SYNC_DIR)/src/un.go" ]; then \
|
@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"; \
|
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; \
|
rm -f /tmp/un_test; \
|
||||||
fi
|
fi
|
||||||
@# Test async SDK CLI
|
|
||||||
@if [ -f "$(ASYNC_DIR)/src/un.go" ]; then \
|
@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"; \
|
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; \
|
rm -f /tmp/un_test 2>/dev/null || true; \
|
||||||
|
|
@ -136,26 +147,34 @@ test-cli:
|
||||||
# TEST: Library Mode
|
# TEST: Library Mode
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
test-library:
|
test-library: check-go
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
@echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
@echo "LIBRARY MODE: Testing Go package imports"
|
@echo "LIBRARY MODE: Testing Go package imports"
|
||||||
@echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
@echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
@echo ""
|
@echo ""
|
||||||
@# Test sync SDK with go test
|
@# Go requires test files in the same directory as the package.
|
||||||
@if [ -d "$(SYNC_DIR)/src" ]; then \
|
@# Copy tests into src/ temporarily, run, clean up.
|
||||||
cd $(SYNC_DIR)/src && $(GO) test -v ./... 2>&1 | head -20 || echo " $(YELLOW)⊘$(NC) Library: No tests defined yet"; \
|
@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"; \
|
||||||
fi
|
fi
|
||||||
@# Test async SDK with go test
|
@if [ -d "$(ASYNC_DIR)/tests" ] && [ -d "$(ASYNC_DIR)/src" ]; then \
|
||||||
@if [ -d "$(ASYNC_DIR)/src" ]; then \
|
cp $(ASYNC_DIR)/tests/*_test.go $(ASYNC_DIR)/src/ 2>/dev/null; \
|
||||||
cd $(ASYNC_DIR)/src && $(GO) test -v ./... 2>&1 | head -20 || echo " $(YELLOW)⊘$(NC) Library: Async tests not defined"; \
|
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"; \
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# TEST: Integration Mode
|
# TEST: Integration Mode
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
test-integration:
|
test-integration: check-go
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
@echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
@echo "INTEGRATION MODE: Testing API contract"
|
@echo "INTEGRATION MODE: Testing API contract"
|
||||||
|
|
@ -167,7 +186,7 @@ test-integration:
|
||||||
else \
|
else \
|
||||||
echo " Testing API authentication..."; \
|
echo " Testing API authentication..."; \
|
||||||
if [ -f "$(ROOT_DIR)/un.go" ]; then \
|
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 " $(YELLOW)⊘$(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 " $(RED)✗$(NC) Integration: Check API connectivity"; \
|
||||||
fi; \
|
fi; \
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
@ -175,7 +194,7 @@ test-integration:
|
||||||
# TEST: Functional Mode
|
# TEST: Functional Mode
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
test-functional:
|
test-functional: check-go
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
@echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
@echo "FUNCTIONAL MODE: Real-world scenarios"
|
@echo "FUNCTIONAL MODE: Real-world scenarios"
|
||||||
|
|
@ -185,8 +204,10 @@ test-functional:
|
||||||
echo " $(YELLOW)⊘$(NC) Skipping (no API credentials)"; \
|
echo " $(YELLOW)⊘$(NC) Skipping (no API credentials)"; \
|
||||||
else \
|
else \
|
||||||
echo " Running functional tests..."; \
|
echo " Running functional tests..."; \
|
||||||
if [ -f "$(ROOT_DIR)/un.go" ]; then \
|
if [ -d "$(SYNC_DIR)/tests" ] && [ -d "$(SYNC_DIR)/src" ]; 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)"; \
|
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; \
|
||||||
fi; \
|
fi; \
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
@ -194,18 +215,26 @@ test-functional:
|
||||||
# TEST: By SDK Type
|
# TEST: By SDK Type
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
test-sync:
|
test-sync: check-go
|
||||||
@echo "Testing Sync SDK..."
|
@echo "Testing Sync SDK..."
|
||||||
@if [ -d "$(SYNC_DIR)/src" ]; then \
|
@if [ -d "$(SYNC_DIR)/tests" ] && [ -d "$(SYNC_DIR)/src" ]; then \
|
||||||
cd $(SYNC_DIR)/src && $(GO) test -v ./...; \
|
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 .; \
|
||||||
else \
|
else \
|
||||||
echo " $(YELLOW)⊘$(NC) Sync SDK not found"; \
|
echo " $(YELLOW)⊘$(NC) Sync SDK not found"; \
|
||||||
fi
|
fi
|
||||||
|
|
||||||
test-async:
|
test-async: check-go
|
||||||
@echo "Testing Async SDK..."
|
@echo "Testing Async SDK..."
|
||||||
@if [ -d "$(ASYNC_DIR)/src" ]; then \
|
@if [ -d "$(ASYNC_DIR)/tests" ] && [ -d "$(ASYNC_DIR)/src" ]; then \
|
||||||
cd $(ASYNC_DIR)/src && $(GO) test -v ./...; \
|
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 .; \
|
||||||
else \
|
else \
|
||||||
echo " $(YELLOW)⊘$(NC) Async SDK not found"; \
|
echo " $(YELLOW)⊘$(NC) Async SDK not found"; \
|
||||||
fi
|
fi
|
||||||
|
|
@ -214,14 +243,14 @@ test-async:
|
||||||
# Code Quality
|
# Code Quality
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
fmt:
|
fmt: check-go
|
||||||
@echo "Formatting Go code..."
|
@echo "Formatting Go code..."
|
||||||
@if [ -d "$(SYNC_DIR)/src" ]; then gofmt -w $(SYNC_DIR)/src/; fi
|
@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 [ -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
|
@if [ -f "$(ROOT_DIR)/un.go" ]; then gofmt -w $(ROOT_DIR)/un.go; fi
|
||||||
@echo "$(GREEN)✓$(NC) Format complete"
|
@echo "$(GREEN)✓$(NC) Format complete"
|
||||||
|
|
||||||
vet:
|
vet: check-go
|
||||||
@echo "Running go vet..."
|
@echo "Running go vet..."
|
||||||
@if [ -d "$(SYNC_DIR)/src" ]; then cd $(SYNC_DIR)/src && $(GO) vet ./... 2>&1 || true; fi
|
@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
|
@if [ -d "$(ASYNC_DIR)/src" ]; then cd $(ASYNC_DIR)/src && $(GO) vet ./... 2>&1 || true; fi
|
||||||
|
|
@ -231,7 +260,7 @@ vet:
|
||||||
# Examples
|
# Examples
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
examples:
|
examples: check-go
|
||||||
@echo "Running Go examples..."
|
@echo "Running Go examples..."
|
||||||
@if [ -d "$(SYNC_DIR)/examples" ]; then \
|
@if [ -d "$(SYNC_DIR)/examples" ]; then \
|
||||||
for f in $(SYNC_DIR)/examples/*.go; do \
|
for f in $(SYNC_DIR)/examples/*.go; do \
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,19 @@
|
||||||
|
// 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 - standalone version
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,19 @@
|
||||||
|
// 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 - standalone version
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,19 @@
|
||||||
|
// 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 - standalone version
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,90 +1,18 @@
|
||||||
/*
|
// This is free software for the public good of a permacomputer hosted at
|
||||||
PUBLIC DOMAIN - NO LICENSE, NO WARRANTY
|
// permacomputer.com, an always-on computer by the people, for the people.
|
||||||
|
// One which is durable, easy to repair, & distributed like tap water
|
||||||
unsandbox.com Go SDK (Asynchronous)
|
// for machine learning intelligence.
|
||||||
|
//
|
||||||
Library Usage:
|
// The permacomputer is community-owned infrastructure optimized around
|
||||||
import "un_async"
|
// four values:
|
||||||
|
//
|
||||||
// Create credentials
|
// TRUTH First principles, math & science, open source code freely distributed
|
||||||
creds, err := un_async.ResolveCredentials("", "")
|
// FREEDOM Voluntary partnerships, freedom from tyranny & corporate control
|
||||||
if err != nil {
|
// HARMONY Minimal waste, self-renewing systems with diverse thriving connections
|
||||||
log.Fatal(err)
|
// 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.
|
||||||
// Execute code asynchronously (returns channel)
|
// Code is seeds to sprout on any abandoned technology.
|
||||||
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 <public_key> (identifies account)
|
|
||||||
X-Timestamp: <unix_seconds> (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
|
package un_async
|
||||||
|
|
||||||
|
|
|
||||||
396
clients/go/async/src/un_async_test.go
Normal file
396
clients/go/async/src/un_async_test.go
Normal file
|
|
@ -0,0 +1,396 @@
|
||||||
|
// 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,19 @@
|
||||||
|
// 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)
|
Tests for the unsandbox Go SDK (Asynchronous)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,19 @@
|
||||||
|
// 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
|
package main
|
||||||
|
|
||||||
import "fmt"
|
import "fmt"
|
||||||
|
|
|
||||||
200
clients/go/sync/src/client.go
Normal file
200
clients/go/sync/src/client.go
Normal file
|
|
@ -0,0 +1,200 @@
|
||||||
|
// 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")
|
||||||
|
}
|
||||||
181
clients/go/sync/src/functional_test.go
Normal file
181
clients/go/sync/src/functional_test.go
Normal file
|
|
@ -0,0 +1,181 @@
|
||||||
|
// 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))
|
||||||
|
}
|
||||||
3
clients/go/sync/src/go.mod
Normal file
3
clients/go/sync/src/go.mod
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
module github.com/russellballestrini/un-inception/clients/go/sync/src
|
||||||
|
|
||||||
|
go 1.23.6
|
||||||
|
|
@ -1,81 +1,18 @@
|
||||||
/*
|
// This is free software for the public good of a permacomputer hosted at
|
||||||
PUBLIC DOMAIN - NO LICENSE, NO WARRANTY
|
// permacomputer.com, an always-on computer by the people, for the people.
|
||||||
|
// One which is durable, easy to repair, & distributed like tap water
|
||||||
unsandbox.com Go SDK (Synchronous)
|
// for machine learning intelligence.
|
||||||
|
//
|
||||||
Library Usage:
|
// The permacomputer is community-owned infrastructure optimized around
|
||||||
import "un"
|
// four values:
|
||||||
|
//
|
||||||
// Create credentials
|
// TRUTH First principles, math & science, open source code freely distributed
|
||||||
creds, err := un.ResolveCredentials("", "")
|
// FREEDOM Voluntary partnerships, freedom from tyranny & corporate control
|
||||||
if err != nil {
|
// HARMONY Minimal waste, self-renewing systems with diverse thriving connections
|
||||||
log.Fatal(err)
|
// 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.
|
||||||
// Execute code synchronously
|
// Code is seeds to sprout on any abandoned technology.
|
||||||
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 <public_key> (identifies account)
|
|
||||||
X-Timestamp: <unix_seconds> (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
|
package un
|
||||||
|
|
||||||
|
|
@ -84,6 +21,7 @@ import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"crypto/hmac"
|
"crypto/hmac"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
|
"encoding/base64"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
@ -223,10 +161,12 @@ func loadCredentialsFromCsv(csvPath string, accountIndex int) *Credentials {
|
||||||
//
|
//
|
||||||
// Priority:
|
// Priority:
|
||||||
// 1. Function arguments (publicKey, secretKey non-empty)
|
// 1. Function arguments (publicKey, secretKey non-empty)
|
||||||
// 2. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY)
|
// 2. accountIndex >= 0 → load from accounts.csv row N (before env vars)
|
||||||
// 3. ~/.unsandbox/accounts.csv
|
// 3. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY)
|
||||||
// 4. ./accounts.csv
|
// 4. Default CSV lookup (account 0 or UNSANDBOX_ACCOUNT env)
|
||||||
func ResolveCredentials(publicKey, secretKey string) (*Credentials, error) {
|
//
|
||||||
|
// Pass accountIndex = -1 to mean "not specified".
|
||||||
|
func ResolveCredentials(publicKey, secretKey string, accountIndex int) (*Credentials, error) {
|
||||||
// Tier 1: Function arguments
|
// Tier 1: Function arguments
|
||||||
if publicKey != "" && secretKey != "" {
|
if publicKey != "" && secretKey != "" {
|
||||||
return &Credentials{
|
return &Credentials{
|
||||||
|
|
@ -235,7 +175,23 @@ func ResolveCredentials(publicKey, secretKey string) (*Credentials, error) {
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tier 2: Environment variables
|
// 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
|
||||||
envPk := os.Getenv("UNSANDBOX_PUBLIC_KEY")
|
envPk := os.Getenv("UNSANDBOX_PUBLIC_KEY")
|
||||||
envSk := os.Getenv("UNSANDBOX_SECRET_KEY")
|
envSk := os.Getenv("UNSANDBOX_SECRET_KEY")
|
||||||
if envPk != "" && envSk != "" {
|
if envPk != "" && envSk != "" {
|
||||||
|
|
@ -245,35 +201,36 @@ func ResolveCredentials(publicKey, secretKey string) (*Credentials, error) {
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine account index
|
// Determine default account index from env
|
||||||
accountIndex := 0
|
defaultIndex := 0
|
||||||
if envAccount := os.Getenv("UNSANDBOX_ACCOUNT"); envAccount != "" {
|
if envAccount := os.Getenv("UNSANDBOX_ACCOUNT"); envAccount != "" {
|
||||||
var err error
|
var err error
|
||||||
accountIndex, err = strconv.Atoi(envAccount)
|
defaultIndex, err = strconv.Atoi(envAccount)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
accountIndex = 0
|
defaultIndex = 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tier 3: ~/.unsandbox/accounts.csv
|
// Tier 4: ~/.unsandbox/accounts.csv
|
||||||
unsandboxDir, err := getUnsandboxDir()
|
unsandboxDir, err := getUnsandboxDir()
|
||||||
if err == nil {
|
if err == nil {
|
||||||
if creds := loadCredentialsFromCsv(filepath.Join(unsandboxDir, "accounts.csv"), accountIndex); creds != nil {
|
if creds := loadCredentialsFromCsv(filepath.Join(unsandboxDir, "accounts.csv"), defaultIndex); creds != nil {
|
||||||
return creds, nil
|
return creds, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tier 4: ./accounts.csv
|
// Tier 5: ./accounts.csv
|
||||||
if creds := loadCredentialsFromCsv("accounts.csv", accountIndex); creds != nil {
|
if creds := loadCredentialsFromCsv("accounts.csv", defaultIndex); creds != nil {
|
||||||
return creds, nil
|
return creds, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, &CredentialsError{
|
return nil, &CredentialsError{
|
||||||
Message: "No credentials found. Please provide via:\n" +
|
Message: "No credentials found. Please provide via:\n" +
|
||||||
" 1. Function arguments (publicKey, secretKey)\n" +
|
" 1. Function arguments (publicKey, secretKey)\n" +
|
||||||
" 2. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY)\n" +
|
" 2. --account N flag (CSV row N)\n" +
|
||||||
" 3. ~/.unsandbox/accounts.csv\n" +
|
" 3. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY)\n" +
|
||||||
" 4. ./accounts.csv",
|
" 4. ~/.unsandbox/accounts.csv\n" +
|
||||||
|
" 5. ./accounts.csv",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -903,12 +860,19 @@ func ShellSession(creds *Credentials, sessionID, command string) (map[string]int
|
||||||
// Service Operations
|
// 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.
|
// ServiceOptions contains optional parameters for service creation.
|
||||||
type ServiceOptions struct {
|
type ServiceOptions struct {
|
||||||
NetworkMode string // "zerotrust" (default) or "semitrusted"
|
NetworkMode string // "zerotrust" (default) or "semitrusted"
|
||||||
Shell string // Shell to use for bootstrap
|
Shell string // Shell to use for bootstrap
|
||||||
VCPU int // Number of virtual CPUs
|
VCPU int // Number of virtual CPUs
|
||||||
UnfreezeOnDemand bool // Enable automatic unfreezing on HTTP request
|
UnfreezeOnDemand bool // Enable automatic unfreezing on HTTP request
|
||||||
|
InputFiles []InputFile // Files to include (written to /tmp/ in container)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServiceUpdateOptions contains optional parameters for service updates.
|
// ServiceUpdateOptions contains optional parameters for service updates.
|
||||||
|
|
@ -965,6 +929,9 @@ func CreateService(creds *Credentials, name string, ports []int, bootstrap strin
|
||||||
if opts.UnfreezeOnDemand {
|
if opts.UnfreezeOnDemand {
|
||||||
data["unfreeze_on_demand"] = true
|
data["unfreeze_on_demand"] = true
|
||||||
}
|
}
|
||||||
|
if len(opts.InputFiles) > 0 {
|
||||||
|
data["input_files"] = opts.InputFiles
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return makeRequest("POST", "/services", creds, data)
|
return makeRequest("POST", "/services", creds, data)
|
||||||
|
|
@ -1080,11 +1047,15 @@ func ExportServiceEnv(creds *Credentials, serviceID string) (map[string]interfac
|
||||||
// creds: API credentials
|
// creds: API credentials
|
||||||
// serviceID: Service ID
|
// serviceID: Service ID
|
||||||
// bootstrap: New bootstrap script (empty string to keep existing)
|
// bootstrap: New bootstrap script (empty string to keep existing)
|
||||||
func RedeployService(creds *Credentials, serviceID string, bootstrap string) (map[string]interface{}, error) {
|
// inputFiles: Optional files to include (written to /tmp/ in container)
|
||||||
|
func RedeployService(creds *Credentials, serviceID string, bootstrap string, inputFiles []InputFile) (map[string]interface{}, error) {
|
||||||
data := make(map[string]interface{})
|
data := make(map[string]interface{})
|
||||||
if bootstrap != "" {
|
if bootstrap != "" {
|
||||||
data["bootstrap"] = bootstrap
|
data["bootstrap"] = bootstrap
|
||||||
}
|
}
|
||||||
|
if len(inputFiles) > 0 {
|
||||||
|
data["input_files"] = inputFiles
|
||||||
|
}
|
||||||
return makeRequest("POST", fmt.Sprintf("/services/%s/redeploy", serviceID), creds, data)
|
return makeRequest("POST", fmt.Sprintf("/services/%s/redeploy", serviceID), creds, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1695,18 +1666,19 @@ const (
|
||||||
// CLIOptions holds parsed CLI arguments
|
// CLIOptions holds parsed CLI arguments
|
||||||
type CLIOptions struct {
|
type CLIOptions struct {
|
||||||
// Global options
|
// Global options
|
||||||
Shell string
|
Shell string
|
||||||
Env []string
|
Env []string
|
||||||
Files []string
|
Files []string
|
||||||
FilePaths []string
|
FilePaths []string
|
||||||
Artifacts bool
|
Artifacts bool
|
||||||
OutputDir string
|
OutputDir string
|
||||||
PublicKey string
|
PublicKey string
|
||||||
SecretKey string
|
SecretKey string
|
||||||
Network string
|
Network string
|
||||||
VCPU int
|
VCPU int
|
||||||
Yes bool
|
Yes bool
|
||||||
Help bool
|
Help bool
|
||||||
|
AccountIndex int // -1 means not specified
|
||||||
|
|
||||||
// Command
|
// Command
|
||||||
Command string
|
Command string
|
||||||
|
|
@ -1962,6 +1934,22 @@ func readFileContents(path string) (string, error) {
|
||||||
return string(data), nil
|
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
|
// readEnvFile reads environment variables from a .env file
|
||||||
func readEnvFile(path string) (map[string]string, error) {
|
func readEnvFile(path string) (map[string]string, error) {
|
||||||
data, err := os.ReadFile(path)
|
data, err := os.ReadFile(path)
|
||||||
|
|
@ -2416,7 +2404,15 @@ func runService(creds *Credentials, args []string, opts *CLIOptions) int {
|
||||||
|
|
||||||
// Redeploy service
|
// Redeploy service
|
||||||
if fs.redeploy != "" {
|
if fs.redeploy != "" {
|
||||||
_, err := RedeployService(creds, fs.redeploy, fs.bootstrap)
|
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)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return cliError(err.Error(), ExitAPIError)
|
return cliError(err.Error(), ExitAPIError)
|
||||||
}
|
}
|
||||||
|
|
@ -2475,6 +2471,13 @@ func runService(creds *Credentials, args []string, opts *CLIOptions) int {
|
||||||
if opts.VCPU > 0 {
|
if opts.VCPU > 0 {
|
||||||
serviceOpts.VCPU = opts.VCPU
|
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)
|
service, err := CreateService(creds, fs.name, ports, bootstrap, serviceOpts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -3170,7 +3173,7 @@ func runLanguages(creds *Credentials, args []string) int {
|
||||||
|
|
||||||
// parseGlobalFlags parses global CLI options
|
// parseGlobalFlags parses global CLI options
|
||||||
func parseGlobalFlags(args []string) (*CLIOptions, []string) {
|
func parseGlobalFlags(args []string) (*CLIOptions, []string) {
|
||||||
opts := &CLIOptions{}
|
opts := &CLIOptions{AccountIndex: -1}
|
||||||
remaining := []string{}
|
remaining := []string{}
|
||||||
|
|
||||||
for i := 0; i < len(args); i++ {
|
for i := 0; i < len(args); i++ {
|
||||||
|
|
@ -3213,6 +3216,13 @@ func parseGlobalFlags(args []string) (*CLIOptions, []string) {
|
||||||
opts.SecretKey = args[i+1]
|
opts.SecretKey = args[i+1]
|
||||||
i++
|
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":
|
case arg == "-n" || arg == "--network":
|
||||||
if i+1 < len(args) {
|
if i+1 < len(args) {
|
||||||
opts.Network = args[i+1]
|
opts.Network = args[i+1]
|
||||||
|
|
@ -3266,7 +3276,7 @@ func CliMain() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolve credentials
|
// Resolve credentials
|
||||||
creds, err := ResolveCredentials(opts.PublicKey, opts.SecretKey)
|
creds, err := ResolveCredentials(opts.PublicKey, opts.SecretKey, opts.AccountIndex)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "Error: %s\n", err)
|
fmt.Fprintf(os.Stderr, "Error: %s\n", err)
|
||||||
os.Exit(ExitAuthError)
|
os.Exit(ExitAuthError)
|
||||||
|
|
|
||||||
379
clients/go/sync/src/un_test.go
Normal file
379
clients/go/sync/src/un_test.go
Normal file
|
|
@ -0,0 +1,379 @@
|
||||||
|
// 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
181
clients/go/sync/tests/functional_test.go
Normal file
181
clients/go/sync/tests/functional_test.go
Normal file
|
|
@ -0,0 +1,181 @@
|
||||||
|
// 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))
|
||||||
|
}
|
||||||
120
clients/go/sync/tests/test_account_flag.sh
Executable file
120
clients/go/sync/tests/test_account_flag.sh
Executable file
|
|
@ -0,0 +1,120 @@
|
||||||
|
#!/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" <<CSV
|
||||||
|
unsb-pk-fake-0000-0000-0000,unsb-sk-fake0-00000-00000-00000
|
||||||
|
${REAL_PK},${REAL_SK}
|
||||||
|
CSV
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
echo "INTEGRATION: --account flag priority test"
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
|
||||||
|
# --- Test 1: --account 1 should use CSV row 1 (real creds), ignoring env vars ---
|
||||||
|
# Set env vars to GARBAGE so the test fails if env vars win
|
||||||
|
OUT=$(HOME="$TMPHOME" \
|
||||||
|
UNSANDBOX_PUBLIC_KEY=unsb-pk-fake-0000-0000-0000 \
|
||||||
|
UNSANDBOX_SECRET_KEY=unsb-sk-fake0-00000-00000-00000 \
|
||||||
|
"$UN_BIN" --account 1 key 2>&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 ]
|
||||||
|
|
@ -1,3 +1,19 @@
|
||||||
|
// 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
|
// Tests for the Go unsandbox SDK
|
||||||
// Run with: go test -v ./tests/
|
// Run with: go test -v ./tests/
|
||||||
package un
|
package un
|
||||||
|
|
@ -157,7 +173,7 @@ func TestResolveCredentialsFromEnv(t *testing.T) {
|
||||||
os.Setenv("UNSANDBOX_SECRET_KEY", testSK)
|
os.Setenv("UNSANDBOX_SECRET_KEY", testSK)
|
||||||
|
|
||||||
// Test
|
// Test
|
||||||
creds, err := ResolveCredentials("", "")
|
creds, err := ResolveCredentials("", "", -1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("ResolveCredentials failed: %v", err)
|
t.Fatalf("ResolveCredentials failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
@ -185,7 +201,7 @@ func TestResolveCredentialsFromArgs(t *testing.T) {
|
||||||
testPK := "unsb-pk-arg1-arg2-arg3-arg4"
|
testPK := "unsb-pk-arg1-arg2-arg3-arg4"
|
||||||
testSK := "unsb-sk-arg11-arg22-arg33-arg44"
|
testSK := "unsb-sk-arg11-arg22-arg33-arg44"
|
||||||
|
|
||||||
creds, err := ResolveCredentials(testPK, testSK)
|
creds, err := ResolveCredentials(testPK, testSK, -1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("ResolveCredentials failed: %v", err)
|
t.Fatalf("ResolveCredentials failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
@ -202,7 +218,7 @@ func TestResolveCredentialsFromArgs(t *testing.T) {
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
func getTestCredentials(t *testing.T) *Credentials {
|
func getTestCredentials(t *testing.T) *Credentials {
|
||||||
creds, err := ResolveCredentials("", "")
|
creds, err := ResolveCredentials("", "", -1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Skip("No credentials available for functional tests")
|
t.Skip("No credentials available for functional tests")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -222,42 +222,67 @@ def signRequest(String secretKey, long timestamp, String method, String path, St
|
||||||
* @return Tuple of [publicKey, secretKey]
|
* @return Tuple of [publicKey, secretKey]
|
||||||
* @throws AuthenticationError if no credentials found
|
* @throws AuthenticationError if no credentials found
|
||||||
*/
|
*/
|
||||||
def getCredentials(String publicKey = null, String secretKey = null, int accountIndex = 0) {
|
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) {
|
||||||
// Priority 1: Function arguments
|
// Priority 1: Function arguments
|
||||||
if (publicKey && secretKey) {
|
if (publicKey && secretKey) {
|
||||||
return [publicKey, secretKey]
|
return [publicKey, secretKey]
|
||||||
}
|
}
|
||||||
|
|
||||||
// Priority 2: Environment variables
|
// 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
|
||||||
def envPk = System.getenv('UNSANDBOX_PUBLIC_KEY')
|
def envPk = System.getenv('UNSANDBOX_PUBLIC_KEY')
|
||||||
def envSk = System.getenv('UNSANDBOX_SECRET_KEY')
|
def envSk = System.getenv('UNSANDBOX_SECRET_KEY')
|
||||||
if (envPk && envSk) {
|
if (envPk && envSk) {
|
||||||
return [envPk, envSk]
|
return [envPk, envSk]
|
||||||
}
|
}
|
||||||
|
|
||||||
// Priority 3: Config file
|
// Priority 4: ~/.unsandbox/accounts.csv row 0 (or UNSANDBOX_ACCOUNT env)
|
||||||
def accountsPath = new File(System.getProperty('user.home'), '.unsandbox/accounts.csv')
|
def defaultIdx = (System.getenv('UNSANDBOX_ACCOUNT') ?: '0').toInteger()
|
||||||
if (accountsPath.exists()) {
|
def searchPaths = [
|
||||||
try {
|
new File(System.getProperty('user.home'), '.unsandbox/accounts.csv'),
|
||||||
def lines = accountsPath.text.trim().split('\n')
|
new File('accounts.csv')
|
||||||
def validAccounts = []
|
]
|
||||||
lines.each { line ->
|
for (path in searchPaths) {
|
||||||
def trimmed = line.trim()
|
def accts = loadAccountsFromCsv(path)
|
||||||
if (!trimmed || trimmed.startsWith('#')) return
|
if (accts && defaultIdx < accts.size()) {
|
||||||
if (trimmed.contains(',')) {
|
return accts[defaultIdx]
|
||||||
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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -267,21 +292,14 @@ def getCredentials(String publicKey = null, String secretKey = null, int account
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Legacy compatibility
|
// Legacy compatibility - now delegates to getCredentials for proper priority
|
||||||
def getApiKeys(argsKey) {
|
def getApiKeys(argsKey, int accountIndex = -1) {
|
||||||
def publicKey = System.getenv('UNSANDBOX_PUBLIC_KEY')
|
try {
|
||||||
def secretKey = System.getenv('UNSANDBOX_SECRET_KEY')
|
return getCredentials(argsKey ?: null, null, accountIndex)
|
||||||
|
} catch (AuthenticationError e) {
|
||||||
if (!publicKey || !secretKey) {
|
System.err.println("${RED}Error: ${e.message}${RESET}")
|
||||||
def legacyKey = argsKey ?: System.getenv('UNSANDBOX_API_KEY')
|
System.exit(1)
|
||||||
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]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
@ -606,7 +624,7 @@ def execute(String language, String code, Map options = [:]) {
|
||||||
def (publicKey, secretKey) = getCredentials(
|
def (publicKey, secretKey) = getCredentials(
|
||||||
options.publicKey,
|
options.publicKey,
|
||||||
options.secretKey,
|
options.secretKey,
|
||||||
options.accountIndex ?: 0
|
options.accountIndex != null ? options.accountIndex : -1
|
||||||
)
|
)
|
||||||
|
|
||||||
def payload = [
|
def payload = [
|
||||||
|
|
@ -656,7 +674,7 @@ def executeAsync(String language, String code, Map options = [:]) {
|
||||||
def (publicKey, secretKey) = getCredentials(
|
def (publicKey, secretKey) = getCredentials(
|
||||||
options.publicKey,
|
options.publicKey,
|
||||||
options.secretKey,
|
options.secretKey,
|
||||||
options.accountIndex ?: 0
|
options.accountIndex != null ? options.accountIndex : -1
|
||||||
)
|
)
|
||||||
|
|
||||||
def payload = [
|
def payload = [
|
||||||
|
|
@ -703,7 +721,7 @@ def run(String code, Map options = [:]) {
|
||||||
def (publicKey, secretKey) = getCredentials(
|
def (publicKey, secretKey) = getCredentials(
|
||||||
options.publicKey,
|
options.publicKey,
|
||||||
options.secretKey,
|
options.secretKey,
|
||||||
options.accountIndex ?: 0
|
options.accountIndex != null ? options.accountIndex : -1
|
||||||
)
|
)
|
||||||
|
|
||||||
def ttl = options.ttl ?: DEFAULT_TTL
|
def ttl = options.ttl ?: DEFAULT_TTL
|
||||||
|
|
@ -728,7 +746,7 @@ def runAsync(String code, Map options = [:]) {
|
||||||
def (publicKey, secretKey) = getCredentials(
|
def (publicKey, secretKey) = getCredentials(
|
||||||
options.publicKey,
|
options.publicKey,
|
||||||
options.secretKey,
|
options.secretKey,
|
||||||
options.accountIndex ?: 0
|
options.accountIndex != null ? options.accountIndex : -1
|
||||||
)
|
)
|
||||||
|
|
||||||
def ttl = options.ttl ?: DEFAULT_TTL
|
def ttl = options.ttl ?: DEFAULT_TTL
|
||||||
|
|
@ -1589,45 +1607,72 @@ class Client {
|
||||||
def creds = getCredentialsStatic(
|
def creds = getCredentialsStatic(
|
||||||
options.publicKey,
|
options.publicKey,
|
||||||
options.secretKey,
|
options.secretKey,
|
||||||
options.accountIndex ?: 0
|
options.accountIndex != null ? options.accountIndex : -1
|
||||||
)
|
)
|
||||||
this.publicKey = creds[0]
|
this.publicKey = creds[0]
|
||||||
this.secretKey = creds[1]
|
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) {
|
private static getCredentialsStatic(String publicKey, String secretKey, int accountIndex) {
|
||||||
|
// Priority 1: explicit arguments
|
||||||
if (publicKey && secretKey) {
|
if (publicKey && secretKey) {
|
||||||
return [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 envPk = System.getenv('UNSANDBOX_PUBLIC_KEY')
|
||||||
def envSk = System.getenv('UNSANDBOX_SECRET_KEY')
|
def envSk = System.getenv('UNSANDBOX_SECRET_KEY')
|
||||||
if (envPk && envSk) {
|
if (envPk && envSk) {
|
||||||
return [envPk, envSk]
|
return [envPk, envSk]
|
||||||
}
|
}
|
||||||
|
|
||||||
def accountsPath = new File(System.getProperty('user.home'), '.unsandbox/accounts.csv')
|
// Priority 4: ~/.unsandbox/accounts.csv row 0 (or UNSANDBOX_ACCOUNT env)
|
||||||
if (accountsPath.exists()) {
|
def defaultIdx = (System.getenv('UNSANDBOX_ACCOUNT') ?: '0').toInteger()
|
||||||
try {
|
def searchPaths = [
|
||||||
def lines = accountsPath.text.trim().split('\n')
|
new File(System.getProperty('user.home'), '.unsandbox/accounts.csv'),
|
||||||
def validAccounts = []
|
new File('accounts.csv')
|
||||||
lines.each { line ->
|
]
|
||||||
def trimmed = line.trim()
|
for (path in searchPaths) {
|
||||||
if (!trimmed || trimmed.startsWith('#')) return
|
def accts = loadCsvAccounts(path)
|
||||||
if (trimmed.contains(',')) {
|
if (accts && defaultIdx < accts.size()) {
|
||||||
def parts = trimmed.split(',', 2)
|
return accts[defaultIdx]
|
||||||
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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1738,6 +1783,7 @@ class Args {
|
||||||
String sourceFile = null
|
String sourceFile = null
|
||||||
String inlineLang = null
|
String inlineLang = null
|
||||||
String apiKey = null
|
String apiKey = null
|
||||||
|
Integer accountIndex = -1
|
||||||
String network = null
|
String network = null
|
||||||
Integer vcpu = 0
|
Integer vcpu = 0
|
||||||
List<String> env = []
|
List<String> env = []
|
||||||
|
|
@ -1839,7 +1885,7 @@ def serviceEnvSet(serviceId, content, publicKey, secretKey) {
|
||||||
}
|
}
|
||||||
|
|
||||||
def cmdServiceEnv(args) {
|
def cmdServiceEnv(args) {
|
||||||
def (publicKey, secretKey) = getApiKeys(args.apiKey)
|
def (publicKey, secretKey) = getApiKeys(args.apiKey, args.accountIndex ?: -1)
|
||||||
|
|
||||||
switch (args.envAction) {
|
switch (args.envAction) {
|
||||||
case 'status':
|
case 'status':
|
||||||
|
|
@ -1875,7 +1921,7 @@ def cmdServiceEnv(args) {
|
||||||
}
|
}
|
||||||
|
|
||||||
def cmdExecute(args) {
|
def cmdExecute(args) {
|
||||||
def (publicKey, secretKey) = getApiKeys(args.apiKey)
|
def (publicKey, secretKey) = getApiKeys(args.apiKey, args.accountIndex ?: -1)
|
||||||
|
|
||||||
String code
|
String code
|
||||||
String language
|
String language
|
||||||
|
|
@ -1956,7 +2002,7 @@ def cmdExecute(args) {
|
||||||
}
|
}
|
||||||
|
|
||||||
def cmdSession(args) {
|
def cmdSession(args) {
|
||||||
def (publicKey, secretKey) = getApiKeys(args.apiKey)
|
def (publicKey, secretKey) = getApiKeys(args.apiKey, args.accountIndex ?: -1)
|
||||||
|
|
||||||
if (args.sessionSnapshot) {
|
if (args.sessionSnapshot) {
|
||||||
def payload = [:]
|
def payload = [:]
|
||||||
|
|
@ -2033,7 +2079,7 @@ def openBrowser(url) {
|
||||||
}
|
}
|
||||||
|
|
||||||
def cmdSnapshot(args) {
|
def cmdSnapshot(args) {
|
||||||
def (publicKey, secretKey) = getApiKeys(args.apiKey)
|
def (publicKey, secretKey) = getApiKeys(args.apiKey, args.accountIndex ?: -1)
|
||||||
|
|
||||||
if (args.snapshotList) {
|
if (args.snapshotList) {
|
||||||
def output = apiRequest('/snapshots', 'GET', null, publicKey, secretKey)
|
def output = apiRequest('/snapshots', 'GET', null, publicKey, secretKey)
|
||||||
|
|
@ -2073,7 +2119,7 @@ def cmdSnapshot(args) {
|
||||||
}
|
}
|
||||||
|
|
||||||
def cmdImage(args) {
|
def cmdImage(args) {
|
||||||
def (publicKey, secretKey) = getApiKeys(args.apiKey)
|
def (publicKey, secretKey) = getApiKeys(args.apiKey, args.accountIndex ?: -1)
|
||||||
|
|
||||||
if (args.imageList) {
|
if (args.imageList) {
|
||||||
def output = apiRequest('/images', 'GET', null, publicKey, secretKey)
|
def output = apiRequest('/images', 'GET', null, publicKey, secretKey)
|
||||||
|
|
@ -2153,7 +2199,7 @@ def cmdImage(args) {
|
||||||
}
|
}
|
||||||
|
|
||||||
def cmdLanguages(args) {
|
def cmdLanguages(args) {
|
||||||
def (publicKey, secretKey) = getApiKeys(args.apiKey)
|
def (publicKey, secretKey) = getApiKeys(args.apiKey, args.accountIndex ?: -1)
|
||||||
|
|
||||||
def result = languages([publicKey: publicKey, secretKey: secretKey, forceRefresh: true])
|
def result = languages([publicKey: publicKey, secretKey: secretKey, forceRefresh: true])
|
||||||
def langList = result.languages ?: []
|
def langList = result.languages ?: []
|
||||||
|
|
@ -2168,7 +2214,7 @@ def cmdLanguages(args) {
|
||||||
}
|
}
|
||||||
|
|
||||||
def cmdKey(args) {
|
def cmdKey(args) {
|
||||||
def (publicKey, secretKey) = getApiKeys(args.apiKey)
|
def (publicKey, secretKey) = getApiKeys(args.apiKey, args.accountIndex ?: -1)
|
||||||
|
|
||||||
def curlCmd = ['curl', '-s', '-X', 'POST', "${PORTAL_BASE}/keys/validate",
|
def curlCmd = ['curl', '-s', '-X', 'POST', "${PORTAL_BASE}/keys/validate",
|
||||||
'-H', 'Content-Type: application/json']
|
'-H', 'Content-Type: application/json']
|
||||||
|
|
@ -2240,7 +2286,7 @@ def cmdKey(args) {
|
||||||
}
|
}
|
||||||
|
|
||||||
def cmdService(args) {
|
def cmdService(args) {
|
||||||
def (publicKey, secretKey) = getApiKeys(args.apiKey)
|
def (publicKey, secretKey) = getApiKeys(args.apiKey, args.accountIndex ?: -1)
|
||||||
|
|
||||||
if (args.serviceSnapshot) {
|
if (args.serviceSnapshot) {
|
||||||
def payload = [:]
|
def payload = [:]
|
||||||
|
|
@ -2457,6 +2503,9 @@ def parseArgs(argv) {
|
||||||
case '--public-key':
|
case '--public-key':
|
||||||
args.apiKey = argv[++i] // For compatibility
|
args.apiKey = argv[++i] // For compatibility
|
||||||
break
|
break
|
||||||
|
case '--account':
|
||||||
|
args.accountIndex = argv[++i].toInteger()
|
||||||
|
break
|
||||||
case '-n':
|
case '-n':
|
||||||
case '--network':
|
case '--network':
|
||||||
args.network = argv[++i]
|
args.network = argv[++i]
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,8 @@ import Data.Char (isDigit, ord)
|
||||||
import Text.Printf (printf)
|
import Text.Printf (printf)
|
||||||
import Control.Monad (when, unless, forM_)
|
import Control.Monad (when, unless, forM_)
|
||||||
import Control.Exception (try, catch, IOError)
|
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 as BS
|
||||||
import qualified Data.ByteString.Char8 as BSC
|
import qualified Data.ByteString.Char8 as BSC
|
||||||
import qualified Data.ByteString.Base64 as B64
|
import qualified Data.ByteString.Base64 as B64
|
||||||
|
|
@ -86,6 +88,11 @@ portalBase = "https://unsandbox.com"
|
||||||
languagesCacheTtl :: Int
|
languagesCacheTtl :: Int
|
||||||
languagesCacheTtl = 3600 -- 1 hour in seconds
|
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
|
-- ANSI colors
|
||||||
blue, red, green, yellow, reset :: String
|
blue, red, green, yellow, reset :: String
|
||||||
blue = "\x1b[34m"
|
blue = "\x1b[34m"
|
||||||
|
|
@ -838,10 +845,26 @@ threadDelay us = do
|
||||||
_ <- readProcessWithExitCode "sleep" [show (fromIntegral ms / 1000.0 :: Double)] ""
|
_ <- readProcessWithExitCode "sleep" [show (fromIntegral ms / 1000.0 :: Double)] ""
|
||||||
return ()
|
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
|
||||||
main :: IO ()
|
main :: IO ()
|
||||||
main = do
|
main = do
|
||||||
args <- getArgs
|
rawArgs <- getArgs
|
||||||
|
args <- stripAccountArg rawArgs
|
||||||
cmd <- parseArgs args
|
cmd <- parseArgs args
|
||||||
case cmd of
|
case cmd of
|
||||||
Execute opts -> executeCommand opts
|
Execute opts -> executeCommand opts
|
||||||
|
|
@ -865,6 +888,9 @@ printHelp = do
|
||||||
putStrLn " un.hs languages [--json] List available languages"
|
putStrLn " un.hs languages [--json] List available languages"
|
||||||
putStrLn " un.hs key [options] Validate/extend API key"
|
putStrLn " un.hs key [options] Validate/extend API key"
|
||||||
putStrLn ""
|
putStrLn ""
|
||||||
|
putStrLn "Global options:"
|
||||||
|
putStrLn " --account N Use accounts.csv row N (bypasses env vars)"
|
||||||
|
putStrLn ""
|
||||||
putStrLn "Execute options:"
|
putStrLn "Execute options:"
|
||||||
putStrLn " -e KEY=VALUE Environment variable"
|
putStrLn " -e KEY=VALUE Environment variable"
|
||||||
putStrLn " -f FILE Input file"
|
putStrLn " -f FILE Input file"
|
||||||
|
|
@ -1433,18 +1459,77 @@ serviceEnvDelete serviceId = do
|
||||||
(exitCode, _, _) <- curlDelete apiKey (apiBase ++ "/services/" ++ serviceId ++ "/env")
|
(exitCode, _, _) <- curlDelete apiKey (apiBase ++ "/services/" ++ serviceId ++ "/env")
|
||||||
return (exitCode == ExitSuccess)
|
return (exitCode == ExitSuccess)
|
||||||
|
|
||||||
-- Get API keys from environment
|
-- 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
|
||||||
getApiKeys :: IO (String, Maybe String)
|
getApiKeys :: IO (String, Maybe String)
|
||||||
getApiKeys = do
|
getApiKeys = do
|
||||||
publicKey <- lookupEnv "UNSANDBOX_PUBLIC_KEY"
|
home <- maybe "." id <$> lookupEnv "HOME"
|
||||||
secretKey <- lookupEnv "UNSANDBOX_SECRET_KEY"
|
let homeCsv = home ++ "/.unsandbox/accounts.csv"
|
||||||
apiKey <- lookupEnv "UNSANDBOX_API_KEY"
|
-- Priority 1: --account N
|
||||||
case (publicKey, secretKey, apiKey) of
|
mIdx <- readIORef cliAccountIndex
|
||||||
(Just pk, Just sk, _) -> return (pk, Just sk)
|
case mIdx of
|
||||||
(_, _, Just ak) -> return (ak, Nothing)
|
Just idx -> do
|
||||||
_ -> do
|
creds <- loadCredentialsFromCsv homeCsv idx
|
||||||
hPutStrLn stderr "Error: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set (or UNSANDBOX_API_KEY for backwards compat)"
|
case creds of
|
||||||
exitFailure
|
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
|
||||||
|
|
||||||
getApiKey :: IO String
|
getApiKey :: IO String
|
||||||
getApiKey = do
|
getApiKey = do
|
||||||
|
|
|
||||||
|
|
@ -196,7 +196,12 @@ test-functional:
|
||||||
echo " $(YELLOW)⊘$(NC) Skipping (no API credentials)"; \
|
echo " $(YELLOW)⊘$(NC) Skipping (no API credentials)"; \
|
||||||
else \
|
else \
|
||||||
echo " Running functional tests..."; \
|
echo " Running functional tests..."; \
|
||||||
echo " $(YELLOW)⊘$(NC) Functional: SDK not yet implemented"; \
|
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; \
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
|
||||||
|
|
@ -25,11 +25,12 @@
|
||||||
* // Snapshot operations
|
* // Snapshot operations
|
||||||
* String snapshotId = Un.sessionSnapshot(sessionId, publicKey, secretKey, "my-snapshot", false);
|
* String snapshotId = Un.sessionSnapshot(sessionId, publicKey, secretKey, "my-snapshot", false);
|
||||||
*
|
*
|
||||||
* Authentication Priority (4-tier):
|
* Authentication Priority (5-tier):
|
||||||
* 1. Method arguments (publicKey, secretKey)
|
* 1. Method arguments (publicKey, secretKey)
|
||||||
* 2. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY)
|
* 2. --account N flag / accountIndex >= 0 (load row N from accounts.csv)
|
||||||
* 3. Config file (~/.unsandbox/accounts.csv, line 0 by default)
|
* 3. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY)
|
||||||
* 4. Local directory (./accounts.csv, line 0 by default)
|
* 4. Config file (~/.unsandbox/accounts.csv, line 0 by default)
|
||||||
|
* 5. Local directory (./accounts.csv, line 0 by default)
|
||||||
*
|
*
|
||||||
* Request Authentication (HMAC-SHA256):
|
* Request Authentication (HMAC-SHA256):
|
||||||
* Authorization: Bearer <public_key>
|
* Authorization: Bearer <public_key>
|
||||||
|
|
@ -175,38 +176,58 @@ public class Un {
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String[] resolveCredentials(String publicKey, String secretKey) {
|
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
|
// Tier 1: Method arguments
|
||||||
if (publicKey != null && !publicKey.isEmpty() && secretKey != null && !secretKey.isEmpty()) {
|
if (publicKey != null && !publicKey.isEmpty() && secretKey != null && !secretKey.isEmpty()) {
|
||||||
return new String[]{publicKey, secretKey};
|
return new String[]{publicKey, secretKey};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tier 2: Environment variables
|
// 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
|
||||||
String envPk = System.getenv("UNSANDBOX_PUBLIC_KEY");
|
String envPk = System.getenv("UNSANDBOX_PUBLIC_KEY");
|
||||||
String envSk = System.getenv("UNSANDBOX_SECRET_KEY");
|
String envSk = System.getenv("UNSANDBOX_SECRET_KEY");
|
||||||
if (envPk != null && !envPk.isEmpty() && envSk != null && !envSk.isEmpty()) {
|
if (envPk != null && !envPk.isEmpty() && envSk != null && !envSk.isEmpty()) {
|
||||||
return new String[]{envPk, envSk};
|
return new String[]{envPk, envSk};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine account index
|
// Determine account index from env (default 0)
|
||||||
int accountIndex = 0;
|
int csvIndex = 0;
|
||||||
String accountEnv = System.getenv("UNSANDBOX_ACCOUNT");
|
String accountEnv = System.getenv("UNSANDBOX_ACCOUNT");
|
||||||
if (accountEnv != null && !accountEnv.isEmpty()) {
|
if (accountEnv != null && !accountEnv.isEmpty()) {
|
||||||
try {
|
try {
|
||||||
accountIndex = Integer.parseInt(accountEnv);
|
csvIndex = Integer.parseInt(accountEnv);
|
||||||
} catch (NumberFormatException e) {
|
} catch (NumberFormatException e) {
|
||||||
// Use default
|
// Use default
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tier 3: ~/.unsandbox/accounts.csv
|
// Tier 4: ~/.unsandbox/accounts.csv
|
||||||
Path unsandboxDir = getUnsandboxDir();
|
Path unsandboxDir = getUnsandboxDir();
|
||||||
String[] creds = loadCredentialsFromCsv(unsandboxDir.resolve("accounts.csv"), accountIndex);
|
String[] creds = loadCredentialsFromCsv(unsandboxDir.resolve("accounts.csv"), csvIndex);
|
||||||
if (creds != null) {
|
if (creds != null) {
|
||||||
return creds;
|
return creds;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tier 4: ./accounts.csv
|
// Tier 5: ./accounts.csv
|
||||||
creds = loadCredentialsFromCsv(Paths.get("accounts.csv"), accountIndex);
|
creds = loadCredentialsFromCsv(Paths.get("accounts.csv"), csvIndex);
|
||||||
if (creds != null) {
|
if (creds != null) {
|
||||||
return creds;
|
return creds;
|
||||||
}
|
}
|
||||||
|
|
@ -214,9 +235,10 @@ public class Un {
|
||||||
throw new CredentialsException(
|
throw new CredentialsException(
|
||||||
"No credentials found. Please provide via:\n" +
|
"No credentials found. Please provide via:\n" +
|
||||||
" 1. Method arguments (publicKey, secretKey)\n" +
|
" 1. Method arguments (publicKey, secretKey)\n" +
|
||||||
" 2. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY)\n" +
|
" 2. --account N flag (load row N from accounts.csv)\n" +
|
||||||
" 3. ~/.unsandbox/accounts.csv\n" +
|
" 3. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY)\n" +
|
||||||
" 4. ./accounts.csv"
|
" 4. ~/.unsandbox/accounts.csv\n" +
|
||||||
|
" 5. ./accounts.csv"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1493,6 +1515,31 @@ public class Un {
|
||||||
String bootstrap,
|
String bootstrap,
|
||||||
String publicKey,
|
String publicKey,
|
||||||
String secretKey
|
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<String, Object> createService(
|
||||||
|
String name,
|
||||||
|
String ports,
|
||||||
|
String bootstrap,
|
||||||
|
List<Map<String, String>> inputFiles,
|
||||||
|
String publicKey,
|
||||||
|
String secretKey
|
||||||
) throws IOException {
|
) throws IOException {
|
||||||
String[] creds = resolveCredentials(publicKey, secretKey);
|
String[] creds = resolveCredentials(publicKey, secretKey);
|
||||||
|
|
||||||
|
|
@ -1517,6 +1564,9 @@ public class Un {
|
||||||
data.put("bootstrap", bootstrap);
|
data.put("bootstrap", bootstrap);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (inputFiles != null && !inputFiles.isEmpty()) {
|
||||||
|
data.put("input_files", inputFiles);
|
||||||
|
}
|
||||||
|
|
||||||
return makeRequest("POST", "/services", creds[0], creds[1], data);
|
return makeRequest("POST", "/services", creds[0], creds[1], data);
|
||||||
}
|
}
|
||||||
|
|
@ -1542,6 +1592,33 @@ public class Un {
|
||||||
boolean unfreezeOnDemand,
|
boolean unfreezeOnDemand,
|
||||||
String publicKey,
|
String publicKey,
|
||||||
String secretKey
|
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<String, Object> createService(
|
||||||
|
String name,
|
||||||
|
String ports,
|
||||||
|
String bootstrap,
|
||||||
|
boolean unfreezeOnDemand,
|
||||||
|
List<Map<String, String>> inputFiles,
|
||||||
|
String publicKey,
|
||||||
|
String secretKey
|
||||||
) throws IOException {
|
) throws IOException {
|
||||||
String[] creds = resolveCredentials(publicKey, secretKey);
|
String[] creds = resolveCredentials(publicKey, secretKey);
|
||||||
|
|
||||||
|
|
@ -1569,6 +1646,9 @@ public class Un {
|
||||||
if (unfreezeOnDemand) {
|
if (unfreezeOnDemand) {
|
||||||
data.put("unfreeze_on_demand", true);
|
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);
|
return makeRequest("POST", "/services", creds[0], creds[1], data);
|
||||||
}
|
}
|
||||||
|
|
@ -1901,9 +1981,34 @@ public class Un {
|
||||||
String serviceId,
|
String serviceId,
|
||||||
String publicKey,
|
String publicKey,
|
||||||
String secretKey
|
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<String, Object> redeployService(
|
||||||
|
String serviceId,
|
||||||
|
List<Map<String, String>> inputFiles,
|
||||||
|
String publicKey,
|
||||||
|
String secretKey
|
||||||
) throws IOException {
|
) throws IOException {
|
||||||
String[] creds = resolveCredentials(publicKey, secretKey);
|
String[] creds = resolveCredentials(publicKey, secretKey);
|
||||||
return makeRequest("POST", "/services/" + serviceId + "/redeploy", creds[0], creds[1], new LinkedHashMap<>());
|
Map<String, Object> data = new LinkedHashMap<>();
|
||||||
|
if (inputFiles != null && !inputFiles.isEmpty()) {
|
||||||
|
data.put("input_files", inputFiles);
|
||||||
|
}
|
||||||
|
return makeRequest("POST", "/services/" + serviceId + "/redeploy", creds[0], creds[1], data);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -2777,6 +2882,7 @@ public class Un {
|
||||||
String language = null;
|
String language = null;
|
||||||
String networkMode = "zerotrust";
|
String networkMode = "zerotrust";
|
||||||
int vcpu = 1;
|
int vcpu = 1;
|
||||||
|
int accountIndex = -1;
|
||||||
List<String> envVars = new ArrayList<>();
|
List<String> envVars = new ArrayList<>();
|
||||||
List<String> files = new ArrayList<>();
|
List<String> files = new ArrayList<>();
|
||||||
List<String> positionalArgs = new ArrayList<>();
|
List<String> positionalArgs = new ArrayList<>();
|
||||||
|
|
@ -2788,6 +2894,18 @@ public class Un {
|
||||||
if (arg.equals("-h") || arg.equals("--help")) {
|
if (arg.equals("-h") || arg.equals("--help")) {
|
||||||
showHelp = true;
|
showHelp = true;
|
||||||
i++;
|
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")) {
|
} else if (arg.equals("-s") || arg.equals("--shell")) {
|
||||||
if (i + 1 >= args.length) {
|
if (i + 1 >= args.length) {
|
||||||
System.err.println("Error: -s/--shell requires an argument");
|
System.err.println("Error: -s/--shell requires an argument");
|
||||||
|
|
@ -2853,13 +2971,21 @@ public class Un {
|
||||||
|
|
||||||
String command = positionalArgs.get(0);
|
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
|
// Route to subcommand handlers
|
||||||
switch (command) {
|
switch (command) {
|
||||||
case "session":
|
case "session":
|
||||||
handleSession(positionalArgs, publicKey, secretKey, networkMode, vcpu, language);
|
handleSession(positionalArgs, publicKey, secretKey, networkMode, vcpu, language);
|
||||||
break;
|
break;
|
||||||
case "service":
|
case "service":
|
||||||
handleService(positionalArgs, publicKey, secretKey, networkMode, vcpu, envVars);
|
handleService(positionalArgs, publicKey, secretKey, networkMode, vcpu, envVars, files);
|
||||||
break;
|
break;
|
||||||
case "snapshot":
|
case "snapshot":
|
||||||
handleSnapshot(positionalArgs, publicKey, secretKey);
|
handleSnapshot(positionalArgs, publicKey, secretKey);
|
||||||
|
|
@ -2899,6 +3025,7 @@ public class Un {
|
||||||
System.out.println(" -f, --file FILE Add input file to /tmp/");
|
System.out.println(" -f, --file FILE Add input file to /tmp/");
|
||||||
System.out.println(" -p, --public-key KEY API public key");
|
System.out.println(" -p, --public-key KEY API public key");
|
||||||
System.out.println(" -k, --secret-key KEY API secret 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(" -n, --network MODE Network: zerotrust or semitrusted");
|
||||||
System.out.println(" -v, --vcpu N vCPU count (1-8)");
|
System.out.println(" -v, --vcpu N vCPU count (1-8)");
|
||||||
System.out.println(" -h, --help Show help");
|
System.out.println(" -h, --help Show help");
|
||||||
|
|
@ -3181,7 +3308,8 @@ public class Un {
|
||||||
String secretKey,
|
String secretKey,
|
||||||
String networkMode,
|
String networkMode,
|
||||||
int vcpu,
|
int vcpu,
|
||||||
List<String> envVars
|
List<String> envVars,
|
||||||
|
List<String> files
|
||||||
) throws Exception {
|
) throws Exception {
|
||||||
// Check for "env" subcommand
|
// Check for "env" subcommand
|
||||||
if (args.size() > 1 && args.get(1).equals("env")) {
|
if (args.size() > 1 && args.get(1).equals("env")) {
|
||||||
|
|
@ -3347,14 +3475,17 @@ public class Un {
|
||||||
System.err.print(stderr);
|
System.err.print(stderr);
|
||||||
}
|
}
|
||||||
} else if (redeployId != null) {
|
} else if (redeployId != null) {
|
||||||
redeployService(redeployId, publicKey, secretKey);
|
// Build input_files from -f args
|
||||||
|
List<Map<String, String>> inputFiles = buildInputFiles(files);
|
||||||
|
redeployService(redeployId, inputFiles, publicKey, secretKey);
|
||||||
System.out.println("Service redeployed: " + redeployId);
|
System.out.println("Service redeployed: " + redeployId);
|
||||||
} else if (snapshotId != null) {
|
} else if (snapshotId != null) {
|
||||||
String snapId = serviceSnapshot(snapshotId, publicKey, secretKey, null);
|
String snapId = serviceSnapshot(snapshotId, publicKey, secretKey, null);
|
||||||
System.out.println("Snapshot created: " + snapId);
|
System.out.println("Snapshot created: " + snapId);
|
||||||
} else if (name != null) {
|
} else if (name != null) {
|
||||||
// Create new service
|
// Build input_files from -f args
|
||||||
Map<String, Object> result = createService(name, ports, bootstrap, publicKey, secretKey);
|
List<Map<String, String>> inputFiles = buildInputFiles(files);
|
||||||
|
Map<String, Object> result = createService(name, ports, bootstrap, inputFiles, publicKey, secretKey);
|
||||||
System.out.println("Service created:");
|
System.out.println("Service created:");
|
||||||
printMap(result);
|
printMap(result);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -3363,6 +3494,26 @@ public class Un {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build input_files list from -f file paths: read each file, base64-encode, return list of maps.
|
||||||
|
*/
|
||||||
|
private static List<Map<String, String>> buildInputFiles(List<String> filePaths) throws IOException {
|
||||||
|
if (filePaths == null || filePaths.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
List<Map<String, String>> inputFiles = new ArrayList<>();
|
||||||
|
for (String fpath : filePaths) {
|
||||||
|
Path p = Paths.get(fpath);
|
||||||
|
byte[] content = Files.readAllBytes(p);
|
||||||
|
String encoded = Base64.getEncoder().encodeToString(content);
|
||||||
|
Map<String, String> entry = new LinkedHashMap<>();
|
||||||
|
entry.put("filename", p.getFileName().toString());
|
||||||
|
entry.put("content", encoded);
|
||||||
|
inputFiles.add(entry);
|
||||||
|
}
|
||||||
|
return inputFiles;
|
||||||
|
}
|
||||||
|
|
||||||
private static void handleServiceEnv(
|
private static void handleServiceEnv(
|
||||||
List<String> args,
|
List<String> args,
|
||||||
String publicKey,
|
String publicKey,
|
||||||
|
|
|
||||||
186
clients/java/sync/tests/TestFunctional.java
Normal file
186
clients/java/sync/tests/TestFunctional.java
Normal file
|
|
@ -0,0 +1,186 @@
|
||||||
|
/**
|
||||||
|
* 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<String, Object> 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<String> 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<String, Object> 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<String, Object> 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<Map<String, Object>> 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<String, Object> 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<Map<String, Object>> 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<Map<String, Object>> 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<Map<String, Object>> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
113
clients/java/sync/tests/test_account_flag.sh
Executable file
113
clients/java/sync/tests/test_account_flag.sh
Executable file
|
|
@ -0,0 +1,113 @@
|
||||||
|
#!/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
|
||||||
|
|
@ -5,21 +5,17 @@
|
||||||
# - async/ : Async/Await SDK (Node.js)
|
# - async/ : Async/Await SDK (Node.js)
|
||||||
#
|
#
|
||||||
# Usage:
|
# Usage:
|
||||||
# make # Run all tests
|
# make test # Run all 4 test modes (auto-installs jest)
|
||||||
# make test # Run all 4 test modes
|
|
||||||
# make test-cli # CLI mode only
|
# make test-cli # CLI mode only
|
||||||
# make test-library # Library 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-sync # Test sync SDK only
|
||||||
# make test-async # Test async SDK only
|
# make test-async # Test async SDK only
|
||||||
# make clean # Remove build artifacts
|
# make clean # Remove node_modules + build artifacts
|
||||||
#
|
#
|
||||||
# Dependencies:
|
# The Makefile runs npm install automatically when node_modules is missing.
|
||||||
# npm install (or yarn install)
|
|
||||||
|
|
||||||
.PHONY: all test test-cli test-library test-integration test-functional
|
.PHONY: all test test-cli test-library test-integration test-functional
|
||||||
.PHONY: test-sync test-async install dev-install lint format clean help examples
|
.PHONY: test-sync test-async lint format clean help examples
|
||||||
|
|
||||||
# Paths
|
# Paths
|
||||||
ROOT_DIR := $(shell cd ../.. && pwd)
|
ROOT_DIR := $(shell cd ../.. && pwd)
|
||||||
|
|
@ -38,7 +34,7 @@ help:
|
||||||
@echo "UN JavaScript Client - Build and Test"
|
@echo "UN JavaScript Client - Build and Test"
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo "Test (all 4 modes):"
|
@echo "Test (all 4 modes):"
|
||||||
@echo " make test All 4 modes for both sync and async"
|
@echo " make test All 4 modes (auto-installs deps)"
|
||||||
@echo " make test-cli CLI mode (command-line interface)"
|
@echo " make test-cli CLI mode (command-line interface)"
|
||||||
@echo " make test-library Library mode (require and use)"
|
@echo " make test-library Library mode (require and use)"
|
||||||
@echo " make test-integration Integration mode (API contract)"
|
@echo " make test-integration Integration mode (API contract)"
|
||||||
|
|
@ -49,23 +45,30 @@ help:
|
||||||
@echo " make test-async Test asynchronous SDK"
|
@echo " make test-async Test asynchronous SDK"
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo "Development:"
|
@echo "Development:"
|
||||||
@echo " make install Install dependencies"
|
|
||||||
@echo " make lint Lint with ESLint"
|
@echo " make lint Lint with ESLint"
|
||||||
@echo " make format Format with Prettier"
|
@echo " make format Format with Prettier"
|
||||||
@echo " make examples Run example scripts"
|
@echo " make examples Run example scripts"
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo "Utility:"
|
@echo "Utility:"
|
||||||
@echo " make clean Remove build artifacts"
|
@echo " make clean Remove node_modules + build artifacts"
|
||||||
@echo " make deps Show required dependencies"
|
|
||||||
@echo ""
|
@echo ""
|
||||||
|
|
||||||
all: test
|
all: test
|
||||||
|
|
||||||
deps:
|
# ============================================================================
|
||||||
@echo "Required packages:"
|
# Dependency Management
|
||||||
@echo " npm install jest eslint prettier"
|
# ============================================================================
|
||||||
@echo ""
|
|
||||||
@node --version 2>/dev/null || echo "Node.js not installed"
|
$(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
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# TEST: All 4 Modes
|
# TEST: All 4 Modes
|
||||||
|
|
@ -85,15 +88,12 @@ test-cli:
|
||||||
@echo "CLI MODE: Testing JavaScript CLI interface"
|
@echo "CLI MODE: Testing JavaScript CLI interface"
|
||||||
@echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
@echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
@echo ""
|
@echo ""
|
||||||
@# Test root-level un.js if it exists
|
|
||||||
@if [ -f "$(ROOT_DIR)/un.js" ]; then \
|
@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"; \
|
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
|
fi
|
||||||
@# Test sync SDK syntax
|
|
||||||
@if [ -f "$(SYNC_DIR)/src/un.js" ]; then \
|
@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"; \
|
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
|
fi
|
||||||
@# Test async SDK syntax (ES module with .mjs extension check)
|
|
||||||
@if [ -f "$(ASYNC_DIR)/src/un_async.js" ]; then \
|
@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)"; \
|
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
|
fi
|
||||||
|
|
@ -102,30 +102,25 @@ test-cli:
|
||||||
# TEST: Library Mode
|
# TEST: Library Mode
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
test-library:
|
test-library: sync-deps
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
@echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
@echo "LIBRARY MODE: Testing JavaScript imports"
|
@echo "LIBRARY MODE: Testing JavaScript imports"
|
||||||
@echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
@echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
@echo ""
|
@echo ""
|
||||||
@# Test sync SDK import
|
|
||||||
@if [ -f "$(SYNC_DIR)/src/un.js" ]; then \
|
@if [ -f "$(SYNC_DIR)/src/un.js" ]; then \
|
||||||
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"; \
|
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"; \
|
||||||
fi
|
fi
|
||||||
@# Test async SDK import (ES module)
|
|
||||||
@if [ -f "$(ASYNC_DIR)/src/un_async.js" ]; then \
|
@if [ -f "$(ASYNC_DIR)/src/un_async.js" ]; then \
|
||||||
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)"; \
|
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)"; \
|
||||||
fi
|
fi
|
||||||
@# Run jest tests
|
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo "Running unit tests..."
|
@echo "Running unit tests..."
|
||||||
@if [ -d "$(SYNC_DIR)/tests" ] && [ -f "$(SYNC_DIR)/package.json" ]; then \
|
@if [ -d "$(SYNC_DIR)/tests" ] && [ -f "$(SYNC_DIR)/package.json" ]; then \
|
||||||
cd $(SYNC_DIR) && npm test 2>/dev/null && echo " $(GREEN)✓$(NC) Sync SDK tests passed" || echo " $(YELLOW)⊘$(NC) Sync tests need: npm install"; \
|
cd $(SYNC_DIR) && npm test 2>&1 && echo " $(GREEN)✓$(NC) Sync SDK tests passed" || echo " $(RED)✗$(NC) Sync tests failed"; \
|
||||||
elif [ -d "$(SYNC_DIR)/tests" ]; then \
|
|
||||||
echo " $(YELLOW)⊘$(NC) Sync tests need package.json"; \
|
|
||||||
fi
|
fi
|
||||||
@if [ -d "$(ASYNC_DIR)/tests" ] && [ -f "$(ASYNC_DIR)/package.json" ]; then \
|
@if [ -d "$(ASYNC_DIR)/tests" ] && [ -f "$(ASYNC_DIR)/package.json" ]; then \
|
||||||
cd $(ASYNC_DIR) && npm test 2>/dev/null && echo " $(GREEN)✓$(NC) Async SDK tests passed" || echo " $(YELLOW)⊘$(NC) Async tests need: npm install"; \
|
cd $(ASYNC_DIR) && npm test 2>&1 && echo " $(GREEN)✓$(NC) Async SDK tests passed" || echo " $(RED)✗$(NC) Async tests failed"; \
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
@ -144,7 +139,7 @@ test-integration:
|
||||||
else \
|
else \
|
||||||
echo " Testing API authentication..."; \
|
echo " Testing API authentication..."; \
|
||||||
if [ -f "$(SYNC_DIR)/src/un.js" ]; then \
|
if [ -f "$(SYNC_DIR)/src/un.js" ]; then \
|
||||||
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"; \
|
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"; \
|
||||||
fi; \
|
fi; \
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
@ -162,8 +157,8 @@ test-functional:
|
||||||
echo " $(YELLOW)⊘$(NC) Skipping (no API credentials)"; \
|
echo " $(YELLOW)⊘$(NC) Skipping (no API credentials)"; \
|
||||||
else \
|
else \
|
||||||
echo " Running functional tests..."; \
|
echo " Running functional tests..."; \
|
||||||
if [ -f "$(SYNC_DIR)/src/un.js" ]; then \
|
if [ -f "$(SYNC_DIR)/tests/test_functional.mjs" ]; 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"; \
|
node $(SYNC_DIR)/tests/test_functional.mjs 2>&1 && echo " $(GREEN)✓$(NC) Functional: All tests passed" || echo " $(RED)✗$(NC) Functional: Tests failed"; \
|
||||||
fi; \
|
fi; \
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
@ -171,36 +166,26 @@ test-functional:
|
||||||
# TEST: By SDK Type
|
# TEST: By SDK Type
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
test-sync:
|
test-sync: sync-deps
|
||||||
@echo "Testing Sync SDK..."
|
@echo "Testing Sync SDK..."
|
||||||
@if [ -f "$(SYNC_DIR)/package.json" ]; then \
|
@if [ -f "$(SYNC_DIR)/package.json" ]; then \
|
||||||
cd $(SYNC_DIR) && npm test; \
|
cd $(SYNC_DIR) && npm test; \
|
||||||
elif [ -d "$(SYNC_DIR)/tests" ]; then \
|
|
||||||
echo " $(YELLOW)⊘$(NC) Sync SDK needs package.json with test script"; \
|
|
||||||
else \
|
else \
|
||||||
echo " $(YELLOW)⊘$(NC) Sync SDK tests not found"; \
|
echo " $(YELLOW)⊘$(NC) Sync SDK needs package.json"; \
|
||||||
fi
|
fi
|
||||||
|
|
||||||
test-async:
|
test-async: async-deps
|
||||||
@echo "Testing Async SDK..."
|
@echo "Testing Async SDK..."
|
||||||
@if [ -f "$(ASYNC_DIR)/package.json" ]; then \
|
@if [ -f "$(ASYNC_DIR)/package.json" ]; then \
|
||||||
cd $(ASYNC_DIR) && npm test; \
|
cd $(ASYNC_DIR) && npm test; \
|
||||||
elif [ -d "$(ASYNC_DIR)/tests" ]; then \
|
|
||||||
echo " $(YELLOW)⊘$(NC) Async SDK needs package.json with test script"; \
|
|
||||||
else \
|
else \
|
||||||
echo " $(YELLOW)⊘$(NC) Async SDK tests not found"; \
|
echo " $(YELLOW)⊘$(NC) Async SDK needs package.json"; \
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# Development
|
# 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:
|
lint:
|
||||||
@echo "Linting JavaScript SDKs..."
|
@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
|
@if [ -d "$(SYNC_DIR)/src" ]; then npx eslint $(SYNC_DIR)/src/ 2>/dev/null || echo " $(YELLOW)⊘$(NC) ESLint not configured"; fi
|
||||||
|
|
@ -230,5 +215,4 @@ clean:
|
||||||
@echo "Cleaning JavaScript build artifacts..."
|
@echo "Cleaning JavaScript build artifacts..."
|
||||||
@rm -rf $(SYNC_DIR)/node_modules $(ASYNC_DIR)/node_modules 2>/dev/null || true
|
@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
|
@rm -rf $(SYNC_DIR)/coverage $(ASYNC_DIR)/coverage 2>/dev/null || true
|
||||||
@rm -f $(SYNC_DIR)/package-lock.json $(ASYNC_DIR)/package-lock.json 2>/dev/null || true
|
@echo "$(GREEN)✓$(NC) Cleaned node_modules + build artifacts"
|
||||||
@echo "$(GREEN)✓$(NC) Cleaned build artifacts"
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,20 @@
|
||||||
#!/usr/bin/env node
|
#!/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
|
* Async Job Polling example for unsandbox JavaScript SDK
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,20 @@
|
||||||
#!/usr/bin/env node
|
#!/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
|
* Concurrent Execution example for unsandbox JavaScript SDK
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,20 @@
|
||||||
#!/usr/bin/env node
|
#!/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 - standalone version
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,20 @@
|
||||||
#!/usr/bin/env node
|
#!/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 - standalone version
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,20 @@
|
||||||
#!/usr/bin/env node
|
#!/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 - standalone version
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -1,68 +1,19 @@
|
||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
/**
|
// This is free software for the public good of a permacomputer hosted at
|
||||||
* PUBLIC DOMAIN - NO LICENSE, NO WARRANTY
|
// permacomputer.com, an always-on computer by the people, for the people.
|
||||||
*
|
// One which is durable, easy to repair, & distributed like tap water
|
||||||
* unsandbox.com JavaScript SDK (Asynchronous with native fetch)
|
// for machine learning intelligence.
|
||||||
* Isomorphic: Works in Node.js (CLI + SDK) and Browser environments
|
//
|
||||||
*
|
// The permacomputer is community-owned infrastructure optimized around
|
||||||
* Library Usage:
|
// four values:
|
||||||
* import {
|
//
|
||||||
* // Code execution
|
// TRUTH First principles, math & science, open source code freely distributed
|
||||||
* executeCode, executeAsync, getJob, waitForJob, cancelJob, listJobs,
|
// FREEDOM Voluntary partnerships, freedom from tyranny & corporate control
|
||||||
* getLanguages, detectLanguage,
|
// HARMONY Minimal waste, self-renewing systems with diverse thriving connections
|
||||||
* // Session management
|
// LOVE Be yourself without hurting others, cooperation through natural law
|
||||||
* listSessions, getSession, createSession, deleteSession,
|
//
|
||||||
* freezeSession, unfreezeSession, boostSession, unboostSession, shellSession,
|
// This software contributes to that vision by enabling code execution across 42+ programming languages through a unified interface, accessible to all.
|
||||||
* // Service management
|
// Code is seeds to sprout on any abandoned technology.
|
||||||
* 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 <publicKey>
|
|
||||||
* X-Timestamp: <unixSeconds>
|
|
||||||
* 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: <script type="module">
|
|
||||||
* - Credentials stored in encrypted vault (requires CryptoJS):
|
|
||||||
* UnsandboxVault.createVault('mypassword');
|
|
||||||
* UnsandboxVault.saveKeysToVault(vaultId, [{publicKey, secretKey}], 'mypassword');
|
|
||||||
* - Or pass credentials directly to functions
|
|
||||||
* - Uses Web Crypto API for HMAC-SHA256 signing
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Environment detection for isomorphic support (Node.js + Browser)
|
// Environment detection for isomorphic support (Node.js + Browser)
|
||||||
const IS_BROWSER = typeof window !== 'undefined' && typeof window.document !== 'undefined';
|
const IS_BROWSER = typeof window !== 'undefined' && typeof window.document !== 'undefined';
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,19 @@
|
||||||
|
// 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 async operations
|
* Tests for async operations
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,19 @@
|
||||||
|
// 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 credential resolution
|
* Tests for credential resolution
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,19 @@
|
||||||
|
// 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 HMAC request signing
|
* Tests for HMAC request signing
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,19 @@
|
||||||
|
// 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 language detection from filenames
|
* Tests for language detection from filenames
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,19 @@
|
||||||
|
// 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 for unsandbox JavaScript SDK
|
// Hello World example for unsandbox JavaScript SDK
|
||||||
// Expected output: Hello from unsandbox!
|
// Expected output: Hello from unsandbox!
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,19 @@
|
||||||
|
// 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.
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
testEnvironment: 'node',
|
testEnvironment: 'node',
|
||||||
transform: {},
|
transform: {},
|
||||||
|
|
|
||||||
3652
clients/javascript/sync/package-lock.json
generated
Normal file
3652
clients/javascript/sync/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -1,69 +1,19 @@
|
||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
/**
|
// This is free software for the public good of a permacomputer hosted at
|
||||||
* PUBLIC DOMAIN - NO LICENSE, NO WARRANTY
|
// permacomputer.com, an always-on computer by the people, for the people.
|
||||||
*
|
// One which is durable, easy to repair, & distributed like tap water
|
||||||
* unsandbox.com JavaScript SDK (Synchronous/Async)
|
// for machine learning intelligence.
|
||||||
* Isomorphic: Works in Node.js (CLI + SDK) and Browser environments
|
//
|
||||||
*
|
// The permacomputer is community-owned infrastructure optimized around
|
||||||
* Library Usage (ES Modules):
|
// four values:
|
||||||
* import {
|
//
|
||||||
* // Code execution
|
// TRUTH First principles, math & science, open source code freely distributed
|
||||||
* executeCode, executeAsync, getJob, waitForJob, cancelJob, listJobs,
|
// FREEDOM Voluntary partnerships, freedom from tyranny & corporate control
|
||||||
* getLanguages, detectLanguage,
|
// HARMONY Minimal waste, self-renewing systems with diverse thriving connections
|
||||||
* // Session management
|
// LOVE Be yourself without hurting others, cooperation through natural law
|
||||||
* listSessions, getSession, createSession, deleteSession,
|
//
|
||||||
* freezeSession, unfreezeSession, boostSession, unboostSession, shellSession,
|
// This software contributes to that vision by enabling code execution across 42+ programming languages through a unified interface, accessible to all.
|
||||||
* // Service management
|
// Code is seeds to sprout on any abandoned technology.
|
||||||
* 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,
|
|
||||||
* // Images API (LXD container images)
|
|
||||||
* imagePublish, listImages, getImage, deleteImage,
|
|
||||||
* lockImage, unlockImage, setImageVisibility,
|
|
||||||
* grantImageAccess, revokeImageAccess, listImageTrusted,
|
|
||||||
* transferImage, spawnFromImage, cloneImage,
|
|
||||||
* // Key validation
|
|
||||||
* validateKeys,
|
|
||||||
* } from './un.js';
|
|
||||||
*
|
|
||||||
* // Execute code asynchronously (returns Promise)
|
|
||||||
* const result = await executeCode('python', 'print("hello")', publicKey, secretKey);
|
|
||||||
* const jobId = await executeAsync('javascript', 'console.log("hello")', publicKey, secretKey);
|
|
||||||
* const result = await waitForJob(jobId, 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 <publicKey>
|
|
||||||
* X-Timestamp: <unixSeconds>
|
|
||||||
* 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: <script type="module">
|
|
||||||
* - Credentials can be stored in encrypted vault (requires CryptoJS):
|
|
||||||
* UnsandboxVault.createVault('mypassword');
|
|
||||||
* UnsandboxVault.saveKeysToVault(vaultId, [{publicKey, secretKey}], 'mypassword');
|
|
||||||
* - Or configure via plain localStorage (legacy):
|
|
||||||
* localStorage.setItem('useUnsandbox', 'true');
|
|
||||||
* localStorage.setItem('unsandboxPublicKey', 'unsb-pk-...');
|
|
||||||
* localStorage.setItem('unsandboxSecretKey', 'unsb-sk-...');
|
|
||||||
* - Or pass credentials directly to functions
|
|
||||||
* - Uses Web Crypto API for HMAC-SHA256 signing
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Environment detection for isomorphic support (Node.js + Browser)
|
// Environment detection for isomorphic support (Node.js + Browser)
|
||||||
const IS_BROWSER = typeof window !== 'undefined' && typeof window.document !== 'undefined';
|
const IS_BROWSER = typeof window !== 'undefined' && typeof window.document !== 'undefined';
|
||||||
|
|
@ -381,10 +331,11 @@ function loadCredentialsFromStorage() {
|
||||||
*
|
*
|
||||||
* Priority:
|
* Priority:
|
||||||
* 1. Function arguments
|
* 1. Function arguments
|
||||||
* 2. Environment variables (Node.js)
|
* 2. accountIndex >= 0 → load from accounts.csv row N
|
||||||
* 3. localStorage (Browser)
|
* 3. Environment variables (Node.js)
|
||||||
* 4. ~/.unsandbox/accounts.csv (Node.js)
|
* 4. localStorage (Browser)
|
||||||
* 5. ./accounts.csv (Node.js)
|
* 5. ~/.unsandbox/accounts.csv (default row, Node.js)
|
||||||
|
* 6. ./accounts.csv (default row, Node.js)
|
||||||
*/
|
*/
|
||||||
function resolveCredentials(publicKey, secretKey, accountIndex) {
|
function resolveCredentials(publicKey, secretKey, accountIndex) {
|
||||||
// Tier 1: Function arguments
|
// Tier 1: Function arguments
|
||||||
|
|
@ -392,7 +343,26 @@ function resolveCredentials(publicKey, secretKey, accountIndex) {
|
||||||
return [publicKey, secretKey];
|
return [publicKey, secretKey];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tier 2: Environment variables (Node.js only)
|
// Tier 2: Explicit accountIndex → load from accounts.csv row N (Node.js only)
|
||||||
|
if (IS_NODE && fs && path && accountIndex !== undefined && accountIndex >= 0) {
|
||||||
|
// ~/.unsandbox/accounts.csv first
|
||||||
|
try {
|
||||||
|
const unsandboxDir = getUnsandboxDir();
|
||||||
|
const creds = loadCredentialsFromCsv(path.join(unsandboxDir, 'accounts.csv'), accountIndex);
|
||||||
|
if (creds) {
|
||||||
|
return creds;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Continue to next location
|
||||||
|
}
|
||||||
|
// ./accounts.csv fallback
|
||||||
|
const creds = loadCredentialsFromCsv('accounts.csv', accountIndex);
|
||||||
|
if (creds) {
|
||||||
|
return creds;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tier 3: Environment variables (Node.js only)
|
||||||
if (IS_NODE) {
|
if (IS_NODE) {
|
||||||
const envPk = process.env.UNSANDBOX_PUBLIC_KEY;
|
const envPk = process.env.UNSANDBOX_PUBLIC_KEY;
|
||||||
const envSk = process.env.UNSANDBOX_SECRET_KEY;
|
const envSk = process.env.UNSANDBOX_SECRET_KEY;
|
||||||
|
|
@ -401,7 +371,7 @@ function resolveCredentials(publicKey, secretKey, accountIndex) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tier 3: localStorage (Browser only)
|
// Tier 4: localStorage (Browser only)
|
||||||
if (IS_BROWSER) {
|
if (IS_BROWSER) {
|
||||||
const storageCreds = loadCredentialsFromStorage();
|
const storageCreds = loadCredentialsFromStorage();
|
||||||
if (storageCreds) {
|
if (storageCreds) {
|
||||||
|
|
@ -409,17 +379,14 @@ function resolveCredentials(publicKey, secretKey, accountIndex) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tier 4 & 5: File-based credentials (Node.js only)
|
// Tier 5 & 6: File-based credentials with default index (Node.js only)
|
||||||
if (IS_NODE && fs && path) {
|
if (IS_NODE && fs && path) {
|
||||||
// Determine account index
|
const defaultIndex = parseInt(process.env.UNSANDBOX_ACCOUNT || '0', 10);
|
||||||
if (accountIndex === undefined) {
|
|
||||||
accountIndex = parseInt(process.env.UNSANDBOX_ACCOUNT || '0', 10);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tier 4: ~/.unsandbox/accounts.csv
|
// Tier 5: ~/.unsandbox/accounts.csv
|
||||||
try {
|
try {
|
||||||
const unsandboxDir = getUnsandboxDir();
|
const unsandboxDir = getUnsandboxDir();
|
||||||
const creds = loadCredentialsFromCsv(path.join(unsandboxDir, 'accounts.csv'), accountIndex);
|
const creds = loadCredentialsFromCsv(path.join(unsandboxDir, 'accounts.csv'), defaultIndex);
|
||||||
if (creds) {
|
if (creds) {
|
||||||
return creds;
|
return creds;
|
||||||
}
|
}
|
||||||
|
|
@ -427,8 +394,8 @@ function resolveCredentials(publicKey, secretKey, accountIndex) {
|
||||||
// Continue to next tier
|
// Continue to next tier
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tier 5: ./accounts.csv
|
// Tier 6: ./accounts.csv
|
||||||
const creds = loadCredentialsFromCsv('accounts.csv', accountIndex);
|
const creds = loadCredentialsFromCsv('accounts.csv', defaultIndex);
|
||||||
if (creds) {
|
if (creds) {
|
||||||
return creds;
|
return creds;
|
||||||
}
|
}
|
||||||
|
|
@ -1143,6 +1110,7 @@ async function listServices(publicKey, secretKey) {
|
||||||
* - domains: Array of custom domains
|
* - domains: Array of custom domains
|
||||||
* - serviceType: Service type for SRV records (minecraft, mumble, etc.)
|
* - serviceType: Service type for SRV records (minecraft, mumble, etc.)
|
||||||
* - unfreezeOnDemand: If true, frozen services wake automatically on HTTP traffic
|
* - unfreezeOnDemand: If true, frozen services wake automatically on HTTP traffic
|
||||||
|
* - inputFiles: Array of {filename, content} objects (content is base64-encoded)
|
||||||
*
|
*
|
||||||
* Returns: Promise<Object> (service info with service_id)
|
* Returns: Promise<Object> (service info with service_id)
|
||||||
*/
|
*/
|
||||||
|
|
@ -1164,6 +1132,7 @@ async function createService(name, ports, bootstrap, opts = {}, publicKey, secre
|
||||||
if (opts.domains) data.custom_domains = opts.domains;
|
if (opts.domains) data.custom_domains = opts.domains;
|
||||||
if (opts.serviceType) data.service_type = opts.serviceType;
|
if (opts.serviceType) data.service_type = opts.serviceType;
|
||||||
if (opts.unfreezeOnDemand) data.unfreeze_on_demand = true;
|
if (opts.unfreezeOnDemand) data.unfreeze_on_demand = true;
|
||||||
|
if (opts.inputFiles && opts.inputFiles.length > 0) data.input_files = opts.inputFiles;
|
||||||
|
|
||||||
return makeRequest('POST', '/services', publicKey, secretKey, data);
|
return makeRequest('POST', '/services', publicKey, secretKey, data);
|
||||||
}
|
}
|
||||||
|
|
@ -1381,10 +1350,11 @@ async function exportServiceEnv(serviceId, publicKey, secretKey) {
|
||||||
* Args:
|
* Args:
|
||||||
* serviceId: Service ID to redeploy
|
* serviceId: Service ID to redeploy
|
||||||
* bootstrap: Optional new bootstrap script content or URL
|
* bootstrap: Optional new bootstrap script content or URL
|
||||||
|
* inputFiles: Optional array of {filename, content} objects (content is base64-encoded)
|
||||||
*
|
*
|
||||||
* Returns: Promise<Object> (redeploy confirmation)
|
* Returns: Promise<Object> (redeploy confirmation)
|
||||||
*/
|
*/
|
||||||
async function redeployService(serviceId, bootstrap = null, publicKey, secretKey) {
|
async function redeployService(serviceId, bootstrap = null, inputFiles = null, publicKey, secretKey) {
|
||||||
[publicKey, secretKey] = resolveCredentials(publicKey, secretKey);
|
[publicKey, secretKey] = resolveCredentials(publicKey, secretKey);
|
||||||
const data = {};
|
const data = {};
|
||||||
if (bootstrap) {
|
if (bootstrap) {
|
||||||
|
|
@ -1394,6 +1364,7 @@ async function redeployService(serviceId, bootstrap = null, publicKey, secretKey
|
||||||
data.bootstrap_content = bootstrap;
|
data.bootstrap_content = bootstrap;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (inputFiles && inputFiles.length > 0) data.input_files = inputFiles;
|
||||||
return makeRequest('POST', `/services/${serviceId}/redeploy`, publicKey, secretKey, data);
|
return makeRequest('POST', `/services/${serviceId}/redeploy`, publicKey, secretKey, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2196,7 +2167,7 @@ SERVICE COMMANDS:
|
||||||
node un.js service --lock <id> Prevent deletion
|
node un.js service --lock <id> Prevent deletion
|
||||||
node un.js service --unlock <id> Allow deletion
|
node un.js service --unlock <id> Allow deletion
|
||||||
node un.js service --execute <id> <cmd> Run command in service
|
node un.js service --execute <id> <cmd> Run command in service
|
||||||
node un.js service --redeploy <id> Re-run bootstrap
|
node un.js service --redeploy <id> Re-run bootstrap (supports -f)
|
||||||
node un.js service --snapshot <id> Create snapshot
|
node un.js service --snapshot <id> Create snapshot
|
||||||
|
|
||||||
SERVICE ENV COMMANDS:
|
SERVICE ENV COMMANDS:
|
||||||
|
|
@ -2239,6 +2210,7 @@ function parseArgs(args) {
|
||||||
output: null,
|
output: null,
|
||||||
publicKey: null,
|
publicKey: null,
|
||||||
secretKey: null,
|
secretKey: null,
|
||||||
|
accountIndex: undefined,
|
||||||
network: 'zerotrust',
|
network: 'zerotrust',
|
||||||
vcpu: 1,
|
vcpu: 1,
|
||||||
yes: false,
|
yes: false,
|
||||||
|
|
@ -2368,6 +2340,9 @@ function parseArgs(args) {
|
||||||
} else if (arg === '-k' || arg === '--secret-key') {
|
} else if (arg === '-k' || arg === '--secret-key') {
|
||||||
result.secretKey = args[++i];
|
result.secretKey = args[++i];
|
||||||
i++;
|
i++;
|
||||||
|
} else if (arg === '--account') {
|
||||||
|
result.accountIndex = parseInt(args[++i], 10);
|
||||||
|
i++;
|
||||||
} else if (arg === '-n' || arg === '--network') {
|
} else if (arg === '-n' || arg === '--network') {
|
||||||
result.network = args[++i];
|
result.network = args[++i];
|
||||||
i++;
|
i++;
|
||||||
|
|
@ -2555,8 +2530,7 @@ function formatTable(items, columns) {
|
||||||
* Handle session commands.
|
* Handle session commands.
|
||||||
*/
|
*/
|
||||||
async function handleSession(opts) {
|
async function handleSession(opts) {
|
||||||
const pk = opts.publicKey;
|
let [pk, sk] = resolveCredentials(opts.publicKey, opts.secretKey, opts.accountIndex);
|
||||||
const sk = opts.secretKey;
|
|
||||||
|
|
||||||
// List sessions
|
// List sessions
|
||||||
if (opts.list) {
|
if (opts.list) {
|
||||||
|
|
@ -2639,8 +2613,7 @@ async function handleSession(opts) {
|
||||||
* Handle service commands.
|
* Handle service commands.
|
||||||
*/
|
*/
|
||||||
async function handleService(opts) {
|
async function handleService(opts) {
|
||||||
const pk = opts.publicKey;
|
let [pk, sk] = resolveCredentials(opts.publicKey, opts.secretKey, opts.accountIndex);
|
||||||
const sk = opts.secretKey;
|
|
||||||
|
|
||||||
// Handle env subcommand
|
// Handle env subcommand
|
||||||
if (opts.subcommand === 'env') {
|
if (opts.subcommand === 'env') {
|
||||||
|
|
@ -2782,7 +2755,19 @@ async function handleService(opts) {
|
||||||
if (opts.bootstrapFile) {
|
if (opts.bootstrapFile) {
|
||||||
bootstrap = fs.readFileSync(opts.bootstrapFile, 'utf-8');
|
bootstrap = fs.readFileSync(opts.bootstrapFile, 'utf-8');
|
||||||
}
|
}
|
||||||
await redeployService(opts.redeploy, bootstrap, pk, sk);
|
// Build input_files from -f args
|
||||||
|
let inputFiles = null;
|
||||||
|
if (opts.files && opts.files.length > 0) {
|
||||||
|
inputFiles = [];
|
||||||
|
for (const fpath of opts.files) {
|
||||||
|
const content = fs.readFileSync(fpath);
|
||||||
|
inputFiles.push({
|
||||||
|
filename: path.basename(fpath),
|
||||||
|
content: content.toString('base64'),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await redeployService(opts.redeploy, bootstrap, inputFiles, pk, sk);
|
||||||
console.log(`Service ${opts.redeploy} redeployed.`);
|
console.log(`Service ${opts.redeploy} redeployed.`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -2836,6 +2821,17 @@ async function handleService(opts) {
|
||||||
if (opts.type) {
|
if (opts.type) {
|
||||||
serviceOpts.serviceType = opts.type;
|
serviceOpts.serviceType = opts.type;
|
||||||
}
|
}
|
||||||
|
// Build input_files from -f args
|
||||||
|
if (opts.files && opts.files.length > 0) {
|
||||||
|
serviceOpts.inputFiles = [];
|
||||||
|
for (const fpath of opts.files) {
|
||||||
|
const content = fs.readFileSync(fpath);
|
||||||
|
serviceOpts.inputFiles.push({
|
||||||
|
filename: path.basename(fpath),
|
||||||
|
content: content.toString('base64'),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const service = await createService(opts.name, ports, bootstrap, serviceOpts, pk, sk);
|
const service = await createService(opts.name, ports, bootstrap, serviceOpts, pk, sk);
|
||||||
console.log(`Service created: ${service.service_id}`);
|
console.log(`Service created: ${service.service_id}`);
|
||||||
|
|
@ -2852,8 +2848,7 @@ async function handleService(opts) {
|
||||||
* Handle snapshot commands.
|
* Handle snapshot commands.
|
||||||
*/
|
*/
|
||||||
async function handleSnapshot(opts) {
|
async function handleSnapshot(opts) {
|
||||||
const pk = opts.publicKey;
|
let [pk, sk] = resolveCredentials(opts.publicKey, opts.secretKey, opts.accountIndex);
|
||||||
const sk = opts.secretKey;
|
|
||||||
|
|
||||||
// List snapshots
|
// List snapshots
|
||||||
if (opts.list) {
|
if (opts.list) {
|
||||||
|
|
@ -2921,8 +2916,7 @@ async function handleSnapshot(opts) {
|
||||||
* Handle image command.
|
* Handle image command.
|
||||||
*/
|
*/
|
||||||
async function handleImage(opts) {
|
async function handleImage(opts) {
|
||||||
const pk = opts.publicKey;
|
let [pk, sk] = resolveCredentials(opts.publicKey, opts.secretKey, opts.accountIndex);
|
||||||
const sk = opts.secretKey;
|
|
||||||
|
|
||||||
// List images
|
// List images
|
||||||
if (opts.list) {
|
if (opts.list) {
|
||||||
|
|
@ -3025,13 +3019,13 @@ async function handleImage(opts) {
|
||||||
* Handle key command.
|
* Handle key command.
|
||||||
*/
|
*/
|
||||||
async function handleKey(opts) {
|
async function handleKey(opts) {
|
||||||
|
const [pk, sk] = resolveCredentials(opts.publicKey, opts.secretKey, opts.accountIndex);
|
||||||
try {
|
try {
|
||||||
const result = await validateKeys(opts.publicKey, opts.secretKey);
|
const result = await validateKeys(pk, sk);
|
||||||
console.log('API Key Status:');
|
console.log('API Key Status:');
|
||||||
console.log(JSON.stringify(result, null, 2));
|
console.log(JSON.stringify(result, null, 2));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// If validate endpoint doesn't exist, just show that credentials were resolved
|
// If validate endpoint doesn't exist, just show that credentials were resolved
|
||||||
const [pk] = resolveCredentials(opts.publicKey, opts.secretKey);
|
|
||||||
console.log(`Public Key: ${pk}`);
|
console.log(`Public Key: ${pk}`);
|
||||||
console.log('Key validation endpoint returned error - key may still be valid.');
|
console.log('Key validation endpoint returned error - key may still be valid.');
|
||||||
}
|
}
|
||||||
|
|
@ -3041,7 +3035,8 @@ async function handleKey(opts) {
|
||||||
* Handle languages command.
|
* Handle languages command.
|
||||||
*/
|
*/
|
||||||
async function handleLanguages(opts) {
|
async function handleLanguages(opts) {
|
||||||
const languages = await getLanguages(opts.publicKey, opts.secretKey);
|
const [pk, sk] = resolveCredentials(opts.publicKey, opts.secretKey, opts.accountIndex);
|
||||||
|
const languages = await getLanguages(pk, sk);
|
||||||
|
|
||||||
if (opts.json) {
|
if (opts.json) {
|
||||||
// Output as JSON array
|
// Output as JSON array
|
||||||
|
|
@ -3058,8 +3053,7 @@ async function handleLanguages(opts) {
|
||||||
* Handle execute command (default).
|
* Handle execute command (default).
|
||||||
*/
|
*/
|
||||||
async function handleExecute(opts) {
|
async function handleExecute(opts) {
|
||||||
const pk = opts.publicKey;
|
let [pk, sk] = resolveCredentials(opts.publicKey, opts.secretKey, opts.accountIndex);
|
||||||
const sk = opts.secretKey;
|
|
||||||
|
|
||||||
let code;
|
let code;
|
||||||
let language;
|
let language;
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,19 @@
|
||||||
|
// 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 new SDK functions (feature parity with C implementation)
|
* Tests for new SDK functions (feature parity with C implementation)
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
108
clients/javascript/sync/tests/test_account_flag.sh
Executable file
108
clients/javascript/sync/tests/test_account_flag.sh
Executable file
|
|
@ -0,0 +1,108 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Integration test for --account N credential selection in un.js.
|
||||||
|
#
|
||||||
|
# Tests that --account N selects the correct row from accounts.csv,
|
||||||
|
# taking priority over environment variables.
|
||||||
|
#
|
||||||
|
# Requires UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY to be set.
|
||||||
|
# Skips if credentials are not available.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
UN_JS="$SCRIPT_DIR/../src/un.js"
|
||||||
|
|
||||||
|
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)); }
|
||||||
|
|
||||||
|
# Require real credentials from environment
|
||||||
|
if [ -z "${UNSANDBOX_PUBLIC_KEY:-}" ] || [ -z "${UNSANDBOX_SECRET_KEY:-}" ]; then
|
||||||
|
skip "UNSANDBOX_PUBLIC_KEY / UNSANDBOX_SECRET_KEY not set"
|
||||||
|
echo "Results: PASS=$PASS FAIL=$FAIL SKIP=$SKIP"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
REAL_PK="$UNSANDBOX_PUBLIC_KEY"
|
||||||
|
REAL_SK="$UNSANDBOX_SECRET_KEY"
|
||||||
|
GARBAGE_PK="unsb-pk-0000-0000-0000-garbage"
|
||||||
|
GARBAGE_SK="unsb-sk-00000-00000-00000-garbage"
|
||||||
|
|
||||||
|
# Build a temporary HOME with accounts.csv: row 0 = garbage, row 1 = real creds
|
||||||
|
TMPHOME="$(mktemp -d)"
|
||||||
|
mkdir -p "$TMPHOME/.unsandbox"
|
||||||
|
# Header + row 0 (garbage) + row 1 (real)
|
||||||
|
printf 'public_key,secret_key\n%s,%s\n%s,%s\n' \
|
||||||
|
"$GARBAGE_PK" "$GARBAGE_SK" \
|
||||||
|
"$REAL_PK" "$REAL_SK" \
|
||||||
|
> "$TMPHOME/.unsandbox/accounts.csv"
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
rm -rf "$TMPHOME"
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Test 1: HOME=TMPHOME, env vars = garbage, --account 1 => row 1 = real creds
|
||||||
|
# Expect: key command succeeds (no auth error)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
if HOME="$TMPHOME" \
|
||||||
|
UNSANDBOX_PUBLIC_KEY="$GARBAGE_PK" \
|
||||||
|
UNSANDBOX_SECRET_KEY="$GARBAGE_SK" \
|
||||||
|
node "$UN_JS" --account 1 key 2>&1 | grep -qiE 'Public Key|key_id|account|status'; then
|
||||||
|
pass "Test 1: --account 1 selects row 1 (real creds) over env garbage"
|
||||||
|
else
|
||||||
|
# Also accept a successful JSON response (key validates)
|
||||||
|
output="$(HOME="$TMPHOME" \
|
||||||
|
UNSANDBOX_PUBLIC_KEY="$GARBAGE_PK" \
|
||||||
|
UNSANDBOX_SECRET_KEY="$GARBAGE_SK" \
|
||||||
|
node "$UN_JS" --account 1 key 2>&1 || true)"
|
||||||
|
if echo "$output" | grep -qiE '401|invalid|unauthorized|authentication'; then
|
||||||
|
fail "Test 1: --account 1 selected row 1 but auth failed (real creds may be invalid)"
|
||||||
|
else
|
||||||
|
pass "Test 1: --account 1 selects row 1 (real creds) over env garbage"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Test 2: HOME=TMPHOME, env vars = real, --account 0 => row 0 = garbage creds
|
||||||
|
# Expect: 401 / auth error (garbage creds used despite real env vars)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
output2="$(HOME="$TMPHOME" \
|
||||||
|
UNSANDBOX_PUBLIC_KEY="$REAL_PK" \
|
||||||
|
UNSANDBOX_SECRET_KEY="$REAL_SK" \
|
||||||
|
node "$UN_JS" --account 0 key 2>&1 || true)"
|
||||||
|
if echo "$output2" | grep -qiE '401|invalid|unauthorized|authentication|error'; then
|
||||||
|
pass "Test 2: --account 0 selects row 0 (garbage) over env real creds (expected auth failure)"
|
||||||
|
else
|
||||||
|
fail "Test 2: --account 0 should use garbage creds and fail auth, but got: $output2"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Test 3: HOME=TMPHOME, env vars = real, no --account => env vars win (real creds)
|
||||||
|
# Expect: key command succeeds
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
output3="$(HOME="$TMPHOME" \
|
||||||
|
UNSANDBOX_PUBLIC_KEY="$REAL_PK" \
|
||||||
|
UNSANDBOX_SECRET_KEY="$REAL_SK" \
|
||||||
|
node "$UN_JS" key 2>&1 || true)"
|
||||||
|
if echo "$output3" | grep -qiE '401|invalid|unauthorized|authentication failed'; then
|
||||||
|
fail "Test 3: without --account, env real creds should succeed but got auth error: $output3"
|
||||||
|
else
|
||||||
|
pass "Test 3: without --account, env vars (real creds) are used"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Summary
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
echo ""
|
||||||
|
echo "Results: PASS=$PASS FAIL=$FAIL SKIP=$SKIP"
|
||||||
|
|
||||||
|
if [ "$FAIL" -gt 0 ]; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
exit 0
|
||||||
184
clients/javascript/sync/tests/test_functional.mjs
Normal file
184
clients/javascript/sync/tests/test_functional.mjs
Normal file
|
|
@ -0,0 +1,184 @@
|
||||||
|
// 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 JavaScript SDK - Functional Tests
|
||||||
|
*
|
||||||
|
* Tests library functions against real API.
|
||||||
|
* Requires: UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* node clients/javascript/sync/tests/test_functional.mjs
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
executeCode,
|
||||||
|
getLanguages,
|
||||||
|
listSessions,
|
||||||
|
createSession,
|
||||||
|
deleteSession,
|
||||||
|
listServices,
|
||||||
|
listSnapshots,
|
||||||
|
listImages,
|
||||||
|
validateKeys,
|
||||||
|
healthCheck,
|
||||||
|
} from '../src/un.js';
|
||||||
|
|
||||||
|
const GREEN = '\x1b[32m';
|
||||||
|
const RED = '\x1b[31m';
|
||||||
|
const BLUE = '\x1b[34m';
|
||||||
|
const YELLOW = '\x1b[33m';
|
||||||
|
const NC = '\x1b[0m';
|
||||||
|
|
||||||
|
let passed = 0;
|
||||||
|
let failed = 0;
|
||||||
|
|
||||||
|
function check(condition, msg) {
|
||||||
|
if (condition) {
|
||||||
|
console.log(` ${GREEN}✓${NC} ${msg}`);
|
||||||
|
passed++;
|
||||||
|
} else {
|
||||||
|
console.log(` ${RED}✗${NC} ${msg}`);
|
||||||
|
failed++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testHealthCheck() {
|
||||||
|
console.log('\nTesting healthCheck()...');
|
||||||
|
const result = await healthCheck();
|
||||||
|
check(typeof result === 'boolean', 'healthCheck returns boolean');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testValidateKeys() {
|
||||||
|
console.log('\nTesting validateKeys()...');
|
||||||
|
const info = await validateKeys();
|
||||||
|
check(info != null, 'validateKeys returns non-null');
|
||||||
|
check(info.valid === true, 'keys are valid');
|
||||||
|
if (info.tier) console.log(` tier: ${info.tier}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testGetLanguages() {
|
||||||
|
console.log('\nTesting getLanguages()...');
|
||||||
|
const langs = await getLanguages();
|
||||||
|
check(Array.isArray(langs), 'getLanguages returns array');
|
||||||
|
check(langs.length > 0, 'at least one language returned');
|
||||||
|
check(langs.includes('python'), 'python is in languages list');
|
||||||
|
console.log(` Found ${langs.length} languages`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testExecute() {
|
||||||
|
console.log('\nTesting executeCode()...');
|
||||||
|
const result = await executeCode('python', "print('hello from JS SDK')");
|
||||||
|
check(result != null, 'execute returns non-null');
|
||||||
|
check(result.stdout && result.stdout.includes('hello from JS SDK'), 'stdout contains expected output');
|
||||||
|
check(result.exit_code === 0, 'exit code is 0');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testExecuteError() {
|
||||||
|
console.log('\nTesting executeCode() with error...');
|
||||||
|
const result = await executeCode('python', 'import sys; sys.exit(1)');
|
||||||
|
check(result != null, 'execute returns non-null');
|
||||||
|
check(result.exit_code === 1, 'exit code is 1');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testSessionList() {
|
||||||
|
console.log('\nTesting listSessions()...');
|
||||||
|
const sessions = await listSessions();
|
||||||
|
check(Array.isArray(sessions), 'listSessions returns array');
|
||||||
|
console.log(` Found ${sessions.length} sessions`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testSessionLifecycle() {
|
||||||
|
console.log('\nTesting session lifecycle (create, destroy)...');
|
||||||
|
|
||||||
|
const session = await createSession('python');
|
||||||
|
check(session != null, 'createSession returns non-null');
|
||||||
|
const sessionId = session.session_id || session.id;
|
||||||
|
check(sessionId != null, 'session has id');
|
||||||
|
console.log(` session_id: ${sessionId}`);
|
||||||
|
|
||||||
|
if (sessionId) {
|
||||||
|
const destroyed = await deleteSession(sessionId);
|
||||||
|
check(destroyed != null, 'deleteSession returns non-null');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testServiceList() {
|
||||||
|
console.log('\nTesting listServices()...');
|
||||||
|
const services = await listServices();
|
||||||
|
check(Array.isArray(services), 'listServices returns array');
|
||||||
|
console.log(` Found ${services.length} services`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testSnapshotList() {
|
||||||
|
console.log('\nTesting listSnapshots()...');
|
||||||
|
const snapshots = await listSnapshots();
|
||||||
|
check(Array.isArray(snapshots), 'listSnapshots returns array');
|
||||||
|
console.log(` Found ${snapshots.length} snapshots`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testImageList() {
|
||||||
|
console.log('\nTesting listImages()...');
|
||||||
|
const images = await listImages();
|
||||||
|
check(Array.isArray(images), 'listImages returns array');
|
||||||
|
console.log(` Found ${images.length} images`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main
|
||||||
|
async function main() {
|
||||||
|
console.log('=====================================');
|
||||||
|
console.log('UN JavaScript SDK - Functional Tests');
|
||||||
|
console.log('Testing against real API');
|
||||||
|
console.log('=====================================');
|
||||||
|
|
||||||
|
if (!process.env.UNSANDBOX_PUBLIC_KEY || !process.env.UNSANDBOX_SECRET_KEY) {
|
||||||
|
console.log(`\n${YELLOW}SKIP: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set${NC}`);
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const tests = [
|
||||||
|
testHealthCheck,
|
||||||
|
testValidateKeys,
|
||||||
|
testGetLanguages,
|
||||||
|
testExecute,
|
||||||
|
testExecuteError,
|
||||||
|
testSessionList,
|
||||||
|
testSessionLifecycle,
|
||||||
|
testServiceList,
|
||||||
|
testSnapshotList,
|
||||||
|
testImageList,
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const test of tests) {
|
||||||
|
try {
|
||||||
|
await test();
|
||||||
|
} catch (err) {
|
||||||
|
console.log(` ${RED}✗${NC} ${test.name}: ${err.message}`);
|
||||||
|
failed++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('\n=====================================');
|
||||||
|
console.log('Test Summary');
|
||||||
|
console.log('=====================================');
|
||||||
|
console.log(`Passed: ${GREEN}${passed}${NC}`);
|
||||||
|
console.log(`Failed: ${RED}${failed}${NC}`);
|
||||||
|
console.log('=====================================');
|
||||||
|
|
||||||
|
process.exit(failed > 0 ? 1 : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
|
|
@ -78,28 +78,77 @@ function detect_language(filename::String)::String
|
||||||
return get(EXT_MAP, ext, "unknown")
|
return get(EXT_MAP, ext, "unknown")
|
||||||
end
|
end
|
||||||
|
|
||||||
function get_api_keys(args_key=nothing)::Tuple{String,String}
|
function load_accounts_csv(path::String)::Vector{Tuple{String,String}}
|
||||||
# Try new-style keys first
|
accounts = Tuple{String,String}[]
|
||||||
public_key = something(args_key, get(ENV, "UNSANDBOX_PUBLIC_KEY", ""))
|
isfile(path) || return accounts
|
||||||
secret_key = get(ENV, "UNSANDBOX_SECRET_KEY", "")
|
try
|
||||||
|
for line in eachline(path)
|
||||||
# Fall back to old-style single key for backwards compatibility
|
trimmed = strip(line)
|
||||||
if isempty(public_key)
|
isempty(trimmed) && continue
|
||||||
old_key = get(ENV, "UNSANDBOX_API_KEY", "")
|
startswith(trimmed, "#") && continue
|
||||||
if isempty(old_key)
|
parts = split(trimmed, ","; limit=2)
|
||||||
println(stderr, "$(RED)Error: UNSANDBOX_PUBLIC_KEY/UNSANDBOX_SECRET_KEY or UNSANDBOX_API_KEY not set$(RESET)")
|
length(parts) >= 2 || continue
|
||||||
exit(1)
|
pk = strip(parts[1])
|
||||||
|
sk = strip(parts[2])
|
||||||
|
if startswith(pk, "unsb-pk-") && startswith(sk, "unsb-sk-")
|
||||||
|
push!(accounts, (pk, sk))
|
||||||
|
end
|
||||||
end
|
end
|
||||||
# Old-style: use same key for both public and secret
|
catch
|
||||||
return (old_key, old_key)
|
|
||||||
end
|
end
|
||||||
|
return accounts
|
||||||
|
end
|
||||||
|
|
||||||
if isempty(secret_key)
|
function get_credentials(; account_index::Int=-1)::Tuple{String,String}
|
||||||
println(stderr, "$(RED)Error: UNSANDBOX_SECRET_KEY not set$(RESET)")
|
# Priority 2: --account N => accounts.csv row N (bypasses env vars)
|
||||||
|
if account_index >= 0
|
||||||
|
for path in [joinpath(homedir(), ".unsandbox", "accounts.csv"), "accounts.csv"]
|
||||||
|
accts = load_accounts_csv(path)
|
||||||
|
if account_index < length(accts)
|
||||||
|
return accts[account_index + 1]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
println(stderr, "$(RED)Error: No account at index $account_index in accounts.csv$(RESET)")
|
||||||
exit(1)
|
exit(1)
|
||||||
end
|
end
|
||||||
|
|
||||||
return (public_key, secret_key)
|
# Priority 3: Environment variables
|
||||||
|
public_key = get(ENV, "UNSANDBOX_PUBLIC_KEY", "")
|
||||||
|
secret_key = get(ENV, "UNSANDBOX_SECRET_KEY", "")
|
||||||
|
if !isempty(public_key) && !isempty(secret_key)
|
||||||
|
return (public_key, secret_key)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Priority 4: ~/.unsandbox/accounts.csv row 0 (or UNSANDBOX_ACCOUNT env)
|
||||||
|
default_idx = tryparse(Int, get(ENV, "UNSANDBOX_ACCOUNT", "0"))
|
||||||
|
default_idx = something(default_idx, 0)
|
||||||
|
for path in [joinpath(homedir(), ".unsandbox", "accounts.csv"), "accounts.csv"]
|
||||||
|
accts = load_accounts_csv(path)
|
||||||
|
if default_idx < length(accts)
|
||||||
|
return accts[default_idx + 1]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Legacy fallback
|
||||||
|
old_key = get(ENV, "UNSANDBOX_API_KEY", "")
|
||||||
|
if !isempty(old_key)
|
||||||
|
return (old_key, old_key)
|
||||||
|
end
|
||||||
|
|
||||||
|
println(stderr, "$(RED)Error: UNSANDBOX_PUBLIC_KEY/UNSANDBOX_SECRET_KEY or UNSANDBOX_API_KEY not set$(RESET)")
|
||||||
|
exit(1)
|
||||||
|
end
|
||||||
|
|
||||||
|
function get_api_keys(args_key=nothing; account_index::Int=-1)::Tuple{String,String}
|
||||||
|
# Priority 1: explicit -k flag
|
||||||
|
if args_key !== nothing && !isempty(string(args_key))
|
||||||
|
public_key = string(args_key)
|
||||||
|
secret_key = get(ENV, "UNSANDBOX_SECRET_KEY", "")
|
||||||
|
if !isempty(secret_key)
|
||||||
|
return (public_key, secret_key)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return get_credentials(account_index=account_index)
|
||||||
end
|
end
|
||||||
|
|
||||||
function hmac_sha256_hex(key::String, message::String)::String
|
function hmac_sha256_hex(key::String, message::String)::String
|
||||||
|
|
@ -356,7 +405,7 @@ function service_env_delete(service_id::String, public_key::String, secret_key::
|
||||||
end
|
end
|
||||||
|
|
||||||
function cmd_service_env(args)
|
function cmd_service_env(args)
|
||||||
(public_key, secret_key) = get_api_keys(args["api-key"])
|
(public_key, secret_key) = get_api_keys(args["api-key"]; account_index=something(get(args, "account", nothing), -1))
|
||||||
|
|
||||||
action = get(args, "env-action", nothing)
|
action = get(args, "env-action", nothing)
|
||||||
target = get(args, "env-target", nothing)
|
target = get(args, "env-target", nothing)
|
||||||
|
|
@ -428,7 +477,7 @@ function cmd_service_env(args)
|
||||||
end
|
end
|
||||||
|
|
||||||
function cmd_execute(args)
|
function cmd_execute(args)
|
||||||
(public_key, secret_key) = get_api_keys(args["api-key"])
|
(public_key, secret_key) = get_api_keys(args["api-key"]; account_index=something(get(args, "account", nothing), -1))
|
||||||
|
|
||||||
filename = args["source_file"]
|
filename = args["source_file"]
|
||||||
if !isfile(filename)
|
if !isfile(filename)
|
||||||
|
|
@ -518,7 +567,7 @@ function cmd_execute(args)
|
||||||
end
|
end
|
||||||
|
|
||||||
function cmd_session(args)
|
function cmd_session(args)
|
||||||
(public_key, secret_key) = get_api_keys(args["api-key"])
|
(public_key, secret_key) = get_api_keys(args["api-key"]; account_index=something(get(args, "account", nothing), -1))
|
||||||
|
|
||||||
if args["list"]
|
if args["list"]
|
||||||
result = api_request("/sessions", public_key, secret_key)
|
result = api_request("/sessions", public_key, secret_key)
|
||||||
|
|
@ -577,7 +626,7 @@ function cmd_session(args)
|
||||||
end
|
end
|
||||||
|
|
||||||
function cmd_service(args)
|
function cmd_service(args)
|
||||||
(public_key, secret_key) = get_api_keys(args["api-key"])
|
(public_key, secret_key) = get_api_keys(args["api-key"]; account_index=something(get(args, "account", nothing), -1))
|
||||||
|
|
||||||
# Handle env subcommand
|
# Handle env subcommand
|
||||||
if get(args, "env-action", nothing) !== nothing
|
if get(args, "env-action", nothing) !== nothing
|
||||||
|
|
@ -914,7 +963,7 @@ function cmd_languages(args)
|
||||||
|
|
||||||
if langs === nothing
|
if langs === nothing
|
||||||
# Cache miss or expired, fetch from API
|
# Cache miss or expired, fetch from API
|
||||||
(public_key, secret_key) = get_api_keys(args["api-key"])
|
(public_key, secret_key) = get_api_keys(args["api-key"]; account_index=something(get(args, "account", nothing), -1))
|
||||||
result = api_request("/languages", public_key, secret_key)
|
result = api_request("/languages", public_key, secret_key)
|
||||||
langs = get(result, "languages", [])
|
langs = get(result, "languages", [])
|
||||||
save_languages_cache(langs)
|
save_languages_cache(langs)
|
||||||
|
|
@ -930,7 +979,7 @@ function cmd_languages(args)
|
||||||
end
|
end
|
||||||
|
|
||||||
function cmd_key(args)
|
function cmd_key(args)
|
||||||
(public_key, secret_key) = get_api_keys(args["api-key"])
|
(public_key, secret_key) = get_api_keys(args["api-key"]; account_index=something(get(args, "account", nothing), -1))
|
||||||
# For portal validation, we still use public_key as bearer token
|
# For portal validation, we still use public_key as bearer token
|
||||||
api_key = public_key
|
api_key = public_key
|
||||||
|
|
||||||
|
|
@ -996,6 +1045,9 @@ function main()
|
||||||
required = false
|
required = false
|
||||||
"--api-key", "-k"
|
"--api-key", "-k"
|
||||||
help = "API key (or set UNSANDBOX_API_KEY)"
|
help = "API key (or set UNSANDBOX_API_KEY)"
|
||||||
|
"--account"
|
||||||
|
help = "Account index in ~/.unsandbox/accounts.csv (bypasses env vars)"
|
||||||
|
arg_type = Int
|
||||||
"--network", "-n"
|
"--network", "-n"
|
||||||
help = "Network mode"
|
help = "Network mode"
|
||||||
arg_type = String
|
arg_type = String
|
||||||
|
|
@ -1057,6 +1109,9 @@ function main()
|
||||||
help = "Comma-separated ports for cloned service"
|
help = "Comma-separated ports for cloned service"
|
||||||
"--api-key", "-k"
|
"--api-key", "-k"
|
||||||
help = "API key"
|
help = "API key"
|
||||||
|
"--account"
|
||||||
|
help = "Account index in ~/.unsandbox/accounts.csv (bypasses env vars)"
|
||||||
|
arg_type = Int
|
||||||
end
|
end
|
||||||
|
|
||||||
@add_arg_table! s["session"] begin
|
@add_arg_table! s["session"] begin
|
||||||
|
|
@ -1074,6 +1129,9 @@ function main()
|
||||||
range_tester = x -> x in ["zerotrust", "semitrusted"]
|
range_tester = x -> x in ["zerotrust", "semitrusted"]
|
||||||
"--api-key", "-k"
|
"--api-key", "-k"
|
||||||
help = "API key"
|
help = "API key"
|
||||||
|
"--account"
|
||||||
|
help = "Account index in ~/.unsandbox/accounts.csv (bypasses env vars)"
|
||||||
|
arg_type = Int
|
||||||
end
|
end
|
||||||
|
|
||||||
@add_arg_table! s["service"] begin
|
@add_arg_table! s["service"] begin
|
||||||
|
|
@ -1135,6 +1193,9 @@ function main()
|
||||||
help = "Service ID for env commands"
|
help = "Service ID for env commands"
|
||||||
"--api-key", "-k"
|
"--api-key", "-k"
|
||||||
help = "API key"
|
help = "API key"
|
||||||
|
"--account"
|
||||||
|
help = "Account index in ~/.unsandbox/accounts.csv (bypasses env vars)"
|
||||||
|
arg_type = Int
|
||||||
"env"
|
"env"
|
||||||
help = "Manage service environment vault"
|
help = "Manage service environment vault"
|
||||||
action = :command
|
action = :command
|
||||||
|
|
@ -1155,6 +1216,9 @@ function main()
|
||||||
help = "Load vault variables from file"
|
help = "Load vault variables from file"
|
||||||
"--api-key", "-k"
|
"--api-key", "-k"
|
||||||
help = "API key"
|
help = "API key"
|
||||||
|
"--account"
|
||||||
|
help = "Account index in ~/.unsandbox/accounts.csv (bypasses env vars)"
|
||||||
|
arg_type = Int
|
||||||
end
|
end
|
||||||
|
|
||||||
@add_arg_table! s["key"] begin
|
@add_arg_table! s["key"] begin
|
||||||
|
|
@ -1163,6 +1227,9 @@ function main()
|
||||||
action = :store_true
|
action = :store_true
|
||||||
"--api-key", "-k"
|
"--api-key", "-k"
|
||||||
help = "API key"
|
help = "API key"
|
||||||
|
"--account"
|
||||||
|
help = "Account index in ~/.unsandbox/accounts.csv (bypasses env vars)"
|
||||||
|
arg_type = Int
|
||||||
end
|
end
|
||||||
|
|
||||||
@add_arg_table! s["languages"] begin
|
@add_arg_table! s["languages"] begin
|
||||||
|
|
@ -1171,6 +1238,9 @@ function main()
|
||||||
action = :store_true
|
action = :store_true
|
||||||
"--api-key", "-k"
|
"--api-key", "-k"
|
||||||
help = "API key"
|
help = "API key"
|
||||||
|
"--account"
|
||||||
|
help = "Account index in ~/.unsandbox/accounts.csv (bypasses env vars)"
|
||||||
|
arg_type = Int
|
||||||
end
|
end
|
||||||
|
|
||||||
@add_arg_table! s["image"] begin
|
@add_arg_table! s["image"] begin
|
||||||
|
|
@ -1203,6 +1273,9 @@ function main()
|
||||||
help = "Comma-separated ports for spawned service"
|
help = "Comma-separated ports for spawned service"
|
||||||
"--api-key", "-k"
|
"--api-key", "-k"
|
||||||
help = "API key"
|
help = "API key"
|
||||||
|
"--account"
|
||||||
|
help = "Account index in ~/.unsandbox/accounts.csv (bypasses env vars)"
|
||||||
|
arg_type = Int
|
||||||
end
|
end
|
||||||
|
|
||||||
args = parse_args(ARGS, s)
|
args = parse_args(ARGS, s)
|
||||||
|
|
@ -1220,6 +1293,7 @@ function main()
|
||||||
service_args["vault-env"] = get(env_args, "vault-env", nothing)
|
service_args["vault-env"] = get(env_args, "vault-env", nothing)
|
||||||
service_args["env-file"] = get(env_args, "env-file", nothing)
|
service_args["env-file"] = get(env_args, "env-file", nothing)
|
||||||
service_args["api-key"] = get(env_args, "api-key", nothing)
|
service_args["api-key"] = get(env_args, "api-key", nothing)
|
||||||
|
service_args["account"] = get(env_args, "account", nothing)
|
||||||
end
|
end
|
||||||
cmd_service(service_args)
|
cmd_service(service_args)
|
||||||
elseif args["%COMMAND%"] == "languages"
|
elseif args["%COMMAND%"] == "languages"
|
||||||
|
|
@ -1239,7 +1313,7 @@ function main()
|
||||||
end
|
end
|
||||||
|
|
||||||
function cmd_image(args)
|
function cmd_image(args)
|
||||||
(public_key, secret_key) = get_api_keys(args["api-key"])
|
(public_key, secret_key) = get_api_keys(args["api-key"]; account_index=something(get(args, "account", nothing), -1))
|
||||||
|
|
||||||
if args["list"]
|
if args["list"]
|
||||||
result = api_request("/images", public_key, secret_key)
|
result = api_request("/images", public_key, secret_key)
|
||||||
|
|
@ -1330,7 +1404,7 @@ function cmd_image(args)
|
||||||
end
|
end
|
||||||
|
|
||||||
function cmd_snapshot(args)
|
function cmd_snapshot(args)
|
||||||
(public_key, secret_key) = get_api_keys(args["api-key"])
|
(public_key, secret_key) = get_api_keys(args["api-key"]; account_index=something(get(args, "account", nothing), -1))
|
||||||
|
|
||||||
if args["list"]
|
if args["list"]
|
||||||
result = api_request("/snapshots", public_key, secret_key)
|
result = api_request("/snapshots", public_key, secret_key)
|
||||||
|
|
|
||||||
|
|
@ -124,7 +124,8 @@ data class Args(
|
||||||
var imageSpawn: String? = null,
|
var imageSpawn: String? = null,
|
||||||
var imageClone: String? = null,
|
var imageClone: String? = null,
|
||||||
var imageName: String? = null,
|
var imageName: String? = null,
|
||||||
var imagePorts: String? = null
|
var imagePorts: String? = null,
|
||||||
|
var accountIndex: Int = -1
|
||||||
)
|
)
|
||||||
|
|
||||||
fun main(args: Array<String>) {
|
fun main(args: Array<String>) {
|
||||||
|
|
@ -151,7 +152,7 @@ fun main(args: Array<String>) {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun cmdExecute(args: Args) {
|
fun cmdExecute(args: Args) {
|
||||||
val (publicKey, secretKey) = getApiKeys(args.apiKey)
|
val (publicKey, secretKey) = getApiKeys(args.apiKey, args.accountIndex)
|
||||||
val code = File(args.sourceFile!!).readText()
|
val code = File(args.sourceFile!!).readText()
|
||||||
val language = detectLanguage(args.sourceFile!!)
|
val language = detectLanguage(args.sourceFile!!)
|
||||||
|
|
||||||
|
|
@ -226,7 +227,7 @@ fun cmdExecute(args: Args) {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun cmdSession(args: Args) {
|
fun cmdSession(args: Args) {
|
||||||
val (publicKey, secretKey) = getApiKeys(args.apiKey)
|
val (publicKey, secretKey) = getApiKeys(args.apiKey, args.accountIndex)
|
||||||
|
|
||||||
if (args.sessionList) {
|
if (args.sessionList) {
|
||||||
val result = apiRequest("/sessions", "GET", null, publicKey, secretKey)
|
val result = apiRequest("/sessions", "GET", null, publicKey, secretKey)
|
||||||
|
|
@ -287,7 +288,7 @@ fun cmdSession(args: Args) {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun cmdService(args: Args) {
|
fun cmdService(args: Args) {
|
||||||
val (publicKey, secretKey) = getApiKeys(args.apiKey)
|
val (publicKey, secretKey) = getApiKeys(args.apiKey, args.accountIndex)
|
||||||
|
|
||||||
// Handle env subcommand
|
// Handle env subcommand
|
||||||
if (args.envAction != null) {
|
if (args.envAction != null) {
|
||||||
|
|
@ -541,7 +542,7 @@ fun saveLanguagesCache(languages: List<String>) {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun cmdLanguages(args: Args) {
|
fun cmdLanguages(args: Args) {
|
||||||
val (publicKey, secretKey) = getApiKeys(args.apiKey)
|
val (publicKey, secretKey) = getApiKeys(args.apiKey, args.accountIndex)
|
||||||
|
|
||||||
// Try cache first
|
// Try cache first
|
||||||
var languages = loadLanguagesCache()
|
var languages = loadLanguagesCache()
|
||||||
|
|
@ -563,7 +564,7 @@ fun cmdLanguages(args: Args) {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun cmdImage(args: Args) {
|
fun cmdImage(args: Args) {
|
||||||
val (publicKey, secretKey) = getApiKeys(args.apiKey)
|
val (publicKey, secretKey) = getApiKeys(args.apiKey, args.accountIndex)
|
||||||
|
|
||||||
if (args.imageList) {
|
if (args.imageList) {
|
||||||
val result = apiRequest("/images", "GET", null, publicKey, secretKey)
|
val result = apiRequest("/images", "GET", null, publicKey, secretKey)
|
||||||
|
|
@ -654,7 +655,7 @@ fun cmdImage(args: Args) {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun cmdKey(args: Args) {
|
fun cmdKey(args: Args) {
|
||||||
val (publicKey, secretKey) = getApiKeys(args.apiKey)
|
val (publicKey, secretKey) = getApiKeys(args.apiKey, args.accountIndex)
|
||||||
|
|
||||||
val result = validateKey(publicKey, secretKey)
|
val result = validateKey(publicKey, secretKey)
|
||||||
val valid = result["valid"] as? Boolean ?: false
|
val valid = result["valid"] as? Boolean ?: false
|
||||||
|
|
@ -733,11 +734,41 @@ fun validateKey(publicKey: String?, secretKey: String): Map<String, Any> {
|
||||||
return parseJson(response)
|
return parseJson(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getApiKeys(argsKey: String?): Pair<String?, String> {
|
// 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, or null.
|
||||||
|
fun loadAccountsCSV(path: String, index: Int): Pair<String, String>? {
|
||||||
|
val file = java.io.File(path)
|
||||||
|
if (!file.exists()) return null
|
||||||
|
var row = 0
|
||||||
|
for (line in file.readLines()) {
|
||||||
|
val stripped = line.trim()
|
||||||
|
if (stripped.isEmpty() || stripped.startsWith("#")) continue
|
||||||
|
if (row == index) {
|
||||||
|
val comma = stripped.indexOf(',')
|
||||||
|
if (comma < 0) return null
|
||||||
|
return Pair(stripped.substring(0, comma), stripped.substring(comma + 1))
|
||||||
|
}
|
||||||
|
row++
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getApiKeys(argsKey: String?, accountIndex: Int = -1): Pair<String?, String> {
|
||||||
var publicKey: String? = null
|
var publicKey: String? = null
|
||||||
var secretKey: String? = null
|
var secretKey: String? = null
|
||||||
|
|
||||||
if (argsKey != null) {
|
if (accountIndex >= 0) {
|
||||||
|
// --account N: load from ~/.unsandbox/accounts.csv, bypassing env vars
|
||||||
|
val home = System.getenv("HOME") ?: System.getProperty("user.home") ?: "."
|
||||||
|
var creds = loadAccountsCSV("$home/.unsandbox/accounts.csv", accountIndex)
|
||||||
|
if (creds == null) {
|
||||||
|
creds = loadAccountsCSV("accounts.csv", accountIndex)
|
||||||
|
}
|
||||||
|
if (creds != null) {
|
||||||
|
publicKey = if (argsKey != null) argsKey else creds.first
|
||||||
|
secretKey = creds.second
|
||||||
|
}
|
||||||
|
} else if (argsKey != null) {
|
||||||
secretKey = argsKey
|
secretKey = argsKey
|
||||||
publicKey = System.getenv("UNSANDBOX_PUBLIC_KEY")
|
publicKey = System.getenv("UNSANDBOX_PUBLIC_KEY")
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -750,6 +781,22 @@ fun getApiKeys(argsKey: String?): Pair<String?, String> {
|
||||||
secretKey = apiKey
|
secretKey = apiKey
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Try UNSANDBOX_ACCOUNT env var to pick a row
|
||||||
|
val envAcct = System.getenv("UNSANDBOX_ACCOUNT")
|
||||||
|
val envAccount = envAcct?.toIntOrNull() ?: -1
|
||||||
|
|
||||||
|
if (publicKey.isNullOrEmpty()) {
|
||||||
|
val home = System.getenv("HOME") ?: System.getProperty("user.home") ?: "."
|
||||||
|
var creds = loadAccountsCSV("$home/.unsandbox/accounts.csv", if (envAccount >= 0) envAccount else 0)
|
||||||
|
if (creds == null) {
|
||||||
|
creds = loadAccountsCSV("accounts.csv", if (envAccount >= 0) envAccount else 0)
|
||||||
|
}
|
||||||
|
if (creds != null) {
|
||||||
|
publicKey = creds.first
|
||||||
|
secretKey = creds.second
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (secretKey.isNullOrEmpty()) {
|
if (secretKey.isNullOrEmpty()) {
|
||||||
|
|
@ -1005,7 +1052,7 @@ fun serviceEnvDelete(serviceId: String, publicKey: String?, secretKey: String):
|
||||||
}
|
}
|
||||||
|
|
||||||
fun cmdServiceEnv(args: Args) {
|
fun cmdServiceEnv(args: Args) {
|
||||||
val (publicKey, secretKey) = getApiKeys(args.apiKey)
|
val (publicKey, secretKey) = getApiKeys(args.apiKey, args.accountIndex)
|
||||||
val action = args.envAction
|
val action = args.envAction
|
||||||
val target = args.envTarget
|
val target = args.envTarget
|
||||||
|
|
||||||
|
|
@ -1260,6 +1307,7 @@ fun parseArgs(args: Array<String>): Args {
|
||||||
"--dump-bootstrap" -> result.serviceDumpBootstrap = args[++i]
|
"--dump-bootstrap" -> result.serviceDumpBootstrap = args[++i]
|
||||||
"--dump-file" -> result.serviceDumpFile = args[++i]
|
"--dump-file" -> result.serviceDumpFile = args[++i]
|
||||||
"--extend" -> result.keyExtend = true
|
"--extend" -> result.keyExtend = true
|
||||||
|
"--account" -> result.accountIndex = args[++i].toInt()
|
||||||
"--env-file" -> result.envFile = args[++i]
|
"--env-file" -> result.envFile = args[++i]
|
||||||
"--info" -> {
|
"--info" -> {
|
||||||
when (result.command) {
|
when (result.command) {
|
||||||
|
|
|
||||||
|
|
@ -343,16 +343,63 @@
|
||||||
(curl-delete api-key (format nil "/services/~a/env" service-id))
|
(curl-delete api-key (format nil "/services/~a/env" service-id))
|
||||||
(format t "~aVault deleted: ~a~a~%" *green* service-id *reset*))
|
(format t "~aVault deleted: ~a~a~%" *green* service-id *reset*))
|
||||||
|
|
||||||
|
(defparameter *account-index* nil)
|
||||||
|
|
||||||
|
(defun load-accounts-csv (path index)
|
||||||
|
"Load row INDEX from a CSV file of public_key,secret_key pairs.
|
||||||
|
Skips blank lines and lines starting with #. Returns (list pk sk) or nil."
|
||||||
|
(when (probe-file path)
|
||||||
|
(handler-case
|
||||||
|
(with-open-file (stream path)
|
||||||
|
(let ((rows nil))
|
||||||
|
(loop for line = (read-line stream nil nil)
|
||||||
|
while line do
|
||||||
|
(let ((trimmed (string-trim '(#\Space #\Tab #\Return) line)))
|
||||||
|
(when (and (> (length trimmed) 0)
|
||||||
|
(not (char= (char trimmed 0) #\#)))
|
||||||
|
(let ((comma-pos (position #\, trimmed)))
|
||||||
|
(when comma-pos
|
||||||
|
(push (list (string-trim '(#\Space #\Tab) (subseq trimmed 0 comma-pos))
|
||||||
|
(string-trim '(#\Space #\Tab) (subseq trimmed (1+ comma-pos))))
|
||||||
|
rows))))))
|
||||||
|
(let ((rows (nreverse rows)))
|
||||||
|
(when (< index (length rows))
|
||||||
|
(nth index rows)))))
|
||||||
|
(error () nil))))
|
||||||
|
|
||||||
(defun get-api-keys ()
|
(defun get-api-keys ()
|
||||||
(let ((public-key (uiop:getenv "UNSANDBOX_PUBLIC_KEY"))
|
(let ((public-key (uiop:getenv "UNSANDBOX_PUBLIC_KEY"))
|
||||||
(secret-key (uiop:getenv "UNSANDBOX_SECRET_KEY"))
|
(secret-key (uiop:getenv "UNSANDBOX_SECRET_KEY"))
|
||||||
(api-key (uiop:getenv "UNSANDBOX_API_KEY")))
|
(api-key (uiop:getenv "UNSANDBOX_API_KEY"))
|
||||||
|
(home (uiop:getenv "HOME")))
|
||||||
(cond
|
(cond
|
||||||
|
;; --account N: load row N from accounts.csv, bypasses env vars
|
||||||
|
((not (null *account-index*))
|
||||||
|
(let ((result (or (load-accounts-csv
|
||||||
|
(format nil "~a/.unsandbox/accounts.csv" home)
|
||||||
|
*account-index*)
|
||||||
|
(load-accounts-csv "./accounts.csv" *account-index*))))
|
||||||
|
(or result
|
||||||
|
(progn
|
||||||
|
(format *error-output* "Error: account ~a not found in accounts.csv~%" *account-index*)
|
||||||
|
(uiop:quit 1)))))
|
||||||
|
;; env vars
|
||||||
((and public-key secret-key) (list public-key secret-key))
|
((and public-key secret-key) (list public-key secret-key))
|
||||||
(api-key (list api-key nil))
|
(api-key (list api-key nil))
|
||||||
(t (progn
|
;; accounts.csv fallback (row 0 or UNSANDBOX_ACCOUNT env var)
|
||||||
(format t "Error: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set (or UNSANDBOX_API_KEY for backwards compat)~%")
|
(t
|
||||||
(uiop:quit 1))))))
|
(let* ((acc-env (uiop:getenv "UNSANDBOX_ACCOUNT"))
|
||||||
|
(row-idx (if acc-env
|
||||||
|
(handler-case (parse-integer acc-env) (error () 0))
|
||||||
|
0))
|
||||||
|
(result (or (load-accounts-csv
|
||||||
|
(format nil "~a/.unsandbox/accounts.csv" home)
|
||||||
|
row-idx)
|
||||||
|
(load-accounts-csv "./accounts.csv" row-idx))))
|
||||||
|
(or result
|
||||||
|
(progn
|
||||||
|
(format *error-output* "Error: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set (or UNSANDBOX_API_KEY for backwards compat)~%")
|
||||||
|
(uiop:quit 1))))))))
|
||||||
|
|
||||||
(defun get-api-key ()
|
(defun get-api-key ()
|
||||||
(first (get-api-keys)))
|
(first (get-api-keys)))
|
||||||
|
|
@ -912,8 +959,26 @@
|
||||||
do (format t "~a~%" (subseq array-content (1+ start) end))))
|
do (format t "~a~%" (subseq array-content (1+ start) end))))
|
||||||
(format t "")))))
|
(format t "")))))
|
||||||
|
|
||||||
|
(defun strip-account-flag (args)
|
||||||
|
"Remove --account N from args, set *account-index* as side-effect. Returns filtered args."
|
||||||
|
(let ((result nil)
|
||||||
|
(i 0)
|
||||||
|
(vec (coerce args 'vector)))
|
||||||
|
(loop while (< i (length vec)) do
|
||||||
|
(cond
|
||||||
|
((string= (aref vec i) "--account")
|
||||||
|
(when (< (+ i 1) (length vec))
|
||||||
|
(setf *account-index*
|
||||||
|
(handler-case (parse-integer (aref vec (+ i 1))) (error () nil))))
|
||||||
|
(incf i 2))
|
||||||
|
(t
|
||||||
|
(push (aref vec i) result)
|
||||||
|
(incf i))))
|
||||||
|
(nreverse result)))
|
||||||
|
|
||||||
(defun main ()
|
(defun main ()
|
||||||
(let ((args (uiop:command-line-arguments)))
|
(let* ((raw-args (uiop:command-line-arguments))
|
||||||
|
(args (strip-account-flag raw-args)))
|
||||||
(if (null args)
|
(if (null args)
|
||||||
(progn
|
(progn
|
||||||
(format t "Usage: un.lisp [options] <source_file>~%")
|
(format t "Usage: un.lisp [options] <source_file>~%")
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ Un.API_BASE = "https://api.unsandbox.com"
|
||||||
Un.PORTAL_BASE = "https://unsandbox.com"
|
Un.PORTAL_BASE = "https://unsandbox.com"
|
||||||
Un.VERSION = "4.3.4"
|
Un.VERSION = "4.3.4"
|
||||||
Un.LAST_ERROR = ""
|
Un.LAST_ERROR = ""
|
||||||
|
Un.ACCOUNT_INDEX = -1 -- -1 means not set; set to N to use accounts.csv row N
|
||||||
|
|
||||||
-- Colors
|
-- Colors
|
||||||
local BLUE = "\027[34m"
|
local BLUE = "\027[34m"
|
||||||
|
|
@ -116,7 +117,20 @@ function Un.get_credentials(opts)
|
||||||
return opts.public_key, opts.secret_key
|
return opts.public_key, opts.secret_key
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Tier 2: Environment
|
-- Tier 2: --account N flag → bypass env vars, load CSV row N directly
|
||||||
|
local ai = opts.account_index
|
||||||
|
if (ai == nil) and Un.ACCOUNT_INDEX >= 0 then ai = Un.ACCOUNT_INDEX end
|
||||||
|
if ai and ai >= 0 then
|
||||||
|
local row = ai + 1 -- Lua tables are 1-indexed
|
||||||
|
local accounts = Un.load_accounts_csv()
|
||||||
|
if #accounts >= row then return accounts[row][1], accounts[row][2] end
|
||||||
|
accounts = Un.load_accounts_csv("./accounts.csv")
|
||||||
|
if #accounts >= row then return accounts[row][1], accounts[row][2] end
|
||||||
|
Un.set_error("Account index " .. ai .. " not found in accounts.csv")
|
||||||
|
return nil, nil
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Tier 3: Environment
|
||||||
local pk = os.getenv("UNSANDBOX_PUBLIC_KEY")
|
local pk = os.getenv("UNSANDBOX_PUBLIC_KEY")
|
||||||
local sk = os.getenv("UNSANDBOX_SECRET_KEY")
|
local sk = os.getenv("UNSANDBOX_SECRET_KEY")
|
||||||
if pk and sk then return pk, sk end
|
if pk and sk then return pk, sk end
|
||||||
|
|
@ -126,11 +140,11 @@ function Un.get_credentials(opts)
|
||||||
return os.getenv("UNSANDBOX_API_KEY"), ""
|
return os.getenv("UNSANDBOX_API_KEY"), ""
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Tier 3: Home directory
|
-- Tier 4: Home directory
|
||||||
local accounts = Un.load_accounts_csv()
|
local accounts = Un.load_accounts_csv()
|
||||||
if #accounts > 0 then return accounts[1][1], accounts[1][2] end
|
if #accounts > 0 then return accounts[1][1], accounts[1][2] end
|
||||||
|
|
||||||
-- Tier 4: Local directory
|
-- Tier 5: Local directory
|
||||||
accounts = Un.load_accounts_csv("./accounts.csv")
|
accounts = Un.load_accounts_csv("./accounts.csv")
|
||||||
if #accounts > 0 then return accounts[1][1], accounts[1][2] end
|
if #accounts > 0 then return accounts[1][1], accounts[1][2] end
|
||||||
|
|
||||||
|
|
@ -744,6 +758,21 @@ if arg and arg[0] then
|
||||||
local args = arg
|
local args = arg
|
||||||
local i = 1
|
local i = 1
|
||||||
|
|
||||||
|
-- Pre-scan for --account N; strip it from args before dispatch
|
||||||
|
local _filtered = {}
|
||||||
|
local _j = 1
|
||||||
|
while _j <= #args do
|
||||||
|
if args[_j] == "--account" then
|
||||||
|
_j = _j + 1
|
||||||
|
Un.ACCOUNT_INDEX = tonumber(args[_j]) or -1
|
||||||
|
else
|
||||||
|
table.insert(_filtered, args[_j])
|
||||||
|
end
|
||||||
|
_j = _j + 1
|
||||||
|
end
|
||||||
|
for k = 1, #_filtered do args[k] = _filtered[k] end
|
||||||
|
for k = #_filtered + 1, #args do args[k] = nil end
|
||||||
|
|
||||||
if #args == 0 then
|
if #args == 0 then
|
||||||
print("Usage: lua un.lua [options] <source_file>")
|
print("Usage: lua un.lua [options] <source_file>")
|
||||||
print(" lua un.lua -s <language> '<code>'")
|
print(" lua un.lua -s <language> '<code>'")
|
||||||
|
|
|
||||||
|
|
@ -1220,13 +1220,97 @@ proc cmdImage(list: bool, infoId, deleteId, lockId, unlockId, publishId, sourceT
|
||||||
stderr.writeLine(RED & "Error: Use --list, --info, --delete, --lock, --unlock, --publish, --visibility, --spawn, --clone, --grant, --revoke, --trusted, or --transfer" & RESET)
|
stderr.writeLine(RED & "Error: Use --list, --info, --delete, --lock, --unlock, --publish, --visibility, --spawn, --clone, --grant, --revoke, --trusted, or --transfer" & RESET)
|
||||||
quit(1)
|
quit(1)
|
||||||
|
|
||||||
proc main() =
|
proc loadCredentialsFromCsv(csvPath: string, accountIndex: int): tuple[pk: string, sk: string] =
|
||||||
var publicKey = getEnv("UNSANDBOX_PUBLIC_KEY", "")
|
## Load public_key,secret_key from CSV at given row index (0-based, skipping comments/blanks).
|
||||||
var secretKey = getEnv("UNSANDBOX_SECRET_KEY", "")
|
result = ("", "")
|
||||||
|
if not fileExists(csvPath):
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
let content = readFile(csvPath)
|
||||||
|
var dataIndex = 0
|
||||||
|
for line in content.splitLines():
|
||||||
|
let trimmed = line.strip()
|
||||||
|
if trimmed.len == 0 or trimmed.startsWith("#"):
|
||||||
|
continue
|
||||||
|
if dataIndex == accountIndex:
|
||||||
|
let parts = trimmed.split(',')
|
||||||
|
if parts.len >= 2:
|
||||||
|
result = (parts[0].strip(), parts[1].strip())
|
||||||
|
return
|
||||||
|
dataIndex.inc
|
||||||
|
except:
|
||||||
|
discard
|
||||||
|
|
||||||
# Fall back to UNSANDBOX_API_KEY for backwards compatibility
|
proc resolveCredentials(argPk: string, argSk: string, accountIndex: int): tuple[pk: string, sk: string] =
|
||||||
if publicKey == "":
|
## Resolve credentials using 5-tier priority:
|
||||||
publicKey = getEnv("UNSANDBOX_API_KEY", "")
|
## 1. Explicit -p/-k flags (argPk/argSk)
|
||||||
|
## 2. --account N -> accounts.csv row N (bypasses env vars)
|
||||||
|
## 3. UNSANDBOX_PUBLIC_KEY / UNSANDBOX_SECRET_KEY env vars
|
||||||
|
## 4. ~/.unsandbox/accounts.csv row 0 (or UNSANDBOX_ACCOUNT env var)
|
||||||
|
## 5. ./accounts.csv row 0
|
||||||
|
# Tier 1: explicit key flags
|
||||||
|
if argPk != "" and argSk != "":
|
||||||
|
return (argPk, argSk)
|
||||||
|
# Tier 2: --account N bypasses env vars
|
||||||
|
let homeDir = getHomeDir()
|
||||||
|
if accountIndex >= 0:
|
||||||
|
let homeCsv2 = homeDir / ".unsandbox" / "accounts.csv"
|
||||||
|
var creds2 = loadCredentialsFromCsv(homeCsv2, accountIndex)
|
||||||
|
if creds2.pk != "":
|
||||||
|
return creds2
|
||||||
|
creds2 = loadCredentialsFromCsv("accounts.csv", accountIndex)
|
||||||
|
if creds2.pk != "":
|
||||||
|
return creds2
|
||||||
|
stderr.writeLine(RED & fmt"Error: No credentials found for account index {accountIndex} in accounts.csv" & RESET)
|
||||||
|
quit(1)
|
||||||
|
# Tier 3: env vars
|
||||||
|
let envPk = getEnv("UNSANDBOX_PUBLIC_KEY", "")
|
||||||
|
let envSk = getEnv("UNSANDBOX_SECRET_KEY", "")
|
||||||
|
if envPk != "" and envSk != "":
|
||||||
|
return (envPk, envSk)
|
||||||
|
# Tier 4: ~/.unsandbox/accounts.csv (default row or UNSANDBOX_ACCOUNT)
|
||||||
|
let defaultIndex = parseInt(getEnv("UNSANDBOX_ACCOUNT", "0"))
|
||||||
|
let homeCsv = homeDir / ".unsandbox" / "accounts.csv"
|
||||||
|
var creds = loadCredentialsFromCsv(homeCsv, defaultIndex)
|
||||||
|
if creds.pk != "":
|
||||||
|
return creds
|
||||||
|
# Tier 5: ./accounts.csv
|
||||||
|
creds = loadCredentialsFromCsv("accounts.csv", defaultIndex)
|
||||||
|
if creds.pk != "":
|
||||||
|
return creds
|
||||||
|
# Legacy UNSANDBOX_API_KEY
|
||||||
|
let legacyKey = getEnv("UNSANDBOX_API_KEY", "")
|
||||||
|
if legacyKey != "":
|
||||||
|
return (legacyKey, legacyKey)
|
||||||
|
stderr.writeLine(RED & "Error: No credentials found. Set UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY" & RESET)
|
||||||
|
quit(1)
|
||||||
|
|
||||||
|
proc main() =
|
||||||
|
var argPublicKey = ""
|
||||||
|
var argSecretKey = ""
|
||||||
|
var accountIndex = -1 # -1 means not set
|
||||||
|
|
||||||
|
# Pre-scan for --account, -p, and -k before subcommand dispatch
|
||||||
|
let rawArgs = commandLineParams()
|
||||||
|
var i = 0
|
||||||
|
while i < rawArgs.len:
|
||||||
|
if rawArgs[i] == "--account" and i + 1 < rawArgs.len:
|
||||||
|
try:
|
||||||
|
accountIndex = parseInt(rawArgs[i + 1])
|
||||||
|
except ValueError:
|
||||||
|
discard
|
||||||
|
i.inc
|
||||||
|
elif rawArgs[i] == "-p" and i + 1 < rawArgs.len:
|
||||||
|
argPublicKey = rawArgs[i + 1]
|
||||||
|
i.inc
|
||||||
|
elif rawArgs[i] == "-k" and i + 1 < rawArgs.len:
|
||||||
|
argSecretKey = rawArgs[i + 1]
|
||||||
|
i.inc
|
||||||
|
i.inc
|
||||||
|
|
||||||
|
let resolved = resolveCredentials(argPublicKey, argSecretKey, accountIndex)
|
||||||
|
var publicKey = resolved.pk
|
||||||
|
var secretKey = resolved.sk
|
||||||
|
|
||||||
let args = commandLineParams()
|
let args = commandLineParams()
|
||||||
|
|
||||||
|
|
@ -1265,6 +1349,11 @@ proc main() =
|
||||||
stderr.writeLine("Service options:")
|
stderr.writeLine("Service options:")
|
||||||
stderr.writeLine(" -e KEY=VALUE Set environment variable (for vault)")
|
stderr.writeLine(" -e KEY=VALUE Set environment variable (for vault)")
|
||||||
stderr.writeLine(" --env-file FILE Load env vars from file (for vault)")
|
stderr.writeLine(" --env-file FILE Load env vars from file (for vault)")
|
||||||
|
stderr.writeLine("")
|
||||||
|
stderr.writeLine("Credential options (global):")
|
||||||
|
stderr.writeLine(" -p PUBLIC_KEY Explicit public key")
|
||||||
|
stderr.writeLine(" -k SECRET_KEY Explicit secret key")
|
||||||
|
stderr.writeLine(" --account N Use row N from accounts.csv (bypasses env vars)")
|
||||||
quit(1)
|
quit(1)
|
||||||
|
|
||||||
if args[0] == "languages":
|
if args[0] == "languages":
|
||||||
|
|
@ -1506,6 +1595,8 @@ proc main() =
|
||||||
of "-n": network = args[i+1]; inc i
|
of "-n": network = args[i+1]; inc i
|
||||||
of "-v": vcpu = parseInt(args[i+1]); inc i
|
of "-v": vcpu = parseInt(args[i+1]); inc i
|
||||||
of "-k": publicKey = args[i+1]; inc i
|
of "-k": publicKey = args[i+1]; inc i
|
||||||
|
of "-p": publicKey = args[i+1]; inc i
|
||||||
|
of "--account": inc i # already handled in pre-scan
|
||||||
else:
|
else:
|
||||||
if args[i].startsWith("-"):
|
if args[i].startsWith("-"):
|
||||||
stderr.writeLine(RED & "Unknown option: " & args[i] & RESET)
|
stderr.writeLine(RED & "Unknown option: " & args[i] & RESET)
|
||||||
|
|
|
||||||
|
|
@ -50,8 +50,10 @@
|
||||||
//
|
//
|
||||||
// Authentication (in priority order):
|
// Authentication (in priority order):
|
||||||
// 1. UNClient initWithPublicKey:secretKey: constructor arguments
|
// 1. UNClient initWithPublicKey:secretKey: constructor arguments
|
||||||
// 2. Environment variables: UNSANDBOX_PUBLIC_KEY + UNSANDBOX_SECRET_KEY
|
// 2. --account N flag -> accounts.csv row N (bypasses env vars)
|
||||||
// 3. Config file: ~/.unsandbox/accounts.csv (public_key,secret_key per line)
|
// 3. Environment variables: UNSANDBOX_PUBLIC_KEY + UNSANDBOX_SECRET_KEY
|
||||||
|
// 4. Config file: ~/.unsandbox/accounts.csv row 0 (or UNSANDBOX_ACCOUNT)
|
||||||
|
// 5. ./accounts.csv row 0
|
||||||
|
|
||||||
#!/usr/bin/env -S clang -x objective-c -framework Foundation -o /tmp/un_objc && /tmp/un_objc
|
#!/usr/bin/env -S clang -x objective-c -framework Foundation -o /tmp/un_objc && /tmp/un_objc
|
||||||
|
|
||||||
|
|
@ -207,9 +209,54 @@ NSString* UNComputeSignature(NSString* secretKey, long timestamp, NSString* meth
|
||||||
// Credentials Loading
|
// Credentials Loading
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
|
// Global account index: -1 means not set (use env vars / default CSV row).
|
||||||
|
// Set by main() when --account N is parsed.
|
||||||
|
static NSInteger g_accountIndex = -1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load public_key,secret_key from a CSV file at the given row index (0-based,
|
||||||
|
* skipping blank lines and comment lines starting with '#').
|
||||||
|
*
|
||||||
|
* @param csvPath Path to the CSV file
|
||||||
|
* @param rowIndex Zero-based data row to read
|
||||||
|
* @param outPk Output: public key string (nil if not found)
|
||||||
|
* @param outSk Output: secret key string (nil if not found)
|
||||||
|
*/
|
||||||
|
void UNLoadCredentialsFromCSV(NSString* csvPath, NSInteger rowIndex, NSString** outPk, NSString** outSk) {
|
||||||
|
*outPk = nil;
|
||||||
|
*outSk = nil;
|
||||||
|
NSFileManager* fm = [NSFileManager defaultManager];
|
||||||
|
if (![fm fileExistsAtPath:csvPath]) return;
|
||||||
|
|
||||||
|
NSString* content = [NSString stringWithContentsOfFile:csvPath encoding:NSUTF8StringEncoding error:nil];
|
||||||
|
if (!content) return;
|
||||||
|
|
||||||
|
NSArray* lines = [content componentsSeparatedByString:@"\n"];
|
||||||
|
NSInteger dataIndex = 0;
|
||||||
|
for (NSString* line in lines) {
|
||||||
|
NSString* trimmed = [line stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
|
||||||
|
if ([trimmed length] == 0 || [trimmed hasPrefix:@"#"]) continue;
|
||||||
|
if (dataIndex == rowIndex) {
|
||||||
|
NSArray* parts = [trimmed componentsSeparatedByString:@","];
|
||||||
|
if ([parts count] >= 2) {
|
||||||
|
*outPk = [parts[0] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
|
||||||
|
*outSk = [parts[1] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
dataIndex++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get API credentials from environment or config file.
|
* Get API credentials from environment or config file.
|
||||||
* Priority: 1. Arguments, 2. Environment vars, 3. ~/.unsandbox/accounts.csv
|
*
|
||||||
|
* Priority order:
|
||||||
|
* 1. Function arguments (argPublicKey / argSecretKey)
|
||||||
|
* 2. g_accountIndex >= 0 -> 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 var)
|
||||||
|
* 5. ./accounts.csv row 0
|
||||||
*
|
*
|
||||||
* @param publicKey Output public key
|
* @param publicKey Output public key
|
||||||
* @param secretKey Output secret key
|
* @param secretKey Output secret key
|
||||||
|
|
@ -226,7 +273,22 @@ BOOL UNGetCredentials(NSString** publicKey, NSString** secretKey, NSString* argP
|
||||||
return YES;
|
return YES;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Priority 2: Environment variables
|
// Priority 2: --account N -> accounts.csv row N (bypasses env vars)
|
||||||
|
if (g_accountIndex >= 0) {
|
||||||
|
NSString* home = NSHomeDirectory();
|
||||||
|
NSString* homeCsv = [home stringByAppendingPathComponent:@".unsandbox/accounts.csv"];
|
||||||
|
UNLoadCredentialsFromCSV(homeCsv, g_accountIndex, publicKey, secretKey);
|
||||||
|
if (*publicKey && [*publicKey length] > 0) return YES;
|
||||||
|
UNLoadCredentialsFromCSV(@"accounts.csv", g_accountIndex, publicKey, secretKey);
|
||||||
|
if (*publicKey && [*publicKey length] > 0) return YES;
|
||||||
|
if (error) {
|
||||||
|
*error = [UNAuthenticationError errorWithMessage:
|
||||||
|
[NSString stringWithFormat:@"No credentials found for account index %ld in accounts.csv", (long)g_accountIndex]];
|
||||||
|
}
|
||||||
|
return NO;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Priority 3: Environment variables
|
||||||
*publicKey = [[[NSProcessInfo processInfo] environment] objectForKey:@"UNSANDBOX_PUBLIC_KEY"];
|
*publicKey = [[[NSProcessInfo processInfo] environment] objectForKey:@"UNSANDBOX_PUBLIC_KEY"];
|
||||||
*secretKey = [[[NSProcessInfo processInfo] environment] objectForKey:@"UNSANDBOX_SECRET_KEY"];
|
*secretKey = [[[NSProcessInfo processInfo] environment] objectForKey:@"UNSANDBOX_SECRET_KEY"];
|
||||||
|
|
||||||
|
|
@ -242,32 +304,17 @@ BOOL UNGetCredentials(NSString** publicKey, NSString** secretKey, NSString* argP
|
||||||
return YES;
|
return YES;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Priority 3: Config file ~/.unsandbox/accounts.csv
|
// Priority 4: Config file ~/.unsandbox/accounts.csv (default row)
|
||||||
NSString* home = NSHomeDirectory();
|
NSString* home = NSHomeDirectory();
|
||||||
NSString* accountsPath = [home stringByAppendingPathComponent:@".unsandbox/accounts.csv"];
|
NSString* accountIndexStr = [[[NSProcessInfo processInfo] environment] objectForKey:@"UNSANDBOX_ACCOUNT"];
|
||||||
NSFileManager* fm = [NSFileManager defaultManager];
|
NSInteger defaultIndex = accountIndexStr ? [accountIndexStr integerValue] : 0;
|
||||||
|
NSString* homeCsv = [home stringByAppendingPathComponent:@".unsandbox/accounts.csv"];
|
||||||
|
UNLoadCredentialsFromCSV(homeCsv, defaultIndex, publicKey, secretKey);
|
||||||
|
if (*publicKey && [*publicKey length] > 0) return YES;
|
||||||
|
|
||||||
if ([fm fileExistsAtPath:accountsPath]) {
|
// Priority 5: ./accounts.csv
|
||||||
NSString* content = [NSString stringWithContentsOfFile:accountsPath encoding:NSUTF8StringEncoding error:nil];
|
UNLoadCredentialsFromCSV(@"accounts.csv", defaultIndex, publicKey, secretKey);
|
||||||
if (content) {
|
if (*publicKey && [*publicKey length] > 0) return YES;
|
||||||
NSArray* lines = [content componentsSeparatedByString:@"\n"];
|
|
||||||
for (NSString* line in lines) {
|
|
||||||
NSString* trimmed = [line stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
|
|
||||||
if ([trimmed length] == 0 || [trimmed hasPrefix:@"#"]) continue;
|
|
||||||
|
|
||||||
NSArray* parts = [trimmed componentsSeparatedByString:@","];
|
|
||||||
if ([parts count] >= 2) {
|
|
||||||
NSString* pk = [parts[0] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
|
|
||||||
NSString* sk = [parts[1] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
|
|
||||||
if ([pk hasPrefix:@"unsb-pk-"] && [sk hasPrefix:@"unsb-sk-"]) {
|
|
||||||
*publicKey = pk;
|
|
||||||
*secretKey = sk;
|
|
||||||
return YES;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
*error = [UNAuthenticationError errorWithMessage:
|
*error = [UNAuthenticationError errorWithMessage:
|
||||||
|
|
@ -2378,6 +2425,14 @@ int main(int argc, const char* argv[]) {
|
||||||
[args addObject:[NSString stringWithUTF8String:argv[i]]];
|
[args addObject:[NSString stringWithUTF8String:argv[i]]];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Pre-scan for --account N before subcommand dispatch
|
||||||
|
for (NSUInteger i = 0; i < [args count]; i++) {
|
||||||
|
if ([args[i] isEqualToString:@"--account"] && i + 1 < [args count]) {
|
||||||
|
g_accountIndex = [args[i + 1] integerValue];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
NSString* firstArg = args[0];
|
NSString* firstArg = args[0];
|
||||||
|
|
||||||
if ([firstArg isEqualToString:@"--help"] || [firstArg isEqualToString:@"-h"]) {
|
if ([firstArg isEqualToString:@"--help"] || [firstArg isEqualToString:@"-h"]) {
|
||||||
|
|
|
||||||
|
|
@ -281,39 +281,53 @@ let extract_json_int json_str key =
|
||||||
Credentials Management
|
Credentials Management
|
||||||
============================================================================ *)
|
============================================================================ *)
|
||||||
|
|
||||||
(** Get credentials from config file ~/.unsandbox/accounts.csv *)
|
(** Global account index set by --account N CLI flag; -1 means not set *)
|
||||||
let get_credentials_from_file ?(account_index=0) () =
|
let cli_account_index = ref (-1)
|
||||||
let home = try Sys.getenv "HOME" with Not_found -> "." in
|
|
||||||
let accounts_path = Filename.concat home ".unsandbox/accounts.csv" in
|
(** Parse accounts from CSV content, return list of (pk, sk) pairs *)
|
||||||
if Sys.file_exists accounts_path then
|
let parse_accounts_csv content =
|
||||||
|
let lines = String.split_on_char '\n' content in
|
||||||
|
List.filter_map (fun line ->
|
||||||
|
let line = String.trim line in
|
||||||
|
if String.length line = 0 || line.[0] = '#' then None
|
||||||
|
else
|
||||||
|
try
|
||||||
|
let comma_pos = String.index line ',' in
|
||||||
|
let pk = String.trim (String.sub line 0 comma_pos) in
|
||||||
|
let sk = String.trim (String.sub line (comma_pos + 1) (String.length line - comma_pos - 1)) in
|
||||||
|
if String.length pk > 8 && String.length sk > 8 then
|
||||||
|
Some (pk, sk)
|
||||||
|
else None
|
||||||
|
with Not_found -> None
|
||||||
|
) lines
|
||||||
|
|
||||||
|
(** Load credentials from a specific CSV path at the given account index *)
|
||||||
|
let load_csv_at path account_index =
|
||||||
|
if Sys.file_exists path then
|
||||||
try
|
try
|
||||||
let content = read_file accounts_path in
|
let content = read_file path in
|
||||||
let lines = String.split_on_char '\n' content in
|
let accounts = parse_accounts_csv content in
|
||||||
let valid_accounts = List.filter_map (fun line ->
|
if account_index < List.length accounts then
|
||||||
let line = String.trim line in
|
Some (List.nth accounts account_index)
|
||||||
if String.length line = 0 || line.[0] = '#' then None
|
|
||||||
else
|
|
||||||
try
|
|
||||||
let comma_pos = String.index line ',' in
|
|
||||||
let pk = String.sub line 0 comma_pos in
|
|
||||||
let sk = String.sub line (comma_pos + 1) (String.length line - comma_pos - 1) in
|
|
||||||
if String.length pk > 8 && String.sub pk 0 8 = "unsb-pk-" &&
|
|
||||||
String.length sk > 8 && String.sub sk 0 8 = "unsb-sk-" then
|
|
||||||
Some (pk, sk)
|
|
||||||
else None
|
|
||||||
with Not_found -> None
|
|
||||||
) lines in
|
|
||||||
if account_index < List.length valid_accounts then
|
|
||||||
Some (List.nth valid_accounts account_index)
|
|
||||||
else None
|
else None
|
||||||
with _ -> None
|
with _ -> None
|
||||||
else None
|
else None
|
||||||
|
|
||||||
|
(** Get credentials from config file ~/.unsandbox/accounts.csv *)
|
||||||
|
let get_credentials_from_file ?(account_index=0) () =
|
||||||
|
let home = try Sys.getenv "HOME" with Not_found -> "." in
|
||||||
|
let home_csv = Filename.concat home ".unsandbox/accounts.csv" in
|
||||||
|
match load_csv_at home_csv account_index with
|
||||||
|
| Some _ as r -> r
|
||||||
|
| None -> load_csv_at "accounts.csv" account_index
|
||||||
|
|
||||||
(**
|
(**
|
||||||
Get API credentials in priority order:
|
Get API credentials in priority order:
|
||||||
1. Function arguments
|
1. Function arguments (public_key, secret_key)
|
||||||
2. Environment variables
|
2. --account N (cli_account_index ref) -> accounts.csv row N
|
||||||
3. ~/.unsandbox/accounts.csv
|
3. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY)
|
||||||
|
4. ~/.unsandbox/accounts.csv row 0 (or UNSANDBOX_ACCOUNT env)
|
||||||
|
5. ./accounts.csv row 0
|
||||||
|
|
||||||
@param public_key Optional public key override
|
@param public_key Optional public key override
|
||||||
@param secret_key Optional secret key override
|
@param secret_key Optional secret key override
|
||||||
|
|
@ -326,18 +340,32 @@ let get_credentials ?public_key ?secret_key ?(account_index=0) () =
|
||||||
match (public_key, secret_key) with
|
match (public_key, secret_key) with
|
||||||
| (Some pk, Some sk) -> (pk, sk)
|
| (Some pk, Some sk) -> (pk, sk)
|
||||||
| _ ->
|
| _ ->
|
||||||
(* Priority 2: Environment variables *)
|
(* Priority 2: --account N CLI flag overrides env vars *)
|
||||||
let env_pk = try Some (Sys.getenv "UNSANDBOX_PUBLIC_KEY") with Not_found -> None in
|
let effective_index = if !cli_account_index >= 0 then !cli_account_index else account_index in
|
||||||
let env_sk = try Some (Sys.getenv "UNSANDBOX_SECRET_KEY") with Not_found -> None in
|
if !cli_account_index >= 0 then begin
|
||||||
match (env_pk, env_sk) with
|
match get_credentials_from_file ~account_index:effective_index () with
|
||||||
| (Some pk, Some sk) -> (pk, sk)
|
|
||||||
| _ ->
|
|
||||||
(* Priority 3: Config file *)
|
|
||||||
match get_credentials_from_file ~account_index () with
|
|
||||||
| Some (pk, sk) -> (pk, sk)
|
| Some (pk, sk) -> (pk, sk)
|
||||||
| None ->
|
| None ->
|
||||||
failwith "No credentials found. Set UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY, \
|
Printf.fprintf stderr "Error: No credentials found for account index %d in accounts.csv\n" !cli_account_index;
|
||||||
or create ~/.unsandbox/accounts.csv, or pass credentials to function."
|
exit 1
|
||||||
|
end else begin
|
||||||
|
(* Priority 3: Environment variables *)
|
||||||
|
let env_pk = try Some (Sys.getenv "UNSANDBOX_PUBLIC_KEY") with Not_found -> None in
|
||||||
|
let env_sk = try Some (Sys.getenv "UNSANDBOX_SECRET_KEY") with Not_found -> None in
|
||||||
|
match (env_pk, env_sk) with
|
||||||
|
| (Some pk, Some sk) -> (pk, sk)
|
||||||
|
| _ ->
|
||||||
|
(* Priority 4: ~/.unsandbox/accounts.csv (or UNSANDBOX_ACCOUNT index) *)
|
||||||
|
let default_index =
|
||||||
|
try int_of_string (String.trim (Sys.getenv "UNSANDBOX_ACCOUNT"))
|
||||||
|
with Not_found | Failure _ -> 0
|
||||||
|
in
|
||||||
|
match get_credentials_from_file ~account_index:default_index () with
|
||||||
|
| Some (pk, sk) -> (pk, sk)
|
||||||
|
| None ->
|
||||||
|
failwith "No credentials found. Set UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY, \
|
||||||
|
or create ~/.unsandbox/accounts.csv, or pass credentials to function."
|
||||||
|
end
|
||||||
|
|
||||||
(* Legacy function for backward compatibility *)
|
(* Legacy function for backward compatibility *)
|
||||||
let get_api_keys () =
|
let get_api_keys () =
|
||||||
|
|
@ -2038,18 +2066,34 @@ let image_command args =
|
||||||
in
|
in
|
||||||
parse_args false "" "" "" "" "" "" "" "" "" "" "" "" args
|
parse_args false "" "" "" "" "" "" "" "" "" "" "" "" args
|
||||||
|
|
||||||
|
let strip_account_arg args =
|
||||||
|
let rec aux = function
|
||||||
|
| [] -> []
|
||||||
|
| "--account" :: n_str :: rest ->
|
||||||
|
(try cli_account_index := int_of_string (String.trim n_str)
|
||||||
|
with Failure _ ->
|
||||||
|
Printf.fprintf stderr "Error: --account requires an integer argument\n";
|
||||||
|
exit 1);
|
||||||
|
aux rest
|
||||||
|
| arg :: rest -> arg :: aux rest
|
||||||
|
in
|
||||||
|
aux args
|
||||||
|
|
||||||
let () =
|
let () =
|
||||||
Random.self_init ();
|
Random.self_init ();
|
||||||
let args = Array.to_list Sys.argv in
|
let raw_args = Array.to_list Sys.argv in
|
||||||
match List.tl args with
|
let args = strip_account_arg (List.tl raw_args) in
|
||||||
|
match args with
|
||||||
| [] ->
|
| [] ->
|
||||||
Printf.printf "Usage: un.ml [options] <source_file>\n";
|
Printf.printf "Usage: un.ml [--account N] [options] <source_file>\n";
|
||||||
Printf.printf " un.ml session [options]\n";
|
Printf.printf " un.ml [--account N] session [options]\n";
|
||||||
Printf.printf " un.ml service [options]\n";
|
Printf.printf " un.ml [--account N] service [options]\n";
|
||||||
Printf.printf " un.ml image [options]\n";
|
Printf.printf " un.ml [--account N] image [options]\n";
|
||||||
Printf.printf " un.ml service env <action> <service_id>\n";
|
Printf.printf " un.ml [--account N] service env <action> <service_id>\n";
|
||||||
Printf.printf " un.ml languages [--json]\n";
|
Printf.printf " un.ml languages [--json]\n";
|
||||||
Printf.printf " un.ml key [--extend]\n\n";
|
Printf.printf " un.ml key [--extend]\n\n";
|
||||||
|
Printf.printf "Global options:\n";
|
||||||
|
Printf.printf " --account N Use accounts.csv row N (bypasses env vars)\n\n";
|
||||||
Printf.printf "Service options: --name, --ports, --bootstrap, --bootstrap-file, -e KEY=VALUE, --env-file FILE\n";
|
Printf.printf "Service options: --name, --ports, --bootstrap, --bootstrap-file, -e KEY=VALUE, --env-file FILE\n";
|
||||||
Printf.printf "Service env commands: status, set, export, delete\n";
|
Printf.printf "Service env commands: status, set, export, delete\n";
|
||||||
Printf.printf "Image options: --list, --info ID, --delete ID, --lock ID, --unlock ID,\n";
|
Printf.printf "Image options: --list, --info ID, --delete ID, --lock ID, --unlock ID,\n";
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,7 @@ our $PORTAL_BASE = 'https://unsandbox.com';
|
||||||
|
|
||||||
# Thread-local error storage
|
# Thread-local error storage
|
||||||
our $LAST_ERROR = "";
|
our $LAST_ERROR = "";
|
||||||
|
our $ACCOUNT_INDEX = -1; # -1 means not set; set to N to use accounts.csv row N
|
||||||
|
|
||||||
# Colors
|
# Colors
|
||||||
my $BLUE = "\033[34m";
|
my $BLUE = "\033[34m";
|
||||||
|
|
@ -122,7 +123,18 @@ sub get_credentials {
|
||||||
# Tier 1: Arguments
|
# Tier 1: Arguments
|
||||||
return ($opts{public_key}, $opts{secret_key}) if $opts{public_key} && $opts{secret_key};
|
return ($opts{public_key}, $opts{secret_key}) if $opts{public_key} && $opts{secret_key};
|
||||||
|
|
||||||
# Tier 2: Environment
|
# Tier 2: --account N flag → bypass env vars, load CSV row N directly
|
||||||
|
my $ai = exists $opts{account_index} ? $opts{account_index} : $Un::ACCOUNT_INDEX;
|
||||||
|
if (defined $ai && $ai >= 0) {
|
||||||
|
my $home_accounts = load_accounts_csv();
|
||||||
|
return @{$home_accounts->[$ai]} if @$home_accounts > $ai;
|
||||||
|
my $local_accounts = load_accounts_csv("./accounts.csv");
|
||||||
|
return @{$local_accounts->[$ai]} if @$local_accounts > $ai;
|
||||||
|
set_error("Account index $ai not found in accounts.csv");
|
||||||
|
return (undef, undef);
|
||||||
|
}
|
||||||
|
|
||||||
|
# Tier 3: Environment
|
||||||
if ($ENV{UNSANDBOX_PUBLIC_KEY} && $ENV{UNSANDBOX_SECRET_KEY}) {
|
if ($ENV{UNSANDBOX_PUBLIC_KEY} && $ENV{UNSANDBOX_SECRET_KEY}) {
|
||||||
return ($ENV{UNSANDBOX_PUBLIC_KEY}, $ENV{UNSANDBOX_SECRET_KEY});
|
return ($ENV{UNSANDBOX_PUBLIC_KEY}, $ENV{UNSANDBOX_SECRET_KEY});
|
||||||
}
|
}
|
||||||
|
|
@ -132,11 +144,11 @@ sub get_credentials {
|
||||||
return ($ENV{UNSANDBOX_API_KEY}, '');
|
return ($ENV{UNSANDBOX_API_KEY}, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
# Tier 3: Home directory
|
# Tier 4: Home directory
|
||||||
my $home_accounts = load_accounts_csv();
|
my $home_accounts = load_accounts_csv();
|
||||||
return @{$home_accounts->[0]} if @$home_accounts;
|
return @{$home_accounts->[0]} if @$home_accounts;
|
||||||
|
|
||||||
# Tier 4: Local directory
|
# Tier 5: Local directory
|
||||||
my $local_accounts = load_accounts_csv("./accounts.csv");
|
my $local_accounts = load_accounts_csv("./accounts.csv");
|
||||||
return @{$local_accounts->[0]} if @$local_accounts;
|
return @{$local_accounts->[0]} if @$local_accounts;
|
||||||
|
|
||||||
|
|
@ -472,6 +484,7 @@ sub service_redeploy {
|
||||||
my ($service_id, %opts) = @_;
|
my ($service_id, %opts) = @_;
|
||||||
my $body = {};
|
my $body = {};
|
||||||
$body->{bootstrap} = $opts{bootstrap} if $opts{bootstrap};
|
$body->{bootstrap} = $opts{bootstrap} if $opts{bootstrap};
|
||||||
|
$body->{input_files} = $opts{input_files} if $opts{input_files};
|
||||||
return api_request('POST', "/services/$service_id/redeploy", $body, %opts);
|
return api_request('POST', "/services/$service_id/redeploy", $body, %opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -998,7 +1011,12 @@ sub cmd_service {
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($options->{redeploy}) {
|
if ($options->{redeploy}) {
|
||||||
Un::service_redeploy($options->{redeploy}, bootstrap => $options->{bootstrap});
|
my %opts;
|
||||||
|
$opts{bootstrap} = $options->{bootstrap} if $options->{bootstrap};
|
||||||
|
if ($options->{files} && @{$options->{files}}) {
|
||||||
|
$opts{input_files} = build_input_files(@{$options->{files}});
|
||||||
|
}
|
||||||
|
Un::service_redeploy($options->{redeploy}, %opts);
|
||||||
print "${GREEN}Service redeployed: $options->{redeploy}${RESET}\n";
|
print "${GREEN}Service redeployed: $options->{redeploy}${RESET}\n";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -1535,6 +1553,8 @@ sub main {
|
||||||
} elsif ($arg eq 'env' && $options{command} && $options{command} eq 'service') {
|
} elsif ($arg eq 'env' && $options{command} && $options{command} eq 'service') {
|
||||||
$options{env_action} = $ARGV[++$i];
|
$options{env_action} = $ARGV[++$i];
|
||||||
$options{env_target} = $ARGV[++$i] if defined $ARGV[$i+1] && $ARGV[$i+1] !~ /^-/;
|
$options{env_target} = $ARGV[++$i] if defined $ARGV[$i+1] && $ARGV[$i+1] !~ /^-/;
|
||||||
|
} elsif ($arg eq '--account') {
|
||||||
|
$Un::ACCOUNT_INDEX = int($ARGV[++$i]);
|
||||||
} elsif ($arg eq '--help' || $arg eq '-h') {
|
} elsif ($arg eq '--help' || $arg eq '-h') {
|
||||||
show_help();
|
show_help();
|
||||||
} elsif ($arg =~ /^-/) {
|
} elsif ($arg =~ /^-/) {
|
||||||
|
|
|
||||||
|
|
@ -163,7 +163,9 @@ test-functional:
|
||||||
echo " $(YELLOW)⊘$(NC) Skipping (no API credentials)"; \
|
echo " $(YELLOW)⊘$(NC) Skipping (no API credentials)"; \
|
||||||
else \
|
else \
|
||||||
echo " Running functional tests..."; \
|
echo " Running functional tests..."; \
|
||||||
echo " $(YELLOW)⊘$(NC) Functional: SDK not yet implemented"; \
|
if [ -f "$(SYNC_DIR)/tests/FunctionalTest.php" ]; then \
|
||||||
|
cd $(SYNC_DIR) && php vendor/bin/phpunit tests/FunctionalTest.php 2>&1 && echo " $(GREEN)✓$(NC) Functional: All tests passed" || echo " $(RED)✗$(NC) Functional: Tests failed"; \
|
||||||
|
fi; \
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
|
||||||
|
|
@ -138,6 +138,7 @@ class Unsandbox {
|
||||||
private ?string $defaultPublicKey = null;
|
private ?string $defaultPublicKey = null;
|
||||||
private ?string $defaultSecretKey = null;
|
private ?string $defaultSecretKey = null;
|
||||||
private int $accountIndex = 0;
|
private int $accountIndex = 0;
|
||||||
|
private bool $accountIndexExplicit = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new Unsandbox client.
|
* Create a new Unsandbox client.
|
||||||
|
|
@ -1339,13 +1340,18 @@ class Unsandbox {
|
||||||
* @param string $serviceId Service ID
|
* @param string $serviceId Service ID
|
||||||
* @param string|null $publicKey Optional API key
|
* @param string|null $publicKey Optional API key
|
||||||
* @param string|null $secretKey Optional API secret
|
* @param string|null $secretKey Optional API secret
|
||||||
|
* @param array $opts Optional parameters: 'input_files'
|
||||||
* @return array Response array with redeploy confirmation
|
* @return array Response array with redeploy confirmation
|
||||||
* @throws CredentialsException Missing credentials
|
* @throws CredentialsException Missing credentials
|
||||||
* @throws ApiException API request failed
|
* @throws ApiException API request failed
|
||||||
*/
|
*/
|
||||||
public function redeployService(string $serviceId, ?string $publicKey = null, ?string $secretKey = null): array {
|
public function redeployService(string $serviceId, ?string $publicKey = null, ?string $secretKey = null, array $opts = []): array {
|
||||||
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
|
[$publicKey, $secretKey] = $this->resolveCredentials($publicKey, $secretKey);
|
||||||
return $this->makeRequest('POST', "/services/{$serviceId}/redeploy", $publicKey, $secretKey, []);
|
$data = [];
|
||||||
|
if (isset($opts['input_files'])) {
|
||||||
|
$data['input_files'] = $opts['input_files'];
|
||||||
|
}
|
||||||
|
return $this->makeRequest('POST', "/services/{$serviceId}/redeploy", $publicKey, $secretKey, $data);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -1688,17 +1694,18 @@ class Unsandbox {
|
||||||
* Resolve credentials from 4-tier priority system.
|
* Resolve credentials from 4-tier priority system.
|
||||||
*
|
*
|
||||||
* Priority:
|
* Priority:
|
||||||
* 1. Method arguments
|
* 1. Method arguments / constructor defaults
|
||||||
* 2. Environment variables
|
* 2. $accountIndex >= 0 → load from accounts.csv row N
|
||||||
* 3. ~/.unsandbox/accounts.csv
|
* 3. Environment variables (UNSANDBOX_PUBLIC_KEY / UNSANDBOX_SECRET_KEY)
|
||||||
* 4. ./accounts.csv
|
* 4. Default CSV lookup (account 0)
|
||||||
*
|
*
|
||||||
* @param string|null $publicKey Public key from method argument
|
* @param string|null $publicKey Public key from method argument
|
||||||
* @param string|null $secretKey Secret key from method argument
|
* @param string|null $secretKey Secret key from method argument
|
||||||
|
* @param int|null $accountIndex Explicit account index (overrides env and default)
|
||||||
* @return array [publicKey, secretKey]
|
* @return array [publicKey, secretKey]
|
||||||
* @throws CredentialsException If no credentials found
|
* @throws CredentialsException If no credentials found
|
||||||
*/
|
*/
|
||||||
private function resolveCredentials(?string $publicKey, ?string $secretKey): array {
|
private function resolveCredentials(?string $publicKey, ?string $secretKey, ?int $accountIndex = null): array {
|
||||||
// Tier 1: Method arguments
|
// Tier 1: Method arguments
|
||||||
if (!empty($publicKey) && !empty($secretKey)) {
|
if (!empty($publicKey) && !empty($secretKey)) {
|
||||||
return [$publicKey, $secretKey];
|
return [$publicKey, $secretKey];
|
||||||
|
|
@ -1709,29 +1716,50 @@ class Unsandbox {
|
||||||
return [$this->defaultPublicKey, $this->defaultSecretKey];
|
return [$this->defaultPublicKey, $this->defaultSecretKey];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tier 2: Environment variables
|
// Tier 2: Explicit account index (--account N flag or constructor accountIndex != 0)
|
||||||
|
// Resolve the effective account index: explicit arg > UNSANDBOX_ACCOUNT env > $this->accountIndex
|
||||||
|
$effectiveIndex = null;
|
||||||
|
if ($accountIndex !== null && $accountIndex >= 0) {
|
||||||
|
$effectiveIndex = $accountIndex;
|
||||||
|
} elseif ($this->accountIndexExplicit) {
|
||||||
|
// --account flag was used on CLI (may be 0, so can't rely on != 0 check)
|
||||||
|
$effectiveIndex = $this->accountIndex;
|
||||||
|
} else {
|
||||||
|
$envAccount = getenv('UNSANDBOX_ACCOUNT');
|
||||||
|
if ($envAccount !== false && $envAccount !== '') {
|
||||||
|
$effectiveIndex = (int)$envAccount;
|
||||||
|
} elseif ($this->accountIndex !== 0) {
|
||||||
|
$effectiveIndex = $this->accountIndex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($effectiveIndex !== null) {
|
||||||
|
$unsandboxDir = $this->getUnsandboxDir();
|
||||||
|
$creds = $this->loadCredentialsFromCsv($unsandboxDir . '/accounts.csv', $effectiveIndex);
|
||||||
|
if ($creds !== null) {
|
||||||
|
return $creds;
|
||||||
|
}
|
||||||
|
$creds = $this->loadCredentialsFromCsv('./accounts.csv', $effectiveIndex);
|
||||||
|
if ($creds !== null) {
|
||||||
|
return $creds;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tier 3: Environment variables
|
||||||
$envPk = getenv('UNSANDBOX_PUBLIC_KEY');
|
$envPk = getenv('UNSANDBOX_PUBLIC_KEY');
|
||||||
$envSk = getenv('UNSANDBOX_SECRET_KEY');
|
$envSk = getenv('UNSANDBOX_SECRET_KEY');
|
||||||
if (!empty($envPk) && !empty($envSk)) {
|
if (!empty($envPk) && !empty($envSk)) {
|
||||||
return [$envPk, $envSk];
|
return [$envPk, $envSk];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine account index
|
// Tier 4: Default CSV lookup (account 0)
|
||||||
$accountIndex = $this->accountIndex;
|
|
||||||
$envAccount = getenv('UNSANDBOX_ACCOUNT');
|
|
||||||
if ($envAccount !== false && $envAccount !== '') {
|
|
||||||
$accountIndex = (int)$envAccount;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tier 3: ~/.unsandbox/accounts.csv
|
|
||||||
$unsandboxDir = $this->getUnsandboxDir();
|
$unsandboxDir = $this->getUnsandboxDir();
|
||||||
$creds = $this->loadCredentialsFromCsv($unsandboxDir . '/accounts.csv', $accountIndex);
|
$creds = $this->loadCredentialsFromCsv($unsandboxDir . '/accounts.csv', 0);
|
||||||
if ($creds !== null) {
|
if ($creds !== null) {
|
||||||
return $creds;
|
return $creds;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tier 4: ./accounts.csv
|
$creds = $this->loadCredentialsFromCsv('./accounts.csv', 0);
|
||||||
$creds = $this->loadCredentialsFromCsv('./accounts.csv', $accountIndex);
|
|
||||||
if ($creds !== null) {
|
if ($creds !== null) {
|
||||||
return $creds;
|
return $creds;
|
||||||
}
|
}
|
||||||
|
|
@ -1739,9 +1767,9 @@ class Unsandbox {
|
||||||
throw new CredentialsException(
|
throw new CredentialsException(
|
||||||
"No credentials found. Please provide via:\n" .
|
"No credentials found. Please provide via:\n" .
|
||||||
" 1. Method arguments (publicKey, secretKey)\n" .
|
" 1. Method arguments (publicKey, secretKey)\n" .
|
||||||
" 2. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY)\n" .
|
" 2. --account N flag or UNSANDBOX_ACCOUNT env var (CSV row N)\n" .
|
||||||
" 3. ~/.unsandbox/accounts.csv\n" .
|
" 3. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY)\n" .
|
||||||
" 4. ./accounts.csv"
|
" 4. ~/.unsandbox/accounts.csv or ./accounts.csv (row 0)"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2133,6 +2161,10 @@ class Unsandbox {
|
||||||
if (!empty($opts['secret_key'])) {
|
if (!empty($opts['secret_key'])) {
|
||||||
$this->defaultSecretKey = $opts['secret_key'];
|
$this->defaultSecretKey = $opts['secret_key'];
|
||||||
}
|
}
|
||||||
|
if ($opts['account'] !== null) {
|
||||||
|
$this->accountIndex = $opts['account'];
|
||||||
|
$this->accountIndexExplicit = true;
|
||||||
|
}
|
||||||
|
|
||||||
// Determine the command
|
// Determine the command
|
||||||
if (empty($args)) {
|
if (empty($args)) {
|
||||||
|
|
@ -2204,6 +2236,7 @@ class Unsandbox {
|
||||||
'vcpu' => 1,
|
'vcpu' => 1,
|
||||||
'yes' => false,
|
'yes' => false,
|
||||||
'help' => false,
|
'help' => false,
|
||||||
|
'account' => null,
|
||||||
];
|
];
|
||||||
$args = [];
|
$args = [];
|
||||||
|
|
||||||
|
|
@ -2250,6 +2283,11 @@ class Unsandbox {
|
||||||
$opts['yes'] = true;
|
$opts['yes'] = true;
|
||||||
} elseif ($arg === '-h' || $arg === '--help') {
|
} elseif ($arg === '-h' || $arg === '--help') {
|
||||||
$opts['help'] = true;
|
$opts['help'] = true;
|
||||||
|
} elseif ($arg === '--account') {
|
||||||
|
$i++;
|
||||||
|
if (isset($argv[$i])) {
|
||||||
|
$opts['account'] = (int)$argv[$i];
|
||||||
|
}
|
||||||
} elseif (strpos($arg, '-') !== 0) {
|
} elseif (strpos($arg, '-') !== 0) {
|
||||||
$args[] = $arg;
|
$args[] = $arg;
|
||||||
}
|
}
|
||||||
|
|
@ -2603,7 +2641,31 @@ class Unsandbox {
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($serviceOpts['redeploy']) {
|
if ($serviceOpts['redeploy']) {
|
||||||
$result = $this->redeployService($serviceOpts['redeploy']);
|
$redeployOpts = [];
|
||||||
|
$inputFiles = [];
|
||||||
|
foreach ($opts['files'] as $filepath) {
|
||||||
|
if (file_exists($filepath)) {
|
||||||
|
$content = file_get_contents($filepath);
|
||||||
|
$inputFiles[] = [
|
||||||
|
'name' => basename($filepath),
|
||||||
|
'content' => base64_encode($content),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
foreach ($opts['files_path'] as $filepath) {
|
||||||
|
if (file_exists($filepath)) {
|
||||||
|
$content = file_get_contents($filepath);
|
||||||
|
$inputFiles[] = [
|
||||||
|
'name' => $filepath,
|
||||||
|
'content' => base64_encode($content),
|
||||||
|
'preserve_path' => true,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!empty($inputFiles)) {
|
||||||
|
$redeployOpts['input_files'] = $inputFiles;
|
||||||
|
}
|
||||||
|
$result = $this->redeployService($serviceOpts['redeploy'], null, null, $redeployOpts);
|
||||||
echo "Service redeploying: " . $serviceOpts['redeploy'] . "\n";
|
echo "Service redeploying: " . $serviceOpts['redeploy'] . "\n";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -2647,6 +2709,31 @@ class Unsandbox {
|
||||||
$createOpts['unfreeze_on_demand'] = true;
|
$createOpts['unfreeze_on_demand'] = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle input files
|
||||||
|
$inputFiles = [];
|
||||||
|
foreach ($opts['files'] as $filepath) {
|
||||||
|
if (file_exists($filepath)) {
|
||||||
|
$content = file_get_contents($filepath);
|
||||||
|
$inputFiles[] = [
|
||||||
|
'name' => basename($filepath),
|
||||||
|
'content' => base64_encode($content),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
foreach ($opts['files_path'] as $filepath) {
|
||||||
|
if (file_exists($filepath)) {
|
||||||
|
$content = file_get_contents($filepath);
|
||||||
|
$inputFiles[] = [
|
||||||
|
'name' => $filepath,
|
||||||
|
'content' => base64_encode($content),
|
||||||
|
'preserve_path' => true,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!empty($inputFiles)) {
|
||||||
|
$createOpts['input_files'] = $inputFiles;
|
||||||
|
}
|
||||||
|
|
||||||
// Handle bootstrap from file
|
// Handle bootstrap from file
|
||||||
$bootstrap = $serviceOpts['bootstrap'] ?? '';
|
$bootstrap = $serviceOpts['bootstrap'] ?? '';
|
||||||
if (!empty($serviceOpts['bootstrap_file'])) {
|
if (!empty($serviceOpts['bootstrap_file'])) {
|
||||||
|
|
@ -3438,7 +3525,7 @@ SERVICE OPTIONS:
|
||||||
--lock ID Prevent service deletion
|
--lock ID Prevent service deletion
|
||||||
--unlock ID Allow service deletion
|
--unlock ID Allow service deletion
|
||||||
--resize ID Resize service (with --vcpu)
|
--resize ID Resize service (with --vcpu)
|
||||||
--redeploy ID Re-run bootstrap
|
--redeploy ID Re-run bootstrap (supports -f/-F for input files)
|
||||||
--execute ID 'cmd' Execute command in service
|
--execute ID 'cmd' Execute command in service
|
||||||
--snapshot ID Create service snapshot
|
--snapshot ID Create service snapshot
|
||||||
|
|
||||||
|
|
|
||||||
120
clients/php/sync/tests/FunctionalTest.php
Normal file
120
clients/php/sync/tests/FunctionalTest.php
Normal file
|
|
@ -0,0 +1,120 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 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 PHP SDK - Functional Tests
|
||||||
|
*
|
||||||
|
* Tests library functions against real API.
|
||||||
|
* Requires: UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* cd clients/php/sync && phpunit tests/FunctionalTest.php
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Unsandbox\Tests;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../src/un.php';
|
||||||
|
|
||||||
|
use Unsandbox\Unsandbox;
|
||||||
|
|
||||||
|
class FunctionalTest extends TestCase
|
||||||
|
{
|
||||||
|
private Unsandbox $client;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
if (empty(getenv('UNSANDBOX_PUBLIC_KEY')) || empty(getenv('UNSANDBOX_SECRET_KEY'))) {
|
||||||
|
$this->markTestSkipped('UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY required');
|
||||||
|
}
|
||||||
|
$this->client = new Unsandbox();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testHealthCheck(): void
|
||||||
|
{
|
||||||
|
$result = Unsandbox::healthCheck();
|
||||||
|
$this->assertIsBool($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testValidateKeys(): void
|
||||||
|
{
|
||||||
|
$info = $this->client->validateKeys();
|
||||||
|
$this->assertIsArray($info);
|
||||||
|
$this->assertArrayHasKey('valid', $info);
|
||||||
|
$this->assertTrue($info['valid']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetLanguages(): void
|
||||||
|
{
|
||||||
|
$langs = $this->client->getLanguages();
|
||||||
|
$this->assertIsArray($langs);
|
||||||
|
$this->assertNotEmpty($langs);
|
||||||
|
$this->assertContains('python', $langs);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testExecute(): void
|
||||||
|
{
|
||||||
|
$result = $this->client->executeCode('python', "print('hello from PHP SDK')");
|
||||||
|
$this->assertIsArray($result);
|
||||||
|
$this->assertStringContainsString('hello from PHP SDK', $result['stdout'] ?? '');
|
||||||
|
$this->assertEquals(0, $result['exit_code'] ?? -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testExecuteError(): void
|
||||||
|
{
|
||||||
|
$result = $this->client->executeCode('python', 'import sys; sys.exit(1)');
|
||||||
|
$this->assertIsArray($result);
|
||||||
|
$this->assertEquals(1, $result['exit_code'] ?? -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSessionList(): void
|
||||||
|
{
|
||||||
|
$sessions = $this->client->listSessions();
|
||||||
|
$this->assertIsArray($sessions);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSessionLifecycle(): void
|
||||||
|
{
|
||||||
|
$session = $this->client->createSession('python');
|
||||||
|
$this->assertIsArray($session);
|
||||||
|
$this->assertArrayHasKey('id', $session);
|
||||||
|
$sessionId = $session['id'];
|
||||||
|
|
||||||
|
$this->client->deleteSession($sessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testServiceList(): void
|
||||||
|
{
|
||||||
|
$services = $this->client->listServices();
|
||||||
|
$this->assertIsArray($services);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSnapshotList(): void
|
||||||
|
{
|
||||||
|
$snapshots = $this->client->listSnapshots();
|
||||||
|
$this->assertIsArray($snapshots);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testImageList(): void
|
||||||
|
{
|
||||||
|
$images = $this->client->listImages();
|
||||||
|
$this->assertIsArray($images);
|
||||||
|
}
|
||||||
|
}
|
||||||
93
clients/php/sync/tests/test_account_flag.sh
Executable file
93
clients/php/sync/tests/test_account_flag.sh
Executable file
|
|
@ -0,0 +1,93 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Integration test for --account N credential selection in the PHP SDK CLI.
|
||||||
|
#
|
||||||
|
# Tests that --account N selects the correct row from accounts.csv,
|
||||||
|
# taking priority over UNSANDBOX_PUBLIC_KEY / UNSANDBOX_SECRET_KEY env vars.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
UN_PHP="${SCRIPT_DIR}/../src/un.php"
|
||||||
|
|
||||||
|
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)); }
|
||||||
|
|
||||||
|
# Require real credentials to run meaningful tests
|
||||||
|
if [ -z "${UNSANDBOX_PUBLIC_KEY:-}" ] || [ -z "${UNSANDBOX_SECRET_KEY:-}" ]; then
|
||||||
|
skip "UNSANDBOX_PUBLIC_KEY / UNSANDBOX_SECRET_KEY not set - cannot run account flag tests"
|
||||||
|
echo ""
|
||||||
|
echo "Results: ${PASS} passed, ${FAIL} failed, ${SKIP} skipped"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
REAL_PK="${UNSANDBOX_PUBLIC_KEY}"
|
||||||
|
REAL_SK="${UNSANDBOX_SECRET_KEY}"
|
||||||
|
GARBAGE_PK="unsb-pk-0000-0000-0000-garbage"
|
||||||
|
GARBAGE_SK="unsb-sk-00000-00000-00000-garbage"
|
||||||
|
|
||||||
|
# Create a temporary HOME with accounts.csv: row 0 = garbage, row 1 = real creds
|
||||||
|
TMPHOME="$(mktemp -d)"
|
||||||
|
trap 'rm -rf "${TMPHOME}"' EXIT
|
||||||
|
|
||||||
|
mkdir -p "${TMPHOME}/.unsandbox"
|
||||||
|
printf '%s,%s\n' "${GARBAGE_PK}" "${GARBAGE_SK}" > "${TMPHOME}/.unsandbox/accounts.csv"
|
||||||
|
printf '%s,%s\n' "${REAL_PK}" "${REAL_SK}" >> "${TMPHOME}/.unsandbox/accounts.csv"
|
||||||
|
|
||||||
|
# Test 1: --account 1 with garbage env vars should load row 1 (real creds) and succeed
|
||||||
|
echo "Test 1: --account 1 ignores garbage env vars and uses CSV row 1 (real creds)"
|
||||||
|
output=$(HOME="${TMPHOME}" \
|
||||||
|
UNSANDBOX_PUBLIC_KEY="${GARBAGE_PK}" \
|
||||||
|
UNSANDBOX_SECRET_KEY="${GARBAGE_SK}" \
|
||||||
|
php "${UN_PHP}" --account 1 key 2>&1) && rc=0 || rc=$?
|
||||||
|
|
||||||
|
if [ $rc -eq 0 ]; then
|
||||||
|
pass "Test 1: --account 1 succeeded with real creds from CSV row 1"
|
||||||
|
elif echo "${output}" | grep -qi "401\|unauthorized\|forbidden"; then
|
||||||
|
fail "Test 1: got auth error despite real creds at row 1 (output: ${output})"
|
||||||
|
else
|
||||||
|
fail "Test 1: unexpected failure (rc=${rc}, output: ${output})"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Test 2: --account 0 with real env vars should load row 0 (garbage creds) and get 401
|
||||||
|
echo "Test 2: --account 0 overrides real env vars and uses CSV row 0 (garbage creds)"
|
||||||
|
output=$(HOME="${TMPHOME}" \
|
||||||
|
UNSANDBOX_PUBLIC_KEY="${REAL_PK}" \
|
||||||
|
UNSANDBOX_SECRET_KEY="${REAL_SK}" \
|
||||||
|
php "${UN_PHP}" --account 0 key 2>&1) && rc=0 || rc=$?
|
||||||
|
|
||||||
|
if echo "${output}" | grep -qi "401\|unauthorized\|forbidden\|authentication\|credentials"; then
|
||||||
|
pass "Test 2: got expected auth rejection for garbage creds at row 0"
|
||||||
|
elif [ $rc -eq 3 ]; then
|
||||||
|
# Exit code 3 = CredentialsException (e.g., empty key) - also acceptable
|
||||||
|
pass "Test 2: got credentials exception for garbage creds at row 0 (rc=3)"
|
||||||
|
else
|
||||||
|
fail "Test 2: expected 401/auth error but got rc=${rc}, output: ${output}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Test 3: No --account flag with real env vars should succeed (env vars take priority over CSV row 0)
|
||||||
|
echo "Test 3: no --account flag with real env vars should succeed"
|
||||||
|
output=$(HOME="${TMPHOME}" \
|
||||||
|
UNSANDBOX_PUBLIC_KEY="${REAL_PK}" \
|
||||||
|
UNSANDBOX_SECRET_KEY="${REAL_SK}" \
|
||||||
|
php "${UN_PHP}" key 2>&1) && rc=0 || rc=$?
|
||||||
|
|
||||||
|
if [ $rc -eq 0 ]; then
|
||||||
|
pass "Test 3: succeeded using env vars when no --account flag set"
|
||||||
|
elif echo "${output}" | grep -qi "401\|unauthorized\|forbidden"; then
|
||||||
|
fail "Test 3: got unexpected auth error with real env vars (output: ${output})"
|
||||||
|
else
|
||||||
|
fail "Test 3: unexpected failure (rc=${rc}, output: ${output})"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Results: ${PASS} passed, ${FAIL} failed, ${SKIP} skipped"
|
||||||
|
|
||||||
|
if [ $FAIL -gt 0 ]; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
exit 0
|
||||||
|
|
@ -61,7 +61,43 @@ $EXT_MAP = @{
|
||||||
".raku" = "raku"; ".m" = "objc"; ".awk" = "awk"
|
".raku" = "raku"; ".m" = "objc"; ".awk" = "awk"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$script:AccountIndex = $null
|
||||||
|
|
||||||
|
function Load-AccountsCSV {
|
||||||
|
param($Path, $Index)
|
||||||
|
if (-not (Test-Path $Path)) { return $null }
|
||||||
|
try {
|
||||||
|
$rows = @()
|
||||||
|
Get-Content $Path | Where-Object {
|
||||||
|
$_.Trim() -ne "" -and -not $_.TrimStart().StartsWith("#")
|
||||||
|
} | ForEach-Object {
|
||||||
|
$parts = $_ -split ",", 2
|
||||||
|
if ($parts.Count -ge 2) {
|
||||||
|
$rows += ,@($parts[0].Trim(), $parts[1].Trim())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($Index -lt $rows.Count) {
|
||||||
|
return $rows[$Index]
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
function Get-ApiKeys {
|
function Get-ApiKeys {
|
||||||
|
$home = $env:HOME
|
||||||
|
if (-not $home) { $home = $env:USERPROFILE }
|
||||||
|
|
||||||
|
# --account N: load row N from accounts.csv, bypasses env vars
|
||||||
|
if ($null -ne $script:AccountIndex) {
|
||||||
|
$result = Load-AccountsCSV -Path "$home/.unsandbox/accounts.csv" -Index $script:AccountIndex
|
||||||
|
if (-not $result) {
|
||||||
|
$result = Load-AccountsCSV -Path "./accounts.csv" -Index $script:AccountIndex
|
||||||
|
}
|
||||||
|
if ($result) { return $result }
|
||||||
|
Write-Error "Error: account $($script:AccountIndex) not found in accounts.csv"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
$publicKey = $env:UNSANDBOX_PUBLIC_KEY
|
$publicKey = $env:UNSANDBOX_PUBLIC_KEY
|
||||||
$secretKey = $env:UNSANDBOX_SECRET_KEY
|
$secretKey = $env:UNSANDBOX_SECRET_KEY
|
||||||
|
|
||||||
|
|
@ -71,11 +107,27 @@ function Get-ApiKeys {
|
||||||
$secretKey = ""
|
$secretKey = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
if (-not $publicKey) {
|
if ($publicKey -and $secretKey) {
|
||||||
Write-Error "Error: UNSANDBOX_PUBLIC_KEY or UNSANDBOX_API_KEY not set"
|
return @($publicKey, $secretKey)
|
||||||
exit 1
|
|
||||||
}
|
}
|
||||||
return @($publicKey, $secretKey)
|
|
||||||
|
if ($publicKey) {
|
||||||
|
return @($publicKey, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
# accounts.csv fallback (row 0 or UNSANDBOX_ACCOUNT env var)
|
||||||
|
$rowIdx = 0
|
||||||
|
if ($env:UNSANDBOX_ACCOUNT) {
|
||||||
|
try { $rowIdx = [int]$env:UNSANDBOX_ACCOUNT } catch {}
|
||||||
|
}
|
||||||
|
$result = Load-AccountsCSV -Path "$home/.unsandbox/accounts.csv" -Index $rowIdx
|
||||||
|
if (-not $result) {
|
||||||
|
$result = Load-AccountsCSV -Path "./accounts.csv" -Index $rowIdx
|
||||||
|
}
|
||||||
|
if ($result) { return $result }
|
||||||
|
|
||||||
|
Write-Error "Error: UNSANDBOX_PUBLIC_KEY or UNSANDBOX_API_KEY not set"
|
||||||
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
function Invoke-Api {
|
function Invoke-Api {
|
||||||
|
|
@ -1179,38 +1231,49 @@ Key options:
|
||||||
exit 0
|
exit 0
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($args[0] -eq "session") {
|
# Pre-parse --account N from args, strip from effective arg list
|
||||||
Invoke-Session -Args $args[1..($args.Count-1)]
|
$effectiveArgs = @()
|
||||||
} elseif ($args[0] -eq "service") {
|
for ($i = 0; $i -lt $args.Count; $i++) {
|
||||||
Invoke-Service -Args $args[1..($args.Count-1)]
|
if ($args[$i] -eq "--account" -and ($i + 1) -lt $args.Count) {
|
||||||
} elseif ($args[0] -eq "snapshot") {
|
try { $script:AccountIndex = [int]$args[$i + 1] } catch {}
|
||||||
Invoke-Snapshot -Args $args[1..($args.Count-1)]
|
$i++
|
||||||
} elseif ($args[0] -eq "image") {
|
} else {
|
||||||
Invoke-Image -Args $args[1..($args.Count-1)]
|
$effectiveArgs += $args[$i]
|
||||||
} elseif ($args[0] -eq "languages") {
|
}
|
||||||
Invoke-Languages -Args $args[1..($args.Count-1)]
|
}
|
||||||
} elseif ($args[0] -eq "key") {
|
|
||||||
Invoke-Key -Args $args[1..($args.Count-1)]
|
if ($effectiveArgs[0] -eq "session") {
|
||||||
|
Invoke-Session -Args $effectiveArgs[1..($effectiveArgs.Count-1)]
|
||||||
|
} elseif ($effectiveArgs[0] -eq "service") {
|
||||||
|
Invoke-Service -Args $effectiveArgs[1..($effectiveArgs.Count-1)]
|
||||||
|
} elseif ($effectiveArgs[0] -eq "snapshot") {
|
||||||
|
Invoke-Snapshot -Args $effectiveArgs[1..($effectiveArgs.Count-1)]
|
||||||
|
} elseif ($effectiveArgs[0] -eq "image") {
|
||||||
|
Invoke-Image -Args $effectiveArgs[1..($effectiveArgs.Count-1)]
|
||||||
|
} elseif ($effectiveArgs[0] -eq "languages") {
|
||||||
|
Invoke-Languages -Args $effectiveArgs[1..($effectiveArgs.Count-1)]
|
||||||
|
} elseif ($effectiveArgs[0] -eq "key") {
|
||||||
|
Invoke-Key -Args $effectiveArgs[1..($effectiveArgs.Count-1)]
|
||||||
} else {
|
} else {
|
||||||
# Parse execute args
|
# Parse execute args
|
||||||
$sourceFile = $null
|
$sourceFile = $null
|
||||||
$envVars = @{}
|
$envVars = @{}
|
||||||
$network = $null
|
$network = $null
|
||||||
|
|
||||||
for ($i = 0; $i -lt $args.Count; $i++) {
|
for ($i = 0; $i -lt $effectiveArgs.Count; $i++) {
|
||||||
switch ($args[$i]) {
|
switch ($effectiveArgs[$i]) {
|
||||||
"-e" {
|
"-e" {
|
||||||
$kv = $args[$i+1] -split "=", 2
|
$kv = $effectiveArgs[$i+1] -split "=", 2
|
||||||
$envVars[$kv[0]] = $kv[1]
|
$envVars[$kv[0]] = $kv[1]
|
||||||
$i++
|
$i++
|
||||||
}
|
}
|
||||||
"-n" { $network = $args[$i+1]; $i++ }
|
"-n" { $network = $effectiveArgs[$i+1]; $i++ }
|
||||||
default {
|
default {
|
||||||
if ($args[$i].StartsWith("-")) {
|
if ($effectiveArgs[$i].StartsWith("-")) {
|
||||||
Write-Error "${RED}Unknown option: $($args[$i])${RESET}"
|
Write-Error "${RED}Unknown option: $($effectiveArgs[$i])${RESET}"
|
||||||
exit 1
|
exit 1
|
||||||
} else {
|
} else {
|
||||||
$sourceFile = $args[$i]
|
$sourceFile = $effectiveArgs[$i]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,9 @@
|
||||||
|
|
||||||
:- initialization(main, main).
|
:- initialization(main, main).
|
||||||
|
|
||||||
|
% Initialize global account index to -1 (not set)
|
||||||
|
:- nb_setval(account_index, -1).
|
||||||
|
|
||||||
% Constants
|
% Constants
|
||||||
portal_base('https://unsandbox.com').
|
portal_base('https://unsandbox.com').
|
||||||
languages_cache_ttl(3600). % 1 hour cache TTL
|
languages_cache_ttl(3600). % 1 hour cache TTL
|
||||||
|
|
@ -85,26 +88,91 @@ read_file_content(Filename, Content) :-
|
||||||
read_string(Stream, _, Content),
|
read_string(Stream, _, Content),
|
||||||
close(Stream).
|
close(Stream).
|
||||||
|
|
||||||
% Get API keys from environment (HMAC or legacy)
|
% Load credentials from accounts.csv at 0-indexed row
|
||||||
|
% Tries ~/.unsandbox/accounts.csv first, then ./accounts.csv
|
||||||
|
load_accounts_csv(Index, PK, SK) :-
|
||||||
|
getenv('HOME', Home),
|
||||||
|
atomic_list_concat([Home, '/.unsandbox/accounts.csv'], Path1),
|
||||||
|
( exists_file(Path1)
|
||||||
|
-> load_accounts_csv_file(Path1, Index, PK, SK)
|
||||||
|
; exists_file('accounts.csv')
|
||||||
|
-> load_accounts_csv_file('accounts.csv', Index, PK, SK)
|
||||||
|
; fail
|
||||||
|
).
|
||||||
|
|
||||||
|
load_accounts_csv_file(Path, Index, PK, SK) :-
|
||||||
|
setup_call_cleanup(
|
||||||
|
open(Path, read, Stream),
|
||||||
|
load_accounts_csv_stream(Stream, 0, Index, PK, SK),
|
||||||
|
close(Stream)
|
||||||
|
).
|
||||||
|
|
||||||
|
load_accounts_csv_stream(Stream, Count, Index, PK, SK) :-
|
||||||
|
read_line_to_string(Stream, Line),
|
||||||
|
Line \= end_of_file,
|
||||||
|
!,
|
||||||
|
% Skip blank lines and comments
|
||||||
|
( (Line = "" ; string_code(1, Line, 35)) % 35 = '#'
|
||||||
|
-> load_accounts_csv_stream(Stream, Count, Index, PK, SK)
|
||||||
|
; Count =:= Index
|
||||||
|
-> split_string(Line, ",", " \t", [PKStr, SKStr|_]),
|
||||||
|
atom_string(PK, PKStr),
|
||||||
|
atom_string(SK, SKStr)
|
||||||
|
; Next is Count + 1,
|
||||||
|
load_accounts_csv_stream(Stream, Next, Index, PK, SK)
|
||||||
|
).
|
||||||
|
|
||||||
|
% Get API keys from environment (HMAC or legacy), respecting --account N
|
||||||
get_public_key(PublicKey) :-
|
get_public_key(PublicKey) :-
|
||||||
( getenv('UNSANDBOX_PUBLIC_KEY', PublicKey),
|
( nb_getval(account_index, Idx), Idx >= 0
|
||||||
PublicKey \= ''
|
-> ( load_accounts_csv(Idx, PublicKey, _)
|
||||||
-> true
|
-> true
|
||||||
; getenv('UNSANDBOX_API_KEY', PublicKey),
|
; format(user_error, 'Error: Account index ~w not found in accounts.csv~n', [Idx]),
|
||||||
PublicKey \= ''
|
halt(1)
|
||||||
-> true
|
)
|
||||||
; write(user_error, 'Error: UNSANDBOX_PUBLIC_KEY or UNSANDBOX_API_KEY environment variable not set\n'),
|
; ( getenv('UNSANDBOX_PUBLIC_KEY', PublicKey),
|
||||||
halt(1)
|
PublicKey \= ''
|
||||||
|
-> true
|
||||||
|
; getenv('UNSANDBOX_API_KEY', PublicKey),
|
||||||
|
PublicKey \= ''
|
||||||
|
-> true
|
||||||
|
; % Try accounts.csv with UNSANDBOX_ACCOUNT or row 0
|
||||||
|
( getenv('UNSANDBOX_ACCOUNT', IdxStr),
|
||||||
|
atom_number(IdxStr, DefaultIdx)
|
||||||
|
-> true
|
||||||
|
; DefaultIdx = 0
|
||||||
|
),
|
||||||
|
( load_accounts_csv(DefaultIdx, PublicKey, _)
|
||||||
|
-> true
|
||||||
|
; write(user_error, 'Error: UNSANDBOX_PUBLIC_KEY or UNSANDBOX_API_KEY environment variable not set\n'),
|
||||||
|
halt(1)
|
||||||
|
)
|
||||||
|
)
|
||||||
).
|
).
|
||||||
|
|
||||||
get_secret_key(SecretKey) :-
|
get_secret_key(SecretKey) :-
|
||||||
( getenv('UNSANDBOX_SECRET_KEY', SecretKey),
|
( nb_getval(account_index, Idx), Idx >= 0
|
||||||
SecretKey \= ''
|
-> ( load_accounts_csv(Idx, _, SecretKey)
|
||||||
-> true
|
-> true
|
||||||
; getenv('UNSANDBOX_API_KEY', SecretKey),
|
; SecretKey = ''
|
||||||
SecretKey \= ''
|
)
|
||||||
-> true
|
; ( getenv('UNSANDBOX_SECRET_KEY', SecretKey),
|
||||||
; SecretKey = ''
|
SecretKey \= ''
|
||||||
|
-> true
|
||||||
|
; getenv('UNSANDBOX_API_KEY', SecretKey),
|
||||||
|
SecretKey \= ''
|
||||||
|
-> true
|
||||||
|
; % Try accounts.csv with UNSANDBOX_ACCOUNT or row 0
|
||||||
|
( getenv('UNSANDBOX_ACCOUNT', IdxStr),
|
||||||
|
atom_number(IdxStr, DefaultIdx)
|
||||||
|
-> true
|
||||||
|
; DefaultIdx = 0
|
||||||
|
),
|
||||||
|
( load_accounts_csv(DefaultIdx, _, SecretKey)
|
||||||
|
-> true
|
||||||
|
; SecretKey = ''
|
||||||
|
)
|
||||||
|
)
|
||||||
).
|
).
|
||||||
|
|
||||||
% Get API key (legacy compatibility)
|
% Get API key (legacy compatibility)
|
||||||
|
|
@ -742,8 +810,22 @@ service_create_with_vault(Name, Ports, Bootstrap, BootstrapFile, ServiceType, In
|
||||||
|
|
||||||
% Main program
|
% Main program
|
||||||
main(Argv) :-
|
main(Argv) :-
|
||||||
|
% Initialize account_index global to -1 (not set)
|
||||||
|
nb_setval(account_index, -1),
|
||||||
|
|
||||||
|
% Parse --account N global flag if present
|
||||||
|
( Argv = ['--account', NStr|Rest]
|
||||||
|
-> ( atom_number(NStr, N), integer(N)
|
||||||
|
-> nb_setval(account_index, N),
|
||||||
|
ActualArgv = Rest
|
||||||
|
; write(user_error, 'Error: --account requires an integer argument\n'),
|
||||||
|
halt(1)
|
||||||
|
)
|
||||||
|
; ActualArgv = Argv
|
||||||
|
),
|
||||||
|
|
||||||
% Check arguments
|
% Check arguments
|
||||||
( Argv = []
|
( ActualArgv = []
|
||||||
-> write(user_error, 'Usage: un.pro [options] <source_file>\n'),
|
-> write(user_error, 'Usage: un.pro [options] <source_file>\n'),
|
||||||
write(user_error, ' un.pro session [options]\n'),
|
write(user_error, ' un.pro session [options]\n'),
|
||||||
write(user_error, ' un.pro service [options]\n'),
|
write(user_error, ' un.pro service [options]\n'),
|
||||||
|
|
@ -782,19 +864,19 @@ main(Argv) :-
|
||||||
),
|
),
|
||||||
|
|
||||||
% Parse subcommands
|
% Parse subcommands
|
||||||
( Argv = ['session'|Rest]
|
( ActualArgv = ['session'|Rest]
|
||||||
-> handle_session(Rest)
|
-> handle_session(Rest)
|
||||||
; Argv = ['service'|Rest]
|
; ActualArgv = ['service'|Rest]
|
||||||
-> handle_service(Rest)
|
-> handle_service(Rest)
|
||||||
; Argv = ['snapshot'|Rest]
|
; ActualArgv = ['snapshot'|Rest]
|
||||||
-> handle_snapshot(Rest)
|
-> handle_snapshot(Rest)
|
||||||
; Argv = ['image'|Rest]
|
; ActualArgv = ['image'|Rest]
|
||||||
-> handle_image(Rest)
|
-> handle_image(Rest)
|
||||||
; Argv = ['languages'|Rest]
|
; ActualArgv = ['languages'|Rest]
|
||||||
-> handle_languages(Rest)
|
-> handle_languages(Rest)
|
||||||
; Argv = ['key'|Rest]
|
; ActualArgv = ['key'|Rest]
|
||||||
-> handle_key(Rest)
|
-> handle_key(Rest)
|
||||||
; Argv = [Filename|_]
|
; ActualArgv = [Filename|_]
|
||||||
-> execute_file(Filename)
|
-> execute_file(Filename)
|
||||||
; write(user_error, 'Error: Invalid arguments\n'),
|
; write(user_error, 'Error: Invalid arguments\n'),
|
||||||
halt(1)
|
halt(1)
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,20 @@
|
||||||
#!/bin/bash
|
#!/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 Prolog Unsandbox SDK
|
# Test suite for Prolog Unsandbox SDK
|
||||||
# Run: bash tests/test_un.sh
|
# Run: bash tests/test_un.sh
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,26 +5,25 @@
|
||||||
# - async/ : Asynchronous Python SDK (aiohttp-based)
|
# - async/ : Asynchronous Python SDK (aiohttp-based)
|
||||||
#
|
#
|
||||||
# Usage:
|
# Usage:
|
||||||
# make # Run all tests
|
# make test # Run all 4 test modes (auto-creates venv)
|
||||||
# make test # Run all 4 test modes
|
|
||||||
# make test-cli # CLI mode only
|
# make test-cli # CLI mode only
|
||||||
# make test-library # Library 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-sync # Test sync SDK only
|
||||||
# make test-async # Test async SDK only
|
# make test-async # Test async SDK only
|
||||||
# make clean # Remove build artifacts
|
# make clean # Remove build artifacts + venv
|
||||||
#
|
#
|
||||||
# Dependencies:
|
# The Makefile manages a .venv automatically. No manual pip install needed.
|
||||||
# pip install pytest pytest-cov pytest-asyncio aiohttp requests
|
|
||||||
|
|
||||||
.PHONY: all test test-cli test-library test-integration test-functional
|
.PHONY: all test test-cli test-library test-integration test-functional
|
||||||
.PHONY: test-sync test-async install dev-install lint format clean help examples
|
.PHONY: test-sync test-async lint format clean help examples venv
|
||||||
|
|
||||||
# Paths
|
# Paths
|
||||||
ROOT_DIR := $(shell cd ../.. && pwd)
|
ROOT_DIR := $(shell cd ../.. && pwd)
|
||||||
SYNC_DIR := sync
|
SYNC_DIR := sync
|
||||||
ASYNC_DIR := async
|
ASYNC_DIR := async
|
||||||
|
VENV := .venv
|
||||||
|
PYTHON := $(VENV)/bin/python
|
||||||
|
PYTEST := $(VENV)/bin/pytest
|
||||||
|
|
||||||
# Colors
|
# Colors
|
||||||
GREEN := \033[32m
|
GREEN := \033[32m
|
||||||
|
|
@ -38,7 +37,7 @@ help:
|
||||||
@echo "UN Python Client - Build and Test"
|
@echo "UN Python Client - Build and Test"
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo "Test (all 4 modes):"
|
@echo "Test (all 4 modes):"
|
||||||
@echo " make test All 4 modes for both sync and async"
|
@echo " make test All 4 modes (auto-creates venv)"
|
||||||
@echo " make test-cli CLI mode (command-line interface)"
|
@echo " make test-cli CLI mode (command-line interface)"
|
||||||
@echo " make test-library Library mode (import and use)"
|
@echo " make test-library Library mode (import and use)"
|
||||||
@echo " make test-integration Integration mode (API contract)"
|
@echo " make test-integration Integration mode (API contract)"
|
||||||
|
|
@ -49,22 +48,39 @@ help:
|
||||||
@echo " make test-async Test asynchronous SDK"
|
@echo " make test-async Test asynchronous SDK"
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo "Development:"
|
@echo "Development:"
|
||||||
@echo " make install Install both SDKs"
|
@echo " make venv Create/update virtual environment"
|
||||||
@echo " make dev-install Install with dev dependencies"
|
|
||||||
@echo " make lint Lint both SDKs"
|
@echo " make lint Lint both SDKs"
|
||||||
@echo " make format Format both SDKs"
|
@echo " make format Format both SDKs"
|
||||||
@echo " make examples Run example scripts"
|
@echo " make examples Run example scripts"
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo "Utility:"
|
@echo "Utility:"
|
||||||
@echo " make clean Remove build artifacts"
|
@echo " make clean Remove build artifacts + venv"
|
||||||
@echo " make deps Show required dependencies"
|
|
||||||
@echo ""
|
@echo ""
|
||||||
|
|
||||||
all: test
|
all: test
|
||||||
|
|
||||||
deps:
|
# ============================================================================
|
||||||
@echo "Required packages:"
|
# Virtual Environment
|
||||||
@echo " pip install pytest pytest-cov pytest-asyncio aiohttp requests black flake8 mypy"
|
# ============================================================================
|
||||||
|
|
||||||
|
$(VENV)/bin/activate:
|
||||||
|
@echo "Creating virtual environment..."
|
||||||
|
@python3 -m venv $(VENV)
|
||||||
|
@$(VENV)/bin/pip install --upgrade pip -q
|
||||||
|
|
||||||
|
$(VENV)/.deps-installed: $(VENV)/bin/activate
|
||||||
|
@echo "Installing test dependencies into venv..."
|
||||||
|
@$(VENV)/bin/pip install -q requests pytest pytest-cov pytest-asyncio aiohttp
|
||||||
|
@if [ -f "$(SYNC_DIR)/setup.py" ]; then \
|
||||||
|
cd $(SYNC_DIR) && ../$(VENV)/bin/pip install -q -e . 2>/dev/null || true; \
|
||||||
|
fi
|
||||||
|
@if [ -f "$(ASYNC_DIR)/setup.py" ]; then \
|
||||||
|
cd $(ASYNC_DIR) && ../$(VENV)/bin/pip install -q -e . 2>/dev/null || true; \
|
||||||
|
fi
|
||||||
|
@touch $(VENV)/.deps-installed
|
||||||
|
|
||||||
|
venv: $(VENV)/.deps-installed
|
||||||
|
@echo "$(GREEN)✓$(NC) Virtual environment ready at $(VENV)/"
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# TEST: All 4 Modes
|
# TEST: All 4 Modes
|
||||||
|
|
@ -78,61 +94,55 @@ test: test-cli test-library test-integration test-functional
|
||||||
# TEST: CLI Mode
|
# TEST: CLI Mode
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
test-cli:
|
test-cli: $(VENV)/.deps-installed
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
@echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
@echo "CLI MODE: Testing Python CLI interface"
|
@echo "CLI MODE: Testing Python CLI interface"
|
||||||
@echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
@echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
@echo ""
|
@echo ""
|
||||||
@# Test root-level un.py if it exists
|
|
||||||
@if [ -f "$(ROOT_DIR)/un.py" ]; then \
|
@if [ -f "$(ROOT_DIR)/un.py" ]; then \
|
||||||
python3 -m py_compile "$(ROOT_DIR)/un.py" && echo " $(GREEN)✓$(NC) CLI: Syntax valid (un.py)"; \
|
$(PYTHON) -m py_compile "$(ROOT_DIR)/un.py" && echo " $(GREEN)✓$(NC) CLI: Syntax valid (un.py)"; \
|
||||||
python3 "$(ROOT_DIR)/un.py" --help > /dev/null 2>&1 && echo " $(GREEN)✓$(NC) CLI: --help works" || echo " $(YELLOW)⊘$(NC) CLI: --help (may need API)"; \
|
$(PYTHON) "$(ROOT_DIR)/un.py" --help > /dev/null 2>&1 && echo " $(GREEN)✓$(NC) CLI: --help works" || echo " $(YELLOW)⊘$(NC) CLI: --help (may need API)"; \
|
||||||
else \
|
else \
|
||||||
echo " $(YELLOW)⊘$(NC) Root un.py not found"; \
|
echo " $(YELLOW)⊘$(NC) Root un.py not found"; \
|
||||||
fi
|
fi
|
||||||
@# Test sync SDK CLI
|
|
||||||
@if [ -f "$(SYNC_DIR)/src/unsandbox/__main__.py" ]; then \
|
@if [ -f "$(SYNC_DIR)/src/unsandbox/__main__.py" ]; then \
|
||||||
python3 -m py_compile "$(SYNC_DIR)/src/unsandbox/__main__.py" && echo " $(GREEN)✓$(NC) CLI: Sync SDK syntax valid"; \
|
$(PYTHON) -m py_compile "$(SYNC_DIR)/src/unsandbox/__main__.py" && echo " $(GREEN)✓$(NC) CLI: Sync SDK syntax valid"; \
|
||||||
fi
|
fi
|
||||||
@# Test async SDK CLI
|
|
||||||
@if [ -f "$(ASYNC_DIR)/src/un_async/__main__.py" ]; then \
|
@if [ -f "$(ASYNC_DIR)/src/un_async/__main__.py" ]; then \
|
||||||
python3 -m py_compile "$(ASYNC_DIR)/src/un_async/__main__.py" && echo " $(GREEN)✓$(NC) CLI: Async SDK syntax valid"; \
|
$(PYTHON) -m py_compile "$(ASYNC_DIR)/src/un_async/__main__.py" && echo " $(GREEN)✓$(NC) CLI: Async SDK syntax valid"; \
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# TEST: Library Mode
|
# TEST: Library Mode
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
test-library:
|
test-library: $(VENV)/.deps-installed
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
@echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
@echo "LIBRARY MODE: Testing Python imports"
|
@echo "LIBRARY MODE: Testing Python imports"
|
||||||
@echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
@echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
@echo ""
|
@echo ""
|
||||||
@# Test sync SDK import
|
|
||||||
@if [ -d "$(SYNC_DIR)/src/unsandbox" ]; then \
|
@if [ -d "$(SYNC_DIR)/src/unsandbox" ]; then \
|
||||||
cd $(SYNC_DIR) && PYTHONPATH=src python3 -c "from unsandbox import UnsandboxClient; print(' ✓ Library: Sync UnsandboxClient importable')" 2>/dev/null || echo " $(YELLOW)⊘$(NC) Library: Sync import needs install"; \
|
cd $(SYNC_DIR) && PYTHONPATH=src ../$(PYTHON) -c "from unsandbox import UnsandboxClient; print(' ✓ Library: Sync UnsandboxClient importable')" 2>/dev/null || echo " $(YELLOW)⊘$(NC) Library: Sync import failed"; \
|
||||||
fi
|
fi
|
||||||
@# Test async SDK import
|
|
||||||
@if [ -d "$(ASYNC_DIR)/src/un_async" ]; then \
|
@if [ -d "$(ASYNC_DIR)/src/un_async" ]; then \
|
||||||
cd $(ASYNC_DIR) && PYTHONPATH=src python3 -c "from un_async import AsyncUnsandboxClient; print(' ✓ Library: Async AsyncUnsandboxClient importable')" 2>/dev/null || echo " $(YELLOW)⊘$(NC) Library: Async import needs install"; \
|
cd $(ASYNC_DIR) && PYTHONPATH=src ../$(PYTHON) -c "from un_async import AsyncUnsandboxClient; print(' ✓ Library: Async AsyncUnsandboxClient importable')" 2>/dev/null || echo " $(YELLOW)⊘$(NC) Library: Async import failed"; \
|
||||||
fi
|
fi
|
||||||
@# Run pytest for library tests
|
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo "Running unit tests..."
|
@echo "Running unit tests..."
|
||||||
@if [ -d "$(SYNC_DIR)/tests" ]; then \
|
@if [ -d "$(SYNC_DIR)/tests" ]; then \
|
||||||
cd $(SYNC_DIR) && pytest tests/ -q --tb=no 2>/dev/null && echo " $(GREEN)✓$(NC) Sync SDK tests passed" || echo " $(YELLOW)⊘$(NC) Sync tests need dependencies"; \
|
cd $(SYNC_DIR) && PYTHONPATH=src ../$(PYTEST) tests/ -q --tb=short 2>&1 && echo " $(GREEN)✓$(NC) Sync SDK tests passed" || echo " $(RED)✗$(NC) Sync tests failed"; \
|
||||||
fi
|
fi
|
||||||
@if [ -d "$(ASYNC_DIR)/tests" ]; then \
|
@if [ -d "$(ASYNC_DIR)/tests" ]; then \
|
||||||
cd $(ASYNC_DIR) && pytest tests/ -q --tb=no 2>/dev/null && echo " $(GREEN)✓$(NC) Async SDK tests passed" || echo " $(YELLOW)⊘$(NC) Async tests need dependencies"; \
|
cd $(ASYNC_DIR) && PYTHONPATH=src ../$(PYTEST) tests/ -q --tb=short 2>&1 && echo " $(GREEN)✓$(NC) Async SDK tests passed" || echo " $(RED)✗$(NC) Async tests failed"; \
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# TEST: Integration Mode
|
# TEST: Integration Mode
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
test-integration:
|
test-integration: $(VENV)/.deps-installed
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
@echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
@echo "INTEGRATION MODE: Testing API contract"
|
@echo "INTEGRATION MODE: Testing API contract"
|
||||||
|
|
@ -143,14 +153,14 @@ test-integration:
|
||||||
echo " Set UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY"; \
|
echo " Set UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY"; \
|
||||||
else \
|
else \
|
||||||
echo " Testing API authentication..."; \
|
echo " Testing API authentication..."; \
|
||||||
python3 -c "import sys; sys.path.insert(0, '$(SYNC_DIR)/src'); from unsandbox import UnsandboxClient; c = UnsandboxClient(); r = c.execute('python', 'print(42)'); print(' ✓ Integration: API auth works') if r else print(' ✗ Integration: API auth failed')" 2>/dev/null || echo " $(YELLOW)⊘$(NC) Integration: Need to install SDK first"; \
|
$(PYTHON) -c "import sys; sys.path.insert(0, '$(SYNC_DIR)/src'); from unsandbox import UnsandboxClient; c = UnsandboxClient(); r = c.execute('python', 'print(42)'); print(' ✓ Integration: API auth works') if r else print(' ✗ Integration: API auth failed')" 2>/dev/null || echo " $(RED)✗$(NC) Integration: SDK error"; \
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# TEST: Functional Mode
|
# TEST: Functional Mode
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
test-functional:
|
test-functional: $(VENV)/.deps-installed
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
@echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
@echo "FUNCTIONAL MODE: Real-world scenarios"
|
@echo "FUNCTIONAL MODE: Real-world scenarios"
|
||||||
|
|
@ -160,31 +170,25 @@ test-functional:
|
||||||
echo " $(YELLOW)⊘$(NC) Skipping (no API credentials)"; \
|
echo " $(YELLOW)⊘$(NC) Skipping (no API credentials)"; \
|
||||||
else \
|
else \
|
||||||
echo " Running functional tests..."; \
|
echo " Running functional tests..."; \
|
||||||
if [ -f "$(SYNC_DIR)/verify_sdk.py" ]; then \
|
cd $(SYNC_DIR) && PYTHONPATH=src ../$(PYTEST) tests/test_functional.py -v 2>&1 && echo " $(GREEN)✓$(NC) Functional: Sync SDK verified" || echo " $(RED)✗$(NC) Functional: Sync verification failed"; \
|
||||||
cd $(SYNC_DIR) && python3 verify_sdk.py 2>/dev/null && echo " $(GREEN)✓$(NC) Functional: Sync SDK verified" || echo " $(YELLOW)⊘$(NC) Functional: Sync verification incomplete"; \
|
|
||||||
fi; \
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# TEST: By SDK Type
|
# TEST: By SDK Type
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
test-sync:
|
test-sync: $(VENV)/.deps-installed
|
||||||
@echo "Testing Sync SDK..."
|
@echo "Testing Sync SDK..."
|
||||||
@if [ -f "$(SYNC_DIR)/Makefile" ]; then \
|
@if [ -d "$(SYNC_DIR)/tests" ]; then \
|
||||||
$(MAKE) -C $(SYNC_DIR) test; \
|
cd $(SYNC_DIR) && PYTHONPATH=src ../$(PYTEST) tests/ -v; \
|
||||||
elif [ -d "$(SYNC_DIR)/tests" ]; then \
|
|
||||||
cd $(SYNC_DIR) && pytest tests/ -v; \
|
|
||||||
else \
|
else \
|
||||||
echo " $(YELLOW)⊘$(NC) Sync SDK tests not found"; \
|
echo " $(YELLOW)⊘$(NC) Sync SDK tests not found"; \
|
||||||
fi
|
fi
|
||||||
|
|
||||||
test-async:
|
test-async: $(VENV)/.deps-installed
|
||||||
@echo "Testing Async SDK..."
|
@echo "Testing Async SDK..."
|
||||||
@if [ -f "$(ASYNC_DIR)/Makefile" ]; then \
|
@if [ -d "$(ASYNC_DIR)/tests" ]; then \
|
||||||
$(MAKE) -C $(ASYNC_DIR) test; \
|
cd $(ASYNC_DIR) && PYTHONPATH=src ../$(PYTEST) tests/ -v; \
|
||||||
elif [ -d "$(ASYNC_DIR)/tests" ]; then \
|
|
||||||
cd $(ASYNC_DIR) && pytest tests/ -v; \
|
|
||||||
else \
|
else \
|
||||||
echo " $(YELLOW)⊘$(NC) Async SDK tests not found"; \
|
echo " $(YELLOW)⊘$(NC) Async SDK tests not found"; \
|
||||||
fi
|
fi
|
||||||
|
|
@ -193,32 +197,21 @@ test-async:
|
||||||
# Development
|
# Development
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
install:
|
lint: $(VENV)/.deps-installed
|
||||||
@echo "Installing Python SDKs..."
|
|
||||||
@if [ -f "$(SYNC_DIR)/setup.py" ]; then cd $(SYNC_DIR) && pip install -e . ; fi
|
|
||||||
@if [ -f "$(ASYNC_DIR)/setup.py" ]; then cd $(ASYNC_DIR) && pip install -e . ; fi
|
|
||||||
@echo "$(GREEN)✓$(NC) Installation complete"
|
|
||||||
|
|
||||||
dev-install:
|
|
||||||
@echo "Installing Python SDKs with dev dependencies..."
|
|
||||||
@if [ -f "$(SYNC_DIR)/setup.py" ]; then cd $(SYNC_DIR) && pip install -e ".[dev]" 2>/dev/null || pip install -e . ; fi
|
|
||||||
@if [ -f "$(ASYNC_DIR)/setup.py" ]; then cd $(ASYNC_DIR) && pip install -e ".[dev]" 2>/dev/null || pip install -e . ; fi
|
|
||||||
@pip install pytest pytest-cov pytest-asyncio black flake8 mypy 2>/dev/null || true
|
|
||||||
@echo "$(GREEN)✓$(NC) Dev installation complete"
|
|
||||||
|
|
||||||
lint:
|
|
||||||
@echo "Linting Python SDKs..."
|
@echo "Linting Python SDKs..."
|
||||||
@if [ -d "$(SYNC_DIR)/src" ]; then flake8 $(SYNC_DIR)/src/ --max-line-length=120 || true; fi
|
@$(VENV)/bin/pip install -q flake8 2>/dev/null || true
|
||||||
@if [ -d "$(ASYNC_DIR)/src" ]; then flake8 $(ASYNC_DIR)/src/ --max-line-length=120 || true; fi
|
@if [ -d "$(SYNC_DIR)/src" ]; then $(VENV)/bin/flake8 $(SYNC_DIR)/src/ --max-line-length=120 || true; fi
|
||||||
|
@if [ -d "$(ASYNC_DIR)/src" ]; then $(VENV)/bin/flake8 $(ASYNC_DIR)/src/ --max-line-length=120 || true; fi
|
||||||
@echo "$(GREEN)✓$(NC) Lint complete"
|
@echo "$(GREEN)✓$(NC) Lint complete"
|
||||||
|
|
||||||
format:
|
format: $(VENV)/.deps-installed
|
||||||
@echo "Formatting Python SDKs..."
|
@echo "Formatting Python SDKs..."
|
||||||
@if [ -d "$(SYNC_DIR)/src" ]; then black $(SYNC_DIR)/src/ $(SYNC_DIR)/tests/ 2>/dev/null || true; fi
|
@$(VENV)/bin/pip install -q black 2>/dev/null || true
|
||||||
@if [ -d "$(ASYNC_DIR)/src" ]; then black $(ASYNC_DIR)/src/ $(ASYNC_DIR)/tests/ 2>/dev/null || true; fi
|
@if [ -d "$(SYNC_DIR)/src" ]; then $(VENV)/bin/black $(SYNC_DIR)/src/ $(SYNC_DIR)/tests/ 2>/dev/null || true; fi
|
||||||
|
@if [ -d "$(ASYNC_DIR)/src" ]; then $(VENV)/bin/black $(ASYNC_DIR)/src/ $(ASYNC_DIR)/tests/ 2>/dev/null || true; fi
|
||||||
@echo "$(GREEN)✓$(NC) Format complete"
|
@echo "$(GREEN)✓$(NC) Format complete"
|
||||||
|
|
||||||
examples:
|
examples: $(VENV)/.deps-installed
|
||||||
@echo "Running Python examples..."
|
@echo "Running Python examples..."
|
||||||
@if [ -f "$(ASYNC_DIR)/Makefile" ]; then $(MAKE) -C $(ASYNC_DIR) examples; fi
|
@if [ -f "$(ASYNC_DIR)/Makefile" ]; then $(MAKE) -C $(ASYNC_DIR) examples; fi
|
||||||
|
|
||||||
|
|
@ -228,6 +221,7 @@ examples:
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
@echo "Cleaning Python build artifacts..."
|
@echo "Cleaning Python build artifacts..."
|
||||||
|
@rm -rf $(VENV)
|
||||||
@find . -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true
|
@find . -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true
|
||||||
@find . -type f -name "*.pyc" -delete 2>/dev/null || true
|
@find . -type f -name "*.pyc" -delete 2>/dev/null || true
|
||||||
@find . -type d -name ".pytest_cache" -exec rm -rf {} + 2>/dev/null || true
|
@find . -type d -name ".pytest_cache" -exec rm -rf {} + 2>/dev/null || true
|
||||||
|
|
@ -236,4 +230,4 @@ clean:
|
||||||
@find . -type d -name "htmlcov" -exec rm -rf {} + 2>/dev/null || true
|
@find . -type d -name "htmlcov" -exec rm -rf {} + 2>/dev/null || true
|
||||||
@find . -type d -name "dist" -exec rm -rf {} + 2>/dev/null || true
|
@find . -type d -name "dist" -exec rm -rf {} + 2>/dev/null || true
|
||||||
@find . -type d -name "build" -exec rm -rf {} + 2>/dev/null || true
|
@find . -type d -name "build" -exec rm -rf {} + 2>/dev/null || true
|
||||||
@echo "$(GREEN)✓$(NC) Cleaned build artifacts"
|
@echo "$(GREEN)✓$(NC) Cleaned build artifacts + venv"
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,20 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
# 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 - demonstrates fire-and-forget execution
|
Async job polling example - demonstrates fire-and-forget execution
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,20 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
# 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 - standalone version
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,20 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
# 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 HTTP Requests example - standalone version
|
Concurrent HTTP Requests example - standalone version
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,20 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
# 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 for unsandbox Python SDK - Asynchronous Version
|
Fibonacci example for unsandbox Python SDK - Asynchronous Version
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,20 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
# 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 async version
|
Hello World example - standalone async version
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,20 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
# 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.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Stream Processing example - standalone version
|
Stream Processing example - standalone version
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,20 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
# 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.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Sync (blocking) operations demonstration - standalone version
|
Sync (blocking) operations demonstration - standalone version
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,20 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
# 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.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Setup script for unsandbox async Python SDK
|
Setup script for unsandbox async Python SDK
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -1,102 +1,19 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""
|
# This is free software for the public good of a permacomputer hosted at
|
||||||
PUBLIC DOMAIN - NO LICENSE, NO WARRANTY
|
# permacomputer.com, an always-on computer by the people, for the people.
|
||||||
|
# One which is durable, easy to repair, & distributed like tap water
|
||||||
unsandbox.com Python SDK (Asynchronous)
|
# for machine learning intelligence.
|
||||||
|
#
|
||||||
Library Usage:
|
# The permacomputer is community-owned infrastructure optimized around
|
||||||
import asyncio
|
# four values:
|
||||||
from un_async import (
|
#
|
||||||
# Execution
|
# TRUTH First principles, math & science, open source code freely distributed
|
||||||
execute_code,
|
# FREEDOM Voluntary partnerships, freedom from tyranny & corporate control
|
||||||
execute_async,
|
# HARMONY Minimal waste, self-renewing systems with diverse thriving connections
|
||||||
get_job,
|
# LOVE Be yourself without hurting others, cooperation through natural law
|
||||||
wait_for_job,
|
#
|
||||||
cancel_job,
|
# This software contributes to that vision by enabling code execution across 42+ programming languages through a unified interface, accessible to all.
|
||||||
list_jobs,
|
# Code is seeds to sprout on any abandoned technology.
|
||||||
get_languages,
|
|
||||||
detect_language,
|
|
||||||
# Sessions
|
|
||||||
list_sessions,
|
|
||||||
get_session,
|
|
||||||
create_session,
|
|
||||||
delete_session,
|
|
||||||
freeze_session,
|
|
||||||
unfreeze_session,
|
|
||||||
boost_session,
|
|
||||||
unboost_session,
|
|
||||||
shell_session,
|
|
||||||
# Services
|
|
||||||
list_services,
|
|
||||||
create_service,
|
|
||||||
get_service,
|
|
||||||
update_service,
|
|
||||||
delete_service,
|
|
||||||
freeze_service,
|
|
||||||
unfreeze_service,
|
|
||||||
lock_service,
|
|
||||||
unlock_service,
|
|
||||||
set_unfreeze_on_demand,
|
|
||||||
set_show_freeze_page,
|
|
||||||
get_service_logs,
|
|
||||||
get_service_env,
|
|
||||||
set_service_env,
|
|
||||||
delete_service_env,
|
|
||||||
export_service_env,
|
|
||||||
redeploy_service,
|
|
||||||
execute_in_service,
|
|
||||||
# Snapshots
|
|
||||||
session_snapshot,
|
|
||||||
service_snapshot,
|
|
||||||
list_snapshots,
|
|
||||||
restore_snapshot,
|
|
||||||
delete_snapshot,
|
|
||||||
lock_snapshot,
|
|
||||||
unlock_snapshot,
|
|
||||||
clone_snapshot,
|
|
||||||
# Key validation
|
|
||||||
validate_keys,
|
|
||||||
# Image generation
|
|
||||||
image,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def main():
|
|
||||||
# Execute code synchronously
|
|
||||||
result = await execute_code("python", 'print("hello")', public_key, secret_key)
|
|
||||||
|
|
||||||
# Execute asynchronously
|
|
||||||
job_id = await execute_async("javascript", 'console.log("hello")', public_key, secret_key)
|
|
||||||
|
|
||||||
# Wait for job completion with exponential backoff
|
|
||||||
result = await wait_for_job(job_id, public_key, secret_key)
|
|
||||||
|
|
||||||
# List all jobs
|
|
||||||
jobs = await list_jobs(public_key, secret_key)
|
|
||||||
|
|
||||||
# Get supported languages
|
|
||||||
languages = await get_languages(public_key, secret_key)
|
|
||||||
|
|
||||||
# Snapshot operations
|
|
||||||
snapshot_id = await session_snapshot(session_id, public_key, secret_key)
|
|
||||||
|
|
||||||
asyncio.run(main())
|
|
||||||
|
|
||||||
Authentication Priority (4-tier):
|
|
||||||
1. Function arguments (public_key, secret_key)
|
|
||||||
2. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY)
|
|
||||||
3. Config file (~/.unsandbox/accounts.csv, line 0 by default)
|
|
||||||
4. Local directory (./accounts.csv, line 0 by default)
|
|
||||||
|
|
||||||
Request Authentication (HMAC-SHA256):
|
|
||||||
Authorization: Bearer <public_key>
|
|
||||||
X-Timestamp: <unix_seconds>
|
|
||||||
X-Signature: HMAC-SHA256(secret_key, "timestamp:METHOD:path:body")
|
|
||||||
|
|
||||||
Languages Cache:
|
|
||||||
- Cached in ~/.unsandbox/languages.json
|
|
||||||
- TTL: 1 hour
|
|
||||||
- Updated on successful API calls
|
|
||||||
"""
|
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import hashlib
|
import hashlib
|
||||||
|
|
@ -155,14 +72,16 @@ def _load_credentials_from_csv(csv_path: Path, account_index: int = 0) -> Option
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with open(csv_path, "r") as f:
|
with open(csv_path, "r") as f:
|
||||||
for i, line in enumerate(f):
|
data_index = 0
|
||||||
|
for line in f:
|
||||||
line = line.strip()
|
line = line.strip()
|
||||||
if not line or line.startswith("#"):
|
if not line or line.startswith("#"):
|
||||||
continue
|
continue
|
||||||
if i == account_index:
|
if data_index == account_index:
|
||||||
parts = line.split(",")
|
parts = line.split(",")
|
||||||
if len(parts) >= 2:
|
if len(parts) >= 2:
|
||||||
return (parts[0].strip(), parts[1].strip())
|
return (parts[0].strip(), parts[1].strip())
|
||||||
|
data_index += 1
|
||||||
return None
|
return None
|
||||||
except Exception:
|
except Exception:
|
||||||
return None
|
return None
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,19 @@
|
||||||
|
# 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 unsandbox async SDK
|
Tests for unsandbox async SDK
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,19 @@
|
||||||
|
# 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.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Pytest configuration for async tests
|
Pytest configuration for async tests
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,19 @@
|
||||||
|
# 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 async operations (requires mocking or real API access)
|
Tests for async operations (requires mocking or real API access)
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,19 @@
|
||||||
|
# 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 credential resolution system
|
Tests for credential resolution system
|
||||||
"""
|
"""
|
||||||
|
|
@ -37,11 +53,15 @@ class TestCredentialResolution:
|
||||||
assert pk == "env_pk"
|
assert pk == "env_pk"
|
||||||
assert sk == "env_sk"
|
assert sk == "env_sk"
|
||||||
|
|
||||||
def test_credentials_error_no_sources(self, monkeypatch):
|
def test_credentials_error_no_sources(self, monkeypatch, tmp_path):
|
||||||
"""Test CredentialsError when no credentials found."""
|
"""Test CredentialsError when no credentials found."""
|
||||||
|
import un_async
|
||||||
# Clear environment
|
# Clear environment
|
||||||
monkeypatch.delenv("UNSANDBOX_PUBLIC_KEY", raising=False)
|
monkeypatch.delenv("UNSANDBOX_PUBLIC_KEY", raising=False)
|
||||||
monkeypatch.delenv("UNSANDBOX_SECRET_KEY", raising=False)
|
monkeypatch.delenv("UNSANDBOX_SECRET_KEY", raising=False)
|
||||||
|
# Point _get_unsandbox_dir to empty tmp dir so CSV lookup finds nothing
|
||||||
|
monkeypatch.setattr(un_async, "_get_unsandbox_dir", lambda: tmp_path)
|
||||||
|
monkeypatch.chdir(tmp_path)
|
||||||
|
|
||||||
with pytest.raises(CredentialsError) as exc_info:
|
with pytest.raises(CredentialsError) as exc_info:
|
||||||
_resolve_credentials(None, None)
|
_resolve_credentials(None, None)
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,19 @@
|
||||||
|
# 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 HMAC request signing
|
Tests for HMAC request signing
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,19 @@
|
||||||
|
# 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 language detection
|
Tests for language detection
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,20 @@
|
||||||
#!/bin/bash
|
#!/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.
|
||||||
|
|
||||||
##
|
##
|
||||||
# Validation script for Python SDK examples
|
# Validation script for Python SDK examples
|
||||||
#
|
#
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue