diff --git a/templates/unsandbox-executor/README.md b/templates/unsandbox-executor/README.md new file mode 100644 index 0000000..2099b94 --- /dev/null +++ b/templates/unsandbox-executor/README.md @@ -0,0 +1,239 @@ +# TPMJS Executor for Unsandbox + +Deploy your own TPMJS tool executor using **Unsandbox** for secure, isolated code execution. + +## Features + +- **Secure Execution**: Tools run in isolated Unsandbox containers +- **Full Control**: Your infrastructure, your environment variables +- **Privacy**: No data passes through TPMJS servers +- **One-Command Deploy**: Deploy with a single CLI command +- **Always-On**: Services stay running with automatic HTTPS + +## One-Command Deploy + +```bash +# Install the Unsandbox CLI (if not already installed) +curl -fsSL https://unsandbox.com/install.sh | bash + +# Deploy the TPMJS executor +un service --name tpmjs-executor --ports 80 -n semitrusted \ + --bootstrap "curl -fsSL https://raw.githubusercontent.com/tpmjs/tpmjs/main/templates/unsandbox-executor/executor.js -o /root/executor.js && node /root/executor.js" +``` + +Your executor will be available at: `https://tpmjs-executor.on.unsandbox.com` + +## How It Works + +This executor runs directly in an Unsandbox container to: + +1. Receive tool execution requests via HTTP +2. Install the npm package in an isolated directory +3. Execute the tool with your parameters +4. Return the result and cleanup + +Since Unsandbox containers are already isolated, we don't need an additional sandbox layer like Vercel Sandbox. + +## API Endpoints + +### GET /api/health + +Check executor health status. + +```bash +curl https://tpmjs-executor.on.unsandbox.com/api/health +``` + +**Response:** +```json +{ + "status": "ok", + "version": "1.0.0", + "info": { + "runtime": "unsandbox", + "timestamp": "2024-01-01T00:00:00.000Z" + } +} +``` + +### POST /api/execute-tool + +Execute a TPMJS tool. + +```bash +curl -X POST https://tpmjs-executor.on.unsandbox.com/api/execute-tool \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer your-api-key" \ + -d '{ + "packageName": "@tpmjs/hello", + "name": "helloWorldTool", + "version": "latest", + "params": { "includeTimestamp": true } + }' +``` + +**Response:** +```json +{ + "success": true, + "output": { + "message": "Hello, World!", + "timestamp": "2024-01-01T00:00:00.000Z" + }, + "executionTimeMs": 2345 +} +``` + +## Configuration + +### Environment Variables + +| Variable | Required | Description | +|----------|----------|-------------| +| `EXECUTOR_API_KEY` | No | API key for authentication. If set, requests must include `Authorization: Bearer ` header. | + +### Setting Up API Key Authentication + +Deploy with an API key for secure access: + +```bash +un service --name tpmjs-executor --ports 80 -n semitrusted \ + -e EXECUTOR_API_KEY=your-secure-random-key \ + --bootstrap "curl -fsSL https://raw.githubusercontent.com/tpmjs/tpmjs/main/templates/unsandbox-executor/executor.js -o /root/executor.js && node /root/executor.js" +``` + +### Adding Tool Environment Variables + +Pass environment variables that your tools need: + +```bash +un service --name tpmjs-executor --ports 80 -n semitrusted \ + -e EXECUTOR_API_KEY=your-key \ + -e OPENAI_API_KEY=sk-xxx \ + -e DATABASE_URL=postgres://... \ + --bootstrap "curl -fsSL https://raw.githubusercontent.com/tpmjs/tpmjs/main/templates/unsandbox-executor/executor.js -o /root/executor.js && node /root/executor.js" +``` + +Or use an env file: + +```bash +un service --name tpmjs-executor --ports 80 -n semitrusted \ + --env-file .env \ + --bootstrap "curl -fsSL https://raw.githubusercontent.com/tpmjs/tpmjs/main/templates/unsandbox-executor/executor.js -o /root/executor.js && node /root/executor.js" +``` + +## Connecting to TPMJS + +1. Go to your TPMJS collection or agent settings +2. In "Executor Configuration", select "Custom Executor" +3. Enter your executor URL: `https://tpmjs-executor.on.unsandbox.com` +4. Enter your API key (if configured) +5. Click "Verify Connection" to test + +## Local Development + +You can run the executor locally for testing: + +```bash +# Clone the repository +git clone https://github.com/tpmjs/tpmjs.git +cd tpmjs/templates/unsandbox-executor + +# Run the executor locally +PORT=3000 node executor.js + +# Test the health endpoint +curl http://localhost:3000/api/health + +# Test tool execution +curl -X POST http://localhost:3000/api/execute-tool \ + -H "Content-Type: application/json" \ + -d '{ + "packageName": "@tpmjs/hello", + "name": "helloWorldTool", + "params": {} + }' +``` + +## Managing Your Service + +### View Logs + +```bash +un service --logs tpmjs-executor +``` + +### Redeploy + +```bash +un service --redeploy tpmjs-executor +``` + +### Freeze/Unfreeze (Save Costs) + +```bash +# Freeze when not in use +un service --freeze tpmjs-executor + +# Unfreeze when needed +un service --unfreeze tpmjs-executor +``` + +### Scale Resources + +```bash +# Scale up to 4 vCPU, 8GB RAM +un service --resize tpmjs-executor --vcpu 4 +``` + +### Destroy + +```bash +un service --destroy tpmjs-executor +``` + +## Security + +- Set `EXECUTOR_API_KEY` to require authentication for all requests +- Tools run in isolated Unsandbox containers +- Each execution uses a fresh temporary directory +- Network access is controlled by Unsandbox's semitrusted mode +- All environment variables are stored encrypted + +## Pricing + +Unsandbox services are billed based on uptime. See [Unsandbox Pricing](https://unsandbox.com/pricing) for details. + +- Services include automatic HTTPS via `*.on.unsandbox.com` +- Can be frozen when not in use to reduce costs +- Support for auto-unfreeze on HTTP request + +## Advanced: Custom Domains + +Add a custom domain to your executor: + +```bash +un service --name tpmjs-executor --ports 80 -n semitrusted \ + --domains executor.yourdomain.com \ + --bootstrap "curl -fsSL https://raw.githubusercontent.com/tpmjs/tpmjs/main/templates/unsandbox-executor/executor.js -o /root/executor.js && node /root/executor.js" +``` + +Then add a CNAME record pointing `executor.yourdomain.com` to your service's Unsandbox domain. + +## Comparison: Unsandbox vs Vercel + +| Feature | Unsandbox | Vercel | +|---------|-----------|--------| +| Isolation | Container-level | VM-level (Sandbox) | +| Always-on | Yes | Serverless (cold starts) | +| Pricing | Per uptime | Per compute time | +| Max runtime | Unlimited | 45min (Hobby) / 5hr (Pro) | +| Network | Full (semitrusted) | Full | +| Custom domains | Yes | Yes | +| Deploy method | CLI | One-click button | + +## Support + +- [TPMJS Custom Executors Documentation](https://tpmjs.com/docs/executors) +- [Unsandbox Documentation](https://unsandbox.com/docs) +- [GitHub Issues](https://github.com/tpmjs/tpmjs/issues) diff --git a/templates/unsandbox-executor/bootstrap-standalone.sh b/templates/unsandbox-executor/bootstrap-standalone.sh new file mode 100644 index 0000000..985bca3 --- /dev/null +++ b/templates/unsandbox-executor/bootstrap-standalone.sh @@ -0,0 +1,297 @@ +#!/bin/bash +# TPMJS Executor Standalone Bootstrap Script for Unsandbox +# This script contains the embedded executor - no network required during bootstrap +set -e + +echo "=== TPMJS Executor for Unsandbox ===" +echo "Starting deployment..." + +# Embedded executor script +cat > /root/executor.js << 'EXECUTOR_EOF' +#!/usr/bin/env node +/** + * TPMJS Executor for Unsandbox + * API-compatible with the Vercel executor. + */ + +const http = require('http'); +const { execSync, spawn } = require('child_process'); +const fs = require('fs'); +const path = require('path'); + +const PORT = process.env.PORT || 80; +const API_KEY = process.env.EXECUTOR_API_KEY || null; + +const corsHeaders = { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type, Authorization', +}; + +function jsonResponse(res, statusCode, data) { + res.writeHead(statusCode, { + 'Content-Type': 'application/json', + ...corsHeaders, + }); + res.end(JSON.stringify(data)); +} + +function checkAuth(req) { + if (!API_KEY) return true; + const authHeader = req.headers.authorization; + return authHeader === `Bearer ${API_KEY}`; +} + +function parseBody(req) { + return new Promise((resolve, reject) => { + let body = ''; + req.on('data', chunk => body += chunk); + req.on('end', () => { + try { + resolve(body ? JSON.parse(body) : {}); + } catch (e) { + reject(new Error('Invalid JSON')); + } + }); + req.on('error', reject); + }); +} + +function handleHealth(req, res) { + jsonResponse(res, 200, { + status: 'ok', + version: '1.0.0', + info: { + runtime: 'unsandbox', + timestamp: new Date().toISOString(), + }, + }); +} + +async function handleExecuteTool(req, res) { + const startTime = Date.now(); + + if (!checkAuth(req)) { + return jsonResponse(res, 401, { + success: false, + error: 'Unauthorized', + executionTimeMs: Date.now() - startTime, + }); + } + + let body; + try { + body = await parseBody(req); + } catch (e) { + return jsonResponse(res, 400, { + success: false, + error: 'Invalid JSON body', + executionTimeMs: Date.now() - startTime, + }); + } + + const { packageName, name, version = 'latest', params = {}, env } = body; + + if (!packageName || !name) { + return jsonResponse(res, 400, { + success: false, + error: 'Missing required fields: packageName, name', + executionTimeMs: Date.now() - startTime, + }); + } + + const packageSpec = `${packageName}@${version}`; + const workDir = `/tmp/tpmjs-exec-${Date.now()}-${Math.random().toString(36).slice(2)}`; + + try { + fs.mkdirSync(workDir, { recursive: true }); + + fs.writeFileSync(path.join(workDir, 'package.json'), JSON.stringify({ + name: 'tpmjs-execution', + private: true, + type: 'commonjs', + })); + + console.log(`[executor] Installing ${packageSpec}...`); + const installStart = Date.now(); + + try { + execSync(`npm install --no-save --omit=dev --no-audit --no-fund ${packageSpec}`, { + cwd: workDir, + stdio: ['pipe', 'pipe', 'pipe'], + timeout: 60000, + }); + } catch (installError) { + console.error(`[executor] npm install failed:`, installError.message); + return jsonResponse(res, 500, { + success: false, + error: `npm install failed: ${installError.message}`, + stderr: installError.stderr?.toString(), + executionTimeMs: Date.now() - startTime, + }); + } + + console.log(`[executor] npm install completed in ${Date.now() - installStart}ms`); + + const envSetup = env + ? Object.entries(env) + .map(([key, value]) => `process.env[${JSON.stringify(key)}] = ${JSON.stringify(value)};`) + .join('\n') + : ''; + + const script = ` +${envSetup} + +(async () => { + try { + const pkg = require(${JSON.stringify(packageName)}); + let tool = pkg[${JSON.stringify(name)}] || pkg.default?.[${JSON.stringify(name)}] || pkg.default; + + if (!tool) { + throw new Error(\`Tool "${name}" not found in package "${packageName}"\`); + } + + if (typeof tool === 'function' && !tool.execute) { + const envVars = ${env ? JSON.stringify(env) : 'null'}; + try { + const result = tool(); + if (result && typeof result.execute === 'function') { + tool = result; + } + } catch {} + if (typeof tool === 'function' && envVars) { + try { + const result = tool(envVars); + if (result && typeof result.execute === 'function') { + tool = result; + } + } catch {} + } + } + + if (!tool || typeof tool.execute !== 'function') { + throw new Error(\`Tool "${name}" does not have an execute() function\`); + } + + const result = await tool.execute(${JSON.stringify(params)}); + process.stdout.write(JSON.stringify({ __tpmjs_result__: result })); + } catch (err) { + process.stderr.write(JSON.stringify({ __tpmjs_error__: err.message || String(err) })); + process.exitCode = 1; + } +})(); +`.trim(); + + fs.writeFileSync(path.join(workDir, 'execute.cjs'), script); + + console.log(`[executor] Running tool ${packageName}/${name}...`); + const runStart = Date.now(); + + const result = await new Promise((resolve) => { + const child = spawn('node', ['execute.cjs'], { + cwd: workDir, + env: { ...process.env, ...env }, + timeout: 120000, + }); + + let stdout = ''; + let stderr = ''; + + child.stdout.on('data', (data) => stdout += data); + child.stderr.on('data', (data) => stderr += data); + + child.on('close', (code) => { + resolve({ exitCode: code, stdout, stderr }); + }); + + child.on('error', (err) => { + resolve({ exitCode: 1, stdout: '', stderr: err.message }); + }); + }); + + console.log(`[executor] Tool execution completed in ${Date.now() - runStart}ms (exit: ${result.exitCode})`); + + try { + fs.rmSync(workDir, { recursive: true, force: true }); + } catch {} + + if (result.exitCode !== 0) { + try { + const errorObj = JSON.parse(result.stderr); + if (errorObj.__tpmjs_error__) { + return jsonResponse(res, 200, { + success: false, + error: errorObj.__tpmjs_error__, + executionTimeMs: Date.now() - startTime, + }); + } + } catch {} + + return jsonResponse(res, 200, { + success: false, + error: result.stderr || `Script exited with code ${result.exitCode}`, + executionTimeMs: Date.now() - startTime, + }); + } + + try { + const parsed = JSON.parse(result.stdout); + if (parsed.__tpmjs_result__ !== undefined) { + return jsonResponse(res, 200, { + success: true, + output: parsed.__tpmjs_result__, + executionTimeMs: Date.now() - startTime, + }); + } + } catch {} + + return jsonResponse(res, 200, { + success: true, + output: result.stdout || null, + stderr: result.stderr || undefined, + executionTimeMs: Date.now() - startTime, + }); + + } catch (error) { + try { + fs.rmSync(workDir, { recursive: true, force: true }); + } catch {} + + return jsonResponse(res, 500, { + success: false, + error: error.message || String(error), + executionTimeMs: Date.now() - startTime, + }); + } +} + +const server = http.createServer(async (req, res) => { + const url = new URL(req.url, `http://localhost:${PORT}`); + const pathname = url.pathname; + + if (req.method === 'OPTIONS') { + res.writeHead(200, corsHeaders); + return res.end(); + } + + if ((pathname === '/api/health' || pathname === '/health') && req.method === 'GET') { + return handleHealth(req, res); + } + + if ((pathname === '/api/execute-tool' || pathname === '/execute-tool') && req.method === 'POST') { + return handleExecuteTool(req, res); + } + + jsonResponse(res, 404, { error: 'Not found' }); +}); + +server.listen(PORT, () => { + console.log(`TPMJS Executor running on port ${PORT}`); + if (API_KEY) { + console.log(`Authentication: Required`); + } +}); +EXECUTOR_EOF + +echo "Starting TPMJS Executor on port 80..." +exec node /root/executor.js diff --git a/templates/unsandbox-executor/bootstrap.sh b/templates/unsandbox-executor/bootstrap.sh new file mode 100644 index 0000000..796962f --- /dev/null +++ b/templates/unsandbox-executor/bootstrap.sh @@ -0,0 +1,16 @@ +#!/bin/bash +# TPMJS Executor Bootstrap Script for Unsandbox +# This script downloads and runs the TPMJS executor +set -e + +echo "=== TPMJS Executor for Unsandbox ===" +echo "Starting deployment..." + +# Download the executor script from GitHub +EXECUTOR_URL="https://raw.githubusercontent.com/tpmjs/tpmjs/main/templates/unsandbox-executor/executor.js" + +echo "Downloading executor from $EXECUTOR_URL..." +curl -fsSL "$EXECUTOR_URL" -o /root/executor.js + +echo "Starting TPMJS Executor on port 80..." +exec node /root/executor.js diff --git a/templates/unsandbox-executor/executor.js b/templates/unsandbox-executor/executor.js new file mode 100644 index 0000000..e65176d --- /dev/null +++ b/templates/unsandbox-executor/executor.js @@ -0,0 +1,351 @@ +#!/usr/bin/env node +/** + * TPMJS Executor for Unsandbox + * + * A lightweight HTTP server that executes TPMJS tools in isolated Unsandbox containers. + * Since Unsandbox IS the sandbox, we don't need an additional isolation layer. + * + * API-compatible with the Vercel executor. + */ + +const http = require('http'); +const { execSync, spawn } = require('child_process'); +const fs = require('fs'); +const path = require('path'); + +const PORT = process.env.PORT || 80; +const API_KEY = process.env.EXECUTOR_API_KEY || null; + +// CORS headers for cross-origin requests +const corsHeaders = { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type, Authorization', +}; + +/** + * Send JSON response with proper headers + */ +function jsonResponse(res, statusCode, data) { + res.writeHead(statusCode, { + 'Content-Type': 'application/json', + ...corsHeaders, + }); + res.end(JSON.stringify(data)); +} + +/** + * Validate API key if configured + */ +function checkAuth(req) { + if (!API_KEY) return true; + const authHeader = req.headers.authorization; + return authHeader === `Bearer ${API_KEY}`; +} + +/** + * Parse JSON request body + */ +function parseBody(req) { + return new Promise((resolve, reject) => { + let body = ''; + req.on('data', (chunk) => (body += chunk)); + req.on('end', () => { + try { + resolve(body ? JSON.parse(body) : {}); + } catch (e) { + reject(new Error('Invalid JSON')); + } + }); + req.on('error', reject); + }); +} + +/** + * GET /api/health - Health check endpoint + */ +function handleHealth(req, res) { + jsonResponse(res, 200, { + status: 'ok', + version: '1.0.0', + info: { + runtime: 'unsandbox', + timestamp: new Date().toISOString(), + }, + }); +} + +/** + * POST /api/execute-tool - Execute a TPMJS tool + * + * Request body: + * { + * packageName: string, // npm package name (e.g., "@tpmjs/hello") + * name: string, // tool export name (e.g., "helloWorldTool") + * version?: string, // package version (default: "latest") + * params: object, // parameters to pass to tool.execute() + * env?: object // environment variables for the tool + * } + */ +async function handleExecuteTool(req, res) { + const startTime = Date.now(); + + // Check authorization + if (!checkAuth(req)) { + return jsonResponse(res, 401, { + success: false, + error: 'Unauthorized', + executionTimeMs: Date.now() - startTime, + }); + } + + // Parse request body + let body; + try { + body = await parseBody(req); + } catch (e) { + return jsonResponse(res, 400, { + success: false, + error: 'Invalid JSON body', + executionTimeMs: Date.now() - startTime, + }); + } + + const { packageName, name, version = 'latest', params = {}, env } = body; + + // Validate required fields + if (!packageName || !name) { + return jsonResponse(res, 400, { + success: false, + error: 'Missing required fields: packageName, name', + executionTimeMs: Date.now() - startTime, + }); + } + + const packageSpec = `${packageName}@${version}`; + const workDir = `/tmp/tpmjs-exec-${Date.now()}-${Math.random().toString(36).slice(2)}`; + + try { + // Create isolated work directory + fs.mkdirSync(workDir, { recursive: true }); + + // Initialize package.json + fs.writeFileSync( + path.join(workDir, 'package.json'), + JSON.stringify({ + name: 'tpmjs-execution', + private: true, + type: 'commonjs', + }) + ); + + // Install the npm package + console.log(`[executor] Installing ${packageSpec}...`); + const installStart = Date.now(); + + try { + execSync(`npm install --no-save --omit=dev --no-audit --no-fund ${packageSpec}`, { + cwd: workDir, + stdio: ['pipe', 'pipe', 'pipe'], + timeout: 60000, // 60s timeout for install + }); + } catch (installError) { + console.error(`[executor] npm install failed:`, installError.message); + return jsonResponse(res, 500, { + success: false, + error: `npm install failed: ${installError.message}`, + stderr: installError.stderr?.toString(), + executionTimeMs: Date.now() - startTime, + }); + } + + console.log(`[executor] npm install completed in ${Date.now() - installStart}ms`); + + // Build environment variable setup code + const envSetup = env + ? Object.entries(env) + .map(([key, value]) => `process.env[${JSON.stringify(key)}] = ${JSON.stringify(value)};`) + .join('\n') + : ''; + + // Generate the execution script + // This script loads the tool and calls tool.execute(params) + const script = ` +${envSetup} + +(async () => { + try { + const pkg = require(${JSON.stringify(packageName)}); + + // Find the tool export - check named export, default.name, or default + let tool = pkg[${JSON.stringify(name)}] || pkg.default?.[${JSON.stringify(name)}] || pkg.default; + + if (!tool) { + throw new Error(\`Tool "${name}" not found in package "${packageName}"\`); + } + + // Handle factory functions (tools that need to be instantiated) + if (typeof tool === 'function' && !tool.execute) { + const envVars = ${env ? JSON.stringify(env) : 'null'}; + + // Try no-arg call first + try { + const result = tool(); + if (result && typeof result.execute === 'function') { + tool = result; + } + } catch {} + + // Try with env config if still a function + if (typeof tool === 'function' && envVars) { + try { + const result = tool(envVars); + if (result && typeof result.execute === 'function') { + tool = result; + } + } catch {} + } + } + + if (!tool || typeof tool.execute !== 'function') { + throw new Error(\`Tool "${name}" does not have an execute() function\`); + } + + // Execute the tool + const result = await tool.execute(${JSON.stringify(params)}); + process.stdout.write(JSON.stringify({ __tpmjs_result__: result })); + } catch (err) { + process.stderr.write(JSON.stringify({ __tpmjs_error__: err.message || String(err) })); + process.exitCode = 1; + } +})(); +`.trim(); + + fs.writeFileSync(path.join(workDir, 'execute.cjs'), script); + + // Run the execution script + console.log(`[executor] Running tool ${packageName}/${name}...`); + const runStart = Date.now(); + + const result = await new Promise((resolve) => { + const child = spawn('node', ['execute.cjs'], { + cwd: workDir, + env: { ...process.env, ...env }, + timeout: 120000, // 2 minute timeout + }); + + let stdout = ''; + let stderr = ''; + + child.stdout.on('data', (data) => (stdout += data)); + child.stderr.on('data', (data) => (stderr += data)); + + child.on('close', (code) => { + resolve({ exitCode: code, stdout, stderr }); + }); + + child.on('error', (err) => { + resolve({ exitCode: 1, stdout: '', stderr: err.message }); + }); + }); + + console.log( + `[executor] Tool execution completed in ${Date.now() - runStart}ms (exit: ${result.exitCode})` + ); + + // Cleanup work directory + try { + fs.rmSync(workDir, { recursive: true, force: true }); + } catch {} + + // Handle execution failure + if (result.exitCode !== 0) { + // Try to parse structured error from stderr + try { + const errorObj = JSON.parse(result.stderr); + if (errorObj.__tpmjs_error__) { + return jsonResponse(res, 200, { + success: false, + error: errorObj.__tpmjs_error__, + executionTimeMs: Date.now() - startTime, + }); + } + } catch {} + + return jsonResponse(res, 200, { + success: false, + error: result.stderr || `Script exited with code ${result.exitCode}`, + executionTimeMs: Date.now() - startTime, + }); + } + + // Parse the result + try { + const parsed = JSON.parse(result.stdout); + if (parsed.__tpmjs_result__ !== undefined) { + return jsonResponse(res, 200, { + success: true, + output: parsed.__tpmjs_result__, + executionTimeMs: Date.now() - startTime, + }); + } + } catch {} + + // If we couldn't parse structured output, return raw + return jsonResponse(res, 200, { + success: true, + output: result.stdout || null, + stderr: result.stderr || undefined, + executionTimeMs: Date.now() - startTime, + }); + } catch (error) { + // Cleanup on error + try { + fs.rmSync(workDir, { recursive: true, force: true }); + } catch {} + + return jsonResponse(res, 500, { + success: false, + error: error.message || String(error), + executionTimeMs: Date.now() - startTime, + }); + } +} + +/** + * Main HTTP server + */ +const server = http.createServer(async (req, res) => { + const url = new URL(req.url, `http://localhost:${PORT}`); + const pathname = url.pathname; + + // Handle CORS preflight + if (req.method === 'OPTIONS') { + res.writeHead(200, corsHeaders); + return res.end(); + } + + // Route requests (support both /api/path and /path) + if ((pathname === '/api/health' || pathname === '/health') && req.method === 'GET') { + return handleHealth(req, res); + } + + if ((pathname === '/api/execute-tool' || pathname === '/execute-tool') && req.method === 'POST') { + return handleExecuteTool(req, res); + } + + // 404 for unknown routes + jsonResponse(res, 404, { error: 'Not found' }); +}); + +// Start server +server.listen(PORT, () => { + console.log(`TPMJS Executor running on port ${PORT}`); + console.log(`Health: http://localhost:${PORT}/api/health`); + console.log(`Execute: POST http://localhost:${PORT}/api/execute-tool`); + if (API_KEY) { + console.log(`Authentication: Required (EXECUTOR_API_KEY is set)`); + } else { + console.log(`Authentication: None (set EXECUTOR_API_KEY to enable)`); + } +});