History

wrangler? hardly know'er!

Rhys Sullivan committed 8 months ago

Commit f202f1e1 parent 889122f5 files · +620 −0Browse files

Unified · Split

5 files
  1. v2/packages/runtime-cloudflare-worker-loader/worker/src/index.ts +382 −0
  2. v2/packages/runtime-cloudflare-worker-loader/worker/src/isolate/globals.isolate.js +1 −0
  3. v2/packages/runtime-cloudflare-worker-loader/worker/src/isolate/harness.isolate.js +202 −0
  4. v2/packages/runtime-cloudflare-worker-loader/worker/wrangler.jsonc +21 −0
  5. v2/packages/schema/src/models/runtime-tool.ts +14 −0
v2/packages/runtime-cloudflare-worker-loader/worker/src/index.ts+382−0
@@ -0,0 +1,382 @@
1import { WorkerEntrypoint } from "cloudflare:workers";
2
3import GLOBALS_MODULE from "./isolate/globals.isolate.js";
4import HARNESS_CODE from "./isolate/harness.isolate.js";
5
6const DEFAULT_TIMEOUT_MS = 30_000;
7const MAX_TIMEOUT_MS = 120_000;
8
9type 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
32type RunRequest = {
33 runId: string;
34 taskId: string;
35 code: string;
36 timeoutMs?: number;
37 callback: {
38 url: string;
39 internalSecret?: string;
40 };
41};
42
43type RunResult = {
44 status: "completed" | "failed" | "timed_out" | "denied";
45 result?: unknown;
46 error?: string;
47 exitCode?: number;
48};
49
50type BridgeProps = {
51 callbackUrl: string;
52 callbackInternalSecret: string | null;
53 runId: string;
54};
55
56type 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
76const json = (value: unknown, status = 200): Response =>
77 new Response(JSON.stringify(value), {
78 status,
79 headers: {
80 "content-type": "application/json",
81 },
82 });
83
84const isObjectRecord = (value: unknown): value is Record<string, unknown> =>
85 typeof value === "object" && value !== null && !Array.isArray(value);
86
87const 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
114const 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
139const toFailedResult = (error: string): RunResult => ({
140 status: "failed",
141 error,
142});
143
144const 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
184const buildUserModule = (userCode: string): string =>
185 `export async function run(tools, console) {\n"use strict";\n${userCode}\n}\n`;
186
187const 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
219class 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
275const 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
351export { ToolBridge };
352
353export 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};
v2/packages/runtime-cloudflare-worker-loader/worker/src/isolate/globals.isolate.js+1−0
@@ -0,0 +1 @@
1export const ResponseJson = Response.json.bind(Response);
v2/packages/runtime-cloudflare-worker-loader/worker/src/isolate/harness.isolate.js+202−0
@@ -0,0 +1,202 @@
1import { ResponseJson as _ResponseJson } from "./globals.js";
2import { run } from "./user-code.js";
3
4const APPROVAL_DENIED_PREFIX = "APPROVAL_DENIED:";
5
6function decodeToolBridgeResult(rawResult) {
7 const parsed = (() => {
8 if (typeof rawResult === "string") {
9 try {
10 return JSON.parse(rawResult);
11 } catch {
12 return null;
13 }
14 }
15
16 if (rawResult && typeof rawResult === "object") {
17 return rawResult;
18 }
19
20 return null;
21 })();
22
23 if (!parsed || typeof parsed !== "object") {
24 return {
25 ok: false,
26 kind: "failed",
27 error: "Tool bridge returned an invalid payload",
28 };
29 }
30
31 if (parsed.ok === true) {
32 return {
33 ok: true,
34 value: parsed.value,
35 };
36 }
37
38 if (
39 parsed.ok === false
40 && parsed.kind === "pending"
41 && typeof parsed.approvalId === "string"
42 && typeof parsed.retryAfterMs === "number"
43 ) {
44 return {
45 ok: false,
46 kind: "pending",
47 approvalId: parsed.approvalId,
48 retryAfterMs: parsed.retryAfterMs,
49 ...(typeof parsed.error === "string" ? { error: parsed.error } : {}),
50 };
51 }
52
53 if (parsed.ok === false && parsed.kind === "denied" && typeof parsed.error === "string") {
54 return {
55 ok: false,
56 kind: "denied",
57 error: parsed.error,
58 };
59 }
60
61 if (parsed.ok === false && parsed.kind === "failed" && typeof parsed.error === "string") {
62 return {
63 ok: false,
64 kind: "failed",
65 error: parsed.error,
66 };
67 }
68
69 return {
70 ok: false,
71 kind: "failed",
72 error: "Tool bridge returned a malformed payload",
73 };
74}
75
76function describeExecutionError(error) {
77 if (error instanceof Error) {
78 const message = typeof error.message === "string" ? error.message.trim() : "";
79 if (message.length > 0) {
80 return message;
81 }
82
83 const stack = typeof error.stack === "string" ? error.stack.trim() : "";
84 if (stack.length > 0) {
85 return stack;
86 }
87
88 return "Execution failed with an empty Error object";
89 }
90
91 const text = String(error).trim();
92 if (text.length > 0) {
93 return text;
94 }
95
96 return "Execution failed with an empty non-Error throw value";
97}
98
99function createToolsProxy(bridge, path = []) {
100 const callable = () => {};
101
102 return new Proxy(callable, {
103 get(_target, prop) {
104 if (prop === "then") {
105 return undefined;
106 }
107
108 if (typeof prop !== "string") {
109 return undefined;
110 }
111
112 return createToolsProxy(bridge, [...path, prop]);
113 },
114
115 async apply(_target, _thisArg, args) {
116 const toolPath = path.join(".");
117 if (!toolPath) {
118 throw new Error("Tool path missing");
119 }
120
121 const input = args.length > 0 ? args[0] : {};
122 const callId = `call_${crypto.randomUUID()}`;
123 const result = decodeToolBridgeResult(
124 await bridge.callTool(toolPath, input, callId),
125 );
126
127 if (result.ok) {
128 return result.value;
129 }
130
131 if (result.kind === "pending") {
132 throw new Error(
133 `Tool call is unexpectedly pending approval (${result.approvalId})`,
134 );
135 }
136
137 if (result.kind === "denied") {
138 throw new Error(APPROVAL_DENIED_PREFIX + result.error);
139 }
140
141 const errorText = typeof result.error === "string" ? result.error.trim() : "";
142 throw new Error(
143 errorText.length > 0
144 ? errorText
145 : `Tool call failed without an error message (${toolPath})`,
146 );
147 },
148 });
149}
150
151function sanitizeExecutionResult(value) {
152 if (value === undefined) {
153 return undefined;
154 }
155
156 try {
157 const serialized = JSON.stringify(value);
158 if (serialized === undefined) {
159 return null;
160 }
161
162 return JSON.parse(serialized);
163 } catch {
164 return String(value);
165 }
166}
167
168export default {
169 async fetch(_request, env, _ctx) {
170 const tools = createToolsProxy(env.TOOL_BRIDGE);
171 const console = {
172 log: (..._args) => {},
173 info: (..._args) => {},
174 warn: (..._args) => {},
175 error: (..._args) => {},
176 };
177
178 try {
179 const value = await run(tools, console);
180 return _ResponseJson({
181 status: "completed",
182 result: sanitizeExecutionResult(value),
183 exitCode: 0,
184 });
185 } catch (error) {
186 const message = describeExecutionError(error);
187
188 if (message.startsWith(APPROVAL_DENIED_PREFIX)) {
189 const denied = message.replace(APPROVAL_DENIED_PREFIX, "").trim();
190 return _ResponseJson({
191 status: "denied",
192 error: denied,
193 });
194 }
195
196 return _ResponseJson({
197 status: "failed",
198 error: message,
199 });
200 }
201 },
202};
v2/packages/runtime-cloudflare-worker-loader/worker/wrangler.jsonc+21−0
@@ -0,0 +1,21 @@
1{
2 "name": "executor-v2-runtime-cloudflare-worker-loader",
3 "main": "src/index.ts",
4 "compatibility_date": "2025-06-01",
5 "compatibility_flags": ["enable_ctx_exports", "nodejs_compat"],
6 "vars": {
7 "AUTH_TOKEN": "sandbox-token"
8 },
9 "rules": [
10 {
11 "type": "Text",
12 "globs": ["**/*.isolate.js"],
13 "fallthrough": true
14 }
15 ],
16 "worker_loaders": [
17 {
18 "binding": "LOADER"
19 }
20 ]
21}
v2/packages/schema/src/models/runtime-tool.ts+14−0
@@ -62,6 +62,12 @@ export const OpenApiInvocationPayloadSchema = Schema.Struct({ · 61 unmodified lines
62 requestBody: Schema.NullOr(OpenApiToolRequestBodySchema),62 requestBody: Schema.NullOr(OpenApiToolRequestBodySchema),
63});63});
64 64
65export const DiscoveryTypingPayloadSchema = Schema.Struct({
66 inputSchemaJson: Schema.optional(Schema.String),
67 outputSchemaJson: Schema.optional(Schema.String),
68 refHintKeys: Schema.optional(Schema.Array(Schema.String)),
69});
70
65export const OpenApiExtractedToolSchema = Schema.Struct({71export const OpenApiExtractedToolSchema = Schema.Struct({
66 toolId: Schema.String,72 toolId: Schema.String,
67 name: Schema.String,73 name: Schema.String,
@@ -70,12 +76,19 @@ export const OpenApiExtractedToolSchema = Schema.Struct({ · 2 unmodified lines
70 path: Schema.String,76 path: Schema.String,
71 invocation: OpenApiInvocationPayloadSchema,77 invocation: OpenApiInvocationPayloadSchema,
72 operationHash: Schema.String,78 operationHash: Schema.String,
79 typing: Schema.optional(DiscoveryTypingPayloadSchema),
73});80});
74 81
75export const OpenApiToolManifestSchema = Schema.Struct({82export const OpenApiToolManifestSchema = Schema.Struct({
76 version: Schema.Literal(1),83 version: Schema.Literal(1),
77 sourceHash: Schema.String,84 sourceHash: Schema.String,
78 tools: Schema.Array(OpenApiExtractedToolSchema),85 tools: Schema.Array(OpenApiExtractedToolSchema),
86 refHintTable: Schema.optional(
87 Schema.Record({
88 key: Schema.String,
89 value: Schema.String,
90 }),
91 ),
79});92});
80 93
81const SourceBackedCanonicalToolDescriptorBaseSchema = Schema.Struct({94const SourceBackedCanonicalToolDescriptorBaseSchema = Schema.Struct({
@@ -170,6 +183,7 @@ export type OpenApiParameterLocation = typeof OpenApiParameterLocationSchema.Typ · 88 unmodified lines
170export type OpenApiToolParameter = typeof OpenApiToolParameterSchema.Type;183export type OpenApiToolParameter = typeof OpenApiToolParameterSchema.Type;
171export type OpenApiToolRequestBody = typeof OpenApiToolRequestBodySchema.Type;184export type OpenApiToolRequestBody = typeof OpenApiToolRequestBodySchema.Type;
172export type OpenApiInvocationPayload = typeof OpenApiInvocationPayloadSchema.Type;185export type OpenApiInvocationPayload = typeof OpenApiInvocationPayloadSchema.Type;
186export type DiscoveryTypingPayload = typeof DiscoveryTypingPayloadSchema.Type;
173export type OpenApiExtractedTool = typeof OpenApiExtractedToolSchema.Type;187export type OpenApiExtractedTool = typeof OpenApiExtractedToolSchema.Type;
174export type OpenApiToolManifest = typeof OpenApiToolManifestSchema.Type;188export type OpenApiToolManifest = typeof OpenApiToolManifestSchema.Type;
175 189