feat: add hot-swappable executor support for collections and agents

- Add executor configuration to Collection and Agent models in Prisma schema
- Create ExecutorConfigPanel component for selecting default or custom executors
- Add executor resolution logic with cascade (Agent → Collection → System Default)
- Create /api/executors/verify endpoint to test custom executor connectivity
- Add executor documentation page at /docs/executors with API specification
- Create deployable Vercel executor template in templates/vercel-executor/
- Update MCP handlers and agent tool execution to use configurable executors
- Add executor types and schemas to @tpmjs/types package

Users can now deploy their own executor instances and configure collections
or agents to use custom executors instead of the TPMJS default executor.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Ajax Davis 2026-01-09 00:19:22 +10:00
parent 84d894e920
commit bc36d366bc
27 changed files with 1909 additions and 85 deletions

View file

@ -36,6 +36,10 @@
"./user": {
"types": "./dist/user.d.ts",
"default": "./dist/user.js"
},
"./executor": {
"types": "./dist/executor.d.ts",
"default": "./dist/executor.js"
}
},
"files": ["dist"],

View file

@ -1,8 +1,19 @@
import { z } from 'zod';
import { ExecutorTypeSchema } from './executor';
// Regex for valid agent UID: lowercase alphanumeric and hyphens
const UID_REGEX = /^[a-z0-9][a-z0-9-]*[a-z0-9]$|^[a-z0-9]$/;
// Executor config for updates (simplified schema that maps to database JSON)
const ExecutorConfigUpdateSchema = z
.object({
url: z.string().url(),
apiKey: z.string().optional(),
})
.nullable()
.optional();
// ============================================================================
// Enums
// ============================================================================
@ -49,6 +60,9 @@ export const UpdateAgentSchema = z.object({
maxToolCallsPerTurn: z.number().int().min(1).max(100).optional(),
maxMessagesInContext: z.number().int().min(1).max(100).optional(),
isPublic: z.boolean().optional(),
// Executor configuration
executorType: ExecutorTypeSchema.nullable().optional(),
executorConfig: ExecutorConfigUpdateSchema,
});
export const AddCollectionToAgentSchema = z.object({

View file

@ -1,8 +1,19 @@
import { z } from 'zod';
import { ExecutorTypeSchema } from './executor';
// Regex for valid collection names: letters, numbers, spaces, hyphens, underscores
const NAME_REGEX = /^[a-zA-Z0-9\s\-_]+$/;
// Executor config for updates (simplified schema that maps to database JSON)
const ExecutorConfigUpdateSchema = z
.object({
url: z.string().url(),
apiKey: z.string().optional(),
})
.nullable()
.optional();
// ============================================================================
// Collection Schemas
// ============================================================================
@ -30,6 +41,9 @@ export const UpdateCollectionSchema = z.object({
.nullable()
.optional(),
isPublic: z.boolean().optional(),
// Executor configuration
executorType: ExecutorTypeSchema.nullable().optional(),
executorConfig: ExecutorConfigUpdateSchema,
});
// ============================================================================

View file

@ -0,0 +1,146 @@
/**
* Executor API Types
*
* These types define the contract between TPMJS and any executor service.
* Custom executors must implement the ExecuteToolRequest/Response interface.
*/
import { z } from 'zod';
// =============================================================================
// Executor API Specification
// =============================================================================
/**
* Request payload for POST /execute-tool
*/
export interface ExecuteToolRequest {
/** NPM package name, e.g., "@tpmjs/hello" */
packageName: string;
/** Tool name within the package, e.g., "helloWorld" */
name: string;
/** Package version, e.g., "1.0.0" or "latest" */
version?: string;
/** Direct esm.sh URL override for the package */
importUrl?: string;
/** Tool parameters to pass to execute() */
params: Record<string, unknown>;
/** Environment variables to inject during execution */
env?: Record<string, string>;
}
/**
* Response from POST /execute-tool
*/
export interface ExecuteToolResponse {
/** Whether the execution succeeded */
success: boolean;
/** Tool output on success */
output?: unknown;
/** Error message on failure */
error?: string;
/** Execution duration in milliseconds */
executionTimeMs: number;
}
/**
* Response from GET /health (optional but recommended)
*/
export interface ExecutorHealthResponse {
/** Executor status */
status: 'ok' | 'degraded' | 'error';
/** Executor version string */
version?: string;
/** Optional additional info */
info?: Record<string, unknown>;
}
// =============================================================================
// Executor Configuration Schemas
// =============================================================================
/**
* Executor type enum
*/
export const ExecutorTypeSchema = z.enum(['default', 'custom_url']);
export type ExecutorType = z.infer<typeof ExecutorTypeSchema>;
/**
* Default executor config (uses TPMJS Railway executor)
*/
export const DefaultExecutorConfigSchema = z.object({
type: z.literal('default'),
});
/**
* Custom URL executor config
*/
export const CustomUrlExecutorConfigSchema = z.object({
type: z.literal('custom_url'),
/** URL of the custom executor (must be HTTPS in production) */
url: z.string().url(),
/** Optional API key for Bearer token authentication */
apiKey: z.string().optional(),
});
/**
* Union of all executor config types
*/
export const ExecutorConfigSchema = z.discriminatedUnion('type', [
DefaultExecutorConfigSchema,
CustomUrlExecutorConfigSchema,
]);
export type ExecutorConfig = z.infer<typeof ExecutorConfigSchema>;
export type DefaultExecutorConfig = z.infer<typeof DefaultExecutorConfigSchema>;
export type CustomUrlExecutorConfig = z.infer<typeof CustomUrlExecutorConfigSchema>;
// =============================================================================
// Zod Schemas for Request/Response Validation
// =============================================================================
export const ExecuteToolRequestSchema = z.object({
packageName: z.string().min(1),
name: z.string().min(1),
version: z.string().optional(),
importUrl: z.string().url().optional(),
params: z.record(z.string(), z.unknown()),
env: z.record(z.string(), z.string()).optional(),
});
export const ExecuteToolResponseSchema = z.object({
success: z.boolean(),
output: z.unknown().optional(),
error: z.string().optional(),
executionTimeMs: z.number(),
});
export const ExecutorHealthResponseSchema = z.object({
status: z.enum(['ok', 'degraded', 'error']),
version: z.string().optional(),
info: z.record(z.string(), z.unknown()).optional(),
});
// =============================================================================
// Executor Verification
// =============================================================================
export const VerifyExecutorRequestSchema = z.object({
url: z.string().url(),
apiKey: z.string().optional(),
});
export interface VerifyExecutorRequest {
url: string;
apiKey?: string;
}
export interface VerifyExecutorResponse {
valid: boolean;
healthCheck?: ExecutorHealthResponse;
testExecution?: {
success: boolean;
executionTimeMs: number;
};
errors?: string[];
}

View file

@ -8,6 +8,7 @@ export default defineConfig({
'src/collection.ts',
'src/agent.ts',
'src/user.ts',
'src/executor.ts',
],
format: ['esm'],
dts: true,