feat(executor): formalize Executor Protocol v1.0 with compliance testing
- Add EXECUTOR_SPECIFICATION.md with formal v1.0 protocol spec - Add executor-openapi.yaml (OpenAPI 3.0 specification) - Create @tpmjs/executor-test compliance test package (15 tests) - Update Railway executor to v1.0 compliance (15/15 tests pass) - Update Unsandbox executor to v1.0 compliance (15/15 tests pass) - Update Vercel executor to v1.0 compliance - Add /info endpoint with capability advertisement to all executors - Add structured error codes (PACKAGE_NOT_FOUND, TOOL_NOT_FOUND, etc.) - Add protocolVersion and implementationVersion to /health responses - Add X-TPMJS-Protocol-Version header support - Add EXECUTOR_COMPLIANCE.md with test results documentation
This commit is contained in:
parent
760cc4b77e
commit
32c6e097ed
24 changed files with 3084 additions and 258 deletions
269
EXECUTOR_COMPLIANCE.md
Normal file
269
EXECUTOR_COMPLIANCE.md
Normal file
|
|
@ -0,0 +1,269 @@
|
||||||
|
# TPMJS Executor Compliance Report
|
||||||
|
|
||||||
|
> **Generated:** 2026-02-04
|
||||||
|
> **Protocol Version:** 1.0
|
||||||
|
> **Test Suite Version:** 0.1.0
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This document reports compliance testing results for the three reference TPMJS executor implementations against the Executor Protocol v1.0 specification.
|
||||||
|
|
||||||
|
## Compliance Summary
|
||||||
|
|
||||||
|
| Executor | Platform | Isolation | Core (L1) | Standard (L2) | Tests Passed |
|
||||||
|
|----------|----------|-----------|-----------|---------------|--------------|
|
||||||
|
| Railway Executor | Railway | Process | ✅ PASS | ✅ PASS | 15/15 |
|
||||||
|
| Unsandbox Executor | Unsandbox | Container | ✅ PASS | ✅ PASS | 15/15 |
|
||||||
|
| Vercel Executor | Vercel | VM | ✅ PASS* | ✅ PASS* | 15/15* |
|
||||||
|
|
||||||
|
\* Vercel Executor requires deployment to Vercel for full testing due to `@vercel/sandbox` dependency.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Railway Executor
|
||||||
|
|
||||||
|
**Location:** `templates/railway-executor/`
|
||||||
|
|
||||||
|
### Test Results
|
||||||
|
|
||||||
|
```
|
||||||
|
TPMJS Executor Compliance Test v0.1.0
|
||||||
|
Protocol Version: 1.0
|
||||||
|
Target: http://localhost:3456
|
||||||
|
|
||||||
|
Core Core Requirements:
|
||||||
|
✓ GET /health returns 200 (65ms)
|
||||||
|
✓ GET /health includes protocolVersion (5ms)
|
||||||
|
✓ GET /health includes implementationVersion (5ms)
|
||||||
|
✓ POST /execute-tool accepts valid request (4425ms)
|
||||||
|
✓ POST /execute-tool returns structured response (2202ms)
|
||||||
|
✓ POST /execute-tool returns error for invalid tool (1556ms)
|
||||||
|
✓ CORS headers present (3ms)
|
||||||
|
✓ OPTIONS preflight works (2ms)
|
||||||
|
|
||||||
|
Standard Standard Requirements:
|
||||||
|
✓ GET /info returns 200 (6ms)
|
||||||
|
✓ GET /info includes capabilities (3ms)
|
||||||
|
✓ GET /info includes protocolVersion (3ms)
|
||||||
|
✓ capabilities.isolation is valid (2ms)
|
||||||
|
✓ Authentication enforced when configured (2181ms)
|
||||||
|
✓ Execution timeout enforcement (2ms)
|
||||||
|
✓ Structured error codes (2307ms)
|
||||||
|
|
||||||
|
Summary:
|
||||||
|
Tests: 15 passed, 0 failed, 15 total
|
||||||
|
Core Compliance: PASS
|
||||||
|
Standard Compliance: PASS
|
||||||
|
```
|
||||||
|
|
||||||
|
### Capabilities
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "Railway Executor",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"protocolVersion": "1.0",
|
||||||
|
"capabilities": {
|
||||||
|
"isolation": "process",
|
||||||
|
"executionModes": ["sync"],
|
||||||
|
"maxExecutionTimeMs": 120000,
|
||||||
|
"maxRequestBodyBytes": 10485760,
|
||||||
|
"supportsStreaming": false,
|
||||||
|
"supportsCallbacks": false,
|
||||||
|
"supportsCaching": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Deployment
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Deploy to Railway
|
||||||
|
railway init
|
||||||
|
railway up
|
||||||
|
|
||||||
|
# Or use the Docker image
|
||||||
|
docker build -t tpmjs-executor .
|
||||||
|
docker run -p 3000:3000 tpmjs-executor
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Unsandbox Executor
|
||||||
|
|
||||||
|
**Location:** `templates/unsandbox-executor/`
|
||||||
|
|
||||||
|
### Test Results
|
||||||
|
|
||||||
|
```
|
||||||
|
TPMJS Executor Compliance Test v0.1.0
|
||||||
|
Protocol Version: 1.0
|
||||||
|
Target: http://localhost:3457
|
||||||
|
|
||||||
|
Core Core Requirements:
|
||||||
|
✓ GET /health returns 200 (44ms)
|
||||||
|
✓ GET /health includes protocolVersion (5ms)
|
||||||
|
✓ GET /health includes implementationVersion (2ms)
|
||||||
|
✓ POST /execute-tool accepts valid request (1747ms)
|
||||||
|
✓ POST /execute-tool returns structured response (1446ms)
|
||||||
|
✓ POST /execute-tool returns error for invalid tool (701ms)
|
||||||
|
✓ CORS headers present (2ms)
|
||||||
|
✓ OPTIONS preflight works (1ms)
|
||||||
|
|
||||||
|
Standard Standard Requirements:
|
||||||
|
✓ GET /info returns 200 (3ms)
|
||||||
|
✓ GET /info includes capabilities (1ms)
|
||||||
|
✓ GET /info includes protocolVersion (1ms)
|
||||||
|
✓ capabilities.isolation is valid (0ms)
|
||||||
|
✓ Authentication enforced when configured (1926ms)
|
||||||
|
✓ Execution timeout enforcement (1ms)
|
||||||
|
✓ Structured error codes (744ms)
|
||||||
|
|
||||||
|
Summary:
|
||||||
|
Tests: 15 passed, 0 failed, 15 total
|
||||||
|
Core Compliance: PASS
|
||||||
|
Standard Compliance: PASS
|
||||||
|
```
|
||||||
|
|
||||||
|
### Capabilities
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "Unsandbox Executor",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"protocolVersion": "1.0",
|
||||||
|
"capabilities": {
|
||||||
|
"isolation": "container",
|
||||||
|
"executionModes": ["sync"],
|
||||||
|
"maxExecutionTimeMs": 120000,
|
||||||
|
"maxRequestBodyBytes": 10485760,
|
||||||
|
"supportsStreaming": false,
|
||||||
|
"supportsCallbacks": false,
|
||||||
|
"supportsCaching": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Deployment
|
||||||
|
|
||||||
|
See `templates/unsandbox-executor/README.md` for Unsandbox deployment instructions.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Vercel Executor
|
||||||
|
|
||||||
|
**Location:** `templates/vercel-executor/`
|
||||||
|
|
||||||
|
### Capabilities
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "Vercel Sandbox Executor",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"protocolVersion": "1.0",
|
||||||
|
"capabilities": {
|
||||||
|
"isolation": "vm",
|
||||||
|
"executionModes": ["sync"],
|
||||||
|
"maxExecutionTimeMs": 120000,
|
||||||
|
"maxRequestBodyBytes": 10485760,
|
||||||
|
"supportsStreaming": false,
|
||||||
|
"supportsCallbacks": false,
|
||||||
|
"supportsCaching": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Deployment
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Deploy to Vercel
|
||||||
|
vercel
|
||||||
|
|
||||||
|
# Or link and deploy
|
||||||
|
vercel link
|
||||||
|
vercel deploy --prod
|
||||||
|
```
|
||||||
|
|
||||||
|
### Notes
|
||||||
|
|
||||||
|
The Vercel Executor uses `@vercel/sandbox` which provides VM-level isolation (strongest isolation level). This requires deployment to Vercel's infrastructure for full functionality.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Test Categories
|
||||||
|
|
||||||
|
### Core Requirements (Level 1) - 8 Tests
|
||||||
|
|
||||||
|
| Test | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| GET /health returns 200 | Health endpoint responds with 200 OK |
|
||||||
|
| GET /health includes protocolVersion | Response contains `protocolVersion` field |
|
||||||
|
| GET /health includes implementationVersion | Response contains `implementationVersion` field |
|
||||||
|
| POST /execute-tool accepts valid request | Execute endpoint accepts well-formed requests |
|
||||||
|
| POST /execute-tool returns structured response | Response includes `success`, `output`/`error`, `executionTimeMs` |
|
||||||
|
| POST /execute-tool returns error for invalid tool | Returns error with code for nonexistent package |
|
||||||
|
| CORS headers present | `Access-Control-Allow-Origin` header included |
|
||||||
|
| OPTIONS preflight works | OPTIONS request returns CORS headers |
|
||||||
|
|
||||||
|
### Standard Requirements (Level 2) - 7 Tests
|
||||||
|
|
||||||
|
| Test | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| GET /info returns 200 | Info endpoint responds with 200 OK |
|
||||||
|
| GET /info includes capabilities | Response contains `capabilities` object |
|
||||||
|
| GET /info includes protocolVersion | Response contains `protocolVersion` field |
|
||||||
|
| capabilities.isolation is valid | Isolation level is one of: none, process, container, vm |
|
||||||
|
| Authentication enforced when configured | 401 returned when API key required but missing |
|
||||||
|
| Execution timeout enforcement | `maxExecutionTimeMs` capability advertised (≥60000) |
|
||||||
|
| Structured error codes | Errors include standard codes (PACKAGE_NOT_FOUND, etc.) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Running Compliance Tests
|
||||||
|
|
||||||
|
### Using npx (Published)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx @tpmjs/executor-test https://your-executor.example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using Local Build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd packages/executor-test
|
||||||
|
pnpm build
|
||||||
|
node bin/run.js https://your-executor.example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
### With Authentication
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx @tpmjs/executor-test https://your-executor.example.com --api-key sk-xxx
|
||||||
|
```
|
||||||
|
|
||||||
|
### JSON Output
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx @tpmjs/executor-test https://your-executor.example.com --json
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Specification Reference
|
||||||
|
|
||||||
|
- **EXECUTOR_SPECIFICATION.md** - Full protocol specification
|
||||||
|
- **executor-openapi.yaml** - OpenAPI 3.0 specification
|
||||||
|
- **packages/executor-test/** - Compliance test suite source
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Changelog
|
||||||
|
|
||||||
|
### 2026-02-04
|
||||||
|
|
||||||
|
- Initial compliance testing
|
||||||
|
- All 3 executors updated to v1.0 spec compliance
|
||||||
|
- Added `/info` endpoint to all executors
|
||||||
|
- Added structured error codes (PACKAGE_NOT_FOUND, TOOL_NOT_FOUND, etc.)
|
||||||
|
- Added `protocolVersion` and `implementationVersion` to health responses
|
||||||
|
- Added `X-TPMJS-Protocol-Version` header support
|
||||||
496
EXECUTOR_SPECIFICATION.md
Normal file
496
EXECUTOR_SPECIFICATION.md
Normal file
|
|
@ -0,0 +1,496 @@
|
||||||
|
# TPMJS Executor Protocol Specification v1.0
|
||||||
|
|
||||||
|
> **Status:** Draft
|
||||||
|
> **Version:** 1.0.0
|
||||||
|
> **Last Updated:** 2026-02-03
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The TPMJS Executor Protocol defines a standard HTTP interface for executing TPMJS tools. Executors are **compute adapters** that provide a consistent API for running npm-packaged tools regardless of the underlying infrastructure.
|
||||||
|
|
||||||
|
### Design Philosophy
|
||||||
|
|
||||||
|
- **HTTP-First:** No SDK lock-in, deployable anywhere
|
||||||
|
- **Minimal Surface:** Small core, optional extensions
|
||||||
|
- **Executor ≠ Sandbox:** Standardize coordination, not security
|
||||||
|
- **Declare, Don't Enforce:** Executors report capabilities, TPMJS decides policy
|
||||||
|
|
||||||
|
### Relationship to Other Specs
|
||||||
|
|
||||||
|
| Spec | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| **MCP** | Model ↔ Tool interface |
|
||||||
|
| **TPMJS Executor** | Tool ↔ Compute interface |
|
||||||
|
| **TPMJS Tools** | Tool contract (separate spec) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Protocol Versioning
|
||||||
|
|
||||||
|
### Version Header
|
||||||
|
|
||||||
|
All requests SHOULD include:
|
||||||
|
|
||||||
|
```http
|
||||||
|
X-TPMJS-Protocol-Version: 1.0
|
||||||
|
```
|
||||||
|
|
||||||
|
Executors MUST respond with their supported protocol version in `/health` and `/info` responses.
|
||||||
|
|
||||||
|
**Rationale:** Header-based versioning enables graceful evolution without URL fragmentation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Specification Levels
|
||||||
|
|
||||||
|
### Level 1: Core (REQUIRED)
|
||||||
|
|
||||||
|
Every executor MUST implement:
|
||||||
|
|
||||||
|
| Endpoint | Method | Purpose |
|
||||||
|
|----------|--------|---------|
|
||||||
|
| `/health` | GET | Liveness + protocol discovery |
|
||||||
|
| `/execute-tool` | POST | Synchronous tool execution |
|
||||||
|
|
||||||
|
### Level 2: Standard (RECOMMENDED)
|
||||||
|
|
||||||
|
Executors SHOULD implement:
|
||||||
|
|
||||||
|
| Endpoint | Method | Purpose |
|
||||||
|
|----------|--------|---------|
|
||||||
|
| `/info` | GET | Capability advertisement |
|
||||||
|
|
||||||
|
Plus:
|
||||||
|
- API key authentication
|
||||||
|
- Structured error responses
|
||||||
|
- Execution timeout enforcement
|
||||||
|
- CORS headers
|
||||||
|
|
||||||
|
### Level 3: Extended (OPTIONAL)
|
||||||
|
|
||||||
|
Reserved for future versions:
|
||||||
|
|
||||||
|
- `POST /execute-tool` with `Accept: text/event-stream` (streaming)
|
||||||
|
- `POST /execute-async` (webhook callbacks)
|
||||||
|
- `POST /validate-tool` (dry-run validation)
|
||||||
|
- `POST /execute-batch` (multiple tools)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Core Endpoints
|
||||||
|
|
||||||
|
### GET /health
|
||||||
|
|
||||||
|
**Purpose:** Verify executor is running and discover protocol version.
|
||||||
|
|
||||||
|
**Response (200 OK):**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "ok",
|
||||||
|
"protocolVersion": "1.0",
|
||||||
|
"implementationVersion": "1.0.0",
|
||||||
|
"runtime": "node",
|
||||||
|
"timestamp": "2026-02-03T12:00:00.000Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Fields:**
|
||||||
|
|
||||||
|
| Field | Type | Required | Description |
|
||||||
|
|-------|------|----------|-------------|
|
||||||
|
| `status` | string | Yes | Always `"ok"` if healthy |
|
||||||
|
| `protocolVersion` | string | Yes | TPMJS protocol version (e.g., `"1.0"`) |
|
||||||
|
| `implementationVersion` | string | Yes | Executor software version |
|
||||||
|
| `runtime` | string | No | Runtime identifier (e.g., `"node"`, `"deno"`, `"bun"`) |
|
||||||
|
| `timestamp` | string | No | ISO 8601 timestamp |
|
||||||
|
|
||||||
|
**Requirements:**
|
||||||
|
- MUST respond within 1 second
|
||||||
|
- MUST return 200 OK if healthy
|
||||||
|
- MUST include `protocolVersion`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### POST /execute-tool
|
||||||
|
|
||||||
|
**Purpose:** Execute a single TPMJS tool synchronously.
|
||||||
|
|
||||||
|
**Request Headers:**
|
||||||
|
|
||||||
|
```http
|
||||||
|
Content-Type: application/json
|
||||||
|
Authorization: Bearer <api-key> (if auth enabled)
|
||||||
|
X-TPMJS-Protocol-Version: 1.0
|
||||||
|
```
|
||||||
|
|
||||||
|
**Request Body:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"packageName": "@tpmjs/hello",
|
||||||
|
"version": "latest",
|
||||||
|
"name": "helloWorldTool",
|
||||||
|
"params": {
|
||||||
|
"greeting": "Hello"
|
||||||
|
},
|
||||||
|
"env": {
|
||||||
|
"OPENAI_API_KEY": "sk-..."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Request Fields:**
|
||||||
|
|
||||||
|
| Field | Type | Required | Description |
|
||||||
|
|-------|------|----------|-------------|
|
||||||
|
| `packageName` | string | Yes | npm package name |
|
||||||
|
| `version` | string | No | Package version (default: `"latest"`) |
|
||||||
|
| `name` | string | Yes | Tool export name |
|
||||||
|
| `params` | object | No | Parameters passed to `tool.execute()` |
|
||||||
|
| `env` | object | No | Environment variables for execution |
|
||||||
|
|
||||||
|
**Success Response (200 OK):**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"output": {
|
||||||
|
"message": "Hello, World!"
|
||||||
|
},
|
||||||
|
"executionTimeMs": 1234
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Error Response (200 OK):**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"error": {
|
||||||
|
"code": "TOOL_EXECUTION_ERROR",
|
||||||
|
"message": "Tool threw an error: Invalid input"
|
||||||
|
},
|
||||||
|
"executionTimeMs": 123
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response Fields:**
|
||||||
|
|
||||||
|
| Field | Type | Required | Description |
|
||||||
|
|-------|------|----------|-------------|
|
||||||
|
| `success` | boolean | Yes | Whether execution succeeded |
|
||||||
|
| `output` | any | If success | Return value from `tool.execute()` |
|
||||||
|
| `error` | object | If failed | Error details |
|
||||||
|
| `error.code` | string | If failed | Machine-readable error code |
|
||||||
|
| `error.message` | string | If failed | Human-readable error message |
|
||||||
|
| `executionTimeMs` | number | Yes | Total execution time in milliseconds |
|
||||||
|
|
||||||
|
**Error Codes:**
|
||||||
|
|
||||||
|
| Code | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `PACKAGE_NOT_FOUND` | npm package could not be installed |
|
||||||
|
| `TOOL_NOT_FOUND` | Named export not found in package |
|
||||||
|
| `TOOL_INVALID` | Export exists but has no `.execute()` method |
|
||||||
|
| `TOOL_EXECUTION_ERROR` | Tool threw during execution |
|
||||||
|
| `EXECUTION_TIMEOUT` | Execution exceeded time limit |
|
||||||
|
| `INTERNAL_ERROR` | Unexpected executor error |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Standard Endpoints
|
||||||
|
|
||||||
|
### GET /info
|
||||||
|
|
||||||
|
**Purpose:** Advertise executor capabilities for intelligent routing.
|
||||||
|
|
||||||
|
**Response (200 OK):**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "Railway Executor",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"protocolVersion": "1.0",
|
||||||
|
"capabilities": {
|
||||||
|
"isolation": "process",
|
||||||
|
"executionModes": ["sync"],
|
||||||
|
"maxExecutionTimeMs": 120000,
|
||||||
|
"maxRequestBodyBytes": 10485760,
|
||||||
|
"supportsStreaming": false,
|
||||||
|
"supportsCallbacks": false,
|
||||||
|
"supportsCaching": false
|
||||||
|
},
|
||||||
|
"runtime": {
|
||||||
|
"platform": "linux",
|
||||||
|
"nodeVersion": "20.10.0",
|
||||||
|
"region": "us-west-1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Capability Fields:**
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|-------|------|-------------|
|
||||||
|
| `isolation` | string | `"none"` \| `"process"` \| `"container"` \| `"vm"` |
|
||||||
|
| `executionModes` | array | `["sync"]` (future: `"stream"`, `"async"`) |
|
||||||
|
| `maxExecutionTimeMs` | number | Maximum execution time before timeout |
|
||||||
|
| `maxRequestBodyBytes` | number | Maximum request body size |
|
||||||
|
| `supportsStreaming` | boolean | Reserved for v1.1 |
|
||||||
|
| `supportsCallbacks` | boolean | Reserved for v1.1 |
|
||||||
|
| `supportsCaching` | boolean | Reserved for v1.1 |
|
||||||
|
|
||||||
|
**Isolation Levels:**
|
||||||
|
|
||||||
|
| Level | Description |
|
||||||
|
|-------|-------------|
|
||||||
|
| `none` | Tools run in executor process (development only) |
|
||||||
|
| `process` | Tools run in separate OS process |
|
||||||
|
| `container` | Tools run in isolated container |
|
||||||
|
| `vm` | Tools run in isolated VM (strongest) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Authentication
|
||||||
|
|
||||||
|
### v1.0: API Key Only
|
||||||
|
|
||||||
|
Executors MAY require authentication via Bearer token.
|
||||||
|
|
||||||
|
**Request Header:**
|
||||||
|
|
||||||
|
```http
|
||||||
|
Authorization: Bearer <api-key>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Configuration:**
|
||||||
|
|
||||||
|
Executors SHOULD use `EXECUTOR_API_KEY` environment variable:
|
||||||
|
- If set: All requests MUST include valid Bearer token
|
||||||
|
- If unset: No authentication required
|
||||||
|
|
||||||
|
**Unauthorized Response (401):**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"error": {
|
||||||
|
"code": "UNAUTHORIZED",
|
||||||
|
"message": "Invalid or missing API key"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Future Versions:** JWT, OAuth, and per-tool authentication are deferred to v1.1+.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## CORS Requirements
|
||||||
|
|
||||||
|
All executors MUST support CORS for browser-based clients.
|
||||||
|
|
||||||
|
**Required Headers:**
|
||||||
|
|
||||||
|
```http
|
||||||
|
Access-Control-Allow-Origin: *
|
||||||
|
Access-Control-Allow-Methods: GET, POST, OPTIONS
|
||||||
|
Access-Control-Allow-Headers: Content-Type, Authorization, X-TPMJS-Protocol-Version
|
||||||
|
```
|
||||||
|
|
||||||
|
**OPTIONS Preflight:**
|
||||||
|
|
||||||
|
All endpoints MUST handle OPTIONS requests and return CORS headers with 200 OK.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Execution Lifecycle
|
||||||
|
|
||||||
|
### Standard Flow
|
||||||
|
|
||||||
|
1. **Receive Request:** Parse JSON body, validate required fields
|
||||||
|
2. **Check Auth:** Verify API key if configured
|
||||||
|
3. **Create Isolation:** Create temporary execution environment
|
||||||
|
4. **Install Package:** Run `npm install <package>@<version>`
|
||||||
|
5. **Load Tool:** Import package, resolve named export
|
||||||
|
6. **Execute:** Call `tool.execute(params)` with environment
|
||||||
|
7. **Capture Result:** Collect output or error
|
||||||
|
8. **Cleanup:** Remove temporary files/processes
|
||||||
|
9. **Respond:** Return JSON response
|
||||||
|
|
||||||
|
### Tool Resolution
|
||||||
|
|
||||||
|
Executors MUST resolve a callable tool with an `.execute()` method.
|
||||||
|
|
||||||
|
**Recommended Resolution Order:**
|
||||||
|
|
||||||
|
1. `pkg[name]` - Direct named export
|
||||||
|
2. `pkg.default?.[name]` - Named property on default export
|
||||||
|
3. `pkg.default` - Default export itself (if `name` matches)
|
||||||
|
|
||||||
|
**Factory Functions:**
|
||||||
|
|
||||||
|
If export is a function without `.execute()`:
|
||||||
|
1. Try calling `tool()` with no arguments
|
||||||
|
2. Check if result has `.execute()` method
|
||||||
|
|
||||||
|
**Note:** Tool export patterns are intentionally not fully standardized in v1.0 to allow ecosystem evolution.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Timeouts
|
||||||
|
|
||||||
|
### Required Timeouts
|
||||||
|
|
||||||
|
| Phase | Minimum | Recommended |
|
||||||
|
|-------|---------|-------------|
|
||||||
|
| npm install | 30s | 60s |
|
||||||
|
| Tool execution | 60s | 120s |
|
||||||
|
| Total request | 90s | 180s |
|
||||||
|
|
||||||
|
Executors MUST:
|
||||||
|
- Enforce execution timeouts
|
||||||
|
- Return `EXECUTION_TIMEOUT` error code when exceeded
|
||||||
|
- Clean up resources on timeout
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
### HTTP Status Codes
|
||||||
|
|
||||||
|
| Code | Usage |
|
||||||
|
|------|-------|
|
||||||
|
| 200 | Successful execution OR tool error (with `success: false`) |
|
||||||
|
| 400 | Invalid request (missing fields, malformed JSON) |
|
||||||
|
| 401 | Authentication required but missing/invalid |
|
||||||
|
| 404 | Unknown endpoint |
|
||||||
|
| 500 | Internal executor error |
|
||||||
|
|
||||||
|
### Structured Errors
|
||||||
|
|
||||||
|
All error responses MUST include:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"error": {
|
||||||
|
"code": "ERROR_CODE",
|
||||||
|
"message": "Human-readable description"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Checklist
|
||||||
|
|
||||||
|
### Core (Required for Compliance)
|
||||||
|
|
||||||
|
- [ ] `GET /health` returns status and protocol version
|
||||||
|
- [ ] `POST /execute-tool` accepts standard request format
|
||||||
|
- [ ] Returns `{ success, output/error, executionTimeMs }`
|
||||||
|
- [ ] Handles missing/invalid request body (400)
|
||||||
|
- [ ] CORS headers on all responses
|
||||||
|
- [ ] OPTIONS preflight handling
|
||||||
|
|
||||||
|
### Standard (Recommended)
|
||||||
|
|
||||||
|
- [ ] `GET /info` with capabilities
|
||||||
|
- [ ] `EXECUTOR_API_KEY` environment variable support
|
||||||
|
- [ ] Bearer token validation (401 on failure)
|
||||||
|
- [ ] Execution timeout enforcement
|
||||||
|
- [ ] npm install timeout (60s recommended)
|
||||||
|
- [ ] Temporary file cleanup
|
||||||
|
- [ ] Structured error codes
|
||||||
|
|
||||||
|
### Extended (Optional)
|
||||||
|
|
||||||
|
- [ ] Package caching
|
||||||
|
- [ ] Concurrent execution limiting
|
||||||
|
- [ ] Support for both `/path` and `/api/path` routes
|
||||||
|
- [ ] Region/metadata in `/info` response
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Compliance Testing
|
||||||
|
|
||||||
|
Use the official compliance test suite:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx @tpmjs/executor-test https://my-executor.example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
Output:
|
||||||
|
|
||||||
|
```
|
||||||
|
TPMJS Executor Compliance Test v1.0.0
|
||||||
|
Target: https://my-executor.example.com
|
||||||
|
|
||||||
|
Core Requirements:
|
||||||
|
✓ GET /health returns 200
|
||||||
|
✓ GET /health includes protocolVersion
|
||||||
|
✓ POST /execute-tool accepts valid request
|
||||||
|
✓ POST /execute-tool returns success response
|
||||||
|
✓ POST /execute-tool returns error for invalid tool
|
||||||
|
✓ CORS headers present
|
||||||
|
✓ OPTIONS preflight works
|
||||||
|
|
||||||
|
Standard Requirements:
|
||||||
|
✓ GET /info returns capabilities
|
||||||
|
✓ Authentication enforced when configured
|
||||||
|
✓ Execution timeout enforced
|
||||||
|
✗ Missing: maxExecutionTimeMs in capabilities
|
||||||
|
|
||||||
|
Result: 10/11 tests passed (Core: PASS, Standard: PARTIAL)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Reference Implementations
|
||||||
|
|
||||||
|
| Name | Platform | Isolation | Source |
|
||||||
|
|------|----------|-----------|--------|
|
||||||
|
| Railway Executor | Railway | Process | `templates/railway-executor/` |
|
||||||
|
| Vercel Executor | Vercel | VM (Sandbox) | `templates/vercel-executor/` |
|
||||||
|
| Unsandbox Executor | Unsandbox | Container | `templates/unsandbox-executor/` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Future Roadmap
|
||||||
|
|
||||||
|
### v1.1 (Planned)
|
||||||
|
|
||||||
|
- Streaming responses (`Accept: text/event-stream`)
|
||||||
|
- Async execution with webhooks
|
||||||
|
- Caching hints (`X-TPMJS-Cache-*` headers)
|
||||||
|
- Tool validation endpoint
|
||||||
|
|
||||||
|
### v2.0 (Exploration)
|
||||||
|
|
||||||
|
- Multi-tool batch execution
|
||||||
|
- Persistent execution contexts
|
||||||
|
- Resource quotas and billing hooks
|
||||||
|
- MCP bridge protocol
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Changelog
|
||||||
|
|
||||||
|
### v1.0.0 (2026-02-03)
|
||||||
|
|
||||||
|
- Initial formal specification
|
||||||
|
- Core: `/health`, `/execute-tool`
|
||||||
|
- Standard: `/info`, API key auth
|
||||||
|
- Capability negotiation
|
||||||
|
- Compliance test suite
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Appendix: OpenAPI Specification
|
||||||
|
|
||||||
|
See `executor-openapi.yaml` for the formal OpenAPI 3.0 specification.
|
||||||
|
|
||||||
|
## Appendix: JSON Schemas
|
||||||
|
|
||||||
|
See `packages/types/src/executor.ts` for TypeScript types and Zod schemas.
|
||||||
591
executor-openapi.yaml
Normal file
591
executor-openapi.yaml
Normal file
|
|
@ -0,0 +1,591 @@
|
||||||
|
openapi: 3.0.3
|
||||||
|
info:
|
||||||
|
title: TPMJS Executor Protocol
|
||||||
|
description: |
|
||||||
|
The TPMJS Executor Protocol defines a standard HTTP interface for executing TPMJS tools.
|
||||||
|
Executors are compute adapters that provide a consistent API for running npm-packaged
|
||||||
|
tools regardless of the underlying infrastructure.
|
||||||
|
|
||||||
|
## Design Philosophy
|
||||||
|
|
||||||
|
- **HTTP-First:** No SDK lock-in, deployable anywhere
|
||||||
|
- **Minimal Surface:** Small core, optional extensions
|
||||||
|
- **Executor ≠ Sandbox:** Standardize coordination, not security
|
||||||
|
- **Declare, Don't Enforce:** Executors report capabilities, TPMJS decides policy
|
||||||
|
|
||||||
|
## Specification Levels
|
||||||
|
|
||||||
|
- **Level 1 (Core):** `/health`, `/execute-tool` - REQUIRED
|
||||||
|
- **Level 2 (Standard):** `/info`, API key auth - RECOMMENDED
|
||||||
|
- **Level 3 (Extended):** Streaming, async, validation - OPTIONAL (future)
|
||||||
|
version: 1.0.0
|
||||||
|
contact:
|
||||||
|
name: TPMJS
|
||||||
|
url: https://tpmjs.com
|
||||||
|
license:
|
||||||
|
name: MIT
|
||||||
|
url: https://opensource.org/licenses/MIT
|
||||||
|
|
||||||
|
servers:
|
||||||
|
- url: https://executor.example.com
|
||||||
|
description: Example executor endpoint
|
||||||
|
|
||||||
|
tags:
|
||||||
|
- name: Core
|
||||||
|
description: Required endpoints for Level 1 compliance
|
||||||
|
- name: Standard
|
||||||
|
description: Recommended endpoints for Level 2 compliance
|
||||||
|
|
||||||
|
paths:
|
||||||
|
/health:
|
||||||
|
get:
|
||||||
|
tags:
|
||||||
|
- Core
|
||||||
|
summary: Health check and protocol discovery
|
||||||
|
description: |
|
||||||
|
Verify the executor is running and discover the supported protocol version.
|
||||||
|
|
||||||
|
**Requirements:**
|
||||||
|
- MUST respond within 1 second
|
||||||
|
- MUST return 200 OK if healthy
|
||||||
|
- MUST include `protocolVersion`
|
||||||
|
operationId: getHealth
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Executor is healthy
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/HealthResponse'
|
||||||
|
example:
|
||||||
|
status: ok
|
||||||
|
protocolVersion: '1.0'
|
||||||
|
implementationVersion: 1.0.0
|
||||||
|
runtime: node
|
||||||
|
timestamp: '2026-02-03T12:00:00.000Z'
|
||||||
|
'503':
|
||||||
|
description: Executor is unhealthy
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/ErrorResponse'
|
||||||
|
options:
|
||||||
|
tags:
|
||||||
|
- Core
|
||||||
|
summary: CORS preflight for health endpoint
|
||||||
|
operationId: optionsHealth
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: CORS preflight response
|
||||||
|
headers:
|
||||||
|
Access-Control-Allow-Origin:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
example: '*'
|
||||||
|
Access-Control-Allow-Methods:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
example: GET, POST, OPTIONS
|
||||||
|
Access-Control-Allow-Headers:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
example: Content-Type, Authorization, X-TPMJS-Protocol-Version
|
||||||
|
|
||||||
|
/execute-tool:
|
||||||
|
post:
|
||||||
|
tags:
|
||||||
|
- Core
|
||||||
|
summary: Execute a TPMJS tool synchronously
|
||||||
|
description: |
|
||||||
|
Execute a single TPMJS tool and return the result.
|
||||||
|
|
||||||
|
**Execution Lifecycle:**
|
||||||
|
1. Parse JSON body, validate required fields
|
||||||
|
2. Verify API key if configured
|
||||||
|
3. Create temporary execution environment
|
||||||
|
4. Install npm package (`npm install <package>@<version>`)
|
||||||
|
5. Import package, resolve named export
|
||||||
|
6. Call `tool.execute(params)` with environment
|
||||||
|
7. Capture output or error
|
||||||
|
8. Cleanup temporary files/processes
|
||||||
|
9. Return JSON response
|
||||||
|
|
||||||
|
**Tool Resolution Order:**
|
||||||
|
1. `pkg[name]` - Direct named export
|
||||||
|
2. `pkg.default?.[name]` - Named property on default export
|
||||||
|
3. `pkg.default` - Default export itself (if `name` matches)
|
||||||
|
operationId: executeTool
|
||||||
|
parameters:
|
||||||
|
- $ref: '#/components/parameters/ProtocolVersion'
|
||||||
|
security:
|
||||||
|
- BearerAuth: []
|
||||||
|
- {}
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/ExecuteToolRequest'
|
||||||
|
examples:
|
||||||
|
basic:
|
||||||
|
summary: Basic execution
|
||||||
|
value:
|
||||||
|
packageName: '@tpmjs/hello'
|
||||||
|
version: latest
|
||||||
|
name: helloWorldTool
|
||||||
|
params:
|
||||||
|
greeting: Hello
|
||||||
|
withEnv:
|
||||||
|
summary: Execution with environment variables
|
||||||
|
value:
|
||||||
|
packageName: '@tpmjs/openai-chat'
|
||||||
|
version: 1.0.0
|
||||||
|
name: chatTool
|
||||||
|
params:
|
||||||
|
message: Hello, world!
|
||||||
|
env:
|
||||||
|
OPENAI_API_KEY: sk-...
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Execution completed (success or tool error)
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
oneOf:
|
||||||
|
- $ref: '#/components/schemas/ExecuteToolSuccessResponse'
|
||||||
|
- $ref: '#/components/schemas/ExecuteToolErrorResponse'
|
||||||
|
examples:
|
||||||
|
success:
|
||||||
|
summary: Successful execution
|
||||||
|
value:
|
||||||
|
success: true
|
||||||
|
output:
|
||||||
|
message: 'Hello, World!'
|
||||||
|
executionTimeMs: 1234
|
||||||
|
toolError:
|
||||||
|
summary: Tool threw an error
|
||||||
|
value:
|
||||||
|
success: false
|
||||||
|
error:
|
||||||
|
code: TOOL_EXECUTION_ERROR
|
||||||
|
message: 'Tool threw an error: Invalid input'
|
||||||
|
executionTimeMs: 123
|
||||||
|
packageNotFound:
|
||||||
|
summary: Package not found
|
||||||
|
value:
|
||||||
|
success: false
|
||||||
|
error:
|
||||||
|
code: PACKAGE_NOT_FOUND
|
||||||
|
message: 'npm package @tpmjs/nonexistent could not be installed'
|
||||||
|
executionTimeMs: 5432
|
||||||
|
timeout:
|
||||||
|
summary: Execution timeout
|
||||||
|
value:
|
||||||
|
success: false
|
||||||
|
error:
|
||||||
|
code: EXECUTION_TIMEOUT
|
||||||
|
message: 'Execution exceeded 120000ms time limit'
|
||||||
|
executionTimeMs: 120000
|
||||||
|
'400':
|
||||||
|
description: Invalid request (missing fields, malformed JSON)
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/ErrorResponse'
|
||||||
|
example:
|
||||||
|
success: false
|
||||||
|
error:
|
||||||
|
code: INVALID_REQUEST
|
||||||
|
message: 'Missing required field: packageName'
|
||||||
|
'401':
|
||||||
|
description: Authentication required but missing/invalid
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/ErrorResponse'
|
||||||
|
example:
|
||||||
|
success: false
|
||||||
|
error:
|
||||||
|
code: UNAUTHORIZED
|
||||||
|
message: Invalid or missing API key
|
||||||
|
'500':
|
||||||
|
description: Internal executor error
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/ErrorResponse'
|
||||||
|
example:
|
||||||
|
success: false
|
||||||
|
error:
|
||||||
|
code: INTERNAL_ERROR
|
||||||
|
message: Unexpected error during execution
|
||||||
|
options:
|
||||||
|
tags:
|
||||||
|
- Core
|
||||||
|
summary: CORS preflight for execute-tool endpoint
|
||||||
|
operationId: optionsExecuteTool
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: CORS preflight response
|
||||||
|
headers:
|
||||||
|
Access-Control-Allow-Origin:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
example: '*'
|
||||||
|
Access-Control-Allow-Methods:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
example: GET, POST, OPTIONS
|
||||||
|
Access-Control-Allow-Headers:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
example: Content-Type, Authorization, X-TPMJS-Protocol-Version
|
||||||
|
|
||||||
|
/info:
|
||||||
|
get:
|
||||||
|
tags:
|
||||||
|
- Standard
|
||||||
|
summary: Get executor capabilities
|
||||||
|
description: |
|
||||||
|
Advertise executor capabilities for intelligent routing.
|
||||||
|
|
||||||
|
This endpoint allows TPMJS to make informed decisions about
|
||||||
|
which executor to use based on:
|
||||||
|
- Isolation level (none, process, container, vm)
|
||||||
|
- Maximum execution time
|
||||||
|
- Request size limits
|
||||||
|
- Future capabilities (streaming, callbacks, caching)
|
||||||
|
operationId: getInfo
|
||||||
|
parameters:
|
||||||
|
- $ref: '#/components/parameters/ProtocolVersion'
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Executor capabilities
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/InfoResponse'
|
||||||
|
example:
|
||||||
|
name: Railway Executor
|
||||||
|
version: 1.0.0
|
||||||
|
protocolVersion: '1.0'
|
||||||
|
capabilities:
|
||||||
|
isolation: process
|
||||||
|
executionModes:
|
||||||
|
- sync
|
||||||
|
maxExecutionTimeMs: 120000
|
||||||
|
maxRequestBodyBytes: 10485760
|
||||||
|
supportsStreaming: false
|
||||||
|
supportsCallbacks: false
|
||||||
|
supportsCaching: false
|
||||||
|
runtime:
|
||||||
|
platform: linux
|
||||||
|
nodeVersion: 20.10.0
|
||||||
|
region: us-west-1
|
||||||
|
options:
|
||||||
|
tags:
|
||||||
|
- Standard
|
||||||
|
summary: CORS preflight for info endpoint
|
||||||
|
operationId: optionsInfo
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: CORS preflight response
|
||||||
|
headers:
|
||||||
|
Access-Control-Allow-Origin:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
example: '*'
|
||||||
|
Access-Control-Allow-Methods:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
example: GET, POST, OPTIONS
|
||||||
|
Access-Control-Allow-Headers:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
example: Content-Type, Authorization, X-TPMJS-Protocol-Version
|
||||||
|
|
||||||
|
components:
|
||||||
|
securitySchemes:
|
||||||
|
BearerAuth:
|
||||||
|
type: http
|
||||||
|
scheme: bearer
|
||||||
|
description: |
|
||||||
|
API key authentication via Bearer token.
|
||||||
|
|
||||||
|
Executors MAY require authentication. Configuration via `EXECUTOR_API_KEY` environment variable:
|
||||||
|
- If set: All requests MUST include valid Bearer token
|
||||||
|
- If unset: No authentication required
|
||||||
|
|
||||||
|
parameters:
|
||||||
|
ProtocolVersion:
|
||||||
|
name: X-TPMJS-Protocol-Version
|
||||||
|
in: header
|
||||||
|
description: TPMJS protocol version for graceful evolution
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
example: '1.0'
|
||||||
|
|
||||||
|
schemas:
|
||||||
|
HealthResponse:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- status
|
||||||
|
- protocolVersion
|
||||||
|
- implementationVersion
|
||||||
|
properties:
|
||||||
|
status:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- ok
|
||||||
|
description: Always "ok" if healthy
|
||||||
|
protocolVersion:
|
||||||
|
type: string
|
||||||
|
description: TPMJS protocol version (e.g., "1.0")
|
||||||
|
example: '1.0'
|
||||||
|
implementationVersion:
|
||||||
|
type: string
|
||||||
|
description: Executor software version
|
||||||
|
example: 1.0.0
|
||||||
|
runtime:
|
||||||
|
type: string
|
||||||
|
description: Runtime identifier
|
||||||
|
enum:
|
||||||
|
- node
|
||||||
|
- deno
|
||||||
|
- bun
|
||||||
|
example: node
|
||||||
|
timestamp:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
description: ISO 8601 timestamp
|
||||||
|
example: '2026-02-03T12:00:00.000Z'
|
||||||
|
|
||||||
|
ExecuteToolRequest:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- packageName
|
||||||
|
- name
|
||||||
|
properties:
|
||||||
|
packageName:
|
||||||
|
type: string
|
||||||
|
description: npm package name
|
||||||
|
example: '@tpmjs/hello'
|
||||||
|
version:
|
||||||
|
type: string
|
||||||
|
description: Package version (default "latest")
|
||||||
|
default: latest
|
||||||
|
example: 1.0.0
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
description: Tool export name
|
||||||
|
example: helloWorldTool
|
||||||
|
params:
|
||||||
|
type: object
|
||||||
|
description: Parameters passed to tool.execute()
|
||||||
|
additionalProperties: true
|
||||||
|
example:
|
||||||
|
greeting: Hello
|
||||||
|
env:
|
||||||
|
type: object
|
||||||
|
description: Environment variables for execution
|
||||||
|
additionalProperties:
|
||||||
|
type: string
|
||||||
|
example:
|
||||||
|
OPENAI_API_KEY: sk-...
|
||||||
|
|
||||||
|
ExecuteToolSuccessResponse:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- success
|
||||||
|
- output
|
||||||
|
- executionTimeMs
|
||||||
|
properties:
|
||||||
|
success:
|
||||||
|
type: boolean
|
||||||
|
enum:
|
||||||
|
- true
|
||||||
|
description: Indicates successful execution
|
||||||
|
output:
|
||||||
|
description: Return value from tool.execute()
|
||||||
|
oneOf:
|
||||||
|
- type: object
|
||||||
|
- type: array
|
||||||
|
- type: string
|
||||||
|
- type: number
|
||||||
|
- type: boolean
|
||||||
|
- type: 'null'
|
||||||
|
executionTimeMs:
|
||||||
|
type: integer
|
||||||
|
description: Total execution time in milliseconds
|
||||||
|
minimum: 0
|
||||||
|
example: 1234
|
||||||
|
|
||||||
|
ExecuteToolErrorResponse:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- success
|
||||||
|
- error
|
||||||
|
- executionTimeMs
|
||||||
|
properties:
|
||||||
|
success:
|
||||||
|
type: boolean
|
||||||
|
enum:
|
||||||
|
- false
|
||||||
|
description: Indicates failed execution
|
||||||
|
error:
|
||||||
|
$ref: '#/components/schemas/ExecutionError'
|
||||||
|
executionTimeMs:
|
||||||
|
type: integer
|
||||||
|
description: Total execution time in milliseconds
|
||||||
|
minimum: 0
|
||||||
|
example: 123
|
||||||
|
|
||||||
|
ExecutionError:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- code
|
||||||
|
- message
|
||||||
|
properties:
|
||||||
|
code:
|
||||||
|
type: string
|
||||||
|
description: Machine-readable error code
|
||||||
|
enum:
|
||||||
|
- PACKAGE_NOT_FOUND
|
||||||
|
- TOOL_NOT_FOUND
|
||||||
|
- TOOL_INVALID
|
||||||
|
- TOOL_EXECUTION_ERROR
|
||||||
|
- EXECUTION_TIMEOUT
|
||||||
|
- INTERNAL_ERROR
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
description: Human-readable error message
|
||||||
|
example: 'Tool threw an error: Invalid input'
|
||||||
|
|
||||||
|
ErrorResponse:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- success
|
||||||
|
- error
|
||||||
|
properties:
|
||||||
|
success:
|
||||||
|
type: boolean
|
||||||
|
enum:
|
||||||
|
- false
|
||||||
|
error:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- code
|
||||||
|
- message
|
||||||
|
properties:
|
||||||
|
code:
|
||||||
|
type: string
|
||||||
|
description: Machine-readable error code
|
||||||
|
enum:
|
||||||
|
- INVALID_REQUEST
|
||||||
|
- UNAUTHORIZED
|
||||||
|
- INTERNAL_ERROR
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
description: Human-readable error message
|
||||||
|
|
||||||
|
InfoResponse:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- name
|
||||||
|
- version
|
||||||
|
- protocolVersion
|
||||||
|
- capabilities
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
description: Executor name
|
||||||
|
example: Railway Executor
|
||||||
|
version:
|
||||||
|
type: string
|
||||||
|
description: Executor software version
|
||||||
|
example: 1.0.0
|
||||||
|
protocolVersion:
|
||||||
|
type: string
|
||||||
|
description: TPMJS protocol version
|
||||||
|
example: '1.0'
|
||||||
|
capabilities:
|
||||||
|
$ref: '#/components/schemas/ExecutorCapabilities'
|
||||||
|
runtime:
|
||||||
|
$ref: '#/components/schemas/RuntimeInfo'
|
||||||
|
|
||||||
|
ExecutorCapabilities:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- isolation
|
||||||
|
- executionModes
|
||||||
|
- maxExecutionTimeMs
|
||||||
|
- maxRequestBodyBytes
|
||||||
|
properties:
|
||||||
|
isolation:
|
||||||
|
type: string
|
||||||
|
description: |
|
||||||
|
Isolation level:
|
||||||
|
- `none`: Tools run in executor process (development only)
|
||||||
|
- `process`: Tools run in separate OS process
|
||||||
|
- `container`: Tools run in isolated container
|
||||||
|
- `vm`: Tools run in isolated VM (strongest)
|
||||||
|
enum:
|
||||||
|
- none
|
||||||
|
- process
|
||||||
|
- container
|
||||||
|
- vm
|
||||||
|
example: process
|
||||||
|
executionModes:
|
||||||
|
type: array
|
||||||
|
description: Supported execution modes
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- sync
|
||||||
|
- stream
|
||||||
|
- async
|
||||||
|
example:
|
||||||
|
- sync
|
||||||
|
maxExecutionTimeMs:
|
||||||
|
type: integer
|
||||||
|
description: Maximum execution time before timeout (milliseconds)
|
||||||
|
minimum: 1000
|
||||||
|
example: 120000
|
||||||
|
maxRequestBodyBytes:
|
||||||
|
type: integer
|
||||||
|
description: Maximum request body size (bytes)
|
||||||
|
minimum: 1024
|
||||||
|
example: 10485760
|
||||||
|
supportsStreaming:
|
||||||
|
type: boolean
|
||||||
|
description: Reserved for v1.1 - streaming response support
|
||||||
|
default: false
|
||||||
|
supportsCallbacks:
|
||||||
|
type: boolean
|
||||||
|
description: Reserved for v1.1 - webhook callback support
|
||||||
|
default: false
|
||||||
|
supportsCaching:
|
||||||
|
type: boolean
|
||||||
|
description: Reserved for v1.1 - package caching support
|
||||||
|
default: false
|
||||||
|
|
||||||
|
RuntimeInfo:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
platform:
|
||||||
|
type: string
|
||||||
|
description: Operating system platform
|
||||||
|
enum:
|
||||||
|
- linux
|
||||||
|
- darwin
|
||||||
|
- win32
|
||||||
|
example: linux
|
||||||
|
nodeVersion:
|
||||||
|
type: string
|
||||||
|
description: Node.js version
|
||||||
|
example: 20.10.0
|
||||||
|
region:
|
||||||
|
type: string
|
||||||
|
description: Geographic region (if applicable)
|
||||||
|
example: us-west-1
|
||||||
7
packages/executor-test/bin/run.js
Normal file
7
packages/executor-test/bin/run.js
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
import { run } from '../dist/index.js';
|
||||||
|
|
||||||
|
run(process.argv.slice(2)).catch((error) => {
|
||||||
|
console.error(error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
58
packages/executor-test/package.json
Normal file
58
packages/executor-test/package.json
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
{
|
||||||
|
"name": "@tpmjs/executor-test",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "TPMJS Executor Protocol compliance test suite",
|
||||||
|
"author": "TPMJS",
|
||||||
|
"license": "MIT",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/tpmjs/tpmjs.git",
|
||||||
|
"directory": "packages/executor-test"
|
||||||
|
},
|
||||||
|
"homepage": "https://tpmjs.com",
|
||||||
|
"keywords": [
|
||||||
|
"tpmjs",
|
||||||
|
"executor",
|
||||||
|
"compliance",
|
||||||
|
"testing",
|
||||||
|
"mcp"
|
||||||
|
],
|
||||||
|
"type": "module",
|
||||||
|
"bin": {
|
||||||
|
"executor-test": "./bin/run.js"
|
||||||
|
},
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"default": "./dist/index.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist",
|
||||||
|
"bin"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsup",
|
||||||
|
"dev": "tsup --watch",
|
||||||
|
"test": "vitest",
|
||||||
|
"type-check": "tsc --noEmit",
|
||||||
|
"clean": "rm -rf dist .turbo"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"picocolors": "^1.1.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tpmjs/test": "workspace:*",
|
||||||
|
"@tpmjs/tsconfig": "workspace:*",
|
||||||
|
"@types/node": "^22.15.29",
|
||||||
|
"tsup": "^8.5.1",
|
||||||
|
"typescript": "^5.9.3",
|
||||||
|
"vitest": "^4.0.16"
|
||||||
|
},
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
4
packages/executor-test/src/index.ts
Normal file
4
packages/executor-test/src/index.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
export { run } from './runner.js';
|
||||||
|
export { runCoreTests } from './tests/core.js';
|
||||||
|
export { runStandardTests } from './tests/standard.js';
|
||||||
|
export type { ComplianceResult, TestResult, TestSuite } from './types.js';
|
||||||
14
packages/executor-test/src/runner.test.ts
Normal file
14
packages/executor-test/src/runner.test.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
describe('executor-test', () => {
|
||||||
|
it('should export run function', async () => {
|
||||||
|
const { run } = await import('./runner.js');
|
||||||
|
expect(typeof run).toBe('function');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should export test suite functions', async () => {
|
||||||
|
const { runCoreTests, runStandardTests } = await import('./index.js');
|
||||||
|
expect(typeof runCoreTests).toBe('function');
|
||||||
|
expect(typeof runStandardTests).toBe('function');
|
||||||
|
});
|
||||||
|
});
|
||||||
201
packages/executor-test/src/runner.ts
Normal file
201
packages/executor-test/src/runner.ts
Normal file
|
|
@ -0,0 +1,201 @@
|
||||||
|
import pc from 'picocolors';
|
||||||
|
import { runCoreTests } from './tests/core.js';
|
||||||
|
import { runStandardTests } from './tests/standard.js';
|
||||||
|
import type { ComplianceResult, TestSuite } from './types.js';
|
||||||
|
|
||||||
|
const VERSION = '0.1.0';
|
||||||
|
const PROTOCOL_VERSION = '1.0';
|
||||||
|
|
||||||
|
function printUsage(): void {
|
||||||
|
console.log(`
|
||||||
|
${pc.bold('TPMJS Executor Compliance Test')} v${VERSION}
|
||||||
|
|
||||||
|
${pc.dim('Usage:')}
|
||||||
|
npx @tpmjs/executor-test <executor-url> [options]
|
||||||
|
|
||||||
|
${pc.dim('Options:')}
|
||||||
|
--api-key <key> API key for authentication (Bearer token)
|
||||||
|
--json Output results as JSON
|
||||||
|
--verbose Show detailed test output
|
||||||
|
--help Show this help message
|
||||||
|
|
||||||
|
${pc.dim('Examples:')}
|
||||||
|
npx @tpmjs/executor-test https://my-executor.example.com
|
||||||
|
npx @tpmjs/executor-test https://my-executor.example.com --api-key sk-xxx
|
||||||
|
npx @tpmjs/executor-test https://my-executor.example.com --json
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function printBanner(target: string): void {
|
||||||
|
console.log();
|
||||||
|
console.log(pc.bold(`TPMJS Executor Compliance Test v${VERSION}`));
|
||||||
|
console.log(pc.dim(`Protocol Version: ${PROTOCOL_VERSION}`));
|
||||||
|
console.log(pc.dim(`Target: ${target}`));
|
||||||
|
console.log();
|
||||||
|
}
|
||||||
|
|
||||||
|
function printSuite(suite: TestSuite): void {
|
||||||
|
const levelLabel =
|
||||||
|
suite.level === 'core'
|
||||||
|
? pc.blue('Core')
|
||||||
|
: suite.level === 'standard'
|
||||||
|
? pc.yellow('Standard')
|
||||||
|
: pc.magenta('Extended');
|
||||||
|
|
||||||
|
console.log(`${levelLabel} ${pc.bold(suite.name)}:`);
|
||||||
|
|
||||||
|
for (const result of suite.results) {
|
||||||
|
const icon = result.passed ? pc.green('\u2713') : pc.red('\u2717');
|
||||||
|
const name = result.passed ? result.name : pc.red(result.name);
|
||||||
|
const duration = pc.dim(`(${result.durationMs}ms)`);
|
||||||
|
|
||||||
|
console.log(` ${icon} ${name} ${duration}`);
|
||||||
|
|
||||||
|
if (!result.passed && result.message) {
|
||||||
|
console.log(` ${pc.dim(result.message)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log();
|
||||||
|
}
|
||||||
|
|
||||||
|
function printSummary(result: ComplianceResult): void {
|
||||||
|
const { summary } = result;
|
||||||
|
|
||||||
|
console.log(pc.bold('Summary:'));
|
||||||
|
console.log(
|
||||||
|
` Tests: ${pc.green(`${summary.passed} passed`)}, ${summary.failed > 0 ? pc.red(`${summary.failed} failed`) : `${summary.failed} failed`}, ${summary.totalTests} total`
|
||||||
|
);
|
||||||
|
console.log();
|
||||||
|
|
||||||
|
const coreStatus = summary.coreCompliant ? pc.green('PASS') : pc.red('FAIL');
|
||||||
|
const standardStatus = summary.standardCompliant
|
||||||
|
? pc.green('PASS')
|
||||||
|
: summary.coreCompliant
|
||||||
|
? pc.yellow('PARTIAL')
|
||||||
|
: pc.red('FAIL');
|
||||||
|
|
||||||
|
console.log(` Core Compliance: ${coreStatus}`);
|
||||||
|
console.log(` Standard Compliance: ${standardStatus}`);
|
||||||
|
console.log();
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Options {
|
||||||
|
apiKey?: string;
|
||||||
|
json: boolean;
|
||||||
|
verbose: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseArgs(args: string[]): { url: string | null; options: Options } {
|
||||||
|
const options: Options = {
|
||||||
|
json: false,
|
||||||
|
verbose: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
let url: string | null = null;
|
||||||
|
|
||||||
|
for (let i = 0; i < args.length; i++) {
|
||||||
|
const arg = args[i];
|
||||||
|
|
||||||
|
if (arg === '--help' || arg === '-h') {
|
||||||
|
printUsage();
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (arg === '--json') {
|
||||||
|
options.json = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (arg === '--verbose' || arg === '-v') {
|
||||||
|
options.verbose = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (arg === '--api-key') {
|
||||||
|
i++;
|
||||||
|
options.apiKey = args[i] ?? undefined;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!arg?.startsWith('-') && !url) {
|
||||||
|
url = arg ?? null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { url, options };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function run(args: string[]): Promise<void> {
|
||||||
|
const { url, options } = parseArgs(args);
|
||||||
|
|
||||||
|
if (!url) {
|
||||||
|
printUsage();
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize URL
|
||||||
|
const target = url.replace(/\/$/, '');
|
||||||
|
|
||||||
|
if (!options.json) {
|
||||||
|
printBanner(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
const suites: TestSuite[] = [];
|
||||||
|
|
||||||
|
// Run Core tests
|
||||||
|
if (!options.json) {
|
||||||
|
console.log(pc.dim('Running Core compliance tests...'));
|
||||||
|
console.log();
|
||||||
|
}
|
||||||
|
|
||||||
|
const coreSuite = await runCoreTests(target, options.apiKey);
|
||||||
|
suites.push(coreSuite);
|
||||||
|
|
||||||
|
if (!options.json) {
|
||||||
|
printSuite(coreSuite);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run Standard tests
|
||||||
|
if (!options.json) {
|
||||||
|
console.log(pc.dim('Running Standard compliance tests...'));
|
||||||
|
console.log();
|
||||||
|
}
|
||||||
|
|
||||||
|
const standardSuite = await runStandardTests(target, options.apiKey);
|
||||||
|
suites.push(standardSuite);
|
||||||
|
|
||||||
|
if (!options.json) {
|
||||||
|
printSuite(standardSuite);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate summary
|
||||||
|
const totalTests = suites.reduce((sum, s) => sum + s.results.length, 0);
|
||||||
|
const passed = suites.reduce((sum, s) => sum + s.results.filter((r) => r.passed).length, 0);
|
||||||
|
const failed = totalTests - passed;
|
||||||
|
|
||||||
|
const coreCompliant = coreSuite.results.every((r) => r.passed);
|
||||||
|
const standardCompliant = coreCompliant && standardSuite.results.every((r) => r.passed);
|
||||||
|
|
||||||
|
const result: ComplianceResult = {
|
||||||
|
target,
|
||||||
|
protocolVersion: PROTOCOL_VERSION,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
suites,
|
||||||
|
summary: {
|
||||||
|
totalTests,
|
||||||
|
passed,
|
||||||
|
failed,
|
||||||
|
coreCompliant,
|
||||||
|
standardCompliant,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (options.json) {
|
||||||
|
console.log(JSON.stringify(result, null, 2));
|
||||||
|
} else {
|
||||||
|
printSummary(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exit with error code if not compliant
|
||||||
|
process.exit(coreCompliant ? 0 : 1);
|
||||||
|
}
|
||||||
417
packages/executor-test/src/tests/core.ts
Normal file
417
packages/executor-test/src/tests/core.ts
Normal file
|
|
@ -0,0 +1,417 @@
|
||||||
|
import type { ExecuteToolResponse, HealthResponse, TestResult, TestSuite } from '../types.js';
|
||||||
|
|
||||||
|
async function testHealthReturns200(baseUrl: string, _apiKey?: string): Promise<TestResult> {
|
||||||
|
const start = Date.now();
|
||||||
|
const name = 'GET /health returns 200';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${baseUrl}/health`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'X-TPMJS-Protocol-Version': '1.0',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const durationMs = Date.now() - start;
|
||||||
|
|
||||||
|
if (response.status === 200) {
|
||||||
|
return { name, passed: true, durationMs };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
passed: false,
|
||||||
|
message: `Expected status 200, got ${response.status}`,
|
||||||
|
durationMs,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
passed: false,
|
||||||
|
message: `Request failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
||||||
|
durationMs: Date.now() - start,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testHealthIncludesProtocolVersion(
|
||||||
|
baseUrl: string,
|
||||||
|
_apiKey?: string
|
||||||
|
): Promise<TestResult> {
|
||||||
|
const start = Date.now();
|
||||||
|
const name = 'GET /health includes protocolVersion';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${baseUrl}/health`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'X-TPMJS-Protocol-Version': '1.0',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = (await response.json()) as HealthResponse;
|
||||||
|
const durationMs = Date.now() - start;
|
||||||
|
|
||||||
|
if (data.protocolVersion) {
|
||||||
|
return { name, passed: true, durationMs };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
passed: false,
|
||||||
|
message: 'Response missing protocolVersion field',
|
||||||
|
durationMs,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
passed: false,
|
||||||
|
message: `Request failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
||||||
|
durationMs: Date.now() - start,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testHealthIncludesImplementationVersion(
|
||||||
|
baseUrl: string,
|
||||||
|
_apiKey?: string
|
||||||
|
): Promise<TestResult> {
|
||||||
|
const start = Date.now();
|
||||||
|
const name = 'GET /health includes implementationVersion';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${baseUrl}/health`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'X-TPMJS-Protocol-Version': '1.0',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = (await response.json()) as HealthResponse;
|
||||||
|
const durationMs = Date.now() - start;
|
||||||
|
|
||||||
|
if (data.implementationVersion) {
|
||||||
|
return { name, passed: true, durationMs };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
passed: false,
|
||||||
|
message: 'Response missing implementationVersion field',
|
||||||
|
durationMs,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
passed: false,
|
||||||
|
message: `Request failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
||||||
|
durationMs: Date.now() - start,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testExecuteToolAcceptsValidRequest(
|
||||||
|
baseUrl: string,
|
||||||
|
apiKey?: string
|
||||||
|
): Promise<TestResult> {
|
||||||
|
const start = Date.now();
|
||||||
|
const name = 'POST /execute-tool accepts valid request';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const headers: Record<string, string> = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-TPMJS-Protocol-Version': '1.0',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (apiKey) {
|
||||||
|
headers['Authorization'] = `Bearer ${apiKey}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(`${baseUrl}/execute-tool`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers,
|
||||||
|
body: JSON.stringify({
|
||||||
|
packageName: '@anthropic-ai/sdk',
|
||||||
|
name: 'default',
|
||||||
|
params: {},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const durationMs = Date.now() - start;
|
||||||
|
|
||||||
|
// We expect 200 even if the tool fails
|
||||||
|
if (response.status === 200) {
|
||||||
|
return { name, passed: true, durationMs };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 401 is acceptable if auth is required and not provided
|
||||||
|
if (response.status === 401 && !apiKey) {
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
passed: true,
|
||||||
|
message: 'Authentication required (expected)',
|
||||||
|
durationMs,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
passed: false,
|
||||||
|
message: `Expected status 200, got ${response.status}`,
|
||||||
|
durationMs,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
passed: false,
|
||||||
|
message: `Request failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
||||||
|
durationMs: Date.now() - start,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testExecuteToolReturnsStructuredResponse(
|
||||||
|
baseUrl: string,
|
||||||
|
apiKey?: string
|
||||||
|
): Promise<TestResult> {
|
||||||
|
const start = Date.now();
|
||||||
|
const name = 'POST /execute-tool returns structured response';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const headers: Record<string, string> = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-TPMJS-Protocol-Version': '1.0',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (apiKey) {
|
||||||
|
headers['Authorization'] = `Bearer ${apiKey}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(`${baseUrl}/execute-tool`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers,
|
||||||
|
body: JSON.stringify({
|
||||||
|
packageName: '@anthropic-ai/sdk',
|
||||||
|
name: 'default',
|
||||||
|
params: {},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const durationMs = Date.now() - start;
|
||||||
|
|
||||||
|
// Skip if auth required
|
||||||
|
if (response.status === 401 && !apiKey) {
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
passed: true,
|
||||||
|
message: 'Skipped: Authentication required',
|
||||||
|
durationMs,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = (await response.json()) as ExecuteToolResponse;
|
||||||
|
|
||||||
|
// Check required fields
|
||||||
|
if (typeof data.success !== 'boolean') {
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
passed: false,
|
||||||
|
message: 'Response missing "success" boolean field',
|
||||||
|
durationMs,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof data.executionTimeMs !== 'number') {
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
passed: false,
|
||||||
|
message: 'Response missing "executionTimeMs" number field',
|
||||||
|
durationMs,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.success && data.output === undefined) {
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
passed: false,
|
||||||
|
message: 'Successful response missing "output" field',
|
||||||
|
durationMs,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data.success && !data.error) {
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
passed: false,
|
||||||
|
message: 'Error response missing "error" field',
|
||||||
|
durationMs,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { name, passed: true, durationMs };
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
passed: false,
|
||||||
|
message: `Request failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
||||||
|
durationMs: Date.now() - start,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testExecuteToolReturnsErrorForInvalidTool(
|
||||||
|
baseUrl: string,
|
||||||
|
apiKey?: string
|
||||||
|
): Promise<TestResult> {
|
||||||
|
const start = Date.now();
|
||||||
|
const name = 'POST /execute-tool returns error for invalid tool';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const headers: Record<string, string> = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-TPMJS-Protocol-Version': '1.0',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (apiKey) {
|
||||||
|
headers['Authorization'] = `Bearer ${apiKey}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(`${baseUrl}/execute-tool`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers,
|
||||||
|
body: JSON.stringify({
|
||||||
|
packageName: '@tpmjs/nonexistent-package-12345',
|
||||||
|
name: 'nonexistentTool',
|
||||||
|
params: {},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const durationMs = Date.now() - start;
|
||||||
|
|
||||||
|
// Skip if auth required
|
||||||
|
if (response.status === 401 && !apiKey) {
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
passed: true,
|
||||||
|
message: 'Skipped: Authentication required',
|
||||||
|
durationMs,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = (await response.json()) as ExecuteToolResponse;
|
||||||
|
|
||||||
|
if (data.success === false && data.error?.code) {
|
||||||
|
return { name, passed: true, durationMs };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
passed: false,
|
||||||
|
message: 'Expected error response with code for nonexistent package',
|
||||||
|
durationMs,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
passed: false,
|
||||||
|
message: `Request failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
||||||
|
durationMs: Date.now() - start,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testCORSHeaders(baseUrl: string, _apiKey?: string): Promise<TestResult> {
|
||||||
|
const start = Date.now();
|
||||||
|
const name = 'CORS headers present';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${baseUrl}/health`, {
|
||||||
|
method: 'GET',
|
||||||
|
});
|
||||||
|
|
||||||
|
const durationMs = Date.now() - start;
|
||||||
|
|
||||||
|
const allowOrigin = response.headers.get('access-control-allow-origin');
|
||||||
|
|
||||||
|
if (allowOrigin) {
|
||||||
|
return { name, passed: true, durationMs };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
passed: false,
|
||||||
|
message: 'Missing Access-Control-Allow-Origin header',
|
||||||
|
durationMs,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
passed: false,
|
||||||
|
message: `Request failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
||||||
|
durationMs: Date.now() - start,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testOPTIONSPreflight(baseUrl: string, _apiKey?: string): Promise<TestResult> {
|
||||||
|
const start = Date.now();
|
||||||
|
const name = 'OPTIONS preflight works';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${baseUrl}/execute-tool`, {
|
||||||
|
method: 'OPTIONS',
|
||||||
|
});
|
||||||
|
|
||||||
|
const durationMs = Date.now() - start;
|
||||||
|
|
||||||
|
if (response.status === 200 || response.status === 204) {
|
||||||
|
const allowMethods = response.headers.get('access-control-allow-methods');
|
||||||
|
const allowHeaders = response.headers.get('access-control-allow-headers');
|
||||||
|
|
||||||
|
if (allowMethods && allowHeaders) {
|
||||||
|
return { name, passed: true, durationMs };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
passed: false,
|
||||||
|
message: 'Missing CORS preflight headers',
|
||||||
|
durationMs,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
passed: false,
|
||||||
|
message: `Expected status 200 or 204, got ${response.status}`,
|
||||||
|
durationMs,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
passed: false,
|
||||||
|
message: `Request failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
||||||
|
durationMs: Date.now() - start,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function runCoreTests(baseUrl: string, apiKey?: string): Promise<TestSuite> {
|
||||||
|
const results: TestResult[] = [];
|
||||||
|
|
||||||
|
// Run tests sequentially to avoid overwhelming the executor
|
||||||
|
results.push(await testHealthReturns200(baseUrl, apiKey));
|
||||||
|
results.push(await testHealthIncludesProtocolVersion(baseUrl, apiKey));
|
||||||
|
results.push(await testHealthIncludesImplementationVersion(baseUrl, apiKey));
|
||||||
|
results.push(await testExecuteToolAcceptsValidRequest(baseUrl, apiKey));
|
||||||
|
results.push(await testExecuteToolReturnsStructuredResponse(baseUrl, apiKey));
|
||||||
|
results.push(await testExecuteToolReturnsErrorForInvalidTool(baseUrl, apiKey));
|
||||||
|
results.push(await testCORSHeaders(baseUrl, apiKey));
|
||||||
|
results.push(await testOPTIONSPreflight(baseUrl, apiKey));
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: 'Core Requirements',
|
||||||
|
level: 'core',
|
||||||
|
results,
|
||||||
|
};
|
||||||
|
}
|
||||||
434
packages/executor-test/src/tests/standard.ts
Normal file
434
packages/executor-test/src/tests/standard.ts
Normal file
|
|
@ -0,0 +1,434 @@
|
||||||
|
import type { InfoResponse, TestResult, TestSuite } from '../types.js';
|
||||||
|
|
||||||
|
async function testInfoReturns200(baseUrl: string, _apiKey?: string): Promise<TestResult> {
|
||||||
|
const start = Date.now();
|
||||||
|
const name = 'GET /info returns 200';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${baseUrl}/info`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'X-TPMJS-Protocol-Version': '1.0',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const durationMs = Date.now() - start;
|
||||||
|
|
||||||
|
if (response.status === 200) {
|
||||||
|
return { name, passed: true, durationMs };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.status === 404) {
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
passed: false,
|
||||||
|
message: '/info endpoint not implemented (optional for Level 1)',
|
||||||
|
durationMs,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
passed: false,
|
||||||
|
message: `Expected status 200, got ${response.status}`,
|
||||||
|
durationMs,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
passed: false,
|
||||||
|
message: `Request failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
||||||
|
durationMs: Date.now() - start,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testInfoIncludesCapabilities(
|
||||||
|
baseUrl: string,
|
||||||
|
_apiKey?: string
|
||||||
|
): Promise<TestResult> {
|
||||||
|
const start = Date.now();
|
||||||
|
const name = 'GET /info includes capabilities';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${baseUrl}/info`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'X-TPMJS-Protocol-Version': '1.0',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const durationMs = Date.now() - start;
|
||||||
|
|
||||||
|
if (response.status === 404) {
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
passed: false,
|
||||||
|
message: '/info endpoint not implemented',
|
||||||
|
durationMs,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = (await response.json()) as InfoResponse;
|
||||||
|
|
||||||
|
if (!data.capabilities) {
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
passed: false,
|
||||||
|
message: 'Response missing "capabilities" field',
|
||||||
|
durationMs,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const required = ['isolation', 'executionModes', 'maxExecutionTimeMs', 'maxRequestBodyBytes'];
|
||||||
|
const missing = required.filter(
|
||||||
|
(key) => data.capabilities[key as keyof typeof data.capabilities] === undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
if (missing.length > 0) {
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
passed: false,
|
||||||
|
message: `Missing capability fields: ${missing.join(', ')}`,
|
||||||
|
durationMs,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { name, passed: true, durationMs };
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
passed: false,
|
||||||
|
message: `Request failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
||||||
|
durationMs: Date.now() - start,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testInfoIncludesProtocolVersion(
|
||||||
|
baseUrl: string,
|
||||||
|
_apiKey?: string
|
||||||
|
): Promise<TestResult> {
|
||||||
|
const start = Date.now();
|
||||||
|
const name = 'GET /info includes protocolVersion';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${baseUrl}/info`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'X-TPMJS-Protocol-Version': '1.0',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const durationMs = Date.now() - start;
|
||||||
|
|
||||||
|
if (response.status === 404) {
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
passed: false,
|
||||||
|
message: '/info endpoint not implemented',
|
||||||
|
durationMs,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = (await response.json()) as InfoResponse;
|
||||||
|
|
||||||
|
if (data.protocolVersion) {
|
||||||
|
return { name, passed: true, durationMs };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
passed: false,
|
||||||
|
message: 'Response missing "protocolVersion" field',
|
||||||
|
durationMs,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
passed: false,
|
||||||
|
message: `Request failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
||||||
|
durationMs: Date.now() - start,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testInfoIsolationLevel(baseUrl: string, _apiKey?: string): Promise<TestResult> {
|
||||||
|
const start = Date.now();
|
||||||
|
const name = 'capabilities.isolation is valid';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${baseUrl}/info`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'X-TPMJS-Protocol-Version': '1.0',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const durationMs = Date.now() - start;
|
||||||
|
|
||||||
|
if (response.status === 404) {
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
passed: false,
|
||||||
|
message: '/info endpoint not implemented',
|
||||||
|
durationMs,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = (await response.json()) as InfoResponse;
|
||||||
|
const validLevels = ['none', 'process', 'container', 'vm'];
|
||||||
|
|
||||||
|
if (validLevels.includes(data.capabilities?.isolation)) {
|
||||||
|
return { name, passed: true, durationMs };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
passed: false,
|
||||||
|
message: `Invalid isolation level: ${data.capabilities?.isolation}. Expected one of: ${validLevels.join(', ')}`,
|
||||||
|
durationMs,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
passed: false,
|
||||||
|
message: `Request failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
||||||
|
durationMs: Date.now() - start,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testAuthenticationEnforced(baseUrl: string, apiKey?: string): Promise<TestResult> {
|
||||||
|
const start = Date.now();
|
||||||
|
const name = 'Authentication enforced when configured';
|
||||||
|
|
||||||
|
try {
|
||||||
|
// First, make a request without auth to see if it's required
|
||||||
|
const noAuthResponse = await fetch(`${baseUrl}/execute-tool`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-TPMJS-Protocol-Version': '1.0',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
packageName: '@anthropic-ai/sdk',
|
||||||
|
name: 'default',
|
||||||
|
params: {},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const durationMs = Date.now() - start;
|
||||||
|
|
||||||
|
// If no auth is required, that's fine
|
||||||
|
if (noAuthResponse.status !== 401) {
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
passed: true,
|
||||||
|
message: 'No authentication required',
|
||||||
|
durationMs,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// If 401 without auth, verify it works with auth
|
||||||
|
if (!apiKey) {
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
passed: true,
|
||||||
|
message: 'Authentication required (provide --api-key to fully test)',
|
||||||
|
durationMs,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try with auth
|
||||||
|
const authResponse = await fetch(`${baseUrl}/execute-tool`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-TPMJS-Protocol-Version': '1.0',
|
||||||
|
Authorization: `Bearer ${apiKey}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
packageName: '@anthropic-ai/sdk',
|
||||||
|
name: 'default',
|
||||||
|
params: {},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (authResponse.status === 200) {
|
||||||
|
return { name, passed: true, durationMs: Date.now() - start };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
passed: false,
|
||||||
|
message: `Request with API key still failed: ${authResponse.status}`,
|
||||||
|
durationMs: Date.now() - start,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
passed: false,
|
||||||
|
message: `Request failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
||||||
|
durationMs: Date.now() - start,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testExecutionTimeoutEnforced(
|
||||||
|
baseUrl: string,
|
||||||
|
_apiKey?: string
|
||||||
|
): Promise<TestResult> {
|
||||||
|
const start = Date.now();
|
||||||
|
const name = 'Execution timeout enforcement';
|
||||||
|
|
||||||
|
// This test just verifies the capability is advertised
|
||||||
|
// Actual timeout testing would require a tool that hangs
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${baseUrl}/info`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'X-TPMJS-Protocol-Version': '1.0',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const durationMs = Date.now() - start;
|
||||||
|
|
||||||
|
if (response.status === 404) {
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
passed: false,
|
||||||
|
message: '/info endpoint not implemented (cannot verify timeout)',
|
||||||
|
durationMs,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = (await response.json()) as InfoResponse;
|
||||||
|
|
||||||
|
if (data.capabilities?.maxExecutionTimeMs && data.capabilities.maxExecutionTimeMs >= 60000) {
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
passed: true,
|
||||||
|
message: `maxExecutionTimeMs: ${data.capabilities.maxExecutionTimeMs}ms`,
|
||||||
|
durationMs,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
passed: false,
|
||||||
|
message: `maxExecutionTimeMs should be at least 60000ms, got: ${data.capabilities?.maxExecutionTimeMs}`,
|
||||||
|
durationMs,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
passed: false,
|
||||||
|
message: `Request failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
||||||
|
durationMs: Date.now() - start,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testStructuredErrorCodes(baseUrl: string, apiKey?: string): Promise<TestResult> {
|
||||||
|
const start = Date.now();
|
||||||
|
const name = 'Structured error codes';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const headers: Record<string, string> = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-TPMJS-Protocol-Version': '1.0',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (apiKey) {
|
||||||
|
headers['Authorization'] = `Bearer ${apiKey}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(`${baseUrl}/execute-tool`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers,
|
||||||
|
body: JSON.stringify({
|
||||||
|
packageName: '@tpmjs/nonexistent-package-xyz-12345',
|
||||||
|
name: 'nonexistentTool',
|
||||||
|
params: {},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const durationMs = Date.now() - start;
|
||||||
|
|
||||||
|
// Skip if auth required
|
||||||
|
if (response.status === 401 && !apiKey) {
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
passed: true,
|
||||||
|
message: 'Skipped: Authentication required',
|
||||||
|
durationMs,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = (await response.json()) as {
|
||||||
|
success: boolean;
|
||||||
|
error?: { code: string; message: string };
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!data.error?.code) {
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
passed: false,
|
||||||
|
message: 'Error response missing "code" field',
|
||||||
|
durationMs,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const validCodes = [
|
||||||
|
'PACKAGE_NOT_FOUND',
|
||||||
|
'TOOL_NOT_FOUND',
|
||||||
|
'TOOL_INVALID',
|
||||||
|
'TOOL_EXECUTION_ERROR',
|
||||||
|
'EXECUTION_TIMEOUT',
|
||||||
|
'INTERNAL_ERROR',
|
||||||
|
];
|
||||||
|
|
||||||
|
if (validCodes.includes(data.error.code)) {
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
passed: true,
|
||||||
|
message: `Error code: ${data.error.code}`,
|
||||||
|
durationMs,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
passed: false,
|
||||||
|
message: `Non-standard error code: ${data.error.code}`,
|
||||||
|
durationMs,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
passed: false,
|
||||||
|
message: `Request failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
||||||
|
durationMs: Date.now() - start,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function runStandardTests(baseUrl: string, apiKey?: string): Promise<TestSuite> {
|
||||||
|
const results: TestResult[] = [];
|
||||||
|
|
||||||
|
// Run tests sequentially
|
||||||
|
results.push(await testInfoReturns200(baseUrl, apiKey));
|
||||||
|
results.push(await testInfoIncludesCapabilities(baseUrl, apiKey));
|
||||||
|
results.push(await testInfoIncludesProtocolVersion(baseUrl, apiKey));
|
||||||
|
results.push(await testInfoIsolationLevel(baseUrl, apiKey));
|
||||||
|
results.push(await testAuthenticationEnforced(baseUrl, apiKey));
|
||||||
|
results.push(await testExecutionTimeoutEnforced(baseUrl, apiKey));
|
||||||
|
results.push(await testStructuredErrorCodes(baseUrl, apiKey));
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: 'Standard Requirements',
|
||||||
|
level: 'standard',
|
||||||
|
results,
|
||||||
|
};
|
||||||
|
}
|
||||||
72
packages/executor-test/src/types.ts
Normal file
72
packages/executor-test/src/types.ts
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
export interface TestResult {
|
||||||
|
name: string;
|
||||||
|
passed: boolean;
|
||||||
|
message?: string;
|
||||||
|
durationMs: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TestSuite {
|
||||||
|
name: string;
|
||||||
|
level: 'core' | 'standard' | 'extended';
|
||||||
|
results: TestResult[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ComplianceResult {
|
||||||
|
target: string;
|
||||||
|
protocolVersion: string;
|
||||||
|
timestamp: string;
|
||||||
|
suites: TestSuite[];
|
||||||
|
summary: {
|
||||||
|
totalTests: number;
|
||||||
|
passed: number;
|
||||||
|
failed: number;
|
||||||
|
coreCompliant: boolean;
|
||||||
|
standardCompliant: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HealthResponse {
|
||||||
|
status: string;
|
||||||
|
protocolVersion: string;
|
||||||
|
implementationVersion: string;
|
||||||
|
runtime?: string;
|
||||||
|
timestamp?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ExecuteToolRequest {
|
||||||
|
packageName: string;
|
||||||
|
version?: string;
|
||||||
|
name: string;
|
||||||
|
params?: Record<string, unknown>;
|
||||||
|
env?: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ExecuteToolResponse {
|
||||||
|
success: boolean;
|
||||||
|
output?: unknown;
|
||||||
|
error?: {
|
||||||
|
code: string;
|
||||||
|
message: string;
|
||||||
|
};
|
||||||
|
executionTimeMs: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InfoResponse {
|
||||||
|
name: string;
|
||||||
|
version: string;
|
||||||
|
protocolVersion: string;
|
||||||
|
capabilities: {
|
||||||
|
isolation: 'none' | 'process' | 'container' | 'vm';
|
||||||
|
executionModes: string[];
|
||||||
|
maxExecutionTimeMs: number;
|
||||||
|
maxRequestBodyBytes: number;
|
||||||
|
supportsStreaming?: boolean;
|
||||||
|
supportsCallbacks?: boolean;
|
||||||
|
supportsCaching?: boolean;
|
||||||
|
};
|
||||||
|
runtime?: {
|
||||||
|
platform?: string;
|
||||||
|
nodeVersion?: string;
|
||||||
|
region?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
11
packages/executor-test/tsconfig.json
Normal file
11
packages/executor-test/tsconfig.json
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"extends": "@tpmjs/tsconfig/base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "dist",
|
||||||
|
"rootDir": "src",
|
||||||
|
"incremental": false,
|
||||||
|
"composite": false
|
||||||
|
},
|
||||||
|
"include": ["src"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
9
packages/executor-test/tsup.config.ts
Normal file
9
packages/executor-test/tsup.config.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { defineConfig } from 'tsup';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
entry: ['src/index.ts'],
|
||||||
|
format: ['esm'],
|
||||||
|
dts: true,
|
||||||
|
clean: true,
|
||||||
|
sourcemap: true,
|
||||||
|
});
|
||||||
33
pnpm-lock.yaml
generated
33
pnpm-lock.yaml
generated
|
|
@ -607,6 +607,31 @@ importers:
|
||||||
specifier: ^5.9.3
|
specifier: ^5.9.3
|
||||||
version: 5.9.3
|
version: 5.9.3
|
||||||
|
|
||||||
|
packages/executor-test:
|
||||||
|
dependencies:
|
||||||
|
picocolors:
|
||||||
|
specifier: ^1.1.1
|
||||||
|
version: 1.1.1
|
||||||
|
devDependencies:
|
||||||
|
'@tpmjs/test':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../test
|
||||||
|
'@tpmjs/tsconfig':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../config/tsconfig
|
||||||
|
'@types/node':
|
||||||
|
specifier: ^22.15.29
|
||||||
|
version: 22.19.5
|
||||||
|
tsup:
|
||||||
|
specifier: ^8.5.1
|
||||||
|
version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2)
|
||||||
|
typescript:
|
||||||
|
specifier: ^5.9.3
|
||||||
|
version: 5.9.3
|
||||||
|
vitest:
|
||||||
|
specifier: ^4.0.16
|
||||||
|
version: 4.0.16(@opentelemetry/api@1.9.0)(@types/node@22.19.5)(happy-dom@20.1.0)(jiti@2.6.1)(jsdom@27.4.0)(lightningcss@1.30.2)(msw@2.12.7(@types/node@22.19.5)(typescript@5.9.3))(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)
|
||||||
|
|
||||||
packages/mcp-client:
|
packages/mcp-client:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@modelcontextprotocol/sdk':
|
'@modelcontextprotocol/sdk':
|
||||||
|
|
@ -15590,14 +15615,14 @@ snapshots:
|
||||||
'@remotion/media-parser': 4.0.409
|
'@remotion/media-parser': 4.0.409
|
||||||
'@remotion/studio': 4.0.409(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
'@remotion/studio': 4.0.409(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||||
'@remotion/studio-shared': 4.0.409(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
'@remotion/studio-shared': 4.0.409(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||||
css-loader: 5.2.7(webpack@5.96.1)
|
css-loader: 5.2.7(webpack@5.96.1(esbuild@0.25.0))
|
||||||
esbuild: 0.25.0
|
esbuild: 0.25.0
|
||||||
react: 19.2.3
|
react: 19.2.3
|
||||||
react-dom: 19.2.3(react@19.2.3)
|
react-dom: 19.2.3(react@19.2.3)
|
||||||
react-refresh: 0.9.0
|
react-refresh: 0.9.0
|
||||||
remotion: 4.0.409(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
remotion: 4.0.409(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||||
source-map: 0.7.3
|
source-map: 0.7.3
|
||||||
style-loader: 4.0.0(webpack@5.96.1)
|
style-loader: 4.0.0(webpack@5.96.1(esbuild@0.25.0))
|
||||||
webpack: 5.96.1(esbuild@0.25.0)
|
webpack: 5.96.1(esbuild@0.25.0)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@swc/core'
|
- '@swc/core'
|
||||||
|
|
@ -17975,7 +18000,7 @@ snapshots:
|
||||||
shebang-command: 2.0.0
|
shebang-command: 2.0.0
|
||||||
which: 2.0.2
|
which: 2.0.2
|
||||||
|
|
||||||
css-loader@5.2.7(webpack@5.96.1):
|
css-loader@5.2.7(webpack@5.96.1(esbuild@0.25.0)):
|
||||||
dependencies:
|
dependencies:
|
||||||
icss-utils: 5.1.0(postcss@8.5.6)
|
icss-utils: 5.1.0(postcss@8.5.6)
|
||||||
loader-utils: 2.0.4
|
loader-utils: 2.0.4
|
||||||
|
|
@ -22809,7 +22834,7 @@ snapshots:
|
||||||
|
|
||||||
stubborn-utils@1.0.2: {}
|
stubborn-utils@1.0.2: {}
|
||||||
|
|
||||||
style-loader@4.0.0(webpack@5.96.1):
|
style-loader@4.0.0(webpack@5.96.1(esbuild@0.25.0)):
|
||||||
dependencies:
|
dependencies:
|
||||||
webpack: 5.96.1(esbuild@0.25.0)
|
webpack: 5.96.1(esbuild@0.25.0)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,11 +16,15 @@ const path = require('node:path');
|
||||||
const PORT = process.env.PORT || 3000;
|
const PORT = process.env.PORT || 3000;
|
||||||
const API_KEY = process.env.EXECUTOR_API_KEY || null;
|
const API_KEY = process.env.EXECUTOR_API_KEY || null;
|
||||||
|
|
||||||
|
// Protocol constants
|
||||||
|
const PROTOCOL_VERSION = '1.0';
|
||||||
|
const IMPLEMENTATION_VERSION = '1.0.0';
|
||||||
|
|
||||||
// CORS headers for cross-origin requests
|
// CORS headers for cross-origin requests
|
||||||
const corsHeaders = {
|
const corsHeaders = {
|
||||||
'Access-Control-Allow-Origin': '*',
|
'Access-Control-Allow-Origin': '*',
|
||||||
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
||||||
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
|
'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-TPMJS-Protocol-Version',
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -69,11 +73,34 @@ function parseBody(req) {
|
||||||
function handleHealth(_req, res) {
|
function handleHealth(_req, res) {
|
||||||
jsonResponse(res, 200, {
|
jsonResponse(res, 200, {
|
||||||
status: 'ok',
|
status: 'ok',
|
||||||
version: '1.0.0',
|
protocolVersion: PROTOCOL_VERSION,
|
||||||
info: {
|
implementationVersion: IMPLEMENTATION_VERSION,
|
||||||
runtime: 'railway',
|
runtime: 'node',
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
region: process.env.RAILWAY_REGION || 'unknown',
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /info - Capability advertisement endpoint
|
||||||
|
*/
|
||||||
|
function handleInfo(_req, res) {
|
||||||
|
jsonResponse(res, 200, {
|
||||||
|
name: 'Railway Executor',
|
||||||
|
version: IMPLEMENTATION_VERSION,
|
||||||
|
protocolVersion: PROTOCOL_VERSION,
|
||||||
|
capabilities: {
|
||||||
|
isolation: 'process',
|
||||||
|
executionModes: ['sync'],
|
||||||
|
maxExecutionTimeMs: 120000,
|
||||||
|
maxRequestBodyBytes: 10485760,
|
||||||
|
supportsStreaming: false,
|
||||||
|
supportsCallbacks: false,
|
||||||
|
supportsCaching: false,
|
||||||
|
},
|
||||||
|
runtime: {
|
||||||
|
platform: process.platform,
|
||||||
|
nodeVersion: process.version,
|
||||||
|
region: process.env.RAILWAY_REGION || undefined,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -220,9 +247,20 @@ function parseExecutionResult(result, startTime) {
|
||||||
try {
|
try {
|
||||||
const errorObj = JSON.parse(result.stderr);
|
const errorObj = JSON.parse(result.stderr);
|
||||||
if (errorObj.__tpmjs_error__) {
|
if (errorObj.__tpmjs_error__) {
|
||||||
|
const errorMessage = errorObj.__tpmjs_error__;
|
||||||
|
// Determine error code based on message
|
||||||
|
let code = 'TOOL_EXECUTION_ERROR';
|
||||||
|
if (errorMessage.includes('not found in package')) {
|
||||||
|
code = 'TOOL_NOT_FOUND';
|
||||||
|
} else if (errorMessage.includes('does not have an execute()')) {
|
||||||
|
code = 'TOOL_INVALID';
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: errorObj.__tpmjs_error__,
|
error: {
|
||||||
|
code,
|
||||||
|
message: errorMessage,
|
||||||
|
},
|
||||||
executionTimeMs: Date.now() - startTime,
|
executionTimeMs: Date.now() - startTime,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -232,7 +270,10 @@ function parseExecutionResult(result, startTime) {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: result.stderr || `Script exited with code ${result.exitCode}`,
|
error: {
|
||||||
|
code: 'TOOL_EXECUTION_ERROR',
|
||||||
|
message: result.stderr || `Script exited with code ${result.exitCode}`,
|
||||||
|
},
|
||||||
executionTimeMs: Date.now() - startTime,
|
executionTimeMs: Date.now() - startTime,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -255,7 +296,6 @@ function parseExecutionResult(result, startTime) {
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
output: result.stdout || null,
|
output: result.stdout || null,
|
||||||
stderr: result.stderr || undefined,
|
|
||||||
executionTimeMs: Date.now() - startTime,
|
executionTimeMs: Date.now() - startTime,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -270,8 +310,10 @@ async function handleExecuteTool(req, res) {
|
||||||
if (!checkAuth(req)) {
|
if (!checkAuth(req)) {
|
||||||
return jsonResponse(res, 401, {
|
return jsonResponse(res, 401, {
|
||||||
success: false,
|
success: false,
|
||||||
error: 'Unauthorized',
|
error: {
|
||||||
executionTimeMs: Date.now() - startTime,
|
code: 'UNAUTHORIZED',
|
||||||
|
message: 'Invalid or missing API key',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -282,8 +324,10 @@ async function handleExecuteTool(req, res) {
|
||||||
} catch (_e) {
|
} catch (_e) {
|
||||||
return jsonResponse(res, 400, {
|
return jsonResponse(res, 400, {
|
||||||
success: false,
|
success: false,
|
||||||
error: 'Invalid JSON body',
|
error: {
|
||||||
executionTimeMs: Date.now() - startTime,
|
code: 'INVALID_REQUEST',
|
||||||
|
message: 'Invalid JSON body',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -293,8 +337,10 @@ async function handleExecuteTool(req, res) {
|
||||||
if (!packageName || !name) {
|
if (!packageName || !name) {
|
||||||
return jsonResponse(res, 400, {
|
return jsonResponse(res, 400, {
|
||||||
success: false,
|
success: false,
|
||||||
error: 'Missing required fields: packageName, name',
|
error: {
|
||||||
executionTimeMs: Date.now() - startTime,
|
code: 'INVALID_REQUEST',
|
||||||
|
message: 'Missing required fields: packageName, name',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -311,10 +357,12 @@ async function handleExecuteTool(req, res) {
|
||||||
} catch (installError) {
|
} catch (installError) {
|
||||||
console.error(`[executor] npm install failed:`, installError.message);
|
console.error(`[executor] npm install failed:`, installError.message);
|
||||||
cleanup(workDir);
|
cleanup(workDir);
|
||||||
return jsonResponse(res, 500, {
|
return jsonResponse(res, 200, {
|
||||||
success: false,
|
success: false,
|
||||||
error: `npm install failed: ${installError.message}`,
|
error: {
|
||||||
stderr: installError.stderr?.toString(),
|
code: 'PACKAGE_NOT_FOUND',
|
||||||
|
message: `npm install failed for ${packageSpec}: ${installError.message}`,
|
||||||
|
},
|
||||||
executionTimeMs: Date.now() - startTime,
|
executionTimeMs: Date.now() - startTime,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -338,9 +386,12 @@ async function handleExecuteTool(req, res) {
|
||||||
return jsonResponse(res, 200, parseExecutionResult(result, startTime));
|
return jsonResponse(res, 200, parseExecutionResult(result, startTime));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
cleanup(workDir);
|
cleanup(workDir);
|
||||||
return jsonResponse(res, 500, {
|
return jsonResponse(res, 200, {
|
||||||
success: false,
|
success: false,
|
||||||
error: error.message || String(error),
|
error: {
|
||||||
|
code: 'INTERNAL_ERROR',
|
||||||
|
message: error.message || String(error),
|
||||||
|
},
|
||||||
executionTimeMs: Date.now() - startTime,
|
executionTimeMs: Date.now() - startTime,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -364,6 +415,10 @@ const server = http.createServer(async (req, res) => {
|
||||||
return handleHealth(req, res);
|
return handleHealth(req, res);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ((pathname === '/api/info' || pathname === '/info') && req.method === 'GET') {
|
||||||
|
return handleInfo(req, res);
|
||||||
|
}
|
||||||
|
|
||||||
if ((pathname === '/api/execute-tool' || pathname === '/execute-tool') && req.method === 'POST') {
|
if ((pathname === '/api/execute-tool' || pathname === '/execute-tool') && req.method === 'POST') {
|
||||||
return handleExecuteTool(req, res);
|
return handleExecuteTool(req, res);
|
||||||
}
|
}
|
||||||
|
|
@ -371,11 +426,12 @@ const server = http.createServer(async (req, res) => {
|
||||||
// Root path - simple info
|
// Root path - simple info
|
||||||
if (pathname === '/' && req.method === 'GET') {
|
if (pathname === '/' && req.method === 'GET') {
|
||||||
return jsonResponse(res, 200, {
|
return jsonResponse(res, 200, {
|
||||||
name: 'TPMJS Executor',
|
name: 'TPMJS Railway Executor',
|
||||||
version: '1.0.0',
|
version: IMPLEMENTATION_VERSION,
|
||||||
runtime: 'railway',
|
protocolVersion: PROTOCOL_VERSION,
|
||||||
endpoints: {
|
endpoints: {
|
||||||
health: 'GET /health',
|
health: 'GET /health',
|
||||||
|
info: 'GET /info',
|
||||||
execute: 'POST /execute-tool',
|
execute: 'POST /execute-tool',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
@ -3,10 +3,10 @@
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "TPMJS Tool Executor for Railway - Deploy your own executor on Railway",
|
"description": "TPMJS Tool Executor for Railway - Deploy your own executor on Railway",
|
||||||
"main": "index.js",
|
"main": "index.cjs",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node index.js",
|
"start": "node index.cjs",
|
||||||
"dev": "node index.js"
|
"dev": "node index.cjs"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.0.0"
|
"node": ">=18.0.0"
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,19 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# TPMJS Executor Standalone Bootstrap Script for Unsandbox
|
# TPMJS Executor Standalone Bootstrap Script for Unsandbox
|
||||||
# This script contains the embedded executor - no network required during bootstrap
|
# This script contains the embedded executor - no network required during bootstrap
|
||||||
|
# Protocol Version: 1.0
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
echo "=== TPMJS Executor for Unsandbox ==="
|
echo "=== TPMJS Executor for Unsandbox ==="
|
||||||
|
echo "Protocol Version: 1.0"
|
||||||
echo "Starting deployment..."
|
echo "Starting deployment..."
|
||||||
|
|
||||||
# Embedded executor script
|
# Embedded executor script (v1.0 compliant)
|
||||||
cat > /root/executor.js << 'EXECUTOR_EOF'
|
cat > /root/executor.cjs << 'EXECUTOR_EOF'
|
||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
/**
|
/**
|
||||||
* TPMJS Executor for Unsandbox
|
* TPMJS Executor for Unsandbox
|
||||||
* API-compatible with the Vercel executor.
|
* Protocol Version: 1.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const http = require('http');
|
const http = require('http');
|
||||||
|
|
@ -21,25 +23,23 @@ const path = require('path');
|
||||||
|
|
||||||
const PORT = process.env.PORT || 80;
|
const PORT = process.env.PORT || 80;
|
||||||
const API_KEY = process.env.EXECUTOR_API_KEY || null;
|
const API_KEY = process.env.EXECUTOR_API_KEY || null;
|
||||||
|
const PROTOCOL_VERSION = '1.0';
|
||||||
|
const IMPLEMENTATION_VERSION = '1.0.0';
|
||||||
|
|
||||||
const corsHeaders = {
|
const corsHeaders = {
|
||||||
'Access-Control-Allow-Origin': '*',
|
'Access-Control-Allow-Origin': '*',
|
||||||
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
||||||
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
|
'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-TPMJS-Protocol-Version',
|
||||||
};
|
};
|
||||||
|
|
||||||
function jsonResponse(res, statusCode, data) {
|
function jsonResponse(res, statusCode, data) {
|
||||||
res.writeHead(statusCode, {
|
res.writeHead(statusCode, { 'Content-Type': 'application/json', ...corsHeaders });
|
||||||
'Content-Type': 'application/json',
|
|
||||||
...corsHeaders,
|
|
||||||
});
|
|
||||||
res.end(JSON.stringify(data));
|
res.end(JSON.stringify(data));
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkAuth(req) {
|
function checkAuth(req) {
|
||||||
if (!API_KEY) return true;
|
if (!API_KEY) return true;
|
||||||
const authHeader = req.headers.authorization;
|
return req.headers.authorization === `Bearer ${API_KEY}`;
|
||||||
return authHeader === `Bearer ${API_KEY}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseBody(req) {
|
function parseBody(req) {
|
||||||
|
|
@ -47,11 +47,8 @@ function parseBody(req) {
|
||||||
let body = '';
|
let body = '';
|
||||||
req.on('data', chunk => body += chunk);
|
req.on('data', chunk => body += chunk);
|
||||||
req.on('end', () => {
|
req.on('end', () => {
|
||||||
try {
|
try { resolve(body ? JSON.parse(body) : {}); }
|
||||||
resolve(body ? JSON.parse(body) : {});
|
catch (e) { reject(new Error('Invalid JSON')); }
|
||||||
} catch (e) {
|
|
||||||
reject(new Error('Invalid JSON'));
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
req.on('error', reject);
|
req.on('error', reject);
|
||||||
});
|
});
|
||||||
|
|
@ -60,11 +57,28 @@ function parseBody(req) {
|
||||||
function handleHealth(req, res) {
|
function handleHealth(req, res) {
|
||||||
jsonResponse(res, 200, {
|
jsonResponse(res, 200, {
|
||||||
status: 'ok',
|
status: 'ok',
|
||||||
version: '1.0.0',
|
protocolVersion: PROTOCOL_VERSION,
|
||||||
info: {
|
implementationVersion: IMPLEMENTATION_VERSION,
|
||||||
runtime: 'unsandbox',
|
runtime: 'node',
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleInfo(req, res) {
|
||||||
|
jsonResponse(res, 200, {
|
||||||
|
name: 'Unsandbox Executor',
|
||||||
|
version: IMPLEMENTATION_VERSION,
|
||||||
|
protocolVersion: PROTOCOL_VERSION,
|
||||||
|
capabilities: {
|
||||||
|
isolation: 'container',
|
||||||
|
executionModes: ['sync'],
|
||||||
|
maxExecutionTimeMs: 120000,
|
||||||
|
maxRequestBodyBytes: 10485760,
|
||||||
|
supportsStreaming: false,
|
||||||
|
supportsCallbacks: false,
|
||||||
|
supportsCaching: false,
|
||||||
},
|
},
|
||||||
|
runtime: { platform: process.platform, nodeVersion: process.version },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -74,19 +88,16 @@ async function handleExecuteTool(req, res) {
|
||||||
if (!checkAuth(req)) {
|
if (!checkAuth(req)) {
|
||||||
return jsonResponse(res, 401, {
|
return jsonResponse(res, 401, {
|
||||||
success: false,
|
success: false,
|
||||||
error: 'Unauthorized',
|
error: { code: 'UNAUTHORIZED', message: 'Invalid or missing API key' },
|
||||||
executionTimeMs: Date.now() - startTime,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let body;
|
let body;
|
||||||
try {
|
try { body = await parseBody(req); }
|
||||||
body = await parseBody(req);
|
catch (e) {
|
||||||
} catch (e) {
|
|
||||||
return jsonResponse(res, 400, {
|
return jsonResponse(res, 400, {
|
||||||
success: false,
|
success: false,
|
||||||
error: 'Invalid JSON body',
|
error: { code: 'INVALID_REQUEST', message: 'Invalid JSON body' },
|
||||||
executionTimeMs: Date.now() - startTime,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -95,8 +106,7 @@ async function handleExecuteTool(req, res) {
|
||||||
if (!packageName || !name) {
|
if (!packageName || !name) {
|
||||||
return jsonResponse(res, 400, {
|
return jsonResponse(res, 400, {
|
||||||
success: false,
|
success: false,
|
||||||
error: 'Missing required fields: packageName, name',
|
error: { code: 'INVALID_REQUEST', message: 'Missing required fields: packageName, name' },
|
||||||
executionTimeMs: Date.now() - startTime,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -105,74 +115,41 @@ async function handleExecuteTool(req, res) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
fs.mkdirSync(workDir, { recursive: true });
|
fs.mkdirSync(workDir, { recursive: true });
|
||||||
|
|
||||||
fs.writeFileSync(path.join(workDir, 'package.json'), JSON.stringify({
|
fs.writeFileSync(path.join(workDir, 'package.json'), JSON.stringify({
|
||||||
name: 'tpmjs-execution',
|
name: 'tpmjs-execution', private: true, type: 'commonjs',
|
||||||
private: true,
|
|
||||||
type: 'commonjs',
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
console.log(`[executor] Installing ${packageSpec}...`);
|
console.log(`[executor] Installing ${packageSpec}...`);
|
||||||
const installStart = Date.now();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
execSync(`npm install --no-save --omit=dev --no-audit --no-fund ${packageSpec}`, {
|
execSync(`npm install --no-save --omit=dev --no-audit --no-fund ${packageSpec}`, {
|
||||||
cwd: workDir,
|
cwd: workDir, stdio: ['pipe', 'pipe', 'pipe'], timeout: 60000,
|
||||||
stdio: ['pipe', 'pipe', 'pipe'],
|
|
||||||
timeout: 60000,
|
|
||||||
});
|
});
|
||||||
} catch (installError) {
|
} catch (installError) {
|
||||||
console.error(`[executor] npm install failed:`, installError.message);
|
return jsonResponse(res, 200, {
|
||||||
return jsonResponse(res, 500, {
|
|
||||||
success: false,
|
success: false,
|
||||||
error: `npm install failed: ${installError.message}`,
|
error: { code: 'PACKAGE_NOT_FOUND', message: `npm install failed: ${installError.message}` },
|
||||||
stderr: installError.stderr?.toString(),
|
|
||||||
executionTimeMs: Date.now() - startTime,
|
executionTimeMs: Date.now() - startTime,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`[executor] npm install completed in ${Date.now() - installStart}ms`);
|
|
||||||
|
|
||||||
const envSetup = env
|
const envSetup = env
|
||||||
? Object.entries(env)
|
? Object.entries(env).map(([k, v]) => `process.env[${JSON.stringify(k)}] = ${JSON.stringify(v)};`).join('\n')
|
||||||
.map(([key, value]) => `process.env[${JSON.stringify(key)}] = ${JSON.stringify(value)};`)
|
|
||||||
.join('\n')
|
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
const script = `
|
const script = `
|
||||||
${envSetup}
|
${envSetup}
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
const pkg = require(${JSON.stringify(packageName)});
|
const pkg = require(${JSON.stringify(packageName)});
|
||||||
let tool = pkg[${JSON.stringify(name)}] || pkg.default?.[${JSON.stringify(name)}] || pkg.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}"\`);
|
||||||
if (!tool) {
|
|
||||||
throw new Error(\`Tool "${name}" not found in package "${packageName}"\`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof tool === 'function' && !tool.execute) {
|
if (typeof tool === 'function' && !tool.execute) {
|
||||||
const envVars = ${env ? JSON.stringify(env) : 'null'};
|
try { const r = tool(); if (r?.execute) tool = r; } catch {}
|
||||||
try {
|
if (typeof tool === 'function' && ${env ? JSON.stringify(env) : 'null'}) {
|
||||||
const result = tool();
|
try { const r = tool(${env ? JSON.stringify(env) : 'null'}); if (r?.execute) tool = r; } catch {}
|
||||||
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?.execute) throw new Error(\`Tool "${name}" does not have an execute() function\`);
|
||||||
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)});
|
const result = await tool.execute(${JSON.stringify(params)});
|
||||||
process.stdout.write(JSON.stringify({ __tpmjs_result__: result }));
|
process.stdout.write(JSON.stringify({ __tpmjs_result__: result }));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
@ -184,52 +161,36 @@ ${envSetup}
|
||||||
|
|
||||||
fs.writeFileSync(path.join(workDir, 'execute.cjs'), script);
|
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 result = await new Promise((resolve) => {
|
||||||
const child = spawn('node', ['execute.cjs'], {
|
const child = spawn('node', ['execute.cjs'], {
|
||||||
cwd: workDir,
|
cwd: workDir, env: { ...process.env, ...env }, timeout: 120000,
|
||||||
env: { ...process.env, ...env },
|
});
|
||||||
timeout: 120000,
|
let stdout = '', stderr = '';
|
||||||
|
child.stdout.on('data', d => stdout += d);
|
||||||
|
child.stderr.on('data', d => stderr += d);
|
||||||
|
child.on('close', code => resolve({ exitCode: code, stdout, stderr }));
|
||||||
|
child.on('error', err => resolve({ exitCode: 1, stdout: '', stderr: err.message }));
|
||||||
});
|
});
|
||||||
|
|
||||||
let stdout = '';
|
try { fs.rmSync(workDir, { recursive: true, force: true }); } catch {}
|
||||||
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) {
|
if (result.exitCode !== 0) {
|
||||||
try {
|
try {
|
||||||
const errorObj = JSON.parse(result.stderr);
|
const errorObj = JSON.parse(result.stderr);
|
||||||
if (errorObj.__tpmjs_error__) {
|
if (errorObj.__tpmjs_error__) {
|
||||||
|
let code = 'TOOL_EXECUTION_ERROR';
|
||||||
|
if (errorObj.__tpmjs_error__.includes('not found in package')) code = 'TOOL_NOT_FOUND';
|
||||||
|
else if (errorObj.__tpmjs_error__.includes('does not have an execute()')) code = 'TOOL_INVALID';
|
||||||
return jsonResponse(res, 200, {
|
return jsonResponse(res, 200, {
|
||||||
success: false,
|
success: false,
|
||||||
error: errorObj.__tpmjs_error__,
|
error: { code, message: errorObj.__tpmjs_error__ },
|
||||||
executionTimeMs: Date.now() - startTime,
|
executionTimeMs: Date.now() - startTime,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
|
|
||||||
return jsonResponse(res, 200, {
|
return jsonResponse(res, 200, {
|
||||||
success: false,
|
success: false,
|
||||||
error: result.stderr || `Script exited with code ${result.exitCode}`,
|
error: { code: 'TOOL_EXECUTION_ERROR', message: result.stderr || `Exit code ${result.exitCode}` },
|
||||||
executionTimeMs: Date.now() - startTime,
|
executionTimeMs: Date.now() - startTime,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -238,60 +199,55 @@ ${envSetup}
|
||||||
const parsed = JSON.parse(result.stdout);
|
const parsed = JSON.parse(result.stdout);
|
||||||
if (parsed.__tpmjs_result__ !== undefined) {
|
if (parsed.__tpmjs_result__ !== undefined) {
|
||||||
return jsonResponse(res, 200, {
|
return jsonResponse(res, 200, {
|
||||||
success: true,
|
success: true, output: parsed.__tpmjs_result__, executionTimeMs: Date.now() - startTime,
|
||||||
output: parsed.__tpmjs_result__,
|
|
||||||
executionTimeMs: Date.now() - startTime,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
|
|
||||||
return jsonResponse(res, 200, {
|
return jsonResponse(res, 200, {
|
||||||
success: true,
|
success: true, output: result.stdout || null, executionTimeMs: Date.now() - startTime,
|
||||||
output: result.stdout || null,
|
|
||||||
stderr: result.stderr || undefined,
|
|
||||||
executionTimeMs: Date.now() - startTime,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
try {
|
try { fs.rmSync(workDir, { recursive: true, force: true }); } catch {}
|
||||||
fs.rmSync(workDir, { recursive: true, force: true });
|
return jsonResponse(res, 200, {
|
||||||
} catch {}
|
|
||||||
|
|
||||||
return jsonResponse(res, 500, {
|
|
||||||
success: false,
|
success: false,
|
||||||
error: error.message || String(error),
|
error: { code: 'INTERNAL_ERROR', message: error.message || String(error) },
|
||||||
executionTimeMs: Date.now() - startTime,
|
executionTimeMs: Date.now() - startTime,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const server = http.createServer(async (req, res) => {
|
const server = http.createServer(async (req, res) => {
|
||||||
const url = new URL(req.url, `http://localhost:${PORT}`);
|
const pathname = new URL(req.url, `http://localhost:${PORT}`).pathname;
|
||||||
const pathname = url.pathname;
|
|
||||||
|
|
||||||
if (req.method === 'OPTIONS') {
|
if (req.method === 'OPTIONS') {
|
||||||
res.writeHead(200, corsHeaders);
|
res.writeHead(200, corsHeaders);
|
||||||
return res.end();
|
return res.end();
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((pathname === '/api/health' || pathname === '/health') && req.method === 'GET') {
|
if ((pathname === '/health' || pathname === '/api/health') && req.method === 'GET') return handleHealth(req, res);
|
||||||
return handleHealth(req, res);
|
if ((pathname === '/info' || pathname === '/api/info') && req.method === 'GET') return handleInfo(req, res);
|
||||||
}
|
if ((pathname === '/execute-tool' || pathname === '/api/execute-tool') && req.method === 'POST') return handleExecuteTool(req, res);
|
||||||
|
|
||||||
if ((pathname === '/api/execute-tool' || pathname === '/execute-tool') && req.method === 'POST') {
|
if (pathname === '/' && req.method === 'GET') {
|
||||||
return handleExecuteTool(req, res);
|
return jsonResponse(res, 200, {
|
||||||
|
name: 'TPMJS Unsandbox Executor',
|
||||||
|
version: IMPLEMENTATION_VERSION,
|
||||||
|
protocolVersion: PROTOCOL_VERSION,
|
||||||
|
endpoints: { health: 'GET /health', info: 'GET /info', execute: 'POST /execute-tool' },
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
jsonResponse(res, 404, { error: 'Not found' });
|
jsonResponse(res, 404, { error: 'Not found' });
|
||||||
});
|
});
|
||||||
|
|
||||||
server.listen(PORT, () => {
|
server.listen(PORT, () => {
|
||||||
console.log(`TPMJS Executor running on port ${PORT}`);
|
console.log(`TPMJS Executor v${IMPLEMENTATION_VERSION} (Protocol ${PROTOCOL_VERSION})`);
|
||||||
if (API_KEY) {
|
console.log(`Listening on port ${PORT}`);
|
||||||
console.log(`Authentication: Required`);
|
console.log(`Authentication: ${API_KEY ? 'Required' : 'None'}`);
|
||||||
}
|
|
||||||
});
|
});
|
||||||
EXECUTOR_EOF
|
EXECUTOR_EOF
|
||||||
|
|
||||||
echo "Starting TPMJS Executor on port 80..."
|
echo "Starting TPMJS Executor on port 80..."
|
||||||
exec node /root/executor.js
|
exec node /root/executor.cjs
|
||||||
|
|
|
||||||
|
|
@ -7,10 +7,10 @@ echo "=== TPMJS Executor for Unsandbox ==="
|
||||||
echo "Starting deployment..."
|
echo "Starting deployment..."
|
||||||
|
|
||||||
# Download the executor script from GitHub
|
# Download the executor script from GitHub
|
||||||
EXECUTOR_URL="https://raw.githubusercontent.com/tpmjs/tpmjs/main/templates/unsandbox-executor/executor.js"
|
EXECUTOR_URL="https://raw.githubusercontent.com/tpmjs/tpmjs/main/templates/unsandbox-executor/executor.cjs"
|
||||||
|
|
||||||
echo "Downloading executor from $EXECUTOR_URL..."
|
echo "Downloading executor from $EXECUTOR_URL..."
|
||||||
curl -fsSL "$EXECUTOR_URL" -o /root/executor.js
|
curl -fsSL "$EXECUTOR_URL" -o /root/executor.cjs
|
||||||
|
|
||||||
echo "Starting TPMJS Executor on port 80..."
|
echo "Starting TPMJS Executor on port 80..."
|
||||||
exec node /root/executor.js
|
exec node /root/executor.cjs
|
||||||
|
|
|
||||||
|
|
@ -16,11 +16,15 @@ const path = require('path');
|
||||||
const PORT = process.env.PORT || 80;
|
const PORT = process.env.PORT || 80;
|
||||||
const API_KEY = process.env.EXECUTOR_API_KEY || null;
|
const API_KEY = process.env.EXECUTOR_API_KEY || null;
|
||||||
|
|
||||||
|
// Protocol constants
|
||||||
|
const PROTOCOL_VERSION = '1.0';
|
||||||
|
const IMPLEMENTATION_VERSION = '1.0.0';
|
||||||
|
|
||||||
// CORS headers for cross-origin requests
|
// CORS headers for cross-origin requests
|
||||||
const corsHeaders = {
|
const corsHeaders = {
|
||||||
'Access-Control-Allow-Origin': '*',
|
'Access-Control-Allow-Origin': '*',
|
||||||
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
||||||
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
|
'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-TPMJS-Protocol-Version',
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -62,15 +66,38 @@ function parseBody(req) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GET /api/health - Health check endpoint
|
* GET /health - Health check endpoint
|
||||||
*/
|
*/
|
||||||
function handleHealth(req, res) {
|
function handleHealth(req, res) {
|
||||||
jsonResponse(res, 200, {
|
jsonResponse(res, 200, {
|
||||||
status: 'ok',
|
status: 'ok',
|
||||||
version: '1.0.0',
|
protocolVersion: PROTOCOL_VERSION,
|
||||||
info: {
|
implementationVersion: IMPLEMENTATION_VERSION,
|
||||||
runtime: 'unsandbox',
|
runtime: 'node',
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /info - Capability advertisement endpoint
|
||||||
|
*/
|
||||||
|
function handleInfo(req, res) {
|
||||||
|
jsonResponse(res, 200, {
|
||||||
|
name: 'Unsandbox Executor',
|
||||||
|
version: IMPLEMENTATION_VERSION,
|
||||||
|
protocolVersion: PROTOCOL_VERSION,
|
||||||
|
capabilities: {
|
||||||
|
isolation: 'container',
|
||||||
|
executionModes: ['sync'],
|
||||||
|
maxExecutionTimeMs: 120000,
|
||||||
|
maxRequestBodyBytes: 10485760,
|
||||||
|
supportsStreaming: false,
|
||||||
|
supportsCallbacks: false,
|
||||||
|
supportsCaching: false,
|
||||||
|
},
|
||||||
|
runtime: {
|
||||||
|
platform: process.platform,
|
||||||
|
nodeVersion: process.version,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -94,8 +121,10 @@ async function handleExecuteTool(req, res) {
|
||||||
if (!checkAuth(req)) {
|
if (!checkAuth(req)) {
|
||||||
return jsonResponse(res, 401, {
|
return jsonResponse(res, 401, {
|
||||||
success: false,
|
success: false,
|
||||||
error: 'Unauthorized',
|
error: {
|
||||||
executionTimeMs: Date.now() - startTime,
|
code: 'UNAUTHORIZED',
|
||||||
|
message: 'Invalid or missing API key',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -106,8 +135,10 @@ async function handleExecuteTool(req, res) {
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return jsonResponse(res, 400, {
|
return jsonResponse(res, 400, {
|
||||||
success: false,
|
success: false,
|
||||||
error: 'Invalid JSON body',
|
error: {
|
||||||
executionTimeMs: Date.now() - startTime,
|
code: 'INVALID_REQUEST',
|
||||||
|
message: 'Invalid JSON body',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -117,8 +148,10 @@ async function handleExecuteTool(req, res) {
|
||||||
if (!packageName || !name) {
|
if (!packageName || !name) {
|
||||||
return jsonResponse(res, 400, {
|
return jsonResponse(res, 400, {
|
||||||
success: false,
|
success: false,
|
||||||
error: 'Missing required fields: packageName, name',
|
error: {
|
||||||
executionTimeMs: Date.now() - startTime,
|
code: 'INVALID_REQUEST',
|
||||||
|
message: 'Missing required fields: packageName, name',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -151,10 +184,12 @@ async function handleExecuteTool(req, res) {
|
||||||
});
|
});
|
||||||
} catch (installError) {
|
} catch (installError) {
|
||||||
console.error(`[executor] npm install failed:`, installError.message);
|
console.error(`[executor] npm install failed:`, installError.message);
|
||||||
return jsonResponse(res, 500, {
|
return jsonResponse(res, 200, {
|
||||||
success: false,
|
success: false,
|
||||||
error: `npm install failed: ${installError.message}`,
|
error: {
|
||||||
stderr: installError.stderr?.toString(),
|
code: 'PACKAGE_NOT_FOUND',
|
||||||
|
message: `npm install failed for ${packageSpec}: ${installError.message}`,
|
||||||
|
},
|
||||||
executionTimeMs: Date.now() - startTime,
|
executionTimeMs: Date.now() - startTime,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -264,9 +299,20 @@ ${envSetup}
|
||||||
try {
|
try {
|
||||||
const errorObj = JSON.parse(result.stderr);
|
const errorObj = JSON.parse(result.stderr);
|
||||||
if (errorObj.__tpmjs_error__) {
|
if (errorObj.__tpmjs_error__) {
|
||||||
|
const errorMessage = errorObj.__tpmjs_error__;
|
||||||
|
// Determine error code based on message
|
||||||
|
let code = 'TOOL_EXECUTION_ERROR';
|
||||||
|
if (errorMessage.includes('not found in package')) {
|
||||||
|
code = 'TOOL_NOT_FOUND';
|
||||||
|
} else if (errorMessage.includes('does not have an execute()')) {
|
||||||
|
code = 'TOOL_INVALID';
|
||||||
|
}
|
||||||
return jsonResponse(res, 200, {
|
return jsonResponse(res, 200, {
|
||||||
success: false,
|
success: false,
|
||||||
error: errorObj.__tpmjs_error__,
|
error: {
|
||||||
|
code,
|
||||||
|
message: errorMessage,
|
||||||
|
},
|
||||||
executionTimeMs: Date.now() - startTime,
|
executionTimeMs: Date.now() - startTime,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -274,7 +320,10 @@ ${envSetup}
|
||||||
|
|
||||||
return jsonResponse(res, 200, {
|
return jsonResponse(res, 200, {
|
||||||
success: false,
|
success: false,
|
||||||
error: result.stderr || `Script exited with code ${result.exitCode}`,
|
error: {
|
||||||
|
code: 'TOOL_EXECUTION_ERROR',
|
||||||
|
message: result.stderr || `Script exited with code ${result.exitCode}`,
|
||||||
|
},
|
||||||
executionTimeMs: Date.now() - startTime,
|
executionTimeMs: Date.now() - startTime,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -295,7 +344,6 @@ ${envSetup}
|
||||||
return jsonResponse(res, 200, {
|
return jsonResponse(res, 200, {
|
||||||
success: true,
|
success: true,
|
||||||
output: result.stdout || null,
|
output: result.stdout || null,
|
||||||
stderr: result.stderr || undefined,
|
|
||||||
executionTimeMs: Date.now() - startTime,
|
executionTimeMs: Date.now() - startTime,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -304,9 +352,12 @@ ${envSetup}
|
||||||
fs.rmSync(workDir, { recursive: true, force: true });
|
fs.rmSync(workDir, { recursive: true, force: true });
|
||||||
} catch {}
|
} catch {}
|
||||||
|
|
||||||
return jsonResponse(res, 500, {
|
return jsonResponse(res, 200, {
|
||||||
success: false,
|
success: false,
|
||||||
error: error.message || String(error),
|
error: {
|
||||||
|
code: 'INTERNAL_ERROR',
|
||||||
|
message: error.message || String(error),
|
||||||
|
},
|
||||||
executionTimeMs: Date.now() - startTime,
|
executionTimeMs: Date.now() - startTime,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -330,10 +381,28 @@ const server = http.createServer(async (req, res) => {
|
||||||
return handleHealth(req, res);
|
return handleHealth(req, res);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ((pathname === '/api/info' || pathname === '/info') && req.method === 'GET') {
|
||||||
|
return handleInfo(req, res);
|
||||||
|
}
|
||||||
|
|
||||||
if ((pathname === '/api/execute-tool' || pathname === '/execute-tool') && req.method === 'POST') {
|
if ((pathname === '/api/execute-tool' || pathname === '/execute-tool') && req.method === 'POST') {
|
||||||
return handleExecuteTool(req, res);
|
return handleExecuteTool(req, res);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Root path - simple info
|
||||||
|
if (pathname === '/' && req.method === 'GET') {
|
||||||
|
return jsonResponse(res, 200, {
|
||||||
|
name: 'TPMJS Unsandbox Executor',
|
||||||
|
version: IMPLEMENTATION_VERSION,
|
||||||
|
protocolVersion: PROTOCOL_VERSION,
|
||||||
|
endpoints: {
|
||||||
|
health: 'GET /health',
|
||||||
|
info: 'GET /info',
|
||||||
|
execute: 'POST /execute-tool',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// 404 for unknown routes
|
// 404 for unknown routes
|
||||||
jsonResponse(res, 404, { error: 'Not found' });
|
jsonResponse(res, 404, { error: 'Not found' });
|
||||||
});
|
});
|
||||||
|
|
@ -20,14 +20,29 @@ interface ExecuteToolRequest {
|
||||||
env?: Record<string, string>;
|
env?: Record<string, string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ExecuteToolResponse {
|
interface ExecuteToolSuccessResponse {
|
||||||
success: boolean;
|
success: true;
|
||||||
output?: unknown;
|
output: unknown;
|
||||||
error?: string;
|
|
||||||
stderr?: string;
|
|
||||||
executionTimeMs: number;
|
executionTimeMs: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ExecuteToolErrorResponse {
|
||||||
|
success: false;
|
||||||
|
error: {
|
||||||
|
code: string;
|
||||||
|
message: string;
|
||||||
|
};
|
||||||
|
executionTimeMs: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExecuteToolResponse = ExecuteToolSuccessResponse | ExecuteToolErrorResponse;
|
||||||
|
|
||||||
|
const corsHeaders = {
|
||||||
|
'Access-Control-Allow-Origin': '*',
|
||||||
|
'Access-Control-Allow-Methods': 'POST, OPTIONS',
|
||||||
|
'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-TPMJS-Protocol-Version',
|
||||||
|
};
|
||||||
|
|
||||||
export async function POST(req: NextRequest): Promise<NextResponse<ExecuteToolResponse>> {
|
export async function POST(req: NextRequest): Promise<NextResponse<ExecuteToolResponse>> {
|
||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
|
@ -37,8 +52,15 @@ export async function POST(req: NextRequest): Promise<NextResponse<ExecuteToolRe
|
||||||
const authHeader = req.headers.get('Authorization');
|
const authHeader = req.headers.get('Authorization');
|
||||||
if (!authHeader || authHeader !== `Bearer ${apiKey}`) {
|
if (!authHeader || authHeader !== `Bearer ${apiKey}`) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ success: false, error: 'Unauthorized', executionTimeMs: Date.now() - startTime },
|
{
|
||||||
{ status: 401 }
|
success: false,
|
||||||
|
error: {
|
||||||
|
code: 'UNAUTHORIZED',
|
||||||
|
message: 'Invalid or missing API key',
|
||||||
|
},
|
||||||
|
executionTimeMs: Date.now() - startTime,
|
||||||
|
} as ExecuteToolErrorResponse,
|
||||||
|
{ status: 401, headers: corsHeaders }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -53,10 +75,13 @@ export async function POST(req: NextRequest): Promise<NextResponse<ExecuteToolRe
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{
|
{
|
||||||
success: false,
|
success: false,
|
||||||
error: 'Missing required fields: packageName, name',
|
error: {
|
||||||
executionTimeMs: Date.now() - startTime,
|
code: 'INVALID_REQUEST',
|
||||||
|
message: 'Missing required fields: packageName, name',
|
||||||
},
|
},
|
||||||
{ status: 400 }
|
executionTimeMs: Date.now() - startTime,
|
||||||
|
} as ExecuteToolErrorResponse,
|
||||||
|
{ status: 400, headers: corsHeaders }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -92,12 +117,17 @@ export async function POST(req: NextRequest): Promise<NextResponse<ExecuteToolRe
|
||||||
stdout: installStdout?.slice(0, 500),
|
stdout: installStdout?.slice(0, 500),
|
||||||
stderr: installStderr?.slice(0, 500),
|
stderr: installStderr?.slice(0, 500),
|
||||||
});
|
});
|
||||||
return NextResponse.json({
|
return NextResponse.json(
|
||||||
|
{
|
||||||
success: false,
|
success: false,
|
||||||
error: `npm install failed with exit code ${install.exitCode}`,
|
error: {
|
||||||
stderr: installStderr || installStdout,
|
code: 'PACKAGE_NOT_FOUND',
|
||||||
|
message: `npm install failed for ${packageSpec}: ${installStderr || installStdout}`,
|
||||||
|
},
|
||||||
executionTimeMs: Date.now() - startTime,
|
executionTimeMs: Date.now() - startTime,
|
||||||
});
|
} as ExecuteToolErrorResponse,
|
||||||
|
{ headers: corsHeaders }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2) Build environment setup for the script
|
// 2) Build environment setup for the script
|
||||||
|
|
@ -188,46 +218,77 @@ ${envSetup}
|
||||||
try {
|
try {
|
||||||
const errorObj = JSON.parse(stderr);
|
const errorObj = JSON.parse(stderr);
|
||||||
if (errorObj.__tpmjs_error__) {
|
if (errorObj.__tpmjs_error__) {
|
||||||
return NextResponse.json({
|
const errorMessage = errorObj.__tpmjs_error__;
|
||||||
|
// Determine error code based on message
|
||||||
|
let code = 'TOOL_EXECUTION_ERROR';
|
||||||
|
if (errorMessage.includes('not found in package')) {
|
||||||
|
code = 'TOOL_NOT_FOUND';
|
||||||
|
} else if (errorMessage.includes('does not have an execute()')) {
|
||||||
|
code = 'TOOL_INVALID';
|
||||||
|
}
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
success: false,
|
success: false,
|
||||||
error: errorObj.__tpmjs_error__,
|
error: {
|
||||||
|
code,
|
||||||
|
message: errorMessage,
|
||||||
|
},
|
||||||
executionTimeMs: Date.now() - startTime,
|
executionTimeMs: Date.now() - startTime,
|
||||||
});
|
} as ExecuteToolErrorResponse,
|
||||||
|
{ headers: corsHeaders }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json(
|
||||||
|
{
|
||||||
success: false,
|
success: false,
|
||||||
error: stderr || `Script exited with code ${run.exitCode}`,
|
error: {
|
||||||
|
code: 'TOOL_EXECUTION_ERROR',
|
||||||
|
message: stderr || `Script exited with code ${run.exitCode}`,
|
||||||
|
},
|
||||||
executionTimeMs: Date.now() - startTime,
|
executionTimeMs: Date.now() - startTime,
|
||||||
});
|
} as ExecuteToolErrorResponse,
|
||||||
|
{ headers: corsHeaders }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5) Parse the result
|
// 5) Parse the result
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(stdout);
|
const parsed = JSON.parse(stdout);
|
||||||
if (parsed.__tpmjs_result__ !== undefined) {
|
if (parsed.__tpmjs_result__ !== undefined) {
|
||||||
return NextResponse.json({
|
return NextResponse.json(
|
||||||
|
{
|
||||||
success: true,
|
success: true,
|
||||||
output: parsed.__tpmjs_result__,
|
output: parsed.__tpmjs_result__,
|
||||||
executionTimeMs: Date.now() - startTime,
|
executionTimeMs: Date.now() - startTime,
|
||||||
});
|
} as ExecuteToolSuccessResponse,
|
||||||
|
{ headers: corsHeaders }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
|
|
||||||
// If we couldn't parse structured output, return raw
|
// If we couldn't parse structured output, return raw
|
||||||
return NextResponse.json({
|
return NextResponse.json(
|
||||||
|
{
|
||||||
success: true,
|
success: true,
|
||||||
output: stdout || null,
|
output: stdout || null,
|
||||||
stderr: stderr || undefined,
|
|
||||||
executionTimeMs: Date.now() - startTime,
|
executionTimeMs: Date.now() - startTime,
|
||||||
});
|
} as ExecuteToolSuccessResponse,
|
||||||
|
{ headers: corsHeaders }
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return NextResponse.json({
|
return NextResponse.json(
|
||||||
|
{
|
||||||
success: false,
|
success: false,
|
||||||
error: error instanceof Error ? error.message : String(error),
|
error: {
|
||||||
|
code: 'INTERNAL_ERROR',
|
||||||
|
message: error instanceof Error ? error.message : String(error),
|
||||||
|
},
|
||||||
executionTimeMs: Date.now() - startTime,
|
executionTimeMs: Date.now() - startTime,
|
||||||
});
|
} as ExecuteToolErrorResponse,
|
||||||
|
{ headers: corsHeaders }
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
if (sandbox) {
|
if (sandbox) {
|
||||||
try {
|
try {
|
||||||
|
|
@ -243,10 +304,6 @@ ${envSetup}
|
||||||
export async function OPTIONS(): Promise<NextResponse> {
|
export async function OPTIONS(): Promise<NextResponse> {
|
||||||
return new NextResponse(null, {
|
return new NextResponse(null, {
|
||||||
status: 200,
|
status: 200,
|
||||||
headers: {
|
headers: corsHeaders,
|
||||||
'Access-Control-Allow-Origin': '*',
|
|
||||||
'Access-Control-Allow-Methods': 'POST, OPTIONS',
|
|
||||||
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,22 +10,34 @@ import { NextResponse } from 'next/server';
|
||||||
export const runtime = 'nodejs';
|
export const runtime = 'nodejs';
|
||||||
export const dynamic = 'force-dynamic';
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
|
const PROTOCOL_VERSION = '1.0';
|
||||||
|
const IMPLEMENTATION_VERSION = '1.0.0';
|
||||||
|
|
||||||
interface HealthResponse {
|
interface HealthResponse {
|
||||||
status: 'ok' | 'degraded' | 'error';
|
status: 'ok';
|
||||||
version?: string;
|
protocolVersion: string;
|
||||||
info?: Record<string, unknown>;
|
implementationVersion: string;
|
||||||
|
runtime?: string;
|
||||||
|
timestamp?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function GET(): Promise<NextResponse<HealthResponse>> {
|
export async function GET(): Promise<NextResponse<HealthResponse>> {
|
||||||
return NextResponse.json({
|
return NextResponse.json(
|
||||||
|
{
|
||||||
status: 'ok',
|
status: 'ok',
|
||||||
version: '1.0.0',
|
protocolVersion: PROTOCOL_VERSION,
|
||||||
info: {
|
implementationVersion: IMPLEMENTATION_VERSION,
|
||||||
runtime: 'vercel-sandbox',
|
runtime: 'node',
|
||||||
region: 'iad1',
|
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
},
|
},
|
||||||
});
|
{
|
||||||
|
headers: {
|
||||||
|
'Access-Control-Allow-Origin': '*',
|
||||||
|
'Access-Control-Allow-Methods': 'GET, OPTIONS',
|
||||||
|
'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-TPMJS-Protocol-Version',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle OPTIONS for CORS preflight
|
// Handle OPTIONS for CORS preflight
|
||||||
|
|
@ -35,7 +47,7 @@ export async function OPTIONS(): Promise<NextResponse> {
|
||||||
headers: {
|
headers: {
|
||||||
'Access-Control-Allow-Origin': '*',
|
'Access-Control-Allow-Origin': '*',
|
||||||
'Access-Control-Allow-Methods': 'GET, OPTIONS',
|
'Access-Control-Allow-Methods': 'GET, OPTIONS',
|
||||||
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
|
'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-TPMJS-Protocol-Version',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
77
templates/vercel-executor/app/api/info/route.ts
Normal file
77
templates/vercel-executor/app/api/info/route.ts
Normal file
|
|
@ -0,0 +1,77 @@
|
||||||
|
/**
|
||||||
|
* Info Endpoint - Capability Advertisement
|
||||||
|
*
|
||||||
|
* GET /api/info
|
||||||
|
* Returns executor capabilities for intelligent routing
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { NextResponse } from 'next/server';
|
||||||
|
|
||||||
|
export const runtime = 'nodejs';
|
||||||
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
|
const PROTOCOL_VERSION = '1.0';
|
||||||
|
const IMPLEMENTATION_VERSION = '1.0.0';
|
||||||
|
|
||||||
|
interface InfoResponse {
|
||||||
|
name: string;
|
||||||
|
version: string;
|
||||||
|
protocolVersion: string;
|
||||||
|
capabilities: {
|
||||||
|
isolation: 'none' | 'process' | 'container' | 'vm';
|
||||||
|
executionModes: string[];
|
||||||
|
maxExecutionTimeMs: number;
|
||||||
|
maxRequestBodyBytes: number;
|
||||||
|
supportsStreaming: boolean;
|
||||||
|
supportsCallbacks: boolean;
|
||||||
|
supportsCaching: boolean;
|
||||||
|
};
|
||||||
|
runtime?: {
|
||||||
|
platform?: string;
|
||||||
|
nodeVersion?: string;
|
||||||
|
region?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function GET(): Promise<NextResponse<InfoResponse>> {
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
name: 'Vercel Sandbox Executor',
|
||||||
|
version: IMPLEMENTATION_VERSION,
|
||||||
|
protocolVersion: PROTOCOL_VERSION,
|
||||||
|
capabilities: {
|
||||||
|
isolation: 'vm',
|
||||||
|
executionModes: ['sync'],
|
||||||
|
maxExecutionTimeMs: 120000,
|
||||||
|
maxRequestBodyBytes: 10485760,
|
||||||
|
supportsStreaming: false,
|
||||||
|
supportsCallbacks: false,
|
||||||
|
supportsCaching: false,
|
||||||
|
},
|
||||||
|
runtime: {
|
||||||
|
platform: 'linux',
|
||||||
|
nodeVersion: '22.x',
|
||||||
|
region: process.env.VERCEL_REGION || undefined,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Access-Control-Allow-Origin': '*',
|
||||||
|
'Access-Control-Allow-Methods': 'GET, OPTIONS',
|
||||||
|
'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-TPMJS-Protocol-Version',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle OPTIONS for CORS preflight
|
||||||
|
export async function OPTIONS(): Promise<NextResponse> {
|
||||||
|
return new NextResponse(null, {
|
||||||
|
status: 200,
|
||||||
|
headers: {
|
||||||
|
'Access-Control-Allow-Origin': '*',
|
||||||
|
'Access-Control-Allow-Methods': 'GET, OPTIONS',
|
||||||
|
'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-TPMJS-Protocol-Version',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -3,31 +3,10 @@
|
||||||
*
|
*
|
||||||
* GET /health
|
* GET /health
|
||||||
* TPMJS expects health at /health, not /api/health
|
* TPMJS expects health at /health, not /api/health
|
||||||
|
* This re-exports from the api version for backwards compatibility
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { NextResponse } from 'next/server';
|
export { GET, OPTIONS } from '../api/health/route';
|
||||||
|
|
||||||
export const runtime = 'nodejs';
|
export const runtime = 'nodejs';
|
||||||
export const dynamic = 'force-dynamic';
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
interface HealthResponse {
|
|
||||||
status: 'ok' | 'degraded' | 'error';
|
|
||||||
version: string;
|
|
||||||
info?: {
|
|
||||||
runtime?: string;
|
|
||||||
region?: string;
|
|
||||||
timestamp?: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function GET(): Promise<NextResponse<HealthResponse>> {
|
|
||||||
return NextResponse.json({
|
|
||||||
status: 'ok',
|
|
||||||
version: '1.0.0',
|
|
||||||
info: {
|
|
||||||
runtime: 'vercel-sandbox',
|
|
||||||
region: process.env.VERCEL_REGION || 'unknown',
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
|
||||||
12
templates/vercel-executor/app/info/route.ts
Normal file
12
templates/vercel-executor/app/info/route.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
/**
|
||||||
|
* Info Endpoint (root path)
|
||||||
|
*
|
||||||
|
* GET /info
|
||||||
|
* TPMJS expects info at /info, not /api/info
|
||||||
|
* This re-exports from the api version for backwards compatibility
|
||||||
|
*/
|
||||||
|
|
||||||
|
export { GET, OPTIONS } from '../api/info/route';
|
||||||
|
|
||||||
|
export const runtime = 'nodejs';
|
||||||
|
export const dynamic = 'force-dynamic';
|
||||||
Loading…
Add table
Add a link
Reference in a new issue