@effect-agent/platform-cloudflare 0.1.0-beta.25 → 0.1.0-beta.26
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/index.d.mts +8 -29
- package/dist/index.mjs +40 -60
- package/dist/index.mjs.map +1 -1
- package/package.json +7 -7
- package/src/code-mode-executor.ts +31 -82
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@effect-agent/platform-cloudflare",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.26",
|
|
4
4
|
"exports": {
|
|
5
5
|
".": {
|
|
6
6
|
"types": "./dist/index.d.mts",
|
|
@@ -8,11 +8,11 @@
|
|
|
8
8
|
}
|
|
9
9
|
},
|
|
10
10
|
"dependencies": {
|
|
11
|
-
"@effect-agent/core": "0.1.0-beta.
|
|
12
|
-
"@effect-agent/engine": "0.1.0-beta.
|
|
13
|
-
"@effect-agent/sandbox": "0.1.0-beta.
|
|
14
|
-
"@effect-agent/session": "0.1.0-beta.
|
|
15
|
-
"@effect-agent/storage-cloudflare": "0.1.0-beta.
|
|
11
|
+
"@effect-agent/core": "0.1.0-beta.26",
|
|
12
|
+
"@effect-agent/engine": "0.1.0-beta.26",
|
|
13
|
+
"@effect-agent/sandbox": "0.1.0-beta.26",
|
|
14
|
+
"@effect-agent/session": "0.1.0-beta.26",
|
|
15
|
+
"@effect-agent/storage-cloudflare": "0.1.0-beta.26",
|
|
16
16
|
"@effect/platform-browser": "4.0.0-rc.110",
|
|
17
17
|
"@effect/sql-sqlite-do": "4.0.0-rc.110",
|
|
18
18
|
"effect": "4.0.0-rc.110"
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"@effect/vitest": "4.0.0-rc.110",
|
|
49
49
|
"effect-cf": "0.27.0",
|
|
50
50
|
"esbuild": "0.28.1",
|
|
51
|
-
"miniflare": "
|
|
51
|
+
"miniflare": "5.20260811.1-alpha",
|
|
52
52
|
"typescript": "7.0.2",
|
|
53
53
|
"vite-plus": "0.2.6",
|
|
54
54
|
"vitest": "4.1.10"
|
|
@@ -18,9 +18,8 @@ import {
|
|
|
18
18
|
type CodeExecutorExecute,
|
|
19
19
|
type CodeExecutionRequest,
|
|
20
20
|
} from "@effect-agent/sandbox";
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
import { Clock, Crypto, Duration, Effect, Exit, Fiber, Layer, Option, Queue, Schema } from "effect";
|
|
21
|
+
import { RpcTarget } from "cloudflare:workers";
|
|
22
|
+
import { Clock, Duration, Effect, Exit, Fiber, Layer, Option, Queue, Schema } from "effect";
|
|
24
23
|
|
|
25
24
|
import { safeCauseDiagnostic, safeCauseMessage } from "./boundary.ts";
|
|
26
25
|
|
|
@@ -28,56 +27,43 @@ import { safeCauseDiagnostic, safeCauseMessage } from "./boundary.ts";
|
|
|
28
27
|
* The Cloudflare Dynamic Worker `CodeExecutor` adapter (C4 of ADR-0017;
|
|
29
28
|
* DEPLOY-011). Each pass loads one fresh Worker through the Worker Loader
|
|
30
29
|
* with `globalOutbound: null`, so generated code has no ambient network,
|
|
31
|
-
* bindings, or secrets; its only authority is the pass-scoped
|
|
32
|
-
* routes back
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
* 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.
|
|
38
36
|
*/
|
|
39
37
|
export const dynamicWorkerImplementation = SandboxImplementation.make({
|
|
40
38
|
isolation: "isolated",
|
|
41
39
|
identity: "cloudflare-dynamic-worker",
|
|
42
40
|
});
|
|
43
41
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
readonly call: (passId: string, hostCall: unknown) => Promise<unknown>;
|
|
42
|
+
interface CodeModePassHost extends Rpc.RpcTargetBranded {
|
|
43
|
+
readonly call: (hostCall: unknown) => Promise<unknown>;
|
|
47
44
|
}
|
|
48
45
|
|
|
49
46
|
interface CodeModeHarnessEntrypoint extends Rpc.WorkerEntrypointBranded {
|
|
50
|
-
readonly run: () => Promise<unknown>;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
interface RegisteredPass {
|
|
54
|
-
readonly dispatch: (hostCall: unknown) => Promise<unknown>;
|
|
47
|
+
readonly run: (host: CodeModePassHost) => Promise<unknown>;
|
|
55
48
|
}
|
|
56
49
|
|
|
57
50
|
/**
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
* `
|
|
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.
|
|
61
56
|
*/
|
|
62
|
-
|
|
57
|
+
class CodeModePassHostTarget extends RpcTarget implements CodeModePassHost {
|
|
58
|
+
readonly #dispatch: (hostCall: unknown) => Promise<unknown>;
|
|
63
59
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
export class CodeModeHostEntrypoint extends WorkerEntrypoint {
|
|
72
|
-
async call(passId: unknown, hostCall: unknown): Promise<unknown> {
|
|
73
|
-
if (typeof passId !== "string") {
|
|
74
|
-
throw new TypeError("Code Mode pass identities must be strings");
|
|
75
|
-
}
|
|
76
|
-
const pass = passRegistry.get(passId);
|
|
77
|
-
if (pass === undefined) {
|
|
78
|
-
throw new Error("Unknown Code Mode pass");
|
|
79
|
-
}
|
|
80
|
-
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);
|
|
81
67
|
}
|
|
82
68
|
}
|
|
83
69
|
|
|
@@ -113,9 +99,8 @@ const safeJson = (value) => {
|
|
|
113
99
|
};
|
|
114
100
|
|
|
115
101
|
export default class CodeModeHarness extends WorkerEntrypoint {
|
|
116
|
-
async run() {
|
|
102
|
+
async run(host) {
|
|
117
103
|
const config = this.env.CODE_MODE_PASS;
|
|
118
|
-
const host = this.env.CODE_MODE_HOST;
|
|
119
104
|
const limits = config.limits;
|
|
120
105
|
const logs = [];
|
|
121
106
|
let logBytes = 0;
|
|
@@ -153,7 +138,7 @@ export default class CodeModeHarness extends WorkerEntrypoint {
|
|
|
153
138
|
};
|
|
154
139
|
throw new Error("code-mode host-call argument limit exceeded");
|
|
155
140
|
}
|
|
156
|
-
const outcome = await host.call(
|
|
141
|
+
const outcome = await host.call({
|
|
157
142
|
namespace,
|
|
158
143
|
method,
|
|
159
144
|
argument: JSON.parse(argText),
|
|
@@ -287,7 +272,6 @@ const HarnessOutcome = Schema.Union([
|
|
|
287
272
|
]);
|
|
288
273
|
|
|
289
274
|
const HarnessPassConfig = Schema.Struct({
|
|
290
|
-
passId: Schema.NonEmptyString,
|
|
291
275
|
namespaces: Schema.Array(
|
|
292
276
|
Schema.Struct({
|
|
293
277
|
name: Schema.NonEmptyString,
|
|
@@ -409,19 +393,12 @@ const reservedHarnessGlobals = new Set(["console"]);
|
|
|
409
393
|
export interface DynamicWorkerCodeExecutorOptions {
|
|
410
394
|
/** The `worker_loader` binding. */
|
|
411
395
|
readonly loader: WorkerLoader;
|
|
412
|
-
/**
|
|
413
|
-
* A SAME-INSTANCE stub of `CodeModeHostEntrypoint`. In production create it
|
|
414
|
-
* with `ctx.exports.CodeModeHostEntrypoint()`; a cross-instance stub would
|
|
415
|
-
* dispatch host calls into an isolate without this pass's registry entry.
|
|
416
|
-
*/
|
|
417
|
-
readonly hostStub: CodeModeHostStub;
|
|
418
396
|
/** Compatibility date for dynamic workers; defaults to `2025-05-01`. */
|
|
419
397
|
readonly compatibilityDate?: string | undefined;
|
|
420
398
|
}
|
|
421
399
|
|
|
422
400
|
const makeExecute = (
|
|
423
401
|
options: DynamicWorkerCodeExecutorOptions,
|
|
424
|
-
crypto: Crypto.Crypto,
|
|
425
402
|
clock: Clock.Clock,
|
|
426
403
|
): CodeExecutorExecute =>
|
|
427
404
|
Effect.fn("DynamicWorkerCodeExecutor.execute")(function* (request: CodeExecutionRequest) {
|
|
@@ -450,25 +427,7 @@ const makeExecute = (
|
|
|
450
427
|
});
|
|
451
428
|
}
|
|
452
429
|
}
|
|
453
|
-
|
|
454
430
|
const host = yield* CodeExecutionHost;
|
|
455
|
-
// The pass id is the only credential a loaded program presents to reach
|
|
456
|
-
// its host authority, so it must be unguessable: a program cannot forge
|
|
457
|
-
// another pass's id even if two passes were ever concurrent (the broker
|
|
458
|
-
// keeps them sequential, but the id must not rely on that).
|
|
459
|
-
const passId = yield* crypto.randomUUIDv4.pipe(
|
|
460
|
-
Effect.mapError((cause) =>
|
|
461
|
-
CodeExecutorStartError.make({
|
|
462
|
-
implementation: dynamicWorkerImplementation,
|
|
463
|
-
message: `Could not mint the Code Mode pass identity: ${safeCauseMessage(
|
|
464
|
-
cause,
|
|
465
|
-
"the crypto service failed without a diagnostic",
|
|
466
|
-
)}`.slice(0, 8_000),
|
|
467
|
-
cause,
|
|
468
|
-
}),
|
|
469
|
-
),
|
|
470
|
-
Effect.map((uuid) => `code-mode-pass-${uuid}`),
|
|
471
|
-
);
|
|
472
431
|
|
|
473
432
|
// This synchronous clock access is confined to callbacks that must compute a timeout
|
|
474
433
|
// immediately. The Clock service remains the authority, so tests and hosts can replace it.
|
|
@@ -604,16 +563,9 @@ const makeExecute = (
|
|
|
604
563
|
|
|
605
564
|
const closeAdmission = Effect.sync(() => {
|
|
606
565
|
passOpen = false;
|
|
607
|
-
passRegistry.delete(passId);
|
|
608
566
|
rejectQueuedHostCalls(new Error("Code Mode pass is closing"));
|
|
609
567
|
});
|
|
610
|
-
|
|
611
|
-
yield* Effect.acquireRelease(
|
|
612
|
-
Effect.sync(() => {
|
|
613
|
-
passRegistry.set(passId, { dispatch });
|
|
614
|
-
}),
|
|
615
|
-
() => closeAdmission.pipe(Effect.andThen(Fiber.interrupt(server))),
|
|
616
|
-
);
|
|
568
|
+
yield* Effect.addFinalizer(() => closeAdmission.pipe(Effect.andThen(Fiber.interrupt(server))));
|
|
617
569
|
|
|
618
570
|
// No `allowExperimental`: the runtime only accepts it when the CALLING
|
|
619
571
|
// worker carries the `experimental` compatibility flag, which deployed
|
|
@@ -627,9 +579,7 @@ const makeExecute = (
|
|
|
627
579
|
"program.js": `export default (\n${request.source}\n);`,
|
|
628
580
|
},
|
|
629
581
|
env: {
|
|
630
|
-
CODE_MODE_HOST: options.hostStub,
|
|
631
582
|
CODE_MODE_PASS: encodeHarnessPassConfig({
|
|
632
|
-
passId,
|
|
633
583
|
namespaces: request.namespaces.map((namespace) => ({
|
|
634
584
|
name: namespace.name,
|
|
635
585
|
methods: namespace.methods,
|
|
@@ -687,7 +637,7 @@ const makeExecute = (
|
|
|
687
637
|
);
|
|
688
638
|
|
|
689
639
|
const rpc = Effect.tryPromise({
|
|
690
|
-
try: () => entrypoint.run(),
|
|
640
|
+
try: () => entrypoint.run(new CodeModePassHostTarget(dispatch)),
|
|
691
641
|
catch: (cause) => classifyWorkerFailure(cause, request.limits.maxWallTime),
|
|
692
642
|
});
|
|
693
643
|
|
|
@@ -860,8 +810,7 @@ export const dynamicWorkerCodeExecutorLayer = (
|
|
|
860
810
|
Layer.effect(
|
|
861
811
|
CodeExecutor,
|
|
862
812
|
Effect.gen(function* () {
|
|
863
|
-
const crypto = yield* Crypto.Crypto;
|
|
864
813
|
const clock = yield* Clock.Clock;
|
|
865
|
-
return CodeExecutor.of({ execute: makeExecute(options,
|
|
814
|
+
return CodeExecutor.of({ execute: makeExecute(options, clock) });
|
|
866
815
|
}),
|
|
867
|
-
)
|
|
816
|
+
);
|