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:
parent
f9c5d903a1
commit
ee066a20ca
4 changed files with 903 additions and 0 deletions
239
templates/unsandbox-executor/README.md
Normal file
239
templates/unsandbox-executor/README.md
Normal 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)
|
||||||
297
templates/unsandbox-executor/bootstrap-standalone.sh
Normal file
297
templates/unsandbox-executor/bootstrap-standalone.sh
Normal 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
|
||||||
16
templates/unsandbox-executor/bootstrap.sh
Normal file
16
templates/unsandbox-executor/bootstrap.sh
Normal 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
|
||||||
351
templates/unsandbox-executor/executor.js
Normal file
351
templates/unsandbox-executor/executor.js
Normal 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)`);
|
||||||
|
}
|
||||||
|
});
|
||||||
Loading…
Add table
Add a link
Reference in a new issue