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:
parent
a825633be5
commit
27a0df3d12
3 changed files with 2968 additions and 167 deletions
163
CLAUDE.md
163
CLAUDE.md
|
|
@ -99,113 +99,134 @@ git remote -v
|
|||
|
||||
### 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):
|
||||
```
|
||||
POST https://api.unsandbox.com/execute
|
||||
```
|
||||
- Executes code immediately and returns results
|
||||
- Use for quick code snippets and interactive execution
|
||||
OpenCompletion uses the official Unsandbox Python SDK (`un.py`) which provides a clean interface to the Unsandbox API.
|
||||
|
||||
**Asynchronous Execution** (long-running tasks):
|
||||
```
|
||||
POST https://api.unsandbox.com/execute/async
|
||||
```
|
||||
- Returns job ID for later retrieval
|
||||
- Use for long-running scripts (up to 15 minutes)
|
||||
**SDK Location**: `/home/fox/git/opencompletion/un.py` (single file, no dependencies beyond `requests`)
|
||||
|
||||
**Auto-Detect Language**:
|
||||
```
|
||||
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
|
||||
**SDK Documentation**: https://unsandbox.com/cli/python
|
||||
|
||||
#### 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
|
||||
{
|
||||
"language": "python",
|
||||
"code": "print('Hello, World!')",
|
||||
"env": {
|
||||
"VAR_NAME": "value"
|
||||
},
|
||||
"env": {"VAR": "value"},
|
||||
"network_mode": "zerotrust",
|
||||
"ttl": 60
|
||||
}
|
||||
```
|
||||
Returns: `{"job_id": "job-xxx"}`
|
||||
|
||||
**Parameters**:
|
||||
- `language` (required): Programming language identifier
|
||||
- `code` (required): Source code to execute
|
||||
- `env` (optional): Environment variables as key-value pairs
|
||||
- `network_mode` (optional): "zerotrust" (default) or "semitrusted"
|
||||
- `ttl` (optional): Timeout in seconds (1-900, default 60)
|
||||
**Get Job Status** (GET `/api/code/jobs/<job_id>`):
|
||||
Returns job status and results when completed.
|
||||
|
||||
**Cancel Job** (DELETE `/api/code/jobs/<job_id>`):
|
||||
Cancels the running or pending job.
|
||||
|
||||
#### Response Format
|
||||
|
||||
**Success Response**:
|
||||
**Job Status Response** (from `un.get_job()`):
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"job_id": "job-xxx",
|
||||
"status": "completed",
|
||||
"stdout": "Hello, World!\n",
|
||||
"stderr": "",
|
||||
"exit_code": 0
|
||||
}
|
||||
```
|
||||
|
||||
**Error Response**:
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"stdout": "",
|
||||
"stderr": "SyntaxError: invalid syntax\n",
|
||||
"exit_code": 1,
|
||||
"error": "Runtime error occurred"
|
||||
"exit_code": 0,
|
||||
"execution_time_ms": 45
|
||||
}
|
||||
```
|
||||
|
||||
**Response Fields**:
|
||||
- `success` (boolean): True if execution completed without errors
|
||||
- `stdout` (string): Standard output from the program
|
||||
- `stderr` (string): Standard error output
|
||||
- `exit_code` (integer): Program exit status (0 = success, non-zero = error)
|
||||
- `error` (string, optional): Detailed error message if execution failed
|
||||
- `detected_language` (string, optional): Language detected by auto-detect endpoint
|
||||
|
||||
#### 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).
|
||||
- `job_id` (string): Unique job identifier
|
||||
- `status` (string): "pending" | "running" | "completed" | "failed"
|
||||
- `stdout` (string): Standard output (when completed)
|
||||
- `stderr` (string): Standard error output (when completed)
|
||||
- `exit_code` (integer): Program exit status (when completed)
|
||||
- `execution_time_ms` (integer): Execution duration in milliseconds (when completed)
|
||||
|
||||
#### Supported Languages
|
||||
|
||||
40+ languages including:
|
||||
The SDK supports 42+ languages including:
|
||||
- **Compiled**: C, C++, Rust, Go, Java, C#, Swift
|
||||
- **Interpreted**: Python, Ruby, JavaScript, PHP, Perl, Lua
|
||||
- **Scripting**: Bash, PowerShell, Fish
|
||||
- **Data**: R, Julia, Octave, MATLAB
|
||||
- **Data**: R, Julia, Octave
|
||||
- **Functional**: Haskell, Scala, Erlang, Elixir
|
||||
- **Esoteric**: Brainfuck, LOLCODE
|
||||
- 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
|
||||
|
||||
- Add play button (▶) next to copy button on code blocks
|
||||
|
|
|
|||
143
app.py
143
app.py
|
|
@ -15,6 +15,9 @@ import random
|
|||
|
||||
import boto3
|
||||
import together
|
||||
|
||||
# Unsandbox SDK for code execution
|
||||
import un
|
||||
from flask import (
|
||||
Flask,
|
||||
render_template,
|
||||
|
|
@ -1243,47 +1246,7 @@ Examples:
|
|||
|
||||
|
||||
# Unsandbox API proxy endpoints - keeps API keys server-side
|
||||
UNSANDBOX_API_URL = "https://api.unsandbox.com"
|
||||
|
||||
|
||||
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,
|
||||
}
|
||||
# Uses official Python SDK (un.py) for authentication and API calls
|
||||
|
||||
|
||||
@app.route("/api/code/execute", methods=["POST"])
|
||||
|
|
@ -1291,34 +1254,43 @@ def proxy_code_execute():
|
|||
"""Proxy code execution requests to Unsandbox API.
|
||||
|
||||
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:
|
||||
data = request.get_json()
|
||||
if not data:
|
||||
return jsonify({"error": "Request body required"}), 400
|
||||
|
||||
body_json = json.dumps(data, separators=(",", ":"))
|
||||
auth_headers = get_unsandbox_auth_headers("POST", "/execute/async", body_json)
|
||||
if not auth_headers:
|
||||
# Check if credentials are configured
|
||||
if not os.environ.get("UNSANDBOX_PUBLIC_KEY") or not os.environ.get("UNSANDBOX_SECRET_KEY"):
|
||||
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:
|
||||
response = client.post(
|
||||
f"{UNSANDBOX_API_URL}/execute/async",
|
||||
headers=headers,
|
||||
content=body_json,
|
||||
)
|
||||
if not language or not code:
|
||||
return jsonify({"error": "Language and code are required"}), 400
|
||||
|
||||
# Return the response from Unsandbox
|
||||
return jsonify(response.json()), response.status_code
|
||||
# Use SDK's execute_async method
|
||||
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:
|
||||
print(f"Error proxying code execution: {e}")
|
||||
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"])
|
||||
def proxy_job_status(job_id):
|
||||
"""Proxy job status requests to Unsandbox API."""
|
||||
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
|
||||
|
||||
"""Proxy job status requests to Unsandbox API using SDK."""
|
||||
try:
|
||||
with httpx.Client(timeout=30.0) as client:
|
||||
response = client.get(
|
||||
f"{UNSANDBOX_API_URL}{path}",
|
||||
headers=auth_headers,
|
||||
)
|
||||
# Check if credentials are configured
|
||||
if not os.environ.get("UNSANDBOX_PUBLIC_KEY") or not os.environ.get("UNSANDBOX_SECRET_KEY"):
|
||||
return jsonify({"error": "Code execution not configured"}), 503
|
||||
|
||||
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:
|
||||
print(f"Error fetching job status: {e}")
|
||||
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"])
|
||||
def proxy_job_cancel(job_id):
|
||||
"""Proxy job cancellation requests to Unsandbox API."""
|
||||
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
|
||||
|
||||
"""Proxy job cancellation requests to Unsandbox API using SDK."""
|
||||
try:
|
||||
with httpx.Client(timeout=30.0) as client:
|
||||
response = client.delete(
|
||||
f"{UNSANDBOX_API_URL}{path}",
|
||||
headers=auth_headers,
|
||||
)
|
||||
# Check if credentials are configured
|
||||
if not os.environ.get("UNSANDBOX_PUBLIC_KEY") or not os.environ.get("UNSANDBOX_SECRET_KEY"):
|
||||
return jsonify({"error": "Code execution not configured"}), 503
|
||||
|
||||
# DELETE may return empty body on success
|
||||
if response.status_code == 204:
|
||||
return "", 204
|
||||
# Use SDK's cancel_job method
|
||||
result = un.cancel_job(job_id)
|
||||
|
||||
try:
|
||||
return jsonify(response.json()), response.status_code
|
||||
except Exception:
|
||||
return "", response.status_code
|
||||
# SDK returns success status
|
||||
return jsonify(result), 200
|
||||
|
||||
except httpx.TimeoutException:
|
||||
return jsonify({"error": "Request to code execution service timed out"}), 504
|
||||
except Exception as e:
|
||||
print(f"Error cancelling job: {e}")
|
||||
return jsonify({"error": "Failed to cancel job"}), 500
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue