Add comprehensive artifact support for code execution

Implement full artifact support for Unsandbox code execution, enabling
download and inline viewing of generated files (binaries, images, videos).

Backend changes (app.py):
- Update /api/code/execute to accept artifacts parameter
- Add /api/code/artifacts/<path> proxy endpoint for authenticated downloads
- Use SDK's _make_request for full parameter support
- Support artifacts in execution request body

Frontend changes (templates/chat.html):
- Pass artifacts=true in all code execution requests
- Display artifacts section with file info (name, type, size)
- Add download button for all artifact types
- Add view button with inline display for images/videos (disabled for binaries)
- Implement formatFileSize, downloadArtifact, viewArtifact helper functions
- Images and videos render directly in chat
- Text files display in formatted <pre> blocks

Documentation (CLAUDE.md):
- Document artifact support section
- List supported artifact types
- Describe frontend and backend features
- Update frontend integration notes

Artifact types supported:
- Compiled binaries (executables from C, C++, Rust, Go, etc.)
- Images (PNG, JPG, GIF, SVG)
- Videos (MP4, WebM)
- Text/data files (JSON, CSV, TXT)
This commit is contained in:
russell@unturf.com 2026-01-20 08:34:35 -05:00
parent 27a0df3d12
commit 5576179ffa
3 changed files with 310 additions and 20 deletions

View file

@ -227,12 +227,35 @@ languages = un.get_languages()
lang = un.detect_language("script.py") # Returns "python"
```
#### Artifact Support
OpenCompletion supports artifacts generated during code execution (compiled binaries, images, videos, etc.).
**Backend Implementation**:
- Pass `artifacts: true` in execution requests to enable artifact collection
- Artifacts proxy endpoint: `/api/code/artifacts/<encoded_url>`
- Handles authenticated download/viewing of artifacts
**Artifact Types**:
- **Binaries**: Compiled executables (C, C++, Rust, Go, etc.)
- **Images**: PNG, JPG, GIF, SVG generated by code
- **Videos**: MP4, WebM, etc. generated by code
- **Text/Data**: JSON, CSV, TXT output files
**Frontend Features**:
- Download button for all artifact types
- View button for images/videos (disabled for binaries)
- Inline display of images/videos in chat
- File size and type information
#### Frontend Integration
- Add play button (▶) next to copy button on code blocks
- Execute code when user clicks play button
- Execute code when user clicks play button with `artifacts: true` parameter
- Display execution results inline below code block
- Show stdout, stderr, and exit_code separately
- Display artifacts with download/view buttons
- Inline viewing of images and videos
- Use syntax highlighting for output
- Handle timeouts gracefully (60s default)
- Support language auto-detection for fenced code blocks

112
app.py
View file

@ -1255,6 +1255,7 @@ def proxy_code_execute():
Keeps UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY secure on the server side.
Uses official Unsandbox Python SDK for authentication and execution.
Supports artifacts parameter for compiled binaries, images, etc.
"""
try:
data = request.get_json()
@ -1262,34 +1263,45 @@ def proxy_code_execute():
return jsonify({"error": "Request body required"}), 400
# Check if credentials are configured
if not os.environ.get("UNSANDBOX_PUBLIC_KEY") or not os.environ.get("UNSANDBOX_SECRET_KEY"):
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 jsonify({"error": "Code execution not configured"}), 503
# 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)
if not language or not code:
return jsonify({"error": "Language and code are required"}), 400
# Use SDK's execute_async method
result = un.execute_async(
language=language,
code=code,
env=env,
network_mode=network_mode,
ttl=ttl
# Build request body with all supported parameters
request_body = {
"language": language,
"code": code
}
# Add optional parameters if provided
if data.get("env"):
request_body["env"] = data.get("env")
if data.get("network_mode"):
request_body["network_mode"] = data.get("network_mode")
if data.get("ttl"):
request_body["ttl"] = data.get("ttl")
if data.get("artifacts"):
request_body["artifacts"] = data.get("artifacts")
# Use SDK's internal _make_request for full parameter support
result = un._make_request(
"POST",
"/execute",
public_key,
secret_key,
request_body
)
# 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
# Return job_id from response
return jsonify({"job_id": result.get("job_id")}), 200
except Exception as e:
print(f"Error proxying code execution: {e}")
@ -1334,6 +1346,72 @@ def proxy_job_cancel(job_id):
return jsonify({"error": "Failed to cancel job"}), 500
@app.route("/api/code/artifacts/<path:artifact_url>", methods=["GET"])
def proxy_artifact_download(artifact_url):
"""Proxy artifact download requests from Unsandbox API.
Artifacts are files generated during code execution (compiled binaries,
images, videos, etc.). This endpoint proxies the download with authentication.
"""
import httpx
try:
# Check if credentials are configured
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 jsonify({"error": "Code execution not configured"}), 503
# Decode the artifact URL (it's passed as part of the path)
import urllib.parse
full_artifact_url = urllib.parse.unquote(artifact_url)
# If it's a relative path, make it absolute
if not full_artifact_url.startswith("http"):
full_artifact_url = f"https://api.unsandbox.com{full_artifact_url}"
# Make authenticated request to download artifact
with httpx.Client(timeout=60.0) as client:
# Extract path for signing
from urllib.parse import urlparse
parsed = urlparse(full_artifact_url)
path = parsed.path
# Sign the request
import time
import hmac
import hashlib
timestamp = int(time.time())
message = f"{timestamp}:GET:{path}:"
signature = hmac.new(
secret_key.encode(),
message.encode(),
hashlib.sha256
).hexdigest()
headers = {
"Authorization": f"Bearer {public_key}",
"X-Timestamp": str(timestamp),
"X-Signature": signature
}
response = client.get(full_artifact_url, headers=headers)
response.raise_for_status()
# Return the artifact with appropriate content type
return Response(
response.content,
mimetype=response.headers.get("Content-Type", "application/octet-stream"),
headers={
"Content-Disposition": response.headers.get("Content-Disposition", "attachment")
}
)
except Exception as e:
print(f"Error downloading artifact: {e}")
return jsonify({"error": "Failed to download artifact"}), 500
@app.route("/api/fix-code", methods=["POST"])
def fix_code():
"""Auto-fix code errors by asking AI to fix issues based on stderr output.

View file

@ -1708,7 +1708,7 @@ async function executeCodeBlock(code, blockElement, playButton) {
body: JSON.stringify({
language: language,
code: code,
return_artifact: true // Request compiled binary for compiled languages
artifacts: true // Request artifacts (binaries, images, videos, etc)
})
});
@ -1908,10 +1908,11 @@ function displayExecutionResults(result, resultsContainer, language, code) {
// Format and display results
let outputHtml = '';
// Unsandbox API returns flat structure: {success, stdout, stderr, exit_code}
// Unsandbox API returns flat structure: {success, stdout, stderr, exit_code, artifacts}
const actualStdout = result.stdout || '';
const actualStderr = result.stderr || '';
const exitCode = result.exit_code;
const artifacts = result.artifacts || [];
// Show language
if (language) {
@ -1943,6 +1944,87 @@ function displayExecutionResults(result, resultsContainer, language, code) {
resultsContainer.innerHTML = outputHtml;
// Handle artifacts (binaries, images, videos, etc.)
if (artifacts && artifacts.length > 0) {
const artifactsDiv = document.createElement('div');
artifactsDiv.style.marginTop = '12px';
artifactsDiv.style.borderTop = '1px solid var(--border-color)';
artifactsDiv.style.paddingTop = '12px';
const artifactsTitle = document.createElement('div');
artifactsTitle.style.color = 'var(--text-info)';
artifactsTitle.style.fontWeight = 'bold';
artifactsTitle.style.marginBottom = '8px';
artifactsTitle.textContent = `Artifacts (${artifacts.length}):`;
artifactsDiv.appendChild(artifactsTitle);
artifacts.forEach((artifact, index) => {
const artifactItem = document.createElement('div');
artifactItem.style.marginBottom = '8px';
artifactItem.style.padding = '8px';
artifactItem.style.backgroundColor = 'var(--bg-secondary)';
artifactItem.style.borderRadius = '4px';
// Artifact name/filename
const artifactName = document.createElement('div');
artifactName.style.fontWeight = 'bold';
artifactName.style.marginBottom = '4px';
artifactName.textContent = artifact.name || artifact.filename || `Artifact ${index + 1}`;
artifactItem.appendChild(artifactName);
// Artifact type/size info
if (artifact.type || artifact.size) {
const artifactInfo = document.createElement('div');
artifactInfo.style.fontSize = '12px';
artifactInfo.style.color = 'var(--text-muted)';
artifactInfo.style.marginBottom = '8px';
let infoText = '';
if (artifact.type) infoText += `Type: ${artifact.type}`;
if (artifact.size) infoText += ` | Size: ${formatFileSize(artifact.size)}`;
artifactInfo.textContent = infoText;
artifactItem.appendChild(artifactInfo);
}
// Buttons container
const buttonsDiv = document.createElement('div');
buttonsDiv.style.display = 'flex';
buttonsDiv.style.gap = '8px';
// Determine if artifact is viewable (images, videos, text)
const mimeType = artifact.mime_type || artifact.type || '';
const isImage = mimeType.startsWith('image/');
const isVideo = mimeType.startsWith('video/');
const isBinary = mimeType.includes('octet-stream') || mimeType.includes('executable');
// Download button (always available)
const downloadBtn = document.createElement('button');
downloadBtn.textContent = '⬇ Download';
downloadBtn.style.padding = '4px 8px';
downloadBtn.style.fontSize = '12px';
downloadBtn.onclick = () => downloadArtifact(artifact);
buttonsDiv.appendChild(downloadBtn);
// View button (disabled for binaries)
const viewBtn = document.createElement('button');
viewBtn.textContent = '👁 View';
viewBtn.style.padding = '4px 8px';
viewBtn.style.fontSize = '12px';
if (isBinary) {
viewBtn.disabled = true;
viewBtn.style.opacity = '0.5';
viewBtn.style.cursor = 'not-allowed';
} else {
viewBtn.onclick = () => viewArtifact(artifact, artifactItem, isImage, isVideo);
}
buttonsDiv.appendChild(viewBtn);
artifactItem.appendChild(buttonsDiv);
artifactsDiv.appendChild(artifactItem);
});
resultsContainer.appendChild(artifactsDiv);
}
// Find the download binary button (it's in the button container next to the Run button)
// resultsContainer is inside <pre>, button container is the next sibling of <pre>
const preElement = resultsContainer.parentNode;
@ -2025,6 +2107,113 @@ function displayExecutionResults(result, resultsContainer, language, code) {
}
}
// Helper function to format file size
function formatFileSize(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i];
}
// Helper function to download artifact
async function downloadArtifact(artifact) {
try {
const artifactUrl = artifact.url || artifact.download_url;
if (!artifactUrl) {
alert('Artifact URL not available');
return;
}
// Use our backend proxy to download with authentication
const encodedUrl = encodeURIComponent(artifactUrl);
const proxyUrl = `/api/code/artifacts/${encodedUrl}`;
// Open in new tab to trigger download
window.open(proxyUrl, '_blank');
} catch (error) {
console.error('Error downloading artifact:', error);
alert('Failed to download artifact: ' + error.message);
}
}
// Helper function to view artifact inline
async function viewArtifact(artifact, artifactItem, isImage, isVideo) {
try {
const artifactUrl = artifact.url || artifact.download_url;
if (!artifactUrl) {
alert('Artifact URL not available');
return;
}
// Check if already viewing
const existingViewer = artifactItem.querySelector('.artifact-viewer');
if (existingViewer) {
existingViewer.remove();
return;
}
// Create viewer container
const viewerDiv = document.createElement('div');
viewerDiv.classList.add('artifact-viewer');
viewerDiv.style.marginTop = '8px';
viewerDiv.style.padding = '8px';
viewerDiv.style.backgroundColor = 'var(--bg-primary)';
viewerDiv.style.borderRadius = '4px';
viewerDiv.style.maxWidth = '100%';
viewerDiv.style.overflow = 'auto';
// Use our backend proxy for authenticated access
const encodedUrl = encodeURIComponent(artifactUrl);
const proxyUrl = `/api/code/artifacts/${encodedUrl}`;
if (isImage) {
const img = document.createElement('img');
img.src = proxyUrl;
img.style.maxWidth = '100%';
img.style.height = 'auto';
img.style.display = 'block';
img.onerror = () => {
viewerDiv.textContent = 'Failed to load image';
viewerDiv.style.color = 'var(--text-error)';
};
viewerDiv.appendChild(img);
} else if (isVideo) {
const video = document.createElement('video');
video.src = proxyUrl;
video.controls = true;
video.style.maxWidth = '100%';
video.style.height = 'auto';
video.style.display = 'block';
video.onerror = () => {
viewerDiv.textContent = 'Failed to load video';
viewerDiv.style.color = 'var(--text-error)';
};
viewerDiv.appendChild(video);
} else {
// For other types, try to fetch and display as text
const response = await fetch(proxyUrl);
if (response.ok) {
const text = await response.text();
const pre = document.createElement('pre');
pre.style.margin = '0';
pre.style.whiteSpace = 'pre-wrap';
pre.style.wordWrap = 'break-word';
pre.textContent = text;
viewerDiv.appendChild(pre);
} else {
viewerDiv.textContent = 'Failed to load content';
viewerDiv.style.color = 'var(--text-error)';
}
}
artifactItem.appendChild(viewerDiv);
} catch (error) {
console.error('Error viewing artifact:', error);
alert('Failed to view artifact: ' + error.message);
}
}
// Helper function to escape HTML
function escapeHtml(text) {
const div = document.createElement('div');