UNPKG

@badc0d3/piece-python-runner

Version:

Run Python code with imports in Activepieces

197 lines (192 loc) 8.21 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.runPythonCodeSandboxed = void 0; const pieces_framework_1 = require("@activepieces/pieces-framework"); const dockerode_1 = __importDefault(require("dockerode")); exports.runPythonCodeSandboxed = (0, pieces_framework_1.createAction)({ name: 'run-python-code-sandboxed', displayName: 'Run Python Code (Sandboxed)', description: 'Execute Python code in a Docker container for enhanced security. Requires Docker socket access (mount with -v /var/run/docker.sock:/var/run/docker.sock)', props: { code: pieces_framework_1.Property.LongText({ displayName: 'Python Code', description: 'The Python code to execute', required: true, defaultValue: `# Example with external package import requests import json # Get external IP response = requests.get('https://api.ipify.org?format=json') data = response.json() print(json.dumps(data, indent=2))`, }), requirements: pieces_framework_1.Property.LongText({ displayName: 'Requirements', description: 'Python packages to install (one per line)', required: false, defaultValue: 'requests', }), timeout: pieces_framework_1.Property.Number({ displayName: 'Timeout (seconds)', description: 'Maximum execution time in seconds', required: false, defaultValue: 30, }), dockerImage: pieces_framework_1.Property.ShortText({ displayName: 'Docker Image', description: 'Docker image to use for running Python code (e.g., python:3.11-slim, python:3.9-alpine)', required: false, defaultValue: 'python:slim', }), }, async run(context) { const { code, requirements, timeout, dockerImage } = context.propsValue; const docker = new dockerode_1.default(); const imageName = dockerImage || 'python:slim'; try { // Check if image exists locally console.log(`Checking for Docker image ${imageName}...`); try { await docker.getImage(imageName).inspect(); console.log(`Image ${imageName} found locally`); } catch (error) { // Image doesn't exist locally, pull it console.log(`Image ${imageName} not found locally, pulling...`); const stream = await docker.pull(imageName); // Wait for pull to complete and show progress await new Promise((resolve, reject) => { docker.modem.followProgress(stream, (err, res) => { if (err) { reject(err); } else { console.log(`Successfully pulled ${imageName}`); resolve(res); } }, (event) => { // Log pull progress if (event.status) { console.log(`${event.status}${event.progress ? ': ' + event.progress : ''}`); } }); }); } // Create container with the code let cmd; if (requirements && requirements.trim()) { // If requirements are specified, install them silently first then run the code const requirementsList = requirements.trim().split('\n').filter((r) => r.trim()).join(' '); // Base64 encode the code to avoid shell escaping issues const encodedCode = Buffer.from(code).toString('base64'); // Create a script that sets up a non-root user and runs the code const setupScript = ` # Create a non-root user (Debian/Ubuntu syntax) useradd -m -d /home/pyuser -s /bin/bash pyuser 2>/dev/null || true # First ensure pip is installed (suppress output) python3 -m ensurepip --upgrade >/dev/null 2>&1 || true # Install packages as root (suppress all output) python3 -m pip install --quiet --no-cache-dir --root-user-action=ignore ${requirementsList} >/dev/null 2>&1 # Switch to non-root user and run the Python code su - pyuser -c " cd /home/pyuser export PATH=/usr/local/bin:/usr/bin:/bin:/home/pyuser/.local/bin:\$PATH export PYTHONPATH=/usr/local/lib/python3.13/site-packages:/usr/local/lib/python3.12/site-packages:/usr/local/lib/python3.11/site-packages:/usr/local/lib/python3.10/site-packages:\$PYTHONPATH echo '${encodedCode}' | base64 -d | python3 " `; cmd = ['sh', '-c', setupScript]; } else { // No requirements, create user and run the code const encodedCode = Buffer.from(code).toString('base64'); const setupScript = ` # Create a non-root user (Debian/Ubuntu syntax) useradd -m -d /home/pyuser -s /bin/bash pyuser 2>/dev/null || true # Switch to non-root user and run the Python code su - pyuser -c " export PATH=/usr/local/bin:/usr/bin:/bin:/home/pyuser/.local/bin:\$PATH echo '${encodedCode}' | base64 -d | python3 " `; cmd = ['sh', '-c', setupScript]; } const container = await docker.createContainer({ Image: imageName, Cmd: cmd, WorkingDir: '/home/pyuser', HostConfig: { AutoRemove: true, Memory: 512 * 1024 * 1024, // 512MB CpuQuota: 50000, // 50% CPU }, AttachStdout: true, AttachStderr: true, }); // Start container const stream = await container.attach({ stream: true, stdout: true, stderr: true }); await container.start(); // Collect output let stdout = ''; let stderr = ''; stream.on('data', (chunk) => { const data = chunk.toString(); if (chunk[0] === 1) { stdout += data.slice(8); } else if (chunk[0] === 2) { stderr += data.slice(8); } }); // Set a timeout for container execution const executionTimeout = (timeout || 30) * 1000; const timeoutHandle = setTimeout(async () => { try { await container.kill(); } catch (e) { // Container might have already stopped } }, executionTimeout); // Wait for container to finish try { await container.wait(); } finally { clearTimeout(timeoutHandle); } // Parse output let parsedOutput; try { parsedOutput = JSON.parse(stdout.trim()); } catch { parsedOutput = stdout.trim(); } return { success: true, output: parsedOutput, stdout, stderr, executionTime: new Date().toISOString(), }; } catch (error) { console.error('Docker execution error:', error); // Provide more specific error messages if (error.message?.includes('connect ENOENT')) { throw new Error('Docker socket not found. Please ensure Docker is running and the socket is mounted with -v /var/run/docker.sock:/var/run/docker.sock'); } else if (error.statusCode === 404) { throw new Error(`Docker image ${imageName} not found and could not be pulled. Please check your internet connection.`); } else { throw error; } } }, }); //# sourceMappingURL=run-python-code-sandboxed.js.map