Add Unsandbox executor template

Adds a new executor template for deploying TPMJS tools on Unsandbox,
providing an alternative to the Vercel executor.

Features:
- One-command deploy via `un` CLI
- API-compatible with Vercel executor
- Standalone bootstrap script (no network required during bootstrap)
- Full documentation with examples
This commit is contained in:
Ajax Davis 2026-02-03 21:29:22 +10:00
parent f9c5d903a1
commit ee066a20ca
4 changed files with 903 additions and 0 deletions

View file

@ -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 <key>` 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)

View file

@ -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

View file

@ -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

View file

@ -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)`);
}
});