@effect-agent/platform-cloudflare 0.0.1-beta.5 → 0.1.0-beta.6
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 +82 -34
- package/dist/index.mjs +609 -51
- package/dist/index.mjs.map +1 -1
- package/package.json +10 -8
- package/src/code-mode-executor.ts +710 -0
- package/src/conversation-object.ts +146 -94
- package/src/index.ts +1 -0
|
@@ -0,0 +1,710 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CodeExecutionHost,
|
|
3
|
+
CodeExecutionRequest,
|
|
4
|
+
CodeExecutionResourceUse,
|
|
5
|
+
CodeExecutionResult,
|
|
6
|
+
CodeExecutionTimeoutError,
|
|
7
|
+
CodeExecutor,
|
|
8
|
+
CodeExecutorStartError,
|
|
9
|
+
CodeExecutorTerminatedError,
|
|
10
|
+
CodeExecutorUnsupportedError,
|
|
11
|
+
CodeExecutionProtocolError,
|
|
12
|
+
CodeHostCall,
|
|
13
|
+
CodeHostCallLimitError,
|
|
14
|
+
CodeHostCallResult,
|
|
15
|
+
CodeOutputLimitError,
|
|
16
|
+
CodeProgramFailedError,
|
|
17
|
+
CodeSourceError,
|
|
18
|
+
SandboxImplementation,
|
|
19
|
+
type CodeExecutorExecute,
|
|
20
|
+
} from "@effect-agent/sandbox";
|
|
21
|
+
import { WorkerEntrypoint } from "cloudflare:workers";
|
|
22
|
+
import { Clock, Duration, Effect, Fiber, Layer, Option, Schema } from "effect";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The Cloudflare Dynamic Worker `CodeExecutor` adapter (C4 of ADR-0017;
|
|
26
|
+
* DEPLOY-011). Each pass loads one fresh Worker through the Worker Loader
|
|
27
|
+
* 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.
|
|
35
|
+
*/
|
|
36
|
+
export const dynamicWorkerImplementation = SandboxImplementation.make({
|
|
37
|
+
isolation: "isolated",
|
|
38
|
+
identity: "cloudflare-dynamic-worker",
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
/** The narrow host stub the dynamic worker receives (Workers RPC). */
|
|
42
|
+
export interface CodeModeHostStub {
|
|
43
|
+
readonly call: (passId: string, hostCall: unknown) => Promise<unknown>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface RegisteredPass {
|
|
47
|
+
readonly dispatch: (hostCall: unknown) => Promise<unknown>;
|
|
48
|
+
}
|
|
49
|
+
|
|
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.
|
|
54
|
+
*/
|
|
55
|
+
const passRegistry = new Map<string, RegisteredPass>();
|
|
56
|
+
|
|
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);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* The fixed harness loaded as the dynamic worker's main module. The generated
|
|
76
|
+
* source becomes `program.mjs` (`export default (<expression>);`) — a module,
|
|
77
|
+
* never `eval`. The harness installs namespace globals and a bounded console,
|
|
78
|
+
* imports the program, invokes it exactly once, and returns one envelope the
|
|
79
|
+
* host validates through Effect Schema.
|
|
80
|
+
*/
|
|
81
|
+
const HARNESS_MODULE = String.raw`
|
|
82
|
+
import { WorkerEntrypoint } from "cloudflare:workers";
|
|
83
|
+
import programDefault from "./program.js";
|
|
84
|
+
|
|
85
|
+
const encoder = new TextEncoder();
|
|
86
|
+
const utf8 = (text) => encoder.encode(text).byteLength;
|
|
87
|
+
const safeText = (value) => {
|
|
88
|
+
try {
|
|
89
|
+
if (value instanceof Error) return (value.name + ": " + value.message).slice(0, 4000);
|
|
90
|
+
if (typeof value === "string") return value.slice(0, 4000);
|
|
91
|
+
const encoded = JSON.stringify(value);
|
|
92
|
+
return (encoded === undefined ? String(value) : encoded).slice(0, 4000);
|
|
93
|
+
} catch {
|
|
94
|
+
return "[unserializable value]";
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
const safeJson = (value) => {
|
|
98
|
+
try {
|
|
99
|
+
const encoded = JSON.stringify(value);
|
|
100
|
+
if (encoded !== undefined && encoded.length <= 4000) return JSON.parse(encoded);
|
|
101
|
+
} catch {}
|
|
102
|
+
return safeText(value);
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
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;
|
|
109
|
+
const limits = config.limits;
|
|
110
|
+
const logs = [];
|
|
111
|
+
let logBytes = 0;
|
|
112
|
+
let fatal;
|
|
113
|
+
const boundedLogs = () => logs.slice(0, 4096);
|
|
114
|
+
const write = (...values) => {
|
|
115
|
+
const joined = values.map(safeText).join(" ");
|
|
116
|
+
const line = joined.length > 16000 ? joined.slice(0, 15999) + "…" : joined;
|
|
117
|
+
const bytes = utf8(line);
|
|
118
|
+
if (logs.length >= 4096 || logBytes + bytes > limits.maxLogBytes) {
|
|
119
|
+
fatal = fatal ?? { _tag: "log-limit", observed: logBytes + bytes, logs: boundedLogs() };
|
|
120
|
+
throw new Error("code-mode log limit exceeded");
|
|
121
|
+
}
|
|
122
|
+
logs.push(line);
|
|
123
|
+
logBytes += bytes;
|
|
124
|
+
};
|
|
125
|
+
globalThis.console = { log: write, info: write, warn: write, error: write, debug: write };
|
|
126
|
+
|
|
127
|
+
let hostCalls = 0;
|
|
128
|
+
const makeMethod = (namespace, method) => async (argument) => {
|
|
129
|
+
hostCalls += 1;
|
|
130
|
+
if (hostCalls > limits.maxHostCalls) {
|
|
131
|
+
fatal = fatal ?? { _tag: "host-call-limit", logs: boundedLogs() };
|
|
132
|
+
throw new Error("code-mode host-call limit exceeded");
|
|
133
|
+
}
|
|
134
|
+
let argText;
|
|
135
|
+
try {
|
|
136
|
+
argText = JSON.stringify(argument);
|
|
137
|
+
} catch {}
|
|
138
|
+
if (argText === undefined || utf8(argText) > limits.maxHostCallArgumentBytes) {
|
|
139
|
+
fatal = fatal ?? {
|
|
140
|
+
_tag: "argument-limit",
|
|
141
|
+
observed: argText === undefined ? 0 : utf8(argText),
|
|
142
|
+
logs: boundedLogs(),
|
|
143
|
+
};
|
|
144
|
+
throw new Error("code-mode host-call argument limit exceeded");
|
|
145
|
+
}
|
|
146
|
+
const outcome = await host.call(config.passId, {
|
|
147
|
+
namespace,
|
|
148
|
+
method,
|
|
149
|
+
argument: JSON.parse(argText),
|
|
150
|
+
});
|
|
151
|
+
if (outcome !== null && typeof outcome === "object" && outcome._tag === "CodeHostCallSuccess") {
|
|
152
|
+
return outcome.value;
|
|
153
|
+
}
|
|
154
|
+
if (outcome !== null && typeof outcome === "object" && outcome._tag === "CodeHostCallFailure") {
|
|
155
|
+
throw outcome.error;
|
|
156
|
+
}
|
|
157
|
+
fatal = fatal ?? { _tag: "protocol", message: "host returned an unrecognized outcome" };
|
|
158
|
+
throw new Error("code-mode host protocol violation");
|
|
159
|
+
};
|
|
160
|
+
for (const namespace of config.namespaces) {
|
|
161
|
+
const methods = {};
|
|
162
|
+
for (const method of namespace.methods) {
|
|
163
|
+
methods[method] = makeMethod(namespace.name, method);
|
|
164
|
+
}
|
|
165
|
+
globalThis[namespace.name] = methods;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// program.js is imported statically at the top of this module, so a
|
|
169
|
+
// syntactically invalid program fails the whole harness at load (mapped
|
|
170
|
+
// to a source error by the host). Using a static import keeps this module
|
|
171
|
+
// free of dynamic-import expressions, which single-script Miniflare hosts
|
|
172
|
+
// reject. The isolation boundary does NOT depend on the ordering of this
|
|
173
|
+
// import versus the console/namespace shims installed below: the loaded
|
|
174
|
+
// Worker has globalOutbound: null and no bindings, secrets, or env from
|
|
175
|
+
// the Worker Loader config BEFORE any module in the graph evaluates, so
|
|
176
|
+
// module-level program code has no ambient authority regardless. The
|
|
177
|
+
// shims below are usability wrappers (bounded console, namespace globals),
|
|
178
|
+
// and the accepted program is a single async-function expression whose
|
|
179
|
+
// body runs only when invoked here — after the shims exist.
|
|
180
|
+
const program = programDefault;
|
|
181
|
+
if (typeof program !== "function") {
|
|
182
|
+
return { _tag: "source-not-a-function", actual: typeof program };
|
|
183
|
+
}
|
|
184
|
+
try {
|
|
185
|
+
const value = await program();
|
|
186
|
+
if (fatal !== undefined) return fatal;
|
|
187
|
+
let text;
|
|
188
|
+
try {
|
|
189
|
+
text = JSON.stringify(value);
|
|
190
|
+
} catch {}
|
|
191
|
+
if (text === undefined) {
|
|
192
|
+
return {
|
|
193
|
+
_tag: "program-failed",
|
|
194
|
+
reason: "non-json-result",
|
|
195
|
+
thrown: null,
|
|
196
|
+
message: "The program must return a JSON value",
|
|
197
|
+
logs: boundedLogs(),
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
const resultBytes = utf8(text);
|
|
201
|
+
if (resultBytes > limits.maxResultBytes) {
|
|
202
|
+
return { _tag: "result-limit", observed: resultBytes, logs: boundedLogs() };
|
|
203
|
+
}
|
|
204
|
+
return {
|
|
205
|
+
_tag: "completed",
|
|
206
|
+
value: JSON.parse(text),
|
|
207
|
+
logs: boundedLogs(),
|
|
208
|
+
hostCalls,
|
|
209
|
+
logBytes,
|
|
210
|
+
resultBytes,
|
|
211
|
+
};
|
|
212
|
+
} catch (cause) {
|
|
213
|
+
if (fatal !== undefined) return fatal;
|
|
214
|
+
return {
|
|
215
|
+
_tag: "program-failed",
|
|
216
|
+
reason: cause instanceof Error ? "threw" : "rejected",
|
|
217
|
+
thrown: safeJson(cause),
|
|
218
|
+
message: safeText(cause),
|
|
219
|
+
logs: boundedLogs(),
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
`;
|
|
225
|
+
|
|
226
|
+
const BoundedLogs = Schema.Array(Schema.String.check(Schema.isMaxLength(16 * 1024))).check(
|
|
227
|
+
Schema.isMaxLength(4_096),
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
const HarnessCompleted = Schema.Struct({
|
|
231
|
+
_tag: Schema.Literal("completed"),
|
|
232
|
+
value: Schema.Json,
|
|
233
|
+
logs: BoundedLogs,
|
|
234
|
+
hostCalls: Schema.Natural,
|
|
235
|
+
logBytes: Schema.Natural,
|
|
236
|
+
resultBytes: Schema.Natural,
|
|
237
|
+
});
|
|
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"),
|
|
244
|
+
actual: Schema.String,
|
|
245
|
+
});
|
|
246
|
+
const HarnessProgramFailed = Schema.Struct({
|
|
247
|
+
_tag: Schema.Literal("program-failed"),
|
|
248
|
+
reason: Schema.Literals(["threw", "rejected", "non-json-result"]),
|
|
249
|
+
thrown: Schema.Json,
|
|
250
|
+
message: Schema.String,
|
|
251
|
+
logs: BoundedLogs,
|
|
252
|
+
});
|
|
253
|
+
const HarnessLogLimit = Schema.Struct({
|
|
254
|
+
_tag: Schema.Literal("log-limit"),
|
|
255
|
+
observed: Schema.Natural,
|
|
256
|
+
logs: BoundedLogs,
|
|
257
|
+
});
|
|
258
|
+
const HarnessArgumentLimit = Schema.Struct({
|
|
259
|
+
_tag: Schema.Literal("argument-limit"),
|
|
260
|
+
observed: Schema.Natural,
|
|
261
|
+
logs: BoundedLogs,
|
|
262
|
+
});
|
|
263
|
+
const HarnessResultLimit = Schema.Struct({
|
|
264
|
+
_tag: Schema.Literal("result-limit"),
|
|
265
|
+
observed: Schema.Natural,
|
|
266
|
+
logs: BoundedLogs,
|
|
267
|
+
});
|
|
268
|
+
const HarnessHostCallLimit = Schema.Struct({
|
|
269
|
+
_tag: Schema.Literal("host-call-limit"),
|
|
270
|
+
logs: BoundedLogs,
|
|
271
|
+
});
|
|
272
|
+
const HarnessProtocol = Schema.Struct({
|
|
273
|
+
_tag: Schema.Literal("protocol"),
|
|
274
|
+
message: Schema.String,
|
|
275
|
+
});
|
|
276
|
+
const HarnessOutcome = Schema.Union([
|
|
277
|
+
HarnessCompleted,
|
|
278
|
+
HarnessSourceInvalid,
|
|
279
|
+
HarnessNotAFunction,
|
|
280
|
+
HarnessProgramFailed,
|
|
281
|
+
HarnessLogLimit,
|
|
282
|
+
HarnessArgumentLimit,
|
|
283
|
+
HarnessResultLimit,
|
|
284
|
+
HarnessHostCallLimit,
|
|
285
|
+
HarnessProtocol,
|
|
286
|
+
]);
|
|
287
|
+
|
|
288
|
+
const decodeHarnessOutcome = (value: unknown) => {
|
|
289
|
+
try {
|
|
290
|
+
return Schema.decodeUnknownOption(HarnessOutcome)(value);
|
|
291
|
+
} catch {
|
|
292
|
+
return Option.none<typeof HarnessOutcome.Type>();
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
const decodeHostCall = (value: unknown) => {
|
|
297
|
+
try {
|
|
298
|
+
return Schema.decodeUnknownOption(CodeHostCall)(value);
|
|
299
|
+
} catch {
|
|
300
|
+
return Option.none<CodeHostCall>();
|
|
301
|
+
}
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Project a host outcome to the plain JSON envelope the harness reads. A
|
|
306
|
+
* `CodeExecutionHost` may return either real `CodeHostCallResult` instances
|
|
307
|
+
* (the substitute and conformance kit) or plain-object equivalents (the Code
|
|
308
|
+
* Mode capability's broker route), so this reads the shared fields rather than
|
|
309
|
+
* `Schema.encodeSync`, which would reject a plain object.
|
|
310
|
+
*/
|
|
311
|
+
const hostResultEnvelope = (outcome: CodeHostCallResult): Record<string, unknown> =>
|
|
312
|
+
outcome._tag === "CodeHostCallSuccess"
|
|
313
|
+
? { _tag: "CodeHostCallSuccess", value: outcome.value }
|
|
314
|
+
: { _tag: "CodeHostCallFailure", error: outcome.error };
|
|
315
|
+
|
|
316
|
+
const utf8ByteLength = (value: string): number => {
|
|
317
|
+
let total = 0;
|
|
318
|
+
for (const character of value) {
|
|
319
|
+
const codePoint = character.codePointAt(0) ?? 0;
|
|
320
|
+
total += codePoint <= 0x7f ? 1 : codePoint <= 0x7ff ? 2 : codePoint <= 0xffff ? 3 : 4;
|
|
321
|
+
}
|
|
322
|
+
return total;
|
|
323
|
+
};
|
|
324
|
+
|
|
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
|
+
}
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
interface PendingHostCall {
|
|
335
|
+
readonly hostCall: unknown;
|
|
336
|
+
readonly resolve: (value: unknown) => void;
|
|
337
|
+
readonly reject: (reason: unknown) => void;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/** Reserved global names the harness owns inside the dynamic worker. */
|
|
341
|
+
const reservedHarnessGlobals = new Set(["console"]);
|
|
342
|
+
|
|
343
|
+
export interface DynamicWorkerCodeExecutorOptions {
|
|
344
|
+
/** The `worker_loader` binding. */
|
|
345
|
+
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
|
+
/** Compatibility date for dynamic workers; defaults to `2025-05-01`. */
|
|
353
|
+
readonly compatibilityDate?: string | undefined;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const passCounterState = { next: 0 };
|
|
357
|
+
|
|
358
|
+
const makeExecute = (options: DynamicWorkerCodeExecutorOptions): CodeExecutorExecute =>
|
|
359
|
+
Effect.fn("DynamicWorkerCodeExecutor.execute")(function* (request: CodeExecutionRequest) {
|
|
360
|
+
if (request.network._tag !== "NetworkDisabled") {
|
|
361
|
+
return yield* CodeExecutorUnsupportedError.make({
|
|
362
|
+
implementation: dynamicWorkerImplementation,
|
|
363
|
+
feature: "network",
|
|
364
|
+
message:
|
|
365
|
+
"The Dynamic Worker executor denies all egress with globalOutbound: null; an allowlist is not supported in the first slice",
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
const sourceBytes = utf8ByteLength(request.source);
|
|
369
|
+
if (sourceBytes > request.limits.maxSourceBytes) {
|
|
370
|
+
return yield* CodeSourceError.make({
|
|
371
|
+
implementation: dynamicWorkerImplementation,
|
|
372
|
+
reason: "oversized",
|
|
373
|
+
message: `Source is ${sourceBytes} bytes; the request allows ${request.limits.maxSourceBytes}`,
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
for (const namespace of request.namespaces) {
|
|
377
|
+
if (reservedHarnessGlobals.has(namespace.name)) {
|
|
378
|
+
return yield* CodeExecutorUnsupportedError.make({
|
|
379
|
+
implementation: dynamicWorkerImplementation,
|
|
380
|
+
feature: "namespaces",
|
|
381
|
+
message: `Namespace ${namespace.name} collides with a harness binding`,
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
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;
|
|
401
|
+
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
|
+
});
|
|
412
|
+
|
|
413
|
+
yield* Effect.acquireRelease(
|
|
414
|
+
Effect.sync(() => {
|
|
415
|
+
passRegistry.set(passId, { dispatch });
|
|
416
|
+
}),
|
|
417
|
+
() =>
|
|
418
|
+
Effect.sync(() => {
|
|
419
|
+
passRegistry.delete(passId);
|
|
420
|
+
}),
|
|
421
|
+
);
|
|
422
|
+
|
|
423
|
+
const nextPending = Effect.suspend(() => {
|
|
424
|
+
const item = pending.shift();
|
|
425
|
+
if (item !== undefined) {
|
|
426
|
+
return Effect.succeed(item);
|
|
427
|
+
}
|
|
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
|
+
};
|
|
436
|
+
});
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
const serveHostCalls = Effect.gen(function* () {
|
|
440
|
+
let served = 0;
|
|
441
|
+
while (true) {
|
|
442
|
+
const item = yield* nextPending;
|
|
443
|
+
served += 1;
|
|
444
|
+
if (served > request.limits.maxHostCalls) {
|
|
445
|
+
return yield* CodeHostCallLimitError.make({
|
|
446
|
+
implementation: dynamicWorkerImplementation,
|
|
447
|
+
limit: request.limits.maxHostCalls,
|
|
448
|
+
logs: [],
|
|
449
|
+
});
|
|
450
|
+
}
|
|
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));
|
|
470
|
+
}
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
const workerCode = {
|
|
474
|
+
compatibilityDate: options.compatibilityDate ?? "2025-05-01",
|
|
475
|
+
allowExperimental: true,
|
|
476
|
+
mainModule: "harness.js",
|
|
477
|
+
modules: {
|
|
478
|
+
"harness.js": HARNESS_MODULE,
|
|
479
|
+
"program.js": `export default (\n${request.source}\n);`,
|
|
480
|
+
},
|
|
481
|
+
env: {
|
|
482
|
+
CODE_MODE_HOST: options.hostStub,
|
|
483
|
+
CODE_MODE_PASS: JSON.stringify({
|
|
484
|
+
passId,
|
|
485
|
+
namespaces: request.namespaces.map((namespace) => ({
|
|
486
|
+
name: namespace.name,
|
|
487
|
+
methods: namespace.methods,
|
|
488
|
+
})),
|
|
489
|
+
limits: {
|
|
490
|
+
maxLogBytes: request.limits.maxLogBytes,
|
|
491
|
+
maxResultBytes: request.limits.maxResultBytes,
|
|
492
|
+
maxHostCalls: request.limits.maxHostCalls,
|
|
493
|
+
maxHostCallArgumentBytes: request.limits.maxHostCallArgumentBytes,
|
|
494
|
+
},
|
|
495
|
+
}),
|
|
496
|
+
},
|
|
497
|
+
globalOutbound: null,
|
|
498
|
+
...(request.limits.cpuMillis === undefined
|
|
499
|
+
? {}
|
|
500
|
+
: {
|
|
501
|
+
limits: {
|
|
502
|
+
cpuMs: request.limits.cpuMillis,
|
|
503
|
+
subRequests: request.limits.maxHostCalls + 8,
|
|
504
|
+
},
|
|
505
|
+
}),
|
|
506
|
+
};
|
|
507
|
+
|
|
508
|
+
const startedAt = yield* Clock.currentTimeMillis;
|
|
509
|
+
const worker = yield* Effect.acquireRelease(
|
|
510
|
+
Effect.try({
|
|
511
|
+
try: () => options.loader.load(workerCode as never),
|
|
512
|
+
catch: (cause) => {
|
|
513
|
+
const text = cause instanceof Error ? cause.message : String(cause);
|
|
514
|
+
// Blame the program's source ONLY on a genuine compile diagnostic;
|
|
515
|
+
// any other load rejection is an infrastructure start failure, not
|
|
516
|
+
// the model's fault (see classifyWorkerFailure for the same split).
|
|
517
|
+
if (/syntaxerror|failed to (compile|parse)/i.test(text)) {
|
|
518
|
+
return CodeSourceError.make({
|
|
519
|
+
implementation: dynamicWorkerImplementation,
|
|
520
|
+
reason: "invalid",
|
|
521
|
+
message: text.slice(0, 8_000),
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
return CodeExecutorStartError.make({
|
|
525
|
+
implementation: dynamicWorkerImplementation,
|
|
526
|
+
message: `The Worker Loader rejected the pass: ${text}`.slice(0, 8_000),
|
|
527
|
+
cause,
|
|
528
|
+
});
|
|
529
|
+
},
|
|
530
|
+
}),
|
|
531
|
+
(stub) =>
|
|
532
|
+
Effect.sync(() => {
|
|
533
|
+
(stub as Partial<Record<typeof Symbol.dispose, () => void>>)[Symbol.dispose]?.();
|
|
534
|
+
}),
|
|
535
|
+
);
|
|
536
|
+
|
|
537
|
+
const server = yield* serveHostCalls.pipe(Effect.forkScoped);
|
|
538
|
+
|
|
539
|
+
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
|
+
},
|
|
546
|
+
catch: (cause) => classifyWorkerFailure(cause, request.limits.maxWallTime),
|
|
547
|
+
});
|
|
548
|
+
|
|
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;
|
|
563
|
+
|
|
564
|
+
const outcome = decodeHarnessOutcome(raw);
|
|
565
|
+
if (Option.isNone(outcome)) {
|
|
566
|
+
return yield* CodeExecutionProtocolError.make({
|
|
567
|
+
implementation: dynamicWorkerImplementation,
|
|
568
|
+
message: "The dynamic worker returned a value outside the harness envelope schema",
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
switch (outcome.value._tag) {
|
|
572
|
+
case "completed": {
|
|
573
|
+
return CodeExecutionResult.make({
|
|
574
|
+
implementation: dynamicWorkerImplementation,
|
|
575
|
+
value: outcome.value.value,
|
|
576
|
+
logs: outcome.value.logs,
|
|
577
|
+
resourceUse: CodeExecutionResourceUse.make({
|
|
578
|
+
wallTime: Duration.millis(Math.max(0, finishedAt - startedAt)),
|
|
579
|
+
hostCalls: outcome.value.hostCalls,
|
|
580
|
+
logBytes: outcome.value.logBytes,
|
|
581
|
+
resultBytes: outcome.value.resultBytes,
|
|
582
|
+
}),
|
|
583
|
+
});
|
|
584
|
+
}
|
|
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
|
+
case "source-not-a-function": {
|
|
593
|
+
return yield* CodeSourceError.make({
|
|
594
|
+
implementation: dynamicWorkerImplementation,
|
|
595
|
+
reason: "not-a-function",
|
|
596
|
+
message: `The source expression evaluated to ${outcome.value.actual}; it must evaluate to one async function`,
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
case "program-failed": {
|
|
600
|
+
return yield* CodeProgramFailedError.make({
|
|
601
|
+
implementation: dynamicWorkerImplementation,
|
|
602
|
+
reason: outcome.value.reason,
|
|
603
|
+
thrown: outcome.value.thrown,
|
|
604
|
+
message: outcome.value.message.slice(0, 8_000),
|
|
605
|
+
logs: outcome.value.logs,
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
case "log-limit": {
|
|
609
|
+
return yield* CodeOutputLimitError.make({
|
|
610
|
+
implementation: dynamicWorkerImplementation,
|
|
611
|
+
surface: "logs",
|
|
612
|
+
limit: request.limits.maxLogBytes,
|
|
613
|
+
observed: outcome.value.observed,
|
|
614
|
+
logs: outcome.value.logs,
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
case "argument-limit": {
|
|
618
|
+
return yield* CodeOutputLimitError.make({
|
|
619
|
+
implementation: dynamicWorkerImplementation,
|
|
620
|
+
surface: "host-call-argument",
|
|
621
|
+
limit: request.limits.maxHostCallArgumentBytes,
|
|
622
|
+
observed: outcome.value.observed,
|
|
623
|
+
logs: outcome.value.logs,
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
case "result-limit": {
|
|
627
|
+
return yield* CodeOutputLimitError.make({
|
|
628
|
+
implementation: dynamicWorkerImplementation,
|
|
629
|
+
surface: "result",
|
|
630
|
+
limit: request.limits.maxResultBytes,
|
|
631
|
+
observed: outcome.value.observed,
|
|
632
|
+
logs: outcome.value.logs,
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
case "host-call-limit": {
|
|
636
|
+
return yield* CodeHostCallLimitError.make({
|
|
637
|
+
implementation: dynamicWorkerImplementation,
|
|
638
|
+
limit: request.limits.maxHostCalls,
|
|
639
|
+
logs: outcome.value.logs,
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
case "protocol": {
|
|
643
|
+
return yield* CodeExecutionProtocolError.make({
|
|
644
|
+
implementation: dynamicWorkerImplementation,
|
|
645
|
+
message: outcome.value.message.slice(0, 8_000),
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
/**
|
|
652
|
+
* Expected worker-level failures map into the typed union with bounded
|
|
653
|
+
* diagnostics; anything unrecognized stays a start/termination error rather
|
|
654
|
+
* than a fabricated program result.
|
|
655
|
+
*/
|
|
656
|
+
const classifyWorkerFailure = (
|
|
657
|
+
cause: unknown,
|
|
658
|
+
maxWallTime: Duration.Duration,
|
|
659
|
+
):
|
|
660
|
+
| CodeExecutionTimeoutError
|
|
661
|
+
| CodeExecutorTerminatedError
|
|
662
|
+
| CodeExecutorStartError
|
|
663
|
+
| 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
|
+
})();
|
|
671
|
+
// `WorkerLoader.load()` is lazy, so a module-compile error in the generated
|
|
672
|
+
// program surfaces here at first use. Blame the program's source ONLY on a
|
|
673
|
+
// genuine compile diagnostic (a `SyntaxError` or an explicit compile
|
|
674
|
+
// failure) — the fixed harness is valid, so the fault is in program.js. A
|
|
675
|
+
// bare "failed to start Worker" without a compile diagnostic is an
|
|
676
|
+
// infrastructure start failure, not the model's fault, so it must NOT be
|
|
677
|
+
// misclassified as a source error.
|
|
678
|
+
if (/syntaxerror|failed to (compile|parse)/i.test(text)) {
|
|
679
|
+
return CodeSourceError.make({
|
|
680
|
+
implementation: dynamicWorkerImplementation,
|
|
681
|
+
reason: "invalid",
|
|
682
|
+
message: text.slice(0, 8_000),
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
if (/cpu/i.test(text)) {
|
|
686
|
+
return CodeExecutionTimeoutError.make({
|
|
687
|
+
implementation: dynamicWorkerImplementation,
|
|
688
|
+
kind: "cpu",
|
|
689
|
+
maxWallTime,
|
|
690
|
+
logs: [],
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
if (/failed to start worker/i.test(text)) {
|
|
694
|
+
return CodeExecutorStartError.make({
|
|
695
|
+
implementation: dynamicWorkerImplementation,
|
|
696
|
+
message: text.slice(0, 8_000),
|
|
697
|
+
cause,
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
return CodeExecutorTerminatedError.make({
|
|
701
|
+
implementation: dynamicWorkerImplementation,
|
|
702
|
+
message: text.slice(0, 8_000),
|
|
703
|
+
});
|
|
704
|
+
};
|
|
705
|
+
|
|
706
|
+
/** Layer building the Dynamic Worker `CodeExecutor` from resolved bindings. */
|
|
707
|
+
export const dynamicWorkerCodeExecutorLayer = (
|
|
708
|
+
options: DynamicWorkerCodeExecutorOptions,
|
|
709
|
+
): Layer.Layer<CodeExecutor> =>
|
|
710
|
+
Layer.succeed(CodeExecutor)(CodeExecutor.of({ execute: makeExecute(options) }));
|