branch:
node-server-client.ts
3610 bytesRaw
import type {
Executor,
ExecuteResult,
ResolvedProvider
} from "@cloudflare/codemode";
type ToolFns = Record<string, (...args: unknown[]) => Promise<unknown>>;
export interface NodeServerExecutorOptions {
/** URL of the Node executor server, e.g. "http://localhost:3001" */
serverUrl: string;
/** Base URL that the Node server will call back to for tool invocations */
callbackUrl: string;
/** Shared registry — the Agent instance owns this Map so it survives across isolate boundaries */
registry: Map<string, ToolFns>;
}
/**
* Executor that delegates code execution to an external Node.js HTTP server.
* Tool calls from the sandboxed code are routed back via HTTP to the callback URL,
* which forwards to the DO where the registry lives.
*/
export class NodeServerExecutor implements Executor {
#serverUrl: string;
#callbackUrl: string;
#registry: Map<string, ToolFns>;
constructor(options: NodeServerExecutorOptions) {
this.#serverUrl = options.serverUrl;
this.#callbackUrl = options.callbackUrl;
this.#registry = options.registry;
}
async execute(
code: string,
providersOrFns: ResolvedProvider[] | ToolFns
): Promise<ExecuteResult> {
const fns: ToolFns = Array.isArray(providersOrFns)
? Object.fromEntries(providersOrFns.flatMap((p) => Object.entries(p.fns)))
: providersOrFns;
const execId = crypto.randomUUID();
// Register tool functions so the DO's onRequest can find them
this.#registry.set(execId, fns);
try {
const res = await fetch(`${this.#serverUrl}/execute`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
code,
callbackUrl: `${this.#callbackUrl}/${execId}`,
tools: Object.keys(fns)
})
});
const data = (await res.json()) as {
result?: unknown;
error?: string;
logs?: string[];
};
if (data.error) {
return { result: undefined, error: data.error, logs: data.logs };
}
return { result: data.result, logs: data.logs };
} finally {
this.#registry.delete(execId);
}
}
}
/**
* Handle an incoming tool callback request.
*
* @param request - the forwarded request (pathname: /node-executor-callback/{agentName}/{execId}/{toolName})
* @param registry - the Agent-owned Map of execution IDs → tool functions
*/
export async function handleToolCallback(
request: Request,
registry: Map<string, ToolFns>
): Promise<Response> {
const url = new URL(request.url);
const parts = url.pathname.split("/").filter(Boolean);
// parts: ["node-executor-callback", agentName, execId, toolName]
const execId = parts[2];
const toolName = parts[3];
if (!execId || !toolName) {
return Response.json(
{
error:
"Invalid callback path — expected /node-executor-callback/{agent}/{execId}/{toolName}"
},
{ status: 400 }
);
}
const fns = registry.get(execId);
if (!fns) {
return Response.json(
{ error: `No execution found for id "${execId}"` },
{ status: 404 }
);
}
const fn = fns[toolName];
if (!fn) {
return Response.json(
{ error: `Tool "${toolName}" not found` },
{ status: 404 }
);
}
try {
const body = await request.text();
const args = body ? JSON.parse(body) : {};
const result = await fn(args);
return Response.json({ result });
} catch (err) {
return Response.json(
{ error: err instanceof Error ? err.message : String(err) },
{ status: 500 }
);
}
}