@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.
Files changed (100) hide show
  1. package/dist/Alarm.d.mts +183 -0
  2. package/dist/Alarm.mjs +454 -0
  3. package/dist/Alarm.mjs.map +1 -0
  4. package/dist/BrowserRestCapture.d.mts +35 -0
  5. package/dist/BrowserRestCapture.mjs +238 -0
  6. package/dist/BrowserRestCapture.mjs.map +1 -0
  7. package/dist/BrowserRestCrawl.d.mts +17 -0
  8. package/dist/BrowserRestCrawl.mjs +244 -0
  9. package/dist/BrowserRestCrawl.mjs.map +1 -0
  10. package/dist/CloudflareAiGateway.d.mts +64 -0
  11. package/dist/CloudflareAiGateway.mjs +70 -0
  12. package/dist/CloudflareAiGateway.mjs.map +1 -0
  13. package/dist/CloudflareBindings.d.mts +112 -0
  14. package/dist/CloudflareBindings.mjs +101 -0
  15. package/dist/CloudflareBindings.mjs.map +1 -0
  16. package/dist/CloudflareBrowser-D4YqaTAl.mjs +495 -0
  17. package/dist/CloudflareBrowser-D4YqaTAl.mjs.map +1 -0
  18. package/dist/CloudflareBrowser-DJq53niJ.d.mts +80 -0
  19. package/dist/CloudflareBrowser.d.mts +2 -0
  20. package/dist/CloudflareBrowser.mjs +2 -0
  21. package/dist/CloudflareCodeMode.d.mts +44 -0
  22. package/dist/CloudflareCodeMode.mjs +616 -0
  23. package/dist/CloudflareCodeMode.mjs.map +1 -0
  24. package/dist/CloudflareConfig-f3CqTel1.d.mts +101 -0
  25. package/dist/CloudflareConfig.d.mts +2 -0
  26. package/dist/CloudflareConfig.mjs +122 -0
  27. package/dist/CloudflareConfig.mjs.map +1 -0
  28. package/dist/CloudflareMemory.d.mts +142 -0
  29. package/dist/CloudflareMemory.mjs +195 -0
  30. package/dist/CloudflareMemory.mjs.map +1 -0
  31. package/dist/CloudflareScheduling.d.mts +52 -0
  32. package/dist/CloudflareScheduling.mjs +389 -0
  33. package/dist/CloudflareScheduling.mjs.map +1 -0
  34. package/dist/CloudflareSubscriptions.d.mts +87 -0
  35. package/dist/CloudflareSubscriptions.mjs +524 -0
  36. package/dist/CloudflareSubscriptions.mjs.map +1 -0
  37. package/dist/CloudflareThreadClient.d.mts +1286 -0
  38. package/dist/CloudflareThreadClient.mjs +386 -0
  39. package/dist/CloudflareThreadClient.mjs.map +1 -0
  40. package/dist/InteractiveBrowser.d.mts +144 -0
  41. package/dist/InteractiveBrowser.mjs +1090 -0
  42. package/dist/InteractiveBrowser.mjs.map +1 -0
  43. package/dist/ProtectedBrowser.d.mts +65 -0
  44. package/dist/ProtectedBrowser.mjs +859 -0
  45. package/dist/ProtectedBrowser.mjs.map +1 -0
  46. package/dist/ThreadObject-3uDpKfB3.d.mts +816 -0
  47. package/dist/ThreadObject-DGkXv41C.mjs +836 -0
  48. package/dist/ThreadObject-DGkXv41C.mjs.map +1 -0
  49. package/dist/ThreadObject.d.mts +2 -0
  50. package/dist/ThreadObject.mjs +3 -0
  51. package/dist/WakeScheduler.d.mts +23 -0
  52. package/dist/WakeScheduler.mjs +60 -0
  53. package/dist/WakeScheduler.mjs.map +1 -0
  54. package/dist/boundary-BguazVkh.mjs +42 -0
  55. package/dist/boundary-BguazVkh.mjs.map +1 -0
  56. package/dist/browser-session-lifecycle-CUbVfEgP.mjs +84 -0
  57. package/dist/browser-session-lifecycle-CUbVfEgP.mjs.map +1 -0
  58. package/dist/browser-session-lifecycle-DJpqLXy_.d.mts +21 -0
  59. package/dist/index.d.mts +13 -1547
  60. package/dist/index.mjs +13 -1636
  61. package/dist/prepared-admission-DSi55W-7.mjs +73 -0
  62. package/dist/prepared-admission-DSi55W-7.mjs.map +1 -0
  63. package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
  64. package/package.json +1 -53
  65. package/src/Alarm.ts +953 -0
  66. package/src/BrowserRestCapture.ts +477 -0
  67. package/src/BrowserRestCrawl.ts +540 -0
  68. package/src/CloudflareAiGateway.ts +170 -0
  69. package/src/CloudflareBindings.ts +180 -0
  70. package/src/CloudflareBrowser.ts +15 -0
  71. package/src/{code-mode-executor.ts → CloudflareCodeMode.ts} +372 -204
  72. package/src/{config.ts → CloudflareConfig.ts} +9 -8
  73. package/src/CloudflareMemory.ts +406 -0
  74. package/src/CloudflareScheduling.ts +733 -0
  75. package/src/CloudflareSubscriptions.ts +1045 -0
  76. package/src/CloudflareThreadClient.ts +807 -0
  77. package/src/InteractiveBrowser.ts +2357 -0
  78. package/src/ProtectedBrowser.ts +2 -0
  79. package/src/ThreadObject.ts +1179 -0
  80. package/src/{wake-scheduler.ts → WakeScheduler.ts} +23 -23
  81. package/src/index.ts +12 -23
  82. package/src/internal/boundary.ts +55 -0
  83. package/src/internal/browser-quick-action.ts +857 -0
  84. package/src/internal/browser-session-lifecycle.ts +160 -0
  85. package/src/internal/layers.ts +751 -0
  86. package/src/internal/message-delivery.ts +148 -0
  87. package/src/internal/prepared-admission.ts +116 -0
  88. package/src/internal/progress-wait.ts +121 -0
  89. package/src/internal/transport.ts +42 -0
  90. package/src/protected-browser/binding.ts +187 -0
  91. package/src/protected-browser/inspect-frame.ts +125 -0
  92. package/src/protected-browser/native.ts +429 -0
  93. package/src/protected-browser/policy.ts +719 -0
  94. package/dist/index.mjs.map +0 -1
  95. package/src/alarm.ts +0 -334
  96. package/src/bindings.ts +0 -145
  97. package/src/client.ts +0 -646
  98. package/src/conversation-object.ts +0 -768
  99. package/src/layers.ts +0 -376
  100. 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
- } from "@effect-agent/sandbox";
21
- import { WorkerEntrypoint } from "cloudflare:workers";
22
- import { Clock, Duration, Effect, Fiber, Layer, Option, Schema } from "effect";
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 host stub that
29
- * routes back to `CodeModeHostEntrypoint` and, from there, into the pass's
30
- * `CodeExecutionHost` service. Platform CPU limits stop synchronous runaway
31
- * programs; the executor-owned wall-clock deadline interrupts asynchronously
32
- * suspended passes. Deployment class `E` only: the adapter records no
33
- * persistent state and a later pass may run in a completely different
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
- /** The narrow host stub the dynamic worker receives (Workers RPC). */
42
- export interface CodeModeHostStub {
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 RegisteredPass {
47
- readonly dispatch: (hostCall: unknown) => Promise<unknown>;
46
+ interface CodeModeHarnessEntrypoint extends Rpc.WorkerEntrypointBranded {
47
+ readonly run: (host: CodeModePassHost) => Promise<unknown>;
48
48
  }
49
49
 
50
50
  /**
51
- * Live passes by identity. Entries are Scope-managed: registered when a pass
52
- * opens and removed by its finalizer, so a stale harness (or a forged
53
- * `passId`) cannot reach any host authority.
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
- const passRegistry = new Map<string, RegisteredPass>();
57
+ class CodeModePassHostTarget extends RpcTarget implements CodeModePassHost {
58
+ readonly #dispatch: (hostCall: unknown) => Promise<unknown>;
56
59
 
57
- /**
58
- * The host-side RPC target for dynamic workers. The application exposes it
59
- * from its Worker entry (`export { CodeModeHostEntrypoint }`) and hands the
60
- * adapter a same-instance stub — `ctx.exports.CodeModeHostEntrypoint()` in
61
- * production (a self service binding may reach a different instance and must
62
- * not be used there); tests bind it through Miniflare's `kCurrentWorker`.
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 = JSON.parse(this.env.CODE_MODE_PASS);
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(config.passId, {
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.Struct({
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
- const HarnessSourceInvalid = Schema.Struct({
239
- _tag: Schema.Literal("source-invalid"),
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
- const HarnessProgramFailed = Schema.Struct({
247
- _tag: Schema.Literal("program-failed"),
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
- const HarnessLogLimit = Schema.Struct({
254
- _tag: Schema.Literal("log-limit"),
243
+
244
+ const HarnessLogLimit = Schema.TaggedStruct("log-limit", {
255
245
  observed: Schema.Natural,
256
246
  logs: BoundedLogs,
257
247
  });
258
- const HarnessArgumentLimit = Schema.Struct({
259
- _tag: Schema.Literal("argument-limit"),
248
+
249
+ const HarnessArgumentLimit = Schema.TaggedStruct("argument-limit", {
260
250
  observed: Schema.Natural,
261
251
  logs: BoundedLogs,
262
252
  });
263
- const HarnessResultLimit = Schema.Struct({
264
- _tag: Schema.Literal("result-limit"),
253
+
254
+ const HarnessResultLimit = Schema.TaggedStruct("result-limit", {
265
255
  observed: Schema.Natural,
266
256
  logs: BoundedLogs,
267
257
  });
268
- const HarnessHostCallLimit = Schema.Struct({
269
- _tag: Schema.Literal("host-call-limit"),
258
+
259
+ const HarnessHostCallLimit = Schema.TaggedStruct("host-call-limit", {
270
260
  logs: BoundedLogs,
271
261
  });
272
- const HarnessProtocol = Schema.Struct({
273
- _tag: Schema.Literal("protocol"),
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
- const hostResultEnvelope = (outcome: CodeHostCallResult): Record<string, unknown> =>
312
- outcome._tag === "CodeHostCallSuccess"
313
- ? { _tag: "CodeHostCallSuccess", value: outcome.value }
314
- : { _tag: "CodeHostCallFailure", error: outcome.error };
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
- const encodedJsonByteLength = (value: unknown): number | undefined => {
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 PendingHostCall {
335
- readonly hostCall: unknown;
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 passCounterState = { next: 0 };
357
-
358
- const makeExecute = (options: DynamicWorkerCodeExecutorOptions): CodeExecutorExecute =>
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
- passCounterState.next += 1;
388
- // The pass id is the only credential a loaded program presents to reach
389
- // its host authority, so it must be unguessable: a program cannot forge
390
- // another pass's id even if two passes were ever concurrent (the broker
391
- // keeps them sequential, but the id must not rely on that). The counter
392
- // prefix keeps ids debuggable; the random suffix makes them unforgeable.
393
- const passId = `code-mode-pass-${passCounterState.next}-${crypto.randomUUID()}`;
394
-
395
- // Promise-side host calls bridge into the Effect world through a pending
396
- // list served by a scoped fiber, exactly like the deterministic
397
- // substitute: interruption reaches in-flight host calls, and pass-fatal
398
- // conditions fail the pass by failing the server.
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
- const dispatch = (hostCall: unknown): Promise<unknown> =>
403
- new Promise((resolve, reject) => {
404
- issuedHostCalls += 1;
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
- yield* Effect.acquireRelease(
414
- Effect.sync(() => {
415
- passRegistry.set(passId, { dispatch });
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 nextPending = Effect.suspend(() => {
424
- const item = pending.shift();
425
- if (item !== undefined) {
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
- return Effect.callback<PendingHostCall>((resume) => {
429
- wake = () => {
430
- wake = undefined;
431
- const next = pending.shift();
432
- if (next !== undefined) {
433
- resume(Effect.succeed(next));
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 item = yield* nextPending;
443
- served += 1;
444
- if (served > request.limits.maxHostCalls) {
445
- return yield* CodeHostCallLimitError.make({
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 decoded = decodeHostCall(item.hostCall);
452
- if (Option.isNone(decoded)) {
453
- item.reject(new TypeError("host calls must match the CodeHostCall schema"));
454
- continue;
455
- }
456
- const outcome = yield* host.call(decoded.value);
457
- if (outcome._tag === "CodeHostCallSuccess") {
458
- const bytes = encodedJsonByteLength(outcome.value);
459
- if (bytes === undefined || bytes > request.limits.maxHostCallResultBytes) {
460
- return yield* CodeOutputLimitError.make({
461
- implementation: dynamicWorkerImplementation,
462
- surface: "host-call-result",
463
- limit: request.limits.maxHostCallResultBytes,
464
- observed: bytes ?? 0,
465
- logs: [],
466
- });
467
- }
468
- }
469
- item.resolve(hostResultEnvelope(outcome));
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
- const workerCode = {
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
- CODE_MODE_HOST: options.hostStub,
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 as never),
651
+ try: () => options.loader.load(workerCode),
512
652
  catch: (cause) => {
513
- const text = cause instanceof Error ? cause.message : String(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
- (stub) =>
532
- Effect.sync(() => {
533
- (stub as Partial<Record<typeof Symbol.dispose, () => void>>)[Symbol.dispose]?.();
534
- }),
673
+ disposeRpcHandle,
535
674
  );
536
675
 
537
- const server = yield* serveHostCalls.pipe(Effect.forkScoped);
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: async () => {
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 raw = yield* Effect.raceFirst(rpc, Fiber.join(server)).pipe(
550
- Effect.timeoutOrElse({
551
- duration: request.limits.maxWallTime,
552
- orElse: () =>
553
- CodeExecutionTimeoutError.make({
554
- implementation: dynamicWorkerImplementation,
555
- kind: "wall-clock",
556
- maxWallTime: request.limits.maxWallTime,
557
- logs: [],
558
- }),
559
- }),
560
- Effect.ensuring(Fiber.interrupt(server)),
561
- );
562
- const finishedAt = yield* Clock.currentTimeMillis;
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.millis(Math.max(0, finishedAt - startedAt)),
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
- try {
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.succeed(CodeExecutor)(CodeExecutor.of({ execute: makeExecute(options) }));
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
+ };