| @@ -0,0 +1,382 @@ |
| | 1 | import { WorkerEntrypoint } from "cloudflare:workers"; |
| | 2 | |
| | 3 | import GLOBALS_MODULE from "./isolate/globals.isolate.js"; |
| | 4 | import HARNESS_CODE from "./isolate/harness.isolate.js"; |
| | 5 | |
| | 6 | const DEFAULT_TIMEOUT_MS = 30_000; |
| | 7 | const MAX_TIMEOUT_MS = 120_000; |
| | 8 | |
| | 9 | type RuntimeToolCallResult = |
| | 10 | | { |
| | 11 | ok: true; |
| | 12 | value: unknown; |
| | 13 | } |
| | 14 | | { |
| | 15 | ok: false; |
| | 16 | kind: "pending"; |
| | 17 | approvalId: string; |
| | 18 | retryAfterMs: number; |
| | 19 | error?: string; |
| | 20 | } |
| | 21 | | { |
| | 22 | ok: false; |
| | 23 | kind: "denied"; |
| | 24 | error: string; |
| | 25 | } |
| | 26 | | { |
| | 27 | ok: false; |
| | 28 | kind: "failed"; |
| | 29 | error: string; |
| | 30 | }; |
| | 31 | |
| | 32 | type RunRequest = { |
| | 33 | runId: string; |
| | 34 | taskId: string; |
| | 35 | code: string; |
| | 36 | timeoutMs?: number; |
| | 37 | callback: { |
| | 38 | url: string; |
| | 39 | internalSecret?: string; |
| | 40 | }; |
| | 41 | }; |
| | 42 | |
| | 43 | type RunResult = { |
| | 44 | status: "completed" | "failed" | "timed_out" | "denied"; |
| | 45 | result?: unknown; |
| | 46 | error?: string; |
| | 47 | exitCode?: number; |
| | 48 | }; |
| | 49 | |
| | 50 | type BridgeProps = { |
| | 51 | callbackUrl: string; |
| | 52 | callbackInternalSecret: string | null; |
| | 53 | runId: string; |
| | 54 | }; |
| | 55 | |
| | 56 | type Env = { |
| | 57 | AUTH_TOKEN: string; |
| | 58 | LOADER: { |
| | 59 | get: ( |
| | 60 | id: string, |
| | 61 | init: () => Promise<{ |
| | 62 | compatibilityDate: string; |
| | 63 | mainModule: string; |
| | 64 | modules: Record<string, string>; |
| | 65 | env: Record<string, unknown>; |
| | 66 | globalOutbound: null; |
| | 67 | }>, |
| | 68 | ) => { |
| | 69 | getEntrypoint: () => { |
| | 70 | fetch: (url: string, init: RequestInit) => Promise<Response>; |
| | 71 | }; |
| | 72 | }; |
| | 73 | }; |
| | 74 | }; |
| | 75 | |
| | 76 | const json = (value: unknown, status = 200): Response => |
| | 77 | new Response(JSON.stringify(value), { |
| | 78 | status, |
| | 79 | headers: { |
| | 80 | "content-type": "application/json", |
| | 81 | }, |
| | 82 | }); |
| | 83 | |
| | 84 | const isObjectRecord = (value: unknown): value is Record<string, unknown> => |
| | 85 | typeof value === "object" && value !== null && !Array.isArray(value); |
| | 86 | |
| | 87 | const isRuntimeToolCallResult = (value: unknown): value is RuntimeToolCallResult => { |
| | 88 | if (!isObjectRecord(value)) { |
| | 89 | return false; |
| | 90 | } |
| | 91 | |
| | 92 | if (value.ok === true) { |
| | 93 | return Object.prototype.hasOwnProperty.call(value, "value"); |
| | 94 | } |
| | 95 | |
| | 96 | if (value.ok === false && value.kind === "pending") { |
| | 97 | return ( |
| | 98 | typeof value.approvalId === "string" && |
| | 99 | typeof value.retryAfterMs === "number" |
| | 100 | ); |
| | 101 | } |
| | 102 | |
| | 103 | if (value.ok === false && value.kind === "denied") { |
| | 104 | return typeof value.error === "string"; |
| | 105 | } |
| | 106 | |
| | 107 | if (value.ok === false && value.kind === "failed") { |
| | 108 | return typeof value.error === "string"; |
| | 109 | } |
| | 110 | |
| | 111 | return false; |
| | 112 | }; |
| | 113 | |
| | 114 | const isRunResult = (value: unknown): value is RunResult => { |
| | 115 | if (!isObjectRecord(value)) { |
| | 116 | return false; |
| | 117 | } |
| | 118 | |
| | 119 | if ( |
| | 120 | value.status !== "completed" && |
| | 121 | value.status !== "failed" && |
| | 122 | value.status !== "timed_out" && |
| | 123 | value.status !== "denied" |
| | 124 | ) { |
| | 125 | return false; |
| | 126 | } |
| | 127 | |
| | 128 | if (value.error !== undefined && typeof value.error !== "string") { |
| | 129 | return false; |
| | 130 | } |
| | 131 | |
| | 132 | if (value.exitCode !== undefined && typeof value.exitCode !== "number") { |
| | 133 | return false; |
| | 134 | } |
| | 135 | |
| | 136 | return true; |
| | 137 | }; |
| | 138 | |
| | 139 | const toFailedResult = (error: string): RunResult => ({ |
| | 140 | status: "failed", |
| | 141 | error, |
| | 142 | }); |
| | 143 | |
| | 144 | const validateRunRequest = (value: unknown): RunRequest | null => { |
| | 145 | if (!isObjectRecord(value)) { |
| | 146 | return null; |
| | 147 | } |
| | 148 | |
| | 149 | if ( |
| | 150 | typeof value.runId !== "string" || |
| | 151 | typeof value.taskId !== "string" || |
| | 152 | typeof value.code !== "string" |
| | 153 | ) { |
| | 154 | return null; |
| | 155 | } |
| | 156 | |
| | 157 | if (!isObjectRecord(value.callback) || typeof value.callback.url !== "string") { |
| | 158 | return null; |
| | 159 | } |
| | 160 | |
| | 161 | if ( |
| | 162 | value.callback.internalSecret !== undefined && |
| | 163 | typeof value.callback.internalSecret !== "string" |
| | 164 | ) { |
| | 165 | return null; |
| | 166 | } |
| | 167 | |
| | 168 | if (value.timeoutMs !== undefined && typeof value.timeoutMs !== "number") { |
| | 169 | return null; |
| | 170 | } |
| | 171 | |
| | 172 | return { |
| | 173 | runId: value.runId, |
| | 174 | taskId: value.taskId, |
| | 175 | code: value.code, |
| | 176 | timeoutMs: value.timeoutMs, |
| | 177 | callback: { |
| | 178 | url: value.callback.url, |
| | 179 | internalSecret: value.callback.internalSecret, |
| | 180 | }, |
| | 181 | }; |
| | 182 | }; |
| | 183 | |
| | 184 | const buildUserModule = (userCode: string): string => |
| | 185 | `export async function run(tools, console) {\n"use strict";\n${userCode}\n}\n`; |
| | 186 | |
| | 187 | const getBridgePropsFromContext = ( |
| | 188 | context: ExecutionContext | Record<string, unknown> | null | undefined, |
| | 189 | ): BridgeProps => { |
| | 190 | if (!isObjectRecord(context) || !isObjectRecord(context.props)) { |
| | 191 | throw new Error("ToolBridge props are missing"); |
| | 192 | } |
| | 193 | |
| | 194 | const props = context.props; |
| | 195 | |
| | 196 | if ( |
| | 197 | typeof props.callbackUrl !== "string" || |
| | 198 | typeof props.runId !== "string" |
| | 199 | ) { |
| | 200 | throw new Error("ToolBridge props are invalid"); |
| | 201 | } |
| | 202 | |
| | 203 | if ( |
| | 204 | props.callbackInternalSecret !== null && |
| | 205 | props.callbackInternalSecret !== undefined && |
| | 206 | typeof props.callbackInternalSecret !== "string" |
| | 207 | ) { |
| | 208 | throw new Error("ToolBridge callback secret is invalid"); |
| | 209 | } |
| | 210 | |
| | 211 | return { |
| | 212 | callbackUrl: props.callbackUrl, |
| | 213 | callbackInternalSecret: |
| | 214 | props.callbackInternalSecret === undefined ? null : props.callbackInternalSecret, |
| | 215 | runId: props.runId, |
| | 216 | }; |
| | 217 | }; |
| | 218 | |
| | 219 | class ToolBridge extends WorkerEntrypoint<Env> { |
| | 220 | private get props(): BridgeProps { |
| | 221 | return getBridgePropsFromContext(this.ctx); |
| | 222 | } |
| | 223 | |
| | 224 | async callTool( |
| | 225 | toolPath: string, |
| | 226 | input: unknown, |
| | 227 | callId?: string, |
| | 228 | ): Promise<RuntimeToolCallResult> { |
| | 229 | const headers = new Headers({ |
| | 230 | "content-type": "application/json", |
| | 231 | }); |
| | 232 | |
| | 233 | if (this.props.callbackInternalSecret) { |
| | 234 | headers.set("x-internal-secret", this.props.callbackInternalSecret); |
| | 235 | } |
| | 236 | |
| | 237 | const response = await fetch(this.props.callbackUrl, { |
| | 238 | method: "POST", |
| | 239 | headers, |
| | 240 | body: JSON.stringify({ |
| | 241 | runId: this.props.runId, |
| | 242 | callId: callId ?? `call_${crypto.randomUUID()}`, |
| | 243 | toolPath, |
| | 244 | input, |
| | 245 | }), |
| | 246 | }).catch((error: unknown) => { |
| | 247 | throw new Error( |
| | 248 | `Tool callback transport failed: ${ |
| | 249 | error instanceof Error ? error.message : String(error) |
| | 250 | }`, |
| | 251 | ); |
| | 252 | }); |
| | 253 | |
| | 254 | if (response.status !== 200) { |
| | 255 | return { |
| | 256 | ok: false, |
| | 257 | kind: "failed", |
| | 258 | error: `Tool callback returned ${response.status}`, |
| | 259 | }; |
| | 260 | } |
| | 261 | |
| | 262 | const payload = await response.json().catch(() => null); |
| | 263 | if (!isRuntimeToolCallResult(payload)) { |
| | 264 | return { |
| | 265 | ok: false, |
| | 266 | kind: "failed", |
| | 267 | error: "Tool callback returned invalid result payload", |
| | 268 | }; |
| | 269 | } |
| | 270 | |
| | 271 | return payload; |
| | 272 | } |
| | 273 | } |
| | 274 | |
| | 275 | const executeSandboxRun = async ( |
| | 276 | request: RunRequest, |
| | 277 | ctx: ExecutionContext, |
| | 278 | env: Env, |
| | 279 | ): Promise<RunResult> => { |
| | 280 | const timeoutMsRaw = request.timeoutMs ?? DEFAULT_TIMEOUT_MS; |
| | 281 | const timeoutMs = Math.max(1, Math.min(MAX_TIMEOUT_MS, Math.floor(timeoutMsRaw))); |
| | 282 | |
| | 283 | const contextAsRecord = ctx as unknown as Record<string, unknown>; |
| | 284 | if (!isObjectRecord(contextAsRecord.exports)) { |
| | 285 | return toFailedResult("Execution context exports are unavailable"); |
| | 286 | } |
| | 287 | |
| | 288 | const toolBridgeFactory = contextAsRecord.exports.ToolBridge; |
| | 289 | if (typeof toolBridgeFactory !== "function") { |
| | 290 | return toFailedResult("Execution context ToolBridge export is unavailable"); |
| | 291 | } |
| | 292 | |
| | 293 | const toolBridgeBinding = toolBridgeFactory({ |
| | 294 | props: { |
| | 295 | callbackUrl: request.callback.url, |
| | 296 | callbackInternalSecret: request.callback.internalSecret ?? null, |
| | 297 | runId: request.runId, |
| | 298 | }, |
| | 299 | }); |
| | 300 | |
| | 301 | const worker = env.LOADER.get(request.taskId, async () => ({ |
| | 302 | compatibilityDate: "2025-06-01", |
| | 303 | mainModule: "harness.js", |
| | 304 | modules: { |
| | 305 | "harness.js": HARNESS_CODE, |
| | 306 | "globals.js": GLOBALS_MODULE, |
| | 307 | "user-code.js": buildUserModule(request.code), |
| | 308 | }, |
| | 309 | env: { |
| | 310 | TOOL_BRIDGE: toolBridgeBinding, |
| | 311 | }, |
| | 312 | globalOutbound: null, |
| | 313 | })); |
| | 314 | |
| | 315 | const entrypoint = worker.getEntrypoint(); |
| | 316 | const controller = new AbortController(); |
| | 317 | const timer = setTimeout(() => controller.abort(), timeoutMs); |
| | 318 | |
| | 319 | let response: Response; |
| | 320 | try { |
| | 321 | response = await entrypoint.fetch("http://sandbox.internal/run", { |
| | 322 | method: "POST", |
| | 323 | signal: controller.signal, |
| | 324 | }); |
| | 325 | } catch (error) { |
| | 326 | clearTimeout(timer); |
| | 327 | if (error instanceof DOMException && error.name === "AbortError") { |
| | 328 | return { |
| | 329 | status: "timed_out", |
| | 330 | error: `Execution timed out after ${timeoutMs}ms`, |
| | 331 | }; |
| | 332 | } |
| | 333 | |
| | 334 | return toFailedResult( |
| | 335 | `Sandbox isolate request failed: ${ |
| | 336 | error instanceof Error ? error.message : String(error) |
| | 337 | }`, |
| | 338 | ); |
| | 339 | } |
| | 340 | |
| | 341 | clearTimeout(timer); |
| | 342 | |
| | 343 | const payload = await response.json().catch(() => null); |
| | 344 | if (!isRunResult(payload)) { |
| | 345 | return toFailedResult("Sandbox isolate returned invalid response payload"); |
| | 346 | } |
| | 347 | |
| | 348 | return payload; |
| | 349 | }; |
| | 350 | |
| | 351 | export { ToolBridge }; |
| | 352 | |
| | 353 | export default { |
| | 354 | async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> { |
| | 355 | const url = new URL(request.url); |
| | 356 | |
| | 357 | if (url.pathname === "/health") { |
| | 358 | return new Response("ok", { status: 200 }); |
| | 359 | } |
| | 360 | |
| | 361 | if (url.pathname !== "/v1/runs" || request.method !== "POST") { |
| | 362 | return json({ error: "not_found" }, 404); |
| | 363 | } |
| | 364 | |
| | 365 | if (typeof env.AUTH_TOKEN !== "string" || env.AUTH_TOKEN.length === 0) { |
| | 366 | return json({ error: "misconfigured_auth_token" }, 500); |
| | 367 | } |
| | 368 | |
| | 369 | if (request.headers.get("authorization") !== `Bearer ${env.AUTH_TOKEN}`) { |
| | 370 | return json({ error: "unauthorized" }, 401); |
| | 371 | } |
| | 372 | |
| | 373 | const payloadRaw = await request.json().catch(() => null); |
| | 374 | const payload = validateRunRequest(payloadRaw); |
| | 375 | if (!payload) { |
| | 376 | return json({ error: "invalid_payload" }, 400); |
| | 377 | } |
| | 378 | |
| | 379 | const runResult = await executeSandboxRun(payload, ctx, env); |
| | 380 | return json(runResult, 200); |
| | 381 | }, |
| | 382 | }; |