/**
* relay_exec_script Tool
*
* Execute RelayPlane SDK scripts in a sandboxed environment.
* Useful for running generated workflow code without leaving the agent context.
*/
import { z } from 'zod';
import { getConfig } from '../config.js';
import { addRun, generateRunId } from './run-store.js';
export const relayExecScriptSchema = z.object({
code: z.string().describe('TypeScript/JavaScript code using @relayplane/sdk'),
timeout: z.number().optional().default(30000).describe('Execution timeout in milliseconds (default: 30000)'),
});
export type RelayExecScriptInput = z.infer<typeof relayExecScriptSchema>;
export interface RelayExecScriptResponse {
success: boolean;
output?: any;
logs: string[];
durationMs: number;
runId: string;
error?: {
code: string;
message: string;
line?: number;
};
}
/**
* Validate script for safety
*/
function validateScript(code: string): { valid: boolean; error?: string } {
// Block dangerous patterns
const dangerousPatterns = [
{ pattern: /process\.exit/i, message: 'process.exit is not allowed' },
{ pattern: /child_process/i, message: 'child_process is not allowed' },
{ pattern: /require\s*\(\s*['"`]fs['"`]\s*\)/i, message: 'Direct fs access is not allowed' },
{ pattern: /require\s*\(\s*['"`]path['"`]\s*\)/i, message: 'Direct path access is not allowed' },
{ pattern: /eval\s*\(/i, message: 'eval is not allowed' },
{ pattern: /Function\s*\(/i, message: 'Function constructor is not allowed' },
{ pattern: /__dirname/i, message: '__dirname is not allowed' },
{ pattern: /__filename/i, message: '__filename is not allowed' },
{ pattern: /\.env/i, message: 'Direct .env access is not allowed' },
{ pattern: /process\.env(?!\.)/, message: 'Direct process.env access is not allowed' },
];
for (const { pattern, message } of dangerousPatterns) {
if (pattern.test(code)) {
return { valid: false, error: message };
}
}
// Must use RelayPlane SDK
if (!code.includes('@relayplane/sdk') && !code.includes('relay.')) {
return {
valid: false,
error: 'Script must use @relayplane/sdk. Import with: import { relay } from "@relayplane/sdk"'
};
}
return { valid: true };
}
/**
* Execute script in sandboxed context
*/
async function executeInSandbox(
code: string,
timeout: number
): Promise<{ output: any; logs: string[] }> {
const logs: string[] = [];
// Create a promise that rejects on timeout
const timeoutPromise = new Promise<never>((_, reject) => {
setTimeout(() => reject(new Error(`Script execution timed out after ${timeout}ms`)), timeout);
});
// Wrap the code execution
const executionPromise = (async () => {
// Dynamically import the SDK
const { relay } = await import('@relayplane/sdk');
// Create sandboxed console
const sandboxConsole = {
log: (...args: any[]) => logs.push(args.map(a => JSON.stringify(a)).join(' ')),
error: (...args: any[]) => logs.push(`[ERROR] ${args.map(a => JSON.stringify(a)).join(' ')}`),
warn: (...args: any[]) => logs.push(`[WARN] ${args.map(a => JSON.stringify(a)).join(' ')}`),
info: (...args: any[]) => logs.push(`[INFO] ${args.map(a => JSON.stringify(a)).join(' ')}`),
};
// Prepare the script - wrap in async IIFE if not already
let wrappedCode = code;
// Remove import statements (we inject relay)
wrappedCode = wrappedCode.replace(/import\s+.*from\s+['"]@relayplane\/sdk['"];?/g, '');
wrappedCode = wrappedCode.replace(/const\s+\{\s*relay\s*\}\s*=\s*require\s*\(['"]@relayplane\/sdk['"]\);?/g, '');
// Check if it's already an async function or IIFE
const isAsync = /^\s*(async\s+function|\(async|async\s*\()/.test(wrappedCode);
if (!isAsync && !wrappedCode.includes('await')) {
// Simple synchronous code - just execute
const AsyncFunction = Object.getPrototypeOf(async function(){}).constructor;
const fn = new AsyncFunction('relay', 'console', wrappedCode);
return await fn(relay, sandboxConsole);
} else {
// Async code - wrap in IIFE if needed
if (!wrappedCode.trim().startsWith('(async')) {
wrappedCode = `(async () => { ${wrappedCode} })()`;
}
const AsyncFunction = Object.getPrototypeOf(async function(){}).constructor;
const fn = new AsyncFunction('relay', 'console', `return ${wrappedCode}`);
return await fn(relay, sandboxConsole);
}
})();
// Race between execution and timeout
const output = await Promise.race([executionPromise, timeoutPromise]);
return { output, logs };
}
export async function relayExecScript(input: RelayExecScriptInput): Promise<RelayExecScriptResponse> {
const startTime = Date.now();
const runId = generateRunId();
const config = getConfig();
try {
// Validate script
const validation = validateScript(input.code);
if (!validation.valid) {
throw new Error(`Script validation failed: ${validation.error}`);
}
// Execute in sandbox
const { output, logs } = await executeInSandbox(input.code, input.timeout);
const durationMs = Date.now() - startTime;
const response: RelayExecScriptResponse = {
success: true,
output,
logs,
durationMs,
runId,
};
// Store run
addRun({
runId,
type: 'script',
model: 'exec_script',
success: true,
startTime: new Date(startTime),
endTime: new Date(),
durationMs,
usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0, estimatedProviderCostUsd: 0 },
input: { code: input.code.substring(0, 500) }, // Truncate for storage
output,
});
return response;
} catch (error) {
const durationMs = Date.now() - startTime;
const errorMessage = error instanceof Error ? error.message : String(error);
// Try to extract line number from error
let lineNumber: number | undefined;
const lineMatch = errorMessage.match(/:(\d+):\d+/);
if (lineMatch) {
lineNumber = parseInt(lineMatch[1], 10);
}
const response: RelayExecScriptResponse = {
success: false,
logs: [],
durationMs,
runId,
error: {
code: 'SCRIPT_ERROR',
message: errorMessage,
line: lineNumber,
},
};
// Store failed run
addRun({
runId,
type: 'script',
model: 'exec_script',
success: false,
startTime: new Date(startTime),
endTime: new Date(),
durationMs,
usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0, estimatedProviderCostUsd: 0 },
input: { code: input.code.substring(0, 500) },
error: errorMessage,
});
return response;
}
}
export const relayExecScriptDefinition = {
name: 'relay_exec_script',
description:
'Execute RelayPlane SDK scripts in a sandboxed environment. Useful for testing generated workflow code. The SDK is automatically imported - just write your workflow code. Scripts have a default 30s timeout.',
inputSchema: {
type: 'object' as const,
properties: {
code: {
type: 'string',
description: 'TypeScript/JavaScript code using @relayplane/sdk. Import is handled automatically.',
},
timeout: {
type: 'number',
description: 'Execution timeout in milliseconds (default: 30000)',
},
},
required: ['code'],
},
};