@effect-agent/platform-cloudflare 0.1.0-beta.8 → 0.1.0-beta.80
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Alarm.d.mts +183 -0
- package/dist/Alarm.mjs +454 -0
- package/dist/Alarm.mjs.map +1 -0
- package/dist/BrowserRestCapture.d.mts +35 -0
- package/dist/BrowserRestCapture.mjs +238 -0
- package/dist/BrowserRestCapture.mjs.map +1 -0
- package/dist/BrowserRestCrawl.d.mts +17 -0
- package/dist/BrowserRestCrawl.mjs +244 -0
- package/dist/BrowserRestCrawl.mjs.map +1 -0
- package/dist/CloudflareAiGateway.d.mts +64 -0
- package/dist/CloudflareAiGateway.mjs +70 -0
- package/dist/CloudflareAiGateway.mjs.map +1 -0
- package/dist/CloudflareBindings.d.mts +112 -0
- package/dist/CloudflareBindings.mjs +101 -0
- package/dist/CloudflareBindings.mjs.map +1 -0
- package/dist/CloudflareBrowser-D4YqaTAl.mjs +495 -0
- package/dist/CloudflareBrowser-D4YqaTAl.mjs.map +1 -0
- package/dist/CloudflareBrowser-DJq53niJ.d.mts +80 -0
- package/dist/CloudflareBrowser.d.mts +2 -0
- package/dist/CloudflareBrowser.mjs +2 -0
- package/dist/CloudflareCodeMode.d.mts +44 -0
- package/dist/CloudflareCodeMode.mjs +616 -0
- package/dist/CloudflareCodeMode.mjs.map +1 -0
- package/dist/CloudflareConfig-f3CqTel1.d.mts +101 -0
- package/dist/CloudflareConfig.d.mts +2 -0
- package/dist/CloudflareConfig.mjs +122 -0
- package/dist/CloudflareConfig.mjs.map +1 -0
- package/dist/CloudflareMemory.d.mts +142 -0
- package/dist/CloudflareMemory.mjs +195 -0
- package/dist/CloudflareMemory.mjs.map +1 -0
- package/dist/CloudflareScheduling.d.mts +52 -0
- package/dist/CloudflareScheduling.mjs +389 -0
- package/dist/CloudflareScheduling.mjs.map +1 -0
- package/dist/CloudflareSubscriptions.d.mts +87 -0
- package/dist/CloudflareSubscriptions.mjs +524 -0
- package/dist/CloudflareSubscriptions.mjs.map +1 -0
- package/dist/CloudflareThreadClient.d.mts +1286 -0
- package/dist/CloudflareThreadClient.mjs +386 -0
- package/dist/CloudflareThreadClient.mjs.map +1 -0
- package/dist/InteractiveBrowser.d.mts +144 -0
- package/dist/InteractiveBrowser.mjs +1090 -0
- package/dist/InteractiveBrowser.mjs.map +1 -0
- package/dist/ProtectedBrowser.d.mts +65 -0
- package/dist/ProtectedBrowser.mjs +859 -0
- package/dist/ProtectedBrowser.mjs.map +1 -0
- package/dist/ThreadObject-3uDpKfB3.d.mts +816 -0
- package/dist/ThreadObject-DGkXv41C.mjs +836 -0
- package/dist/ThreadObject-DGkXv41C.mjs.map +1 -0
- package/dist/ThreadObject.d.mts +2 -0
- package/dist/ThreadObject.mjs +3 -0
- package/dist/WakeScheduler.d.mts +23 -0
- package/dist/WakeScheduler.mjs +60 -0
- package/dist/WakeScheduler.mjs.map +1 -0
- package/dist/boundary-BguazVkh.mjs +42 -0
- package/dist/boundary-BguazVkh.mjs.map +1 -0
- package/dist/browser-session-lifecycle-CUbVfEgP.mjs +84 -0
- package/dist/browser-session-lifecycle-CUbVfEgP.mjs.map +1 -0
- package/dist/browser-session-lifecycle-DJpqLXy_.d.mts +21 -0
- package/dist/index.d.mts +13 -1547
- package/dist/index.mjs +13 -1636
- package/dist/prepared-admission-DSi55W-7.mjs +73 -0
- package/dist/prepared-admission-DSi55W-7.mjs.map +1 -0
- package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
- package/package.json +1 -53
- package/src/Alarm.ts +953 -0
- package/src/BrowserRestCapture.ts +477 -0
- package/src/BrowserRestCrawl.ts +540 -0
- package/src/CloudflareAiGateway.ts +170 -0
- package/src/CloudflareBindings.ts +180 -0
- package/src/CloudflareBrowser.ts +15 -0
- package/src/{code-mode-executor.ts → CloudflareCodeMode.ts} +372 -204
- package/src/{config.ts → CloudflareConfig.ts} +9 -8
- package/src/CloudflareMemory.ts +406 -0
- package/src/CloudflareScheduling.ts +733 -0
- package/src/CloudflareSubscriptions.ts +1045 -0
- package/src/CloudflareThreadClient.ts +807 -0
- package/src/InteractiveBrowser.ts +2357 -0
- package/src/ProtectedBrowser.ts +2 -0
- package/src/ThreadObject.ts +1179 -0
- package/src/{wake-scheduler.ts → WakeScheduler.ts} +23 -23
- package/src/index.ts +12 -23
- package/src/internal/boundary.ts +55 -0
- package/src/internal/browser-quick-action.ts +857 -0
- package/src/internal/browser-session-lifecycle.ts +160 -0
- package/src/internal/layers.ts +751 -0
- package/src/internal/message-delivery.ts +148 -0
- package/src/internal/prepared-admission.ts +116 -0
- package/src/internal/progress-wait.ts +121 -0
- package/src/internal/transport.ts +42 -0
- package/src/protected-browser/binding.ts +187 -0
- package/src/protected-browser/inspect-frame.ts +125 -0
- package/src/protected-browser/native.ts +429 -0
- package/src/protected-browser/policy.ts +719 -0
- package/dist/index.mjs.map +0 -1
- package/src/alarm.ts +0 -334
- package/src/bindings.ts +0 -145
- package/src/client.ts +0 -646
- package/src/conversation-object.ts +0 -768
- package/src/layers.ts +0 -376
- package/src/transport.ts +0 -38
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
2
|
CodeExecutionHost,
|
|
3
|
-
CodeExecutionRequest,
|
|
4
3
|
CodeExecutionResourceUse,
|
|
5
4
|
CodeExecutionResult,
|
|
6
5
|
CodeExecutionTimeoutError,
|
|
@@ -15,59 +14,56 @@ import {
|
|
|
15
14
|
CodeOutputLimitError,
|
|
16
15
|
CodeProgramFailedError,
|
|
17
16
|
CodeSourceError,
|
|
18
|
-
SandboxImplementation,
|
|
19
17
|
type CodeExecutorExecute,
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
import {
|
|
18
|
+
type CodeExecutionRequest,
|
|
19
|
+
} from "@effect-agent/sandbox/CodeExecutor";
|
|
20
|
+
import { SandboxImplementation } from "@effect-agent/sandbox/Sandbox";
|
|
21
|
+
import { RpcTarget } from "cloudflare:workers";
|
|
22
|
+
import { Clock, Duration, Effect, Exit, Fiber, Layer, Option, Queue, Schema } from "effect";
|
|
23
|
+
|
|
24
|
+
import { safeCauseDiagnostic, safeCauseMessage } from "./internal/boundary.ts";
|
|
23
25
|
|
|
24
26
|
/**
|
|
25
27
|
* The Cloudflare Dynamic Worker `CodeExecutor` adapter (C4 of ADR-0017;
|
|
26
28
|
* DEPLOY-011). Each pass loads one fresh Worker through the Worker Loader
|
|
27
29
|
* with `globalOutbound: null`, so generated code has no ambient network,
|
|
28
|
-
* bindings, or secrets; its only authority is the pass-scoped
|
|
29
|
-
* routes back
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
* isolate.
|
|
30
|
+
* bindings, or secrets; its only authority is the pass-scoped RPC target that
|
|
31
|
+
* routes back into the owning event's `CodeExecutionHost` service. Platform
|
|
32
|
+
* CPU limits stop synchronous runaway programs; the executor-owned wall-clock
|
|
33
|
+
* deadline interrupts asynchronously suspended passes. Deployment class `E`
|
|
34
|
+
* only: the adapter records no persistent state and a later pass may run in a
|
|
35
|
+
* completely different isolate.
|
|
35
36
|
*/
|
|
36
37
|
export const dynamicWorkerImplementation = SandboxImplementation.make({
|
|
37
38
|
isolation: "isolated",
|
|
38
39
|
identity: "cloudflare-dynamic-worker",
|
|
39
40
|
});
|
|
40
41
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
readonly call: (passId: string, hostCall: unknown) => Promise<unknown>;
|
|
42
|
+
interface CodeModePassHost extends Rpc.RpcTargetBranded {
|
|
43
|
+
readonly call: (hostCall: unknown) => Promise<unknown>;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
interface
|
|
47
|
-
readonly
|
|
46
|
+
interface CodeModeHarnessEntrypoint extends Rpc.WorkerEntrypointBranded {
|
|
47
|
+
readonly run: (host: CodeModePassHost) => Promise<unknown>;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
/**
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
* `
|
|
51
|
+
* One object-capability endpoint for one execution pass. Workers RPC invokes
|
|
52
|
+
* the target in the request context where it was created, so the native
|
|
53
|
+
* Promise returned by `dispatch` and the Effect fiber that settles it share
|
|
54
|
+
* one I/O owner. Passing the target as `run()`'s argument also scopes the
|
|
55
|
+
* remote stub to that RPC call; no request state lives at module scope.
|
|
54
56
|
*/
|
|
55
|
-
|
|
57
|
+
class CodeModePassHostTarget extends RpcTarget implements CodeModePassHost {
|
|
58
|
+
readonly #dispatch: (hostCall: unknown) => Promise<unknown>;
|
|
56
59
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
export class CodeModeHostEntrypoint extends WorkerEntrypoint {
|
|
65
|
-
async call(passId: unknown, hostCall: unknown): Promise<unknown> {
|
|
66
|
-
const pass = passRegistry.get(String(passId));
|
|
67
|
-
if (pass === undefined) {
|
|
68
|
-
throw new Error("Unknown Code Mode pass");
|
|
69
|
-
}
|
|
70
|
-
return pass.dispatch(hostCall);
|
|
60
|
+
constructor(dispatch: (hostCall: unknown) => Promise<unknown>) {
|
|
61
|
+
super();
|
|
62
|
+
this.#dispatch = dispatch;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
call(hostCall: unknown): Promise<unknown> {
|
|
66
|
+
return this.#dispatch(hostCall);
|
|
71
67
|
}
|
|
72
68
|
}
|
|
73
69
|
|
|
@@ -103,9 +99,8 @@ const safeJson = (value) => {
|
|
|
103
99
|
};
|
|
104
100
|
|
|
105
101
|
export default class CodeModeHarness extends WorkerEntrypoint {
|
|
106
|
-
async run() {
|
|
107
|
-
const config =
|
|
108
|
-
const host = this.env.CODE_MODE_HOST;
|
|
102
|
+
async run(host) {
|
|
103
|
+
const config = this.env.CODE_MODE_PASS;
|
|
109
104
|
const limits = config.limits;
|
|
110
105
|
const logs = [];
|
|
111
106
|
let logBytes = 0;
|
|
@@ -143,7 +138,7 @@ export default class CodeModeHarness extends WorkerEntrypoint {
|
|
|
143
138
|
};
|
|
144
139
|
throw new Error("code-mode host-call argument limit exceeded");
|
|
145
140
|
}
|
|
146
|
-
const outcome = await host.call(
|
|
141
|
+
const outcome = await host.call({
|
|
147
142
|
namespace,
|
|
148
143
|
method,
|
|
149
144
|
argument: JSON.parse(argText),
|
|
@@ -227,55 +222,50 @@ const BoundedLogs = Schema.Array(Schema.String.check(Schema.isMaxLength(16 * 102
|
|
|
227
222
|
Schema.isMaxLength(4_096),
|
|
228
223
|
);
|
|
229
224
|
|
|
230
|
-
const HarnessCompleted = Schema.
|
|
231
|
-
_tag: Schema.Literal("completed"),
|
|
225
|
+
const HarnessCompleted = Schema.TaggedStruct("completed", {
|
|
232
226
|
value: Schema.Json,
|
|
233
227
|
logs: BoundedLogs,
|
|
234
228
|
hostCalls: Schema.Natural,
|
|
235
229
|
logBytes: Schema.Natural,
|
|
236
230
|
resultBytes: Schema.Natural,
|
|
237
231
|
});
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
message: Schema.String,
|
|
241
|
-
});
|
|
242
|
-
const HarnessNotAFunction = Schema.Struct({
|
|
243
|
-
_tag: Schema.Literal("source-not-a-function"),
|
|
232
|
+
|
|
233
|
+
const HarnessNotAFunction = Schema.TaggedStruct("source-not-a-function", {
|
|
244
234
|
actual: Schema.String,
|
|
245
235
|
});
|
|
246
|
-
|
|
247
|
-
|
|
236
|
+
|
|
237
|
+
const HarnessProgramFailed = Schema.TaggedStruct("program-failed", {
|
|
248
238
|
reason: Schema.Literals(["threw", "rejected", "non-json-result"]),
|
|
249
239
|
thrown: Schema.Json,
|
|
250
240
|
message: Schema.String,
|
|
251
241
|
logs: BoundedLogs,
|
|
252
242
|
});
|
|
253
|
-
|
|
254
|
-
|
|
243
|
+
|
|
244
|
+
const HarnessLogLimit = Schema.TaggedStruct("log-limit", {
|
|
255
245
|
observed: Schema.Natural,
|
|
256
246
|
logs: BoundedLogs,
|
|
257
247
|
});
|
|
258
|
-
|
|
259
|
-
|
|
248
|
+
|
|
249
|
+
const HarnessArgumentLimit = Schema.TaggedStruct("argument-limit", {
|
|
260
250
|
observed: Schema.Natural,
|
|
261
251
|
logs: BoundedLogs,
|
|
262
252
|
});
|
|
263
|
-
|
|
264
|
-
|
|
253
|
+
|
|
254
|
+
const HarnessResultLimit = Schema.TaggedStruct("result-limit", {
|
|
265
255
|
observed: Schema.Natural,
|
|
266
256
|
logs: BoundedLogs,
|
|
267
257
|
});
|
|
268
|
-
|
|
269
|
-
|
|
258
|
+
|
|
259
|
+
const HarnessHostCallLimit = Schema.TaggedStruct("host-call-limit", {
|
|
270
260
|
logs: BoundedLogs,
|
|
271
261
|
});
|
|
272
|
-
|
|
273
|
-
|
|
262
|
+
|
|
263
|
+
const HarnessProtocol = Schema.TaggedStruct("protocol", {
|
|
274
264
|
message: Schema.String,
|
|
275
265
|
});
|
|
266
|
+
|
|
276
267
|
const HarnessOutcome = Schema.Union([
|
|
277
268
|
HarnessCompleted,
|
|
278
|
-
HarnessSourceInvalid,
|
|
279
269
|
HarnessNotAFunction,
|
|
280
270
|
HarnessProgramFailed,
|
|
281
271
|
HarnessLogLimit,
|
|
@@ -285,6 +275,25 @@ const HarnessOutcome = Schema.Union([
|
|
|
285
275
|
HarnessProtocol,
|
|
286
276
|
]);
|
|
287
277
|
|
|
278
|
+
const HarnessPassConfig = Schema.Struct({
|
|
279
|
+
namespaces: Schema.Array(
|
|
280
|
+
Schema.Struct({
|
|
281
|
+
name: Schema.NonEmptyString,
|
|
282
|
+
methods: Schema.Array(Schema.NonEmptyString).check(Schema.isMaxLength(64)),
|
|
283
|
+
}),
|
|
284
|
+
).check(Schema.isMaxLength(32)),
|
|
285
|
+
limits: Schema.Struct({
|
|
286
|
+
maxLogBytes: Schema.Natural,
|
|
287
|
+
maxResultBytes: Schema.Natural,
|
|
288
|
+
maxHostCalls: Schema.Natural,
|
|
289
|
+
maxHostCallArgumentBytes: Schema.Natural,
|
|
290
|
+
}),
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
const encodeHarnessPassConfig = Schema.encodeSync(HarnessPassConfig);
|
|
294
|
+
const encodeJsonPayload = Schema.encodeSync(Schema.fromJsonString(Schema.Json));
|
|
295
|
+
const decodeJsonPayload = Schema.decodeUnknownOption(Schema.fromJsonString(Schema.Json));
|
|
296
|
+
|
|
288
297
|
const decodeHarnessOutcome = (value: unknown) => {
|
|
289
298
|
try {
|
|
290
299
|
return Schema.decodeUnknownOption(HarnessOutcome)(value);
|
|
@@ -301,6 +310,36 @@ const decodeHostCall = (value: unknown) => {
|
|
|
301
310
|
}
|
|
302
311
|
};
|
|
303
312
|
|
|
313
|
+
const decodeHostCallResult = (value: unknown) => {
|
|
314
|
+
try {
|
|
315
|
+
return Schema.decodeUnknownOption(CodeHostCallResult)(value);
|
|
316
|
+
} catch {
|
|
317
|
+
return Option.none<CodeHostCallResult>();
|
|
318
|
+
}
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
/** Dispose a Cloudflare RPC handle when the runtime supplies its untyped disposal hook. @internal */
|
|
322
|
+
export const disposeRpcHandle = (handle: unknown): Effect.Effect<void> =>
|
|
323
|
+
Effect.try({
|
|
324
|
+
try: () => {
|
|
325
|
+
if ((typeof handle !== "object" && typeof handle !== "function") || handle === null) return;
|
|
326
|
+
if (!(Symbol.dispose in handle)) return;
|
|
327
|
+
const dispose = Reflect.get(handle, Symbol.dispose);
|
|
328
|
+
|
|
329
|
+
if (typeof dispose === "function") {
|
|
330
|
+
Reflect.apply(dispose, handle, []);
|
|
331
|
+
}
|
|
332
|
+
},
|
|
333
|
+
catch: (cause) =>
|
|
334
|
+
safeCauseDiagnostic(cause, "The Cloudflare RPC disposal hook failed without a diagnostic"),
|
|
335
|
+
}).pipe(
|
|
336
|
+
Effect.catch((diagnostic) =>
|
|
337
|
+
Effect.logWarning(`Cloudflare RPC handle disposal failed: ${diagnostic}`).pipe(
|
|
338
|
+
Effect.ignoreCause,
|
|
339
|
+
),
|
|
340
|
+
),
|
|
341
|
+
);
|
|
342
|
+
|
|
304
343
|
/**
|
|
305
344
|
* Project a host outcome to the plain JSON envelope the harness reads. A
|
|
306
345
|
* `CodeExecutionHost` may return either real `CodeHostCallResult` instances
|
|
@@ -308,54 +347,69 @@ const decodeHostCall = (value: unknown) => {
|
|
|
308
347
|
* Mode capability's broker route), so this reads the shared fields rather than
|
|
309
348
|
* `Schema.encodeSync`, which would reject a plain object.
|
|
310
349
|
*/
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
350
|
+
interface EncodedHostResultPayload {
|
|
351
|
+
readonly encodedPayload: string;
|
|
352
|
+
readonly resultBytes: number;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const encodeHostResultPayload = (
|
|
356
|
+
outcome: CodeHostCallResult,
|
|
357
|
+
): EncodedHostResultPayload | undefined => {
|
|
358
|
+
try {
|
|
359
|
+
const payload = outcome._tag === "CodeHostCallSuccess" ? outcome.value : outcome.error;
|
|
360
|
+
const encodedPayload = encodeJsonPayload(payload);
|
|
361
|
+
|
|
362
|
+
return {
|
|
363
|
+
encodedPayload,
|
|
364
|
+
resultBytes: utf8ByteLength(encodedPayload),
|
|
365
|
+
};
|
|
366
|
+
} catch {
|
|
367
|
+
return undefined;
|
|
368
|
+
}
|
|
369
|
+
};
|
|
315
370
|
|
|
316
371
|
const utf8ByteLength = (value: string): number => {
|
|
317
372
|
let total = 0;
|
|
373
|
+
|
|
318
374
|
for (const character of value) {
|
|
319
375
|
const codePoint = character.codePointAt(0) ?? 0;
|
|
376
|
+
|
|
320
377
|
total += codePoint <= 0x7f ? 1 : codePoint <= 0x7ff ? 2 : codePoint <= 0xffff ? 3 : 4;
|
|
321
378
|
}
|
|
322
|
-
return total;
|
|
323
|
-
};
|
|
324
379
|
|
|
325
|
-
|
|
326
|
-
try {
|
|
327
|
-
const encoded = JSON.stringify(value);
|
|
328
|
-
return encoded === undefined ? undefined : utf8ByteLength(encoded);
|
|
329
|
-
} catch {
|
|
330
|
-
return undefined;
|
|
331
|
-
}
|
|
380
|
+
return total;
|
|
332
381
|
};
|
|
333
382
|
|
|
334
|
-
interface
|
|
335
|
-
readonly
|
|
383
|
+
interface QueuedHostCall {
|
|
384
|
+
readonly call: CodeHostCall;
|
|
336
385
|
readonly resolve: (value: unknown) => void;
|
|
337
386
|
readonly reject: (reason: unknown) => void;
|
|
338
387
|
}
|
|
339
388
|
|
|
389
|
+
type HostWork =
|
|
390
|
+
| { readonly _tag: "call"; readonly queued: QueuedHostCall }
|
|
391
|
+
| { readonly _tag: "limit" };
|
|
392
|
+
|
|
393
|
+
type HostDispatchError =
|
|
394
|
+
| CodeExecutionTimeoutError
|
|
395
|
+
| CodeOutputLimitError
|
|
396
|
+
| CodeExecutionProtocolError
|
|
397
|
+
| CodeHostCallLimitError;
|
|
398
|
+
|
|
340
399
|
/** Reserved global names the harness owns inside the dynamic worker. */
|
|
341
400
|
const reservedHarnessGlobals = new Set(["console"]);
|
|
342
401
|
|
|
343
402
|
export interface DynamicWorkerCodeExecutorOptions {
|
|
344
403
|
/** The `worker_loader` binding. */
|
|
345
404
|
readonly loader: WorkerLoader;
|
|
346
|
-
/**
|
|
347
|
-
* A SAME-INSTANCE stub of `CodeModeHostEntrypoint`. In production create it
|
|
348
|
-
* with `ctx.exports.CodeModeHostEntrypoint()`; a cross-instance stub would
|
|
349
|
-
* dispatch host calls into an isolate without this pass's registry entry.
|
|
350
|
-
*/
|
|
351
|
-
readonly hostStub: CodeModeHostStub;
|
|
352
405
|
/** Compatibility date for dynamic workers; defaults to `2025-05-01`. */
|
|
353
406
|
readonly compatibilityDate?: string | undefined;
|
|
354
407
|
}
|
|
355
408
|
|
|
356
|
-
const
|
|
357
|
-
|
|
358
|
-
|
|
409
|
+
const makeExecute = (
|
|
410
|
+
options: DynamicWorkerCodeExecutorOptions,
|
|
411
|
+
clock: Clock.Clock,
|
|
412
|
+
): CodeExecutorExecute =>
|
|
359
413
|
Effect.fn("DynamicWorkerCodeExecutor.execute")(function* (request: CodeExecutionRequest) {
|
|
360
414
|
if (request.network._tag !== "NetworkDisabled") {
|
|
361
415
|
return yield* CodeExecutorUnsupportedError.make({
|
|
@@ -366,6 +420,7 @@ const makeExecute = (options: DynamicWorkerCodeExecutorOptions): CodeExecutorExe
|
|
|
366
420
|
});
|
|
367
421
|
}
|
|
368
422
|
const sourceBytes = utf8ByteLength(request.source);
|
|
423
|
+
|
|
369
424
|
if (sourceBytes > request.limits.maxSourceBytes) {
|
|
370
425
|
return yield* CodeSourceError.make({
|
|
371
426
|
implementation: dynamicWorkerImplementation,
|
|
@@ -382,106 +437,192 @@ const makeExecute = (options: DynamicWorkerCodeExecutorOptions): CodeExecutorExe
|
|
|
382
437
|
});
|
|
383
438
|
}
|
|
384
439
|
}
|
|
385
|
-
|
|
386
440
|
const host = yield* CodeExecutionHost;
|
|
387
|
-
|
|
388
|
-
//
|
|
389
|
-
//
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
const
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
const pending: Array<PendingHostCall> = [];
|
|
400
|
-
let wake: (() => void) | undefined;
|
|
441
|
+
|
|
442
|
+
// This synchronous clock access is confined to callbacks that must compute a timeout
|
|
443
|
+
// immediately. The Clock service remains the authority, so tests and hosts can replace it.
|
|
444
|
+
const startedAt = clock.monotonicTimeNanosUnsafe();
|
|
445
|
+
const passDeadline = startedAt + Duration.toNanosUnsafe(request.limits.maxWallTime);
|
|
446
|
+
|
|
447
|
+
const remainingPassWallTime = (): Duration.Duration => {
|
|
448
|
+
const now = clock.monotonicTimeNanosUnsafe();
|
|
449
|
+
|
|
450
|
+
return Duration.nanos(passDeadline > now ? passDeadline - now : 0n);
|
|
451
|
+
};
|
|
452
|
+
|
|
401
453
|
let issuedHostCalls = 0;
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
if (issuedHostCalls > request.limits.maxHostCalls + 1) {
|
|
406
|
-
reject(new Error("host-call limit exceeded"));
|
|
407
|
-
return;
|
|
408
|
-
}
|
|
409
|
-
pending.push({ hostCall, resolve, reject });
|
|
410
|
-
wake?.();
|
|
411
|
-
});
|
|
454
|
+
let passOpen = true;
|
|
455
|
+
const queuedHostCalls: Array<QueuedHostCall> = [];
|
|
456
|
+
let passFailure: HostDispatchError | undefined;
|
|
412
457
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
}),
|
|
417
|
-
() =>
|
|
418
|
-
Effect.sync(() => {
|
|
419
|
-
passRegistry.delete(passId);
|
|
420
|
-
}),
|
|
421
|
-
);
|
|
458
|
+
const failPass = (error: HostDispatchError): void => {
|
|
459
|
+
if (passFailure === undefined) passFailure = error;
|
|
460
|
+
};
|
|
422
461
|
|
|
423
|
-
const
|
|
424
|
-
const
|
|
425
|
-
|
|
426
|
-
return Effect.succeed(item);
|
|
462
|
+
const rejectQueuedHostCalls = (reason: Error): void => {
|
|
463
|
+
for (const queued of queuedHostCalls.splice(0)) {
|
|
464
|
+
queued.reject(reason);
|
|
427
465
|
}
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
466
|
+
};
|
|
467
|
+
|
|
468
|
+
const queue = yield* Queue.unbounded<HostWork>();
|
|
469
|
+
|
|
470
|
+
const deliverHostOutcome = (
|
|
471
|
+
queued: QueuedHostCall,
|
|
472
|
+
outcome: CodeHostCallResult,
|
|
473
|
+
): Effect.Effect<void, CodeExecutionProtocolError | CodeOutputLimitError> =>
|
|
474
|
+
Effect.gen(function* () {
|
|
475
|
+
const decoded = decodeHostCallResult(outcome);
|
|
476
|
+
|
|
477
|
+
if (Option.isNone(decoded)) {
|
|
478
|
+
const error = CodeExecutionProtocolError.make({
|
|
479
|
+
implementation: dynamicWorkerImplementation,
|
|
480
|
+
message: "The execution host returned a value outside the CodeHostCallResult schema",
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
failPass(error);
|
|
484
|
+
|
|
485
|
+
return yield* error;
|
|
486
|
+
}
|
|
487
|
+
const encoded = encodeHostResultPayload(decoded.value);
|
|
488
|
+
|
|
489
|
+
if (encoded === undefined || encoded.resultBytes > request.limits.maxHostCallResultBytes) {
|
|
490
|
+
const error = CodeOutputLimitError.make({
|
|
491
|
+
implementation: dynamicWorkerImplementation,
|
|
492
|
+
surface: "host-call-result",
|
|
493
|
+
limit: request.limits.maxHostCallResultBytes,
|
|
494
|
+
observed: encoded?.resultBytes ?? 0,
|
|
495
|
+
logs: [],
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
failPass(error);
|
|
499
|
+
|
|
500
|
+
return yield* error;
|
|
501
|
+
}
|
|
502
|
+
const normalizedPayload = decodeJsonPayload(encoded.encodedPayload);
|
|
503
|
+
|
|
504
|
+
if (Option.isNone(normalizedPayload)) {
|
|
505
|
+
const error = CodeExecutionProtocolError.make({
|
|
506
|
+
implementation: dynamicWorkerImplementation,
|
|
507
|
+
message: "The execution host returned a result that could not cross the JSON boundary",
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
failPass(error);
|
|
511
|
+
|
|
512
|
+
return yield* error;
|
|
513
|
+
}
|
|
514
|
+
queued.resolve(
|
|
515
|
+
decoded.value._tag === "CodeHostCallSuccess"
|
|
516
|
+
? { _tag: "CodeHostCallSuccess", value: normalizedPayload.value }
|
|
517
|
+
: { _tag: "CodeHostCallFailure", error: normalizedPayload.value },
|
|
518
|
+
);
|
|
436
519
|
});
|
|
437
|
-
});
|
|
438
520
|
|
|
521
|
+
// Workers RPC into the loader isolate cannot settle on the fiber blocked
|
|
522
|
+
// in `entrypoint.run()`. A Scope-owned sibling fiber keeps that
|
|
523
|
+
// independence while inheriting the pass Context and dying with the Scope.
|
|
439
524
|
const serveHostCalls = Effect.gen(function* () {
|
|
440
|
-
let served = 0;
|
|
441
525
|
while (true) {
|
|
442
|
-
const
|
|
443
|
-
|
|
444
|
-
if (
|
|
445
|
-
|
|
526
|
+
const work = yield* Queue.take(queue);
|
|
527
|
+
|
|
528
|
+
if (work._tag === "limit") {
|
|
529
|
+
const error = CodeHostCallLimitError.make({
|
|
446
530
|
implementation: dynamicWorkerImplementation,
|
|
447
531
|
limit: request.limits.maxHostCalls,
|
|
448
532
|
logs: [],
|
|
449
533
|
});
|
|
534
|
+
|
|
535
|
+
failPass(error);
|
|
536
|
+
|
|
537
|
+
return yield* error;
|
|
450
538
|
}
|
|
451
|
-
const
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
539
|
+
const queued = work.queued;
|
|
540
|
+
|
|
541
|
+
yield* host.call(queued.call).pipe(
|
|
542
|
+
Effect.timeoutOrElse({
|
|
543
|
+
duration: remainingPassWallTime(),
|
|
544
|
+
orElse: () => {
|
|
545
|
+
const error = CodeExecutionTimeoutError.make({
|
|
546
|
+
implementation: dynamicWorkerImplementation,
|
|
547
|
+
kind: "wall-clock",
|
|
548
|
+
maxWallTime: request.limits.maxWallTime,
|
|
549
|
+
logs: [],
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
failPass(error);
|
|
553
|
+
|
|
554
|
+
return error;
|
|
555
|
+
},
|
|
556
|
+
}),
|
|
557
|
+
Effect.flatMap((outcome) => deliverHostOutcome(queued, outcome)),
|
|
558
|
+
Effect.tapError(() =>
|
|
559
|
+
Effect.sync(() => queued.reject(new Error("Code Mode host call failed"))),
|
|
560
|
+
),
|
|
561
|
+
Effect.onInterrupt(() =>
|
|
562
|
+
Effect.sync(() => queued.reject(new Error("Code Mode pass is closing"))),
|
|
563
|
+
),
|
|
564
|
+
);
|
|
565
|
+
}
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
const concurrency = request.limits.maxHostCallConcurrency ?? 4;
|
|
569
|
+
|
|
570
|
+
const server = yield* Effect.all(
|
|
571
|
+
Array.from({ length: concurrency }, () => serveHostCalls),
|
|
572
|
+
{ concurrency, discard: true },
|
|
573
|
+
).pipe(Effect.andThen(Effect.never), Effect.forkScoped);
|
|
574
|
+
|
|
575
|
+
const dispatch = (hostCall: unknown): Promise<unknown> => {
|
|
576
|
+
if (!passOpen) {
|
|
577
|
+
return Promise.reject(new Error("Code Mode pass is closing"));
|
|
578
|
+
}
|
|
579
|
+
issuedHostCalls += 1;
|
|
580
|
+
if (issuedHostCalls > request.limits.maxHostCalls) {
|
|
581
|
+
const error = CodeHostCallLimitError.make({
|
|
582
|
+
implementation: dynamicWorkerImplementation,
|
|
583
|
+
limit: request.limits.maxHostCalls,
|
|
584
|
+
logs: [],
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
failPass(error);
|
|
588
|
+
Queue.offerUnsafe(queue, { _tag: "limit" });
|
|
589
|
+
|
|
590
|
+
return Promise.reject(new Error("host-call limit exceeded"));
|
|
591
|
+
}
|
|
592
|
+
const decoded = decodeHostCall(hostCall);
|
|
593
|
+
|
|
594
|
+
if (Option.isNone(decoded)) {
|
|
595
|
+
return Promise.reject(new TypeError("host calls must match the CodeHostCall schema"));
|
|
470
596
|
}
|
|
597
|
+
|
|
598
|
+
return new Promise((resolve, reject) => {
|
|
599
|
+
const queued = { call: decoded.value, resolve, reject };
|
|
600
|
+
|
|
601
|
+
queuedHostCalls.push(queued);
|
|
602
|
+
Queue.offerUnsafe(queue, { _tag: "call", queued });
|
|
603
|
+
});
|
|
604
|
+
};
|
|
605
|
+
|
|
606
|
+
const closeAdmission = Effect.sync(() => {
|
|
607
|
+
passOpen = false;
|
|
608
|
+
rejectQueuedHostCalls(new Error("Code Mode pass is closing"));
|
|
471
609
|
});
|
|
472
610
|
|
|
473
|
-
|
|
611
|
+
yield* Effect.addFinalizer(() => closeAdmission.pipe(Effect.andThen(Fiber.interrupt(server))));
|
|
612
|
+
|
|
613
|
+
// No `allowExperimental`: the runtime only accepts it when the CALLING
|
|
614
|
+
// worker carries the `experimental` compatibility flag, which deployed
|
|
615
|
+
// consumers cannot set — the option would reject every pass in
|
|
616
|
+
// production. The harness needs no experimental runtime features.
|
|
617
|
+
const workerCode: WorkerLoaderWorkerCode = {
|
|
474
618
|
compatibilityDate: options.compatibilityDate ?? "2025-05-01",
|
|
475
|
-
allowExperimental: true,
|
|
476
619
|
mainModule: "harness.js",
|
|
477
620
|
modules: {
|
|
478
621
|
"harness.js": HARNESS_MODULE,
|
|
479
622
|
"program.js": `export default (\n${request.source}\n);`,
|
|
480
623
|
},
|
|
481
624
|
env: {
|
|
482
|
-
|
|
483
|
-
CODE_MODE_PASS: JSON.stringify({
|
|
484
|
-
passId,
|
|
625
|
+
CODE_MODE_PASS: encodeHarnessPassConfig({
|
|
485
626
|
namespaces: request.namespaces.map((namespace) => ({
|
|
486
627
|
name: namespace.name,
|
|
487
628
|
methods: namespace.methods,
|
|
@@ -505,12 +646,12 @@ const makeExecute = (options: DynamicWorkerCodeExecutorOptions): CodeExecutorExe
|
|
|
505
646
|
}),
|
|
506
647
|
};
|
|
507
648
|
|
|
508
|
-
const startedAt = yield* Clock.currentTimeMillis;
|
|
509
649
|
const worker = yield* Effect.acquireRelease(
|
|
510
650
|
Effect.try({
|
|
511
|
-
try: () => options.loader.load(workerCode
|
|
651
|
+
try: () => options.loader.load(workerCode),
|
|
512
652
|
catch: (cause) => {
|
|
513
|
-
const text = cause
|
|
653
|
+
const text = safeCauseMessage(cause, "The Worker Loader failed without a diagnostic");
|
|
654
|
+
|
|
514
655
|
// Blame the program's source ONLY on a genuine compile diagnostic;
|
|
515
656
|
// any other load rejection is an infrastructure start failure, not
|
|
516
657
|
// the model's fault (see classifyWorkerFailure for the same split).
|
|
@@ -521,6 +662,7 @@ const makeExecute = (options: DynamicWorkerCodeExecutorOptions): CodeExecutorExe
|
|
|
521
662
|
message: text.slice(0, 8_000),
|
|
522
663
|
});
|
|
523
664
|
}
|
|
665
|
+
|
|
524
666
|
return CodeExecutorStartError.make({
|
|
525
667
|
implementation: dynamicWorkerImplementation,
|
|
526
668
|
message: `The Worker Loader rejected the pass: ${text}`.slice(0, 8_000),
|
|
@@ -528,40 +670,51 @@ const makeExecute = (options: DynamicWorkerCodeExecutorOptions): CodeExecutorExe
|
|
|
528
670
|
});
|
|
529
671
|
},
|
|
530
672
|
}),
|
|
531
|
-
|
|
532
|
-
Effect.sync(() => {
|
|
533
|
-
(stub as Partial<Record<typeof Symbol.dispose, () => void>>)[Symbol.dispose]?.();
|
|
534
|
-
}),
|
|
673
|
+
disposeRpcHandle,
|
|
535
674
|
);
|
|
536
675
|
|
|
537
|
-
const
|
|
676
|
+
const entrypoint = yield* Effect.acquireRelease(
|
|
677
|
+
Effect.try({
|
|
678
|
+
try: () => worker.getEntrypoint<CodeModeHarnessEntrypoint>(),
|
|
679
|
+
catch: (cause) => classifyWorkerFailure(cause, request.limits.maxWallTime),
|
|
680
|
+
}),
|
|
681
|
+
disposeRpcHandle,
|
|
682
|
+
);
|
|
538
683
|
|
|
539
684
|
const rpc = Effect.tryPromise({
|
|
540
|
-
try:
|
|
541
|
-
const entrypoint = worker.getEntrypoint() as unknown as {
|
|
542
|
-
run(): Promise<unknown>;
|
|
543
|
-
};
|
|
544
|
-
return await entrypoint.run();
|
|
545
|
-
},
|
|
685
|
+
try: () => entrypoint.run(new CodeModePassHostTarget(dispatch)),
|
|
546
686
|
catch: (cause) => classifyWorkerFailure(cause, request.limits.maxWallTime),
|
|
547
687
|
});
|
|
548
688
|
|
|
549
|
-
const
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
689
|
+
const exit = yield* Effect.raceFirst(
|
|
690
|
+
rpc.pipe(
|
|
691
|
+
Effect.timeoutOrElse({
|
|
692
|
+
duration: remainingPassWallTime(),
|
|
693
|
+
orElse: () =>
|
|
694
|
+
CodeExecutionTimeoutError.make({
|
|
695
|
+
implementation: dynamicWorkerImplementation,
|
|
696
|
+
kind: "wall-clock",
|
|
697
|
+
maxWallTime: request.limits.maxWallTime,
|
|
698
|
+
logs: [],
|
|
699
|
+
}),
|
|
700
|
+
}),
|
|
701
|
+
),
|
|
702
|
+
Fiber.join(server),
|
|
703
|
+
).pipe(Effect.exit);
|
|
704
|
+
|
|
705
|
+
yield* closeAdmission;
|
|
706
|
+
yield* Fiber.interrupt(server);
|
|
707
|
+
if (passFailure !== undefined) {
|
|
708
|
+
return yield* passFailure;
|
|
709
|
+
}
|
|
710
|
+
if (Exit.isFailure(exit)) {
|
|
711
|
+
return yield* Effect.failCause(exit.cause);
|
|
712
|
+
}
|
|
713
|
+
const raw = exit.value;
|
|
714
|
+
const finishedAt = clock.monotonicTimeNanosUnsafe();
|
|
563
715
|
|
|
564
716
|
const outcome = decodeHarnessOutcome(raw);
|
|
717
|
+
|
|
565
718
|
if (Option.isNone(outcome)) {
|
|
566
719
|
return yield* CodeExecutionProtocolError.make({
|
|
567
720
|
implementation: dynamicWorkerImplementation,
|
|
@@ -575,20 +728,13 @@ const makeExecute = (options: DynamicWorkerCodeExecutorOptions): CodeExecutorExe
|
|
|
575
728
|
value: outcome.value.value,
|
|
576
729
|
logs: outcome.value.logs,
|
|
577
730
|
resourceUse: CodeExecutionResourceUse.make({
|
|
578
|
-
wallTime: Duration.
|
|
731
|
+
wallTime: Duration.nanos(finishedAt > startedAt ? finishedAt - startedAt : 0n),
|
|
579
732
|
hostCalls: outcome.value.hostCalls,
|
|
580
733
|
logBytes: outcome.value.logBytes,
|
|
581
734
|
resultBytes: outcome.value.resultBytes,
|
|
582
735
|
}),
|
|
583
736
|
});
|
|
584
737
|
}
|
|
585
|
-
case "source-invalid": {
|
|
586
|
-
return yield* CodeSourceError.make({
|
|
587
|
-
implementation: dynamicWorkerImplementation,
|
|
588
|
-
reason: "invalid",
|
|
589
|
-
message: outcome.value.message.slice(0, 8_000),
|
|
590
|
-
});
|
|
591
|
-
}
|
|
592
738
|
case "source-not-a-function": {
|
|
593
739
|
return yield* CodeSourceError.make({
|
|
594
740
|
implementation: dynamicWorkerImplementation,
|
|
@@ -661,13 +807,8 @@ const classifyWorkerFailure = (
|
|
|
661
807
|
| CodeExecutorTerminatedError
|
|
662
808
|
| CodeExecutorStartError
|
|
663
809
|
| CodeSourceError => {
|
|
664
|
-
const text = (
|
|
665
|
-
|
|
666
|
-
return cause instanceof Error ? `${cause.name}: ${cause.message}` : String(cause);
|
|
667
|
-
} catch {
|
|
668
|
-
return "[unserializable worker failure]";
|
|
669
|
-
}
|
|
670
|
-
})();
|
|
810
|
+
const text = safeCauseDiagnostic(cause, "[unserializable worker failure]");
|
|
811
|
+
|
|
671
812
|
// `WorkerLoader.load()` is lazy, so a module-compile error in the generated
|
|
672
813
|
// program surfaces here at first use. Blame the program's source ONLY on a
|
|
673
814
|
// genuine compile diagnostic (a `SyntaxError` or an explicit compile
|
|
@@ -697,6 +838,7 @@ const classifyWorkerFailure = (
|
|
|
697
838
|
cause,
|
|
698
839
|
});
|
|
699
840
|
}
|
|
841
|
+
|
|
700
842
|
return CodeExecutorTerminatedError.make({
|
|
701
843
|
implementation: dynamicWorkerImplementation,
|
|
702
844
|
message: text.slice(0, 8_000),
|
|
@@ -707,4 +849,30 @@ const classifyWorkerFailure = (
|
|
|
707
849
|
export const dynamicWorkerCodeExecutorLayer = (
|
|
708
850
|
options: DynamicWorkerCodeExecutorOptions,
|
|
709
851
|
): Layer.Layer<CodeExecutor> =>
|
|
710
|
-
Layer.
|
|
852
|
+
Layer.effect(
|
|
853
|
+
CodeExecutor,
|
|
854
|
+
Effect.gen(function* () {
|
|
855
|
+
const clock = yield* Clock.Clock;
|
|
856
|
+
|
|
857
|
+
return CodeExecutor.of({ execute: makeExecute(options, clock) });
|
|
858
|
+
}),
|
|
859
|
+
);
|
|
860
|
+
|
|
861
|
+
/** Assemble Code Mode handlers with the isolated Dynamic Worker executor. */
|
|
862
|
+
export const CloudflareCodeMode = {
|
|
863
|
+
/**
|
|
864
|
+
* Provide the selected tool handlers at construction, where Code Mode captures them.
|
|
865
|
+
* Their errors and remaining dependencies stay visible. The definition still owns the
|
|
866
|
+
* allowlist and limits; the runtime supplies the live Tool broker for each scoped pass.
|
|
867
|
+
*/
|
|
868
|
+
layer: <A, E, R, Handlers, HandlerError, HandlerRequirements>(
|
|
869
|
+
definition: { readonly handlers: Layer.Layer<A, E, R> },
|
|
870
|
+
options: DynamicWorkerCodeExecutorOptions & {
|
|
871
|
+
readonly handlers: Layer.Layer<Handlers, HandlerError, HandlerRequirements>;
|
|
872
|
+
},
|
|
873
|
+
) =>
|
|
874
|
+
definition.handlers.pipe(
|
|
875
|
+
Layer.provide(options.handlers),
|
|
876
|
+
Layer.provide(dynamicWorkerCodeExecutorLayer(options)),
|
|
877
|
+
),
|
|
878
|
+
};
|