Migrate to official Unsandbox Python SDK

Replace manual HMAC authentication with official Unsandbox Python SDK (un.py).
Simplifies code execution proxy endpoints by using SDK methods: execute_async,
get_job, and cancel_job. Removes ~40 lines of manual HTTP/auth code.

Changes:
- Add official Unsandbox Python SDK (un.py)
- Refactor app.py proxy endpoints to use SDK methods
- Update CLAUDE.md documentation with SDK setup and usage
- Remove manual HMAC signing code
- Maintain backward compatibility with existing API endpoints
This commit is contained in:
russell@unturf.com 2026-01-20 08:21:58 -05:00
parent a825633be5
commit 27a0df3d12
3 changed files with 2968 additions and 167 deletions

163
CLAUDE.md
View file

@ -99,113 +99,134 @@ git remote -v
### Code Execution Integration ### Code Execution Integration
OpenCompletion integrates with the Unsandbox API (https://api.unsandbox.com) for secure code execution in 40+ programming languages. OpenCompletion integrates with the Unsandbox API (https://unsandbox.com) for secure code execution in 42+ programming languages using the official Python SDK.
#### API Endpoints #### SDK Setup
**Synchronous Execution** (immediate results): OpenCompletion uses the official Unsandbox Python SDK (`un.py`) which provides a clean interface to the Unsandbox API.
```
POST https://api.unsandbox.com/execute
```
- Executes code immediately and returns results
- Use for quick code snippets and interactive execution
**Asynchronous Execution** (long-running tasks): **SDK Location**: `/home/fox/git/opencompletion/un.py` (single file, no dependencies beyond `requests`)
```
POST https://api.unsandbox.com/execute/async
```
- Returns job ID for later retrieval
- Use for long-running scripts (up to 15 minutes)
**Auto-Detect Language**: **SDK Documentation**: https://unsandbox.com/cli/python
```
POST https://api.unsandbox.com/run
```
- Automatically detects language from shebang
- Send raw code as request body
- Useful when language is unknown or embedded in script
#### Request Format **Installation**:
```bash
# SDK is already included in the repository
# To update to latest version:
curl -O https://git.unturf.com/engineering/unturf/un-inception/-/raw/main/clients/python/sync/src/un.py
```
#### Authentication
The SDK uses HMAC-SHA256 authentication automatically via environment variables:
**Environment Variables:**
- `UNSANDBOX_PUBLIC_KEY` - Public key (unsb-pk-xxxx) used as Bearer token to identify account
- `UNSANDBOX_SECRET_KEY` - Secret key (unsb-sk-xxxx) used for HMAC signing (never transmitted)
The SDK handles all authentication automatically. No manual HMAC signing required.
#### Core SDK Methods
OpenCompletion uses three primary SDK methods:
**1. Asynchronous Execution** (default for frontend):
```python
import un
# Submit code for execution, get job_id immediately
job_id = un.execute_async(
language="python",
code="print('Hello, World!')",
env={"VAR": "value"}, # Optional
network_mode="zerotrust", # Optional: zerotrust or semitrusted
ttl=60 # Optional: timeout in seconds (1-900)
)
```
**2. Job Status Polling**:
```python
# Check job status and get results
result = un.get_job(job_id)
# Result contains:
# - status: "pending" | "running" | "completed" | "failed"
# - stdout: program output (when completed)
# - stderr: error output (when completed)
# - exit_code: exit status (when completed)
# - execution_time_ms: execution duration (when completed)
```
**3. Job Cancellation**:
```python
# Cancel running or pending job
un.cancel_job(job_id)
```
#### OpenCompletion API Proxy Endpoints
OpenCompletion provides proxy endpoints that keep credentials server-side:
**Execute Code** (POST `/api/code/execute`):
```json ```json
{ {
"language": "python", "language": "python",
"code": "print('Hello, World!')", "code": "print('Hello, World!')",
"env": { "env": {"VAR": "value"},
"VAR_NAME": "value"
},
"network_mode": "zerotrust", "network_mode": "zerotrust",
"ttl": 60 "ttl": 60
} }
``` ```
Returns: `{"job_id": "job-xxx"}`
**Parameters**: **Get Job Status** (GET `/api/code/jobs/<job_id>`):
- `language` (required): Programming language identifier Returns job status and results when completed.
- `code` (required): Source code to execute
- `env` (optional): Environment variables as key-value pairs **Cancel Job** (DELETE `/api/code/jobs/<job_id>`):
- `network_mode` (optional): "zerotrust" (default) or "semitrusted" Cancels the running or pending job.
- `ttl` (optional): Timeout in seconds (1-900, default 60)
#### Response Format #### Response Format
**Success Response**: **Job Status Response** (from `un.get_job()`):
```json ```json
{ {
"success": true, "job_id": "job-xxx",
"status": "completed",
"stdout": "Hello, World!\n", "stdout": "Hello, World!\n",
"stderr": "", "stderr": "",
"exit_code": 0 "exit_code": 0,
} "execution_time_ms": 45
```
**Error Response**:
```json
{
"success": false,
"stdout": "",
"stderr": "SyntaxError: invalid syntax\n",
"exit_code": 1,
"error": "Runtime error occurred"
} }
``` ```
**Response Fields**: **Response Fields**:
- `success` (boolean): True if execution completed without errors - `job_id` (string): Unique job identifier
- `stdout` (string): Standard output from the program - `status` (string): "pending" | "running" | "completed" | "failed"
- `stderr` (string): Standard error output - `stdout` (string): Standard output (when completed)
- `exit_code` (integer): Program exit status (0 = success, non-zero = error) - `stderr` (string): Standard error output (when completed)
- `error` (string, optional): Detailed error message if execution failed - `exit_code` (integer): Program exit status (when completed)
- `detected_language` (string, optional): Language detected by auto-detect endpoint - `execution_time_ms` (integer): Execution duration in milliseconds (when completed)
#### Authentication
Uses HMAC-SHA256 authentication with public/secret key pairs:
**Environment Variables:**
- `UNSANDBOX_PUBLIC_KEY` - Public key (unsb-pk-xxxx) used as Bearer token to identify account
- `UNSANDBOX_SECRET_KEY` - Secret key (unsb-sk-xxxx) used for HMAC signing, never transmitted
**Request Headers:**
```
Authorization: Bearer <public_key>
X-Timestamp: <unix_seconds>
X-Signature: HMAC-SHA256(secret_key, timestamp:method:path:body)
```
The secret key is never transmitted - server verifies HMAC using its stored copy.
Timestamp must be within ±5 minutes of server time (replay attack prevention).
#### Supported Languages #### Supported Languages
40+ languages including: The SDK supports 42+ languages including:
- **Compiled**: C, C++, Rust, Go, Java, C#, Swift - **Compiled**: C, C++, Rust, Go, Java, C#, Swift
- **Interpreted**: Python, Ruby, JavaScript, PHP, Perl, Lua - **Interpreted**: Python, Ruby, JavaScript, PHP, Perl, Lua
- **Scripting**: Bash, PowerShell, Fish - **Scripting**: Bash, PowerShell, Fish
- **Data**: R, Julia, Octave, MATLAB - **Data**: R, Julia, Octave
- **Functional**: Haskell, Scala, Erlang, Elixir - **Functional**: Haskell, Scala, Erlang, Elixir
- **Esoteric**: Brainfuck, LOLCODE - **Esoteric**: Brainfuck, LOLCODE
- And many more... - And many more...
**SDK Methods for Language Support**:
```python
# List all supported languages
languages = un.get_languages()
# Auto-detect language from filename
lang = un.detect_language("script.py") # Returns "python"
```
#### Frontend Integration #### Frontend Integration
- Add play button (▶) next to copy button on code blocks - Add play button (▶) next to copy button on code blocks

143
app.py
View file

@ -15,6 +15,9 @@ import random
import boto3 import boto3
import together import together
# Unsandbox SDK for code execution
import un
from flask import ( from flask import (
Flask, Flask,
render_template, render_template,
@ -1243,47 +1246,7 @@ Examples:
# Unsandbox API proxy endpoints - keeps API keys server-side # Unsandbox API proxy endpoints - keeps API keys server-side
UNSANDBOX_API_URL = "https://api.unsandbox.com" # Uses official Python SDK (un.py) for authentication and API calls
def get_unsandbox_auth_headers(method, path, body=None):
"""Generate HMAC authentication headers for Unsandbox API.
Authentication scheme:
Authorization: Bearer <public_key> - identifies account
X-Timestamp: <unix_seconds> - replay prevention
X-Signature: HMAC-SHA256(secret_key, ts:method:path:body) - proves secret
The secret key is NEVER transmitted. Server verifies HMAC with stored secret.
"""
import hmac
import hashlib
import time
public_key = os.environ.get("UNSANDBOX_PUBLIC_KEY")
secret_key = os.environ.get("UNSANDBOX_SECRET_KEY")
if not public_key or not secret_key:
return None
timestamp = int(time.time())
body_str = body if body else ""
# Build message: "timestamp:method:path:body"
message = f"{timestamp}:{method}:{path}:{body_str}"
# Compute HMAC-SHA256
signature = hmac.new(
secret_key.encode("utf-8"),
message.encode("utf-8"),
hashlib.sha256,
).hexdigest()
return {
"Authorization": f"Bearer {public_key}",
"X-Timestamp": str(timestamp),
"X-Signature": signature,
}
@app.route("/api/code/execute", methods=["POST"]) @app.route("/api/code/execute", methods=["POST"])
@ -1291,34 +1254,43 @@ def proxy_code_execute():
"""Proxy code execution requests to Unsandbox API. """Proxy code execution requests to Unsandbox API.
Keeps UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY secure on the server side. Keeps UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY secure on the server side.
Uses HMAC-SHA256 authentication. Uses official Unsandbox Python SDK for authentication and execution.
""" """
import httpx
try: try:
data = request.get_json() data = request.get_json()
if not data: if not data:
return jsonify({"error": "Request body required"}), 400 return jsonify({"error": "Request body required"}), 400
body_json = json.dumps(data, separators=(",", ":")) # Check if credentials are configured
auth_headers = get_unsandbox_auth_headers("POST", "/execute/async", body_json) if not os.environ.get("UNSANDBOX_PUBLIC_KEY") or not os.environ.get("UNSANDBOX_SECRET_KEY"):
if not auth_headers:
return jsonify({"error": "Code execution not configured"}), 503 return jsonify({"error": "Code execution not configured"}), 503
headers = {"Content-Type": "application/json", **auth_headers} # Extract parameters from request
language = data.get("language")
code = data.get("code")
env = data.get("env")
network_mode = data.get("network_mode", "zerotrust")
ttl = data.get("ttl", 60)
with httpx.Client(timeout=30.0) as client: if not language or not code:
response = client.post( return jsonify({"error": "Language and code are required"}), 400
f"{UNSANDBOX_API_URL}/execute/async",
headers=headers,
content=body_json,
)
# Return the response from Unsandbox # Use SDK's execute_async method
return jsonify(response.json()), response.status_code result = un.execute_async(
language=language,
code=code,
env=env,
network_mode=network_mode,
ttl=ttl
)
# SDK returns the job_id directly as a string, or a dict with error
if isinstance(result, str):
return jsonify({"job_id": result}), 200
else:
# Result is already a dict (possibly with error)
return jsonify(result), 200
except httpx.TimeoutException:
return jsonify({"error": "Request to code execution service timed out"}), 504
except Exception as e: except Exception as e:
print(f"Error proxying code execution: {e}") print(f"Error proxying code execution: {e}")
return jsonify({"error": "Failed to execute code"}), 500 return jsonify({"error": "Failed to execute code"}), 500
@ -1326,25 +1298,18 @@ def proxy_code_execute():
@app.route("/api/code/jobs/<job_id>", methods=["GET"]) @app.route("/api/code/jobs/<job_id>", methods=["GET"])
def proxy_job_status(job_id): def proxy_job_status(job_id):
"""Proxy job status requests to Unsandbox API.""" """Proxy job status requests to Unsandbox API using SDK."""
import httpx
path = f"/jobs/{job_id}"
auth_headers = get_unsandbox_auth_headers("GET", path, None)
if not auth_headers:
return jsonify({"error": "Code execution not configured"}), 503
try: try:
with httpx.Client(timeout=30.0) as client: # Check if credentials are configured
response = client.get( if not os.environ.get("UNSANDBOX_PUBLIC_KEY") or not os.environ.get("UNSANDBOX_SECRET_KEY"):
f"{UNSANDBOX_API_URL}{path}", return jsonify({"error": "Code execution not configured"}), 503
headers=auth_headers,
)
return jsonify(response.json()), response.status_code # Use SDK's get_job method
result = un.get_job(job_id)
# SDK returns job status dict
return jsonify(result), 200
except httpx.TimeoutException:
return jsonify({"error": "Request to code execution service timed out"}), 504
except Exception as e: except Exception as e:
print(f"Error fetching job status: {e}") print(f"Error fetching job status: {e}")
return jsonify({"error": "Failed to fetch job status"}), 500 return jsonify({"error": "Failed to fetch job status"}), 500
@ -1352,32 +1317,18 @@ def proxy_job_status(job_id):
@app.route("/api/code/jobs/<job_id>", methods=["DELETE"]) @app.route("/api/code/jobs/<job_id>", methods=["DELETE"])
def proxy_job_cancel(job_id): def proxy_job_cancel(job_id):
"""Proxy job cancellation requests to Unsandbox API.""" """Proxy job cancellation requests to Unsandbox API using SDK."""
import httpx
path = f"/jobs/{job_id}"
auth_headers = get_unsandbox_auth_headers("DELETE", path, None)
if not auth_headers:
return jsonify({"error": "Code execution not configured"}), 503
try: try:
with httpx.Client(timeout=30.0) as client: # Check if credentials are configured
response = client.delete( if not os.environ.get("UNSANDBOX_PUBLIC_KEY") or not os.environ.get("UNSANDBOX_SECRET_KEY"):
f"{UNSANDBOX_API_URL}{path}", return jsonify({"error": "Code execution not configured"}), 503
headers=auth_headers,
)
# DELETE may return empty body on success # Use SDK's cancel_job method
if response.status_code == 204: result = un.cancel_job(job_id)
return "", 204
try: # SDK returns success status
return jsonify(response.json()), response.status_code return jsonify(result), 200
except Exception:
return "", response.status_code
except httpx.TimeoutException:
return jsonify({"error": "Request to code execution service timed out"}), 504
except Exception as e: except Exception as e:
print(f"Error cancelling job: {e}") print(f"Error cancelling job: {e}")
return jsonify({"error": "Failed to cancel job"}), 500 return jsonify({"error": "Failed to cancel job"}), 500

2829
un.py Normal file

File diff suppressed because it is too large Load diff