From 5576179ffac55d8da117a9d5c3e775bc944ea314 Mon Sep 17 00:00:00 2001 From: "russell@unturf.com" Date: Tue, 20 Jan 2026 08:34:35 -0500 Subject: [PATCH] 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/ 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
 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)
---
 CLAUDE.md           |  25 +++++-
 app.py              | 112 +++++++++++++++++++++----
 templates/chat.html | 193 +++++++++++++++++++++++++++++++++++++++++++-
 3 files changed, 310 insertions(+), 20 deletions(-)

diff --git a/CLAUDE.md b/CLAUDE.md
index 60f8ef4..733eb26 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -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/`
+- 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
diff --git a/app.py b/app.py
index f1b7a49..05d9277 100644
--- a/app.py
+++ b/app.py
@@ -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/", 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.
diff --git a/templates/chat.html b/templates/chat.html
index 55d88a9..ad4cb50 100644
--- a/templates/chat.html
+++ b/templates/chat.html
@@ -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 
, button container is the next sibling of 
     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');