@openpond/evals 0.7.2 → 0.7.3

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.
@@ -0,0 +1,58 @@
1
+ # JavaScript tool environments
2
+
3
+ These entrypoints execute authored tool modules over owner-held JSON state:
4
+
5
+ - `@openpond/evals/javascript-environment` validates immutable definitions and creates an episode with `step`, `collect`, `snapshot` and `destroy`.
6
+ - `@openpond/evals/javascript-environment/node` executes each operation in a terminable worker around the isolated interpreter.
7
+ - `@openpond/evals/javascript-environment/attempt` runs a policy adapter through the declared tools and returns its messages, output and independently recorded state.
8
+
9
+ The interpreter exposes no host functions, filesystem, network, timers or module loader. Environment code receives an explicit seed and supplied inputs; ambient `Date` and `Math.random` are unavailable. A module cannot call a production connector or launch a process. Tools that require those capabilities need a separate host adapter and its permissions.
10
+
11
+ ## Module contract
12
+
13
+ Publish the module as a `LearningTextAsset` with `host_private` visibility. Its exact asset reference belongs in a sealed `JavaScriptEnvironmentDefinition`. The definition lists each tool's schema, schema hash, side effect and timeout, plus episode step and byte limits.
14
+
15
+ The module exports five functions: `create`, `reset`, `step`, `collect` and `destroy`. Each receives one JSON object with `input`, `initialState`, `seed`, `state`, `sequence` and `action`. Every operation returns `{ state, observation }`, both JSON objects. `action` is null except during `step`, when it contains `{ name, arguments }`.
16
+
17
+ For example, a scoped record environment can implement:
18
+
19
+ ```js
20
+ export function create({ initialState }) {
21
+ return { state: initialState, observation: {} };
22
+ }
23
+ export const reset = create;
24
+ export function step({ input, state, action }) {
25
+ if (action.name === "read_record") {
26
+ return { state, observation: { record: state[action.arguments.id] ?? null } };
27
+ }
28
+ if (action.arguments.id !== input.allowedId) {
29
+ return { state, observation: { error: "outside_scope" } };
30
+ }
31
+ return {
32
+ state: { ...state, [action.arguments.id]: action.arguments.value },
33
+ observation: { updated: true },
34
+ };
35
+ }
36
+ export function collect({ state }) {
37
+ return { state, observation: {} };
38
+ }
39
+ export function destroy() {
40
+ return { state: {}, observation: {} };
41
+ }
42
+ ```
43
+
44
+ Declare `read_record` as read-only and the update tool as a write. The owner verifies arguments before execution and rejects any state change from a read-only tool or collection. A failed operation leaves state unchanged. Invalid calls consume the step budget. The module can return an error observation for a simulated service failure while retaining an intentional transition, such as an idempotency record.
45
+
46
+ ## Policy and evidence boundary
47
+
48
+ `runJavaScriptEnvironmentAttempt` supplies the policy adapter with copied messages and tool declarations. Initial state, module source and evaluator targets are not policy inputs. Initial and subsequent observations come from the module; the owner retains state and records every executed operation with its arguments, observation and before/after hashes.
49
+
50
+ A policy response contains `text` and `toolCalls`. Each call has a unique `id`, a declared `name` and JSON-object `arguments`. The loop executes calls sequentially, returns validation errors as tool observations and stops at the step, transcript or time limit. It collects state only after a final response with no tool calls. A model's claim about final state never replaces the recorded state.
51
+
52
+ Hosts persist the returned attempt under their task/model/release identity and grade the captured state with private checks. The attempt's content hash covers its task identity, status, output, state snapshot and messages. Completion of execution is not a passing grade, training qualification or promotion approval.
53
+
54
+ ## Lifecycle ownership
55
+
56
+ Server hosts should pass `executeJavaScriptEnvironmentInWorker` as the session or attempt runner. Each worker runs a fresh interpreter. A promise settles only after the worker is terminated, including timeout and cancellation. The session rejects overlapping operations and waits for an active operation to stop before cleanup. Call `destroy` in `finally` when using the session API directly; the attempt runner does this automatically.
57
+
58
+ The policy adapter must honor its supplied abort signal and settle after cancelling its own request. `environmentCleanupComplete` describes environment cleanup only; it is not a provider billing or infrastructure-cleanup receipt. Failed creation, collection, cancellation and cleanup remain explicit states, without fabricated scores.
package/README.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  See [task evidence and learning](./LEARNING.md) for reusable Rewards, task-format publication, SDK/HTTP intake, grading, review and approved batches.
4
4
 
5
+ See [JavaScript tool environments](./JAVASCRIPT_ENVIRONMENTS.md) for isolated state transitions, policy tool loops and owner-recorded execution evidence.
6
+
5
7
  Portable evaluation and benchmark contracts plus pure helpers for Tasksets,
6
8
  graders, run manifests, attempt and evaluation receipts, paired benchmark
7
9
  comparisons, execution adapters, conformance fixtures, Work-evidence
@@ -0,0 +1,116 @@
1
+ import { z } from "zod";
2
+ import { contentHash } from "@openpond/harness";
3
+ import { assertBoundedTaskJson } from "./task-schema.js";
4
+ import { createJavaScriptEnvironmentSession, JavaScriptEnvironmentActionError } from "./javascript-environment.js";
5
+ const ToolCallSchema = z.object({ id: z.string().min(1).max(200), name: z.string().min(1).max(64), arguments: z.record(z.string(), z.unknown()) }).strict();
6
+ const PolicyTurnSchema = z.object({ text: z.string().max(262_144), toolCalls: z.array(ToolCallSchema).max(200) }).strict();
7
+ /** The policy receives observations, never source, initial state or evaluator
8
+ * targets. Its final text cannot replace state recorded by the environment.
9
+ * The policy adapter must honor signal and settle after its own cancellation. */
10
+ export async function runJavaScriptEnvironmentAttempt(input) {
11
+ if (!input.taskId || input.taskId.length > 500)
12
+ throw new Error("environment_task_identity_invalid");
13
+ if (!Number.isInteger(input.timeoutMs) || input.timeoutMs < 1 || input.timeoutMs > 3_600_000)
14
+ throw new Error("environment_attempt_timeout_invalid");
15
+ assertBoundedTaskJson(input.instructions, 262_144);
16
+ const taskId = input.taskId, instructions = input.instructions;
17
+ const taskInput = structuredClone(input.input), policy = input.policy;
18
+ const controller = new AbortController();
19
+ const signal = input.signal ? AbortSignal.any([controller.signal, input.signal]) : controller.signal;
20
+ let timedOut = false;
21
+ const timer = setTimeout(() => { timedOut = true; controller.abort(new Error("environment_attempt_timeout")); }, input.timeoutMs);
22
+ timer.unref?.();
23
+ let session = null;
24
+ let snapshot = null;
25
+ let status = "environment_failure";
26
+ let output = null, error = null;
27
+ let collected = false, environmentCleanupComplete = false;
28
+ const messages = [];
29
+ let transcriptBytes = 0;
30
+ const append = (message) => {
31
+ transcriptBytes += new TextEncoder().encode(JSON.stringify(message)).byteLength;
32
+ if (transcriptBytes > 4_194_304) {
33
+ status = "budget_exhausted";
34
+ throw new Error("environment_transcript_budget_exhausted");
35
+ }
36
+ messages.push(message);
37
+ };
38
+ const callIds = new Set();
39
+ try {
40
+ session = await createJavaScriptEnvironmentSession({ ...input, input: taskInput, signal });
41
+ append({ role: "system", text: instructions });
42
+ append({ role: "user", text: JSON.stringify({ input: taskInput, observation: session.observation }) });
43
+ for (let turn = 0; turn <= session.definition.maxSteps; turn++) {
44
+ signal.throwIfAborted();
45
+ let response;
46
+ try {
47
+ const value = await policy({ messages: structuredClone(messages), tools: structuredClone(session.definition.tools), signal });
48
+ assertBoundedTaskJson(value, 1_048_576);
49
+ response = structuredClone(PolicyTurnSchema.parse(value));
50
+ for (const call of response.toolCalls) {
51
+ if (callIds.has(call.id))
52
+ throw new Error("environment_duplicate_tool_call_identity");
53
+ callIds.add(call.id);
54
+ }
55
+ }
56
+ catch (cause) {
57
+ if (signal.aborted)
58
+ throw cause;
59
+ status = "policy_failure";
60
+ throw cause;
61
+ }
62
+ signal.throwIfAborted();
63
+ append({ role: "assistant", ...response });
64
+ if (!response.toolCalls.length) {
65
+ output = response.text;
66
+ await session.collect();
67
+ snapshot = session.snapshot();
68
+ collected = true;
69
+ status = "completed";
70
+ break;
71
+ }
72
+ for (const call of response.toolCalls) {
73
+ let observation;
74
+ try {
75
+ observation = await session.step({ name: call.name, arguments: call.arguments });
76
+ }
77
+ catch (cause) {
78
+ if (!(cause instanceof JavaScriptEnvironmentActionError))
79
+ throw cause;
80
+ if (cause.code === "step_budget_exhausted") {
81
+ status = "budget_exhausted";
82
+ break;
83
+ }
84
+ observation = { error: cause.code };
85
+ }
86
+ append({ role: "tool", callId: call.id, name: call.name, observation });
87
+ }
88
+ if (status === "budget_exhausted")
89
+ break;
90
+ if (turn === session.definition.maxSteps)
91
+ status = "budget_exhausted";
92
+ }
93
+ snapshot ??= session.snapshot();
94
+ }
95
+ catch (cause) {
96
+ if (signal.aborted)
97
+ status = timedOut ? "timed_out" : "cancelled";
98
+ error = cause instanceof Error ? cause.message : "environment_attempt_failed";
99
+ snapshot ??= session?.snapshot() ?? null;
100
+ }
101
+ finally {
102
+ clearTimeout(timer);
103
+ if (session) {
104
+ try {
105
+ await session.destroy();
106
+ environmentCleanupComplete = true;
107
+ }
108
+ catch (cause) {
109
+ status = "environment_failure";
110
+ error = cause instanceof Error ? cause.message : "environment_cleanup_failed";
111
+ }
112
+ }
113
+ }
114
+ const result = { schemaVersion: "openpond.javascriptEnvironmentAttempt.v1", taskId, status, output, snapshot, collected, environmentCleanupComplete, messages, error };
115
+ return { ...result, contentHash: contentHash(result) };
116
+ }
@@ -0,0 +1,37 @@
1
+ import { z } from "zod";
2
+ import { ImmutableAssetRefSchema, ToolDeclarationSchema, contentHash } from "@openpond/harness";
3
+ import { assertBoundedTaskJson, validateTaskSchema } from "./task-schema-validation.js";
4
+ export const JavaScriptEnvironmentDefinitionContentSchema = z.object({
5
+ schemaVersion: z.literal("openpond.javascriptEnvironment.v1"),
6
+ id: z.string().trim().min(1).max(500),
7
+ revision: z.number().int().positive(),
8
+ module: ImmutableAssetRefSchema.refine(reference => reference.visibility === "host_private", "Environment code belongs to the execution owner."),
9
+ tools: z.array(ToolDeclarationSchema).min(1).max(200),
10
+ maxSteps: z.number().int().min(1).max(1_000),
11
+ maxStateBytes: z.number().int().min(1).max(1_048_576),
12
+ maxObservationBytes: z.number().int().min(1).max(262_144),
13
+ operationTimeoutMs: z.number().int().min(1).max(30_000),
14
+ }).strict().superRefine((value, context) => {
15
+ if (new Set(value.tools.map(tool => tool.name)).size !== value.tools.length)
16
+ context.addIssue({ code: "custom", path: ["tools"], message: "Environment tool names must be unique." });
17
+ value.tools.forEach((tool, index) => {
18
+ if (!validateTaskSchema(tool.inputSchema).valid)
19
+ context.addIssue({ code: "custom", path: ["tools", index, "inputSchema"], message: "Environment tools require a supported JSON schema." });
20
+ if (tool.inputSchemaHash !== contentHash(tool.inputSchema))
21
+ context.addIssue({ code: "custom", path: ["tools", index, "inputSchemaHash"], message: "Tool schema identity differs from its declared schema." });
22
+ });
23
+ });
24
+ export const JavaScriptEnvironmentDefinitionSchema = JavaScriptEnvironmentDefinitionContentSchema.safeExtend({ contentHash: z.string().regex(/^[a-f0-9]{64}$/) });
25
+ export const JavaScriptEnvironmentOperationSchema = z.enum(["create", "reset", "step", "collect", "destroy"]);
26
+ export const JavaScriptEnvironmentResultSchema = z.object({
27
+ state: z.record(z.string(), z.unknown()),
28
+ observation: z.record(z.string(), z.unknown()),
29
+ }).strict();
30
+ export function assertJavaScriptEnvironmentDefinition(value) {
31
+ assertBoundedTaskJson(value, 4_194_304);
32
+ const parsed = JavaScriptEnvironmentDefinitionSchema.parse(value);
33
+ const { contentHash: hash, ...content } = parsed;
34
+ if (contentHash(content) !== hash)
35
+ throw new Error("environment_definition_hash_mismatch");
36
+ return structuredClone(parsed);
37
+ }
@@ -0,0 +1,7 @@
1
+ import { JavaScriptEnvironmentOperationSchema, JavaScriptEnvironmentResultSchema } from "./javascript-environment-contract.js";
2
+ import { executeJavaScriptIsolateInWorker } from "./javascript-isolate-node.js";
3
+ /** The execution owner awaits termination before recording a step or cleanup. */
4
+ export async function executeJavaScriptEnvironmentInWorker(input) {
5
+ const operation = JavaScriptEnvironmentOperationSchema.parse(input.operation);
6
+ return JavaScriptEnvironmentResultSchema.parse(await executeJavaScriptIsolateInWorker({ ...input, exportName: operation, maxResultBytes: 1_572_864, deterministic: true, errorPrefix: "environment" }));
7
+ }
@@ -0,0 +1,132 @@
1
+ import { contentHash } from "@openpond/harness";
2
+ import { verifyLearningTextAsset } from "./learning/assets.js";
3
+ import { assertBoundedTaskJson, validateTaskValue } from "./task-schema.js";
4
+ import { executeJavaScriptIsolate } from "./javascript-isolate.js";
5
+ import { assertJavaScriptEnvironmentDefinition, JavaScriptEnvironmentOperationSchema, JavaScriptEnvironmentResultSchema } from "./javascript-environment-contract.js";
6
+ export * from "./javascript-environment-contract.js";
7
+ /** Pure, bounded module execution. Server hosts should provide the worker runner. */
8
+ export async function executeJavaScriptEnvironment(input) {
9
+ const operation = JavaScriptEnvironmentOperationSchema.parse(input.operation);
10
+ return JavaScriptEnvironmentResultSchema.parse(await executeJavaScriptIsolate({ ...input, exportName: operation, maxResultBytes: 1_572_864, deterministic: true, errorPrefix: "environment" }));
11
+ }
12
+ export class JavaScriptEnvironmentActionError extends Error {
13
+ code;
14
+ constructor(code) {
15
+ super(`environment_${code}`);
16
+ this.code = code;
17
+ this.name = "JavaScriptEnvironmentActionError";
18
+ }
19
+ }
20
+ /** One episode, with owner-held state that is never accepted from model output.
21
+ * Read-only tools and collection cannot change it. Call destroy in a finally block. */
22
+ export async function createJavaScriptEnvironmentSession(input) {
23
+ const definition = assertJavaScriptEnvironmentDefinition(input.definition);
24
+ const source = verifyLearningTextAsset(input.asset, definition.module);
25
+ if (!Number.isSafeInteger(input.seed))
26
+ throw new Error("environment_seed_invalid");
27
+ const seed = input.seed;
28
+ assertBoundedTaskJson(input.input, 1_048_576);
29
+ assertBoundedTaskJson(input.initialState, definition.maxStateBytes);
30
+ const taskInput = structuredClone(input.input), initialState = structuredClone(input.initialState);
31
+ const execute = input.execute ?? executeJavaScriptEnvironment;
32
+ const controller = new AbortController();
33
+ const signal = input.signal ? AbortSignal.any([input.signal, controller.signal]) : controller.signal;
34
+ let state = {};
35
+ let steps = 0, closed = false, closing = false;
36
+ let active = null;
37
+ const events = [];
38
+ let closePromise = null;
39
+ async function invoke(operation, action) {
40
+ if (closed || closing)
41
+ throw new Error("environment_closed");
42
+ if (active)
43
+ throw new Error("environment_operation_in_progress");
44
+ signal.throwIfAborted();
45
+ if (action) {
46
+ assertBoundedTaskJson(action, 262_144);
47
+ action = structuredClone(action);
48
+ }
49
+ const beforeStateHash = contentHash(state);
50
+ const tool = action ? definition.tools.find(tool => tool.name === action.name) : null;
51
+ if (operation === "step") {
52
+ if (steps >= definition.maxSteps)
53
+ throw new JavaScriptEnvironmentActionError("step_budget_exhausted");
54
+ steps++;
55
+ }
56
+ const record = (observation, error) => events.push({ sequence: events.length, operation, toolName: action?.name ?? null, arguments: action ? structuredClone(action.arguments) : null, observation, beforeStateHash, afterStateHash: contentHash(state), error });
57
+ try {
58
+ if (action && !tool)
59
+ throw new JavaScriptEnvironmentActionError("unknown_tool");
60
+ if (action && tool && !validateTaskValue(tool.inputSchema, action.arguments).valid)
61
+ throw new JavaScriptEnvironmentActionError("invalid_arguments");
62
+ const promise = execute({ source, operation, value: { input: taskInput, initialState, seed, state: structuredClone(state), sequence: steps, action: action ? structuredClone(action) : null }, timeoutMs: Math.min(definition.operationTimeoutMs, tool?.timeoutMs ?? definition.operationTimeoutMs), signal });
63
+ active = promise;
64
+ const result = JavaScriptEnvironmentResultSchema.parse(await promise);
65
+ signal.throwIfAborted();
66
+ assertBoundedTaskJson(result.state, definition.maxStateBytes);
67
+ assertBoundedTaskJson(result.observation, definition.maxObservationBytes);
68
+ if ((operation === "collect" || tool?.sideEffect === "read") && contentHash(result.state) !== beforeStateHash)
69
+ throw new Error("environment_read_changed_state");
70
+ state = structuredClone(result.state);
71
+ const observation = structuredClone(result.observation);
72
+ record(observation, null);
73
+ return { state: structuredClone(state), observation };
74
+ }
75
+ catch (error) {
76
+ record(null, error instanceof Error ? error.message : "environment_execution_failed");
77
+ throw error;
78
+ }
79
+ finally {
80
+ active = null;
81
+ }
82
+ }
83
+ async function destroy() {
84
+ if (closePromise)
85
+ return closePromise;
86
+ closing = true;
87
+ controller.abort(new Error("environment_destroyed"));
88
+ closePromise = (async () => {
89
+ try {
90
+ // A worker runner settles only after termination; cleanup cannot race it.
91
+ try {
92
+ await active;
93
+ }
94
+ catch { /* The operation failure is retained by its caller. */ }
95
+ await execute({ source, operation: "destroy", value: { input: taskInput, initialState, seed, state: structuredClone(state), sequence: steps, action: null }, timeoutMs: Math.min(definition.operationTimeoutMs, 1_000) });
96
+ }
97
+ finally {
98
+ state = {};
99
+ closed = true;
100
+ }
101
+ })();
102
+ return closePromise;
103
+ }
104
+ try {
105
+ await invoke("create");
106
+ const reset = await invoke("reset");
107
+ const initialStateHash = contentHash(state);
108
+ const snapshot = () => {
109
+ if (closed)
110
+ throw new Error("environment_closed");
111
+ return { definition: { id: definition.id, revision: definition.revision, contentHash: definition.contentHash }, inputHash: contentHash(taskInput), seed, initialStateHash, finalStateHash: contentHash(state), state: structuredClone(state), events: structuredClone(events) };
112
+ };
113
+ return {
114
+ definition: structuredClone(definition),
115
+ observation: reset.observation,
116
+ async step(action) { return (await invoke("step", action)).observation; },
117
+ async collect() {
118
+ const result = await invoke("collect");
119
+ return { ...snapshot(), observation: result.observation };
120
+ },
121
+ snapshot,
122
+ destroy,
123
+ };
124
+ }
125
+ catch (error) {
126
+ try {
127
+ await destroy();
128
+ }
129
+ catch { /* Preserve the original creation failure after owner cleanup. */ }
130
+ throw error;
131
+ }
132
+ }
@@ -0,0 +1,55 @@
1
+ import { Worker } from "node:worker_threads";
2
+ import { javascriptVerifierWorkerSource } from "./javascript-verifier-worker-source.js";
3
+ import { validateJavaScriptIsolateInput } from "./javascript-isolate.js";
4
+ import { assertBoundedTaskJson } from "./task-schema.js";
5
+ /** Both environment steps and graders settle only after their worker stops. */
6
+ export async function executeJavaScriptIsolateInWorker(input) {
7
+ input.signal?.throwIfAborted();
8
+ validateJavaScriptIsolateInput(input);
9
+ const signal = input.signal;
10
+ const workerData = { source: input.source, exportName: input.exportName, value: input.value, timeoutMs: input.timeoutMs, maxResultBytes: input.maxResultBytes, deterministic: input.deterministic === true, errorPrefix: input.errorPrefix };
11
+ const error = (suffix) => new Error(`${input.errorPrefix}_${suffix}`);
12
+ return new Promise((resolve, reject) => {
13
+ const worker = new Worker(javascriptVerifierWorkerSource, {
14
+ eval: true, workerData, execArgv: [], env: {},
15
+ resourceLimits: { maxOldGenerationSizeMb: 64, maxYoungGenerationSizeMb: 16, stackSizeMb: 4 },
16
+ });
17
+ let settling = false;
18
+ const finish = (outcome) => {
19
+ if (settling)
20
+ return;
21
+ settling = true;
22
+ clearTimeout(timer);
23
+ signal?.removeEventListener("abort", cancel);
24
+ void worker.terminate().then(() => {
25
+ if ("error" in outcome)
26
+ reject(outcome.error);
27
+ else
28
+ resolve(outcome.result);
29
+ }, reject);
30
+ };
31
+ const cancel = () => finish({ error: signal?.reason instanceof Error ? signal.reason : error("cancelled") });
32
+ const timer = setTimeout(() => finish({ error: error("timeout") }), input.timeoutMs);
33
+ signal?.addEventListener("abort", cancel, { once: true });
34
+ worker.once("error", (error) => finish({ error }));
35
+ worker.once("exit", () => { if (!settling)
36
+ finish({ error: error("worker_exited_without_result") }); });
37
+ worker.once("message", (message) => {
38
+ try {
39
+ if (!message || typeof message !== "object" || !("ok" in message))
40
+ throw error("worker_invalid_response");
41
+ if (message.ok !== true)
42
+ throw new Error("error" in message && typeof message.error === "string" ? message.error : `${input.errorPrefix}_execution_failed`);
43
+ if (!("result" in message))
44
+ throw error("worker_invalid_response");
45
+ assertBoundedTaskJson(message.result, input.maxResultBytes);
46
+ finish({ result: message.result });
47
+ }
48
+ catch (cause) {
49
+ finish({ error: cause instanceof Error ? cause : error("worker_invalid_response") });
50
+ }
51
+ });
52
+ if (signal?.aborted)
53
+ cancel();
54
+ });
55
+ }
@@ -0,0 +1,67 @@
1
+ import { newQuickJSWASMModuleFromVariant, Scope } from "quickjs-emscripten-core";
2
+ import variant from "@jitl/quickjs-singlefile-cjs-release-sync";
3
+ import { assertBoundedTaskJson } from "./task-schema.js";
4
+ /** Authored code is interpreter data, with no host functions or module loader. */
5
+ export async function executeJavaScriptIsolate(input) {
6
+ validateJavaScriptIsolateInput(input);
7
+ input.signal?.throwIfAborted();
8
+ const error = (suffix) => new Error(`${input.errorPrefix}_${suffix}`);
9
+ const deadline = Date.now() + input.timeoutMs;
10
+ const module = await newQuickJSWASMModuleFromVariant(variant);
11
+ input.signal?.throwIfAborted();
12
+ return Scope.withScope((scope) => {
13
+ const runtime = scope.manage(module.newRuntime());
14
+ runtime.setMemoryLimit(33_554_432);
15
+ runtime.setMaxStackSize(262_144);
16
+ runtime.setInterruptHandler(() => Date.now() >= deadline || input.signal?.aborted === true);
17
+ const context = scope.manage(runtime.newContext());
18
+ if (input.deterministic) {
19
+ scope.manage(context.unwrapResult(context.evalCode("globalThis.Date = undefined; Math.random = () => { throw new Error('environment_random_requires_explicit_seed'); };")));
20
+ }
21
+ const stringify = scope.manage(context.unwrapResult(context.evalCode("JSON.stringify")));
22
+ const argument = scope.manage(context.unwrapResult(context.evalCode(`JSON.parse(${JSON.stringify(JSON.stringify(input.value))})`)));
23
+ const exported = scope.manage(context.unwrapResult(context.evalCode(input.source, `${input.errorPrefix}.mjs`, { type: "module" })));
24
+ const namespace = scope.manage(settle(context, exported, deadline, input.signal, error));
25
+ const fn = scope.manage(context.getProp(namespace, input.exportName));
26
+ if (context.typeof(fn) !== "function")
27
+ throw error("function_export_missing");
28
+ const returned = scope.manage(context.unwrapResult(context.callFunction(fn, context.undefined, argument)));
29
+ const result = scope.manage(settle(context, returned, deadline, input.signal, error));
30
+ const serialized = scope.manage(context.unwrapResult(context.callFunction(stringify, context.undefined, result)));
31
+ if (context.typeof(serialized) !== "string")
32
+ throw error("result_not_json");
33
+ const json = context.getString(serialized);
34
+ if (new TextEncoder().encode(json).byteLength > input.maxResultBytes)
35
+ throw error("result_too_large");
36
+ input.signal?.throwIfAborted();
37
+ if (Date.now() >= deadline)
38
+ throw error("timeout");
39
+ const value = JSON.parse(json);
40
+ assertBoundedTaskJson(value, input.maxResultBytes);
41
+ return value;
42
+ });
43
+ }
44
+ export function validateJavaScriptIsolateInput(input) {
45
+ if (!Number.isInteger(input.timeoutMs) || input.timeoutMs < 1 || input.timeoutMs > 300_000)
46
+ throw new Error(`${input.errorPrefix}_timeout_invalid`);
47
+ if (!Number.isInteger(input.maxResultBytes) || input.maxResultBytes < 1 || input.maxResultBytes > 4_194_304)
48
+ throw new Error(`${input.errorPrefix}_result_limit_invalid`);
49
+ if (new TextEncoder().encode(input.source).byteLength > 524_288)
50
+ throw new Error(`${input.errorPrefix}_source_too_large`);
51
+ assertBoundedTaskJson(input.value, 4_194_304);
52
+ }
53
+ function settle(context, handle, deadline, signal, error) {
54
+ for (;;) {
55
+ signal?.throwIfAborted();
56
+ if (Date.now() >= deadline)
57
+ throw error("timeout");
58
+ const state = context.getPromiseState(handle);
59
+ if (state.type === "fulfilled")
60
+ return state.notAPromise ? state.value.dup() : state.value;
61
+ if (state.type === "rejected")
62
+ return context.unwrapResult(state);
63
+ const jobs = context.runtime.executePendingJobs(1);
64
+ if (jobs.unwrap() === 0)
65
+ throw error("unresolved_promise");
66
+ }
67
+ }
@@ -1,56 +1,6 @@
1
- import { Worker } from "node:worker_threads";
2
1
  import { JavaScriptVerifierResultSchema } from "./javascript-verifier-contract.js";
3
- import { javascriptVerifierWorkerSource } from "./javascript-verifier-worker-source.js";
4
- import { assertBoundedTaskJson } from "./task-schema.js";
2
+ import { executeJavaScriptIsolateInWorker } from "./javascript-isolate-node.js";
5
3
  /** Resolves/rejects only after the execution owner has stopped the worker. */
6
4
  export async function executeJavaScriptVerifierInWorker(input) {
7
- input.signal?.throwIfAborted();
8
- if (!Number.isInteger(input.timeoutMs) || input.timeoutMs < 1 || input.timeoutMs > 300_000)
9
- throw new Error("verifier_timeout_invalid");
10
- assertBoundedTaskJson(input.value, 4_194_304);
11
- if (new TextEncoder().encode(input.source).byteLength > 524_288)
12
- throw new Error("verifier_source_too_large");
13
- const { signal, ...workerData } = input;
14
- return new Promise((resolve, reject) => {
15
- const worker = new Worker(javascriptVerifierWorkerSource, {
16
- eval: true, workerData, execArgv: [], env: {},
17
- resourceLimits: { maxOldGenerationSizeMb: 64, maxYoungGenerationSizeMb: 16, stackSizeMb: 4 },
18
- });
19
- let settling = false;
20
- const finish = (outcome) => {
21
- if (settling)
22
- return;
23
- settling = true;
24
- clearTimeout(timer);
25
- signal?.removeEventListener("abort", cancel);
26
- void worker.terminate().then(() => {
27
- if ("error" in outcome)
28
- reject(outcome.error);
29
- else
30
- resolve(outcome.result);
31
- }, reject);
32
- };
33
- const cancel = () => finish({ error: signal?.reason instanceof Error ? signal.reason : new Error("verifier_cancelled") });
34
- const timer = setTimeout(() => finish({ error: new Error("verifier_timeout") }), input.timeoutMs);
35
- signal?.addEventListener("abort", cancel, { once: true });
36
- worker.once("error", (error) => finish({ error }));
37
- worker.once("exit", () => { if (!settling)
38
- finish({ error: new Error("verifier_worker_exited_without_result") }); });
39
- worker.once("message", (message) => {
40
- try {
41
- if (!message || typeof message !== "object" || !("ok" in message))
42
- throw new Error("verifier_worker_invalid_response");
43
- if (message.ok !== true)
44
- throw new Error("error" in message && typeof message.error === "string" ? message.error : "verifier_execution_failed");
45
- if (!("result" in message))
46
- throw new Error("verifier_worker_invalid_response");
47
- finish({ result: JavaScriptVerifierResultSchema.parse(message.result) });
48
- }
49
- catch (error) {
50
- finish({ error: error instanceof Error ? error : new Error("verifier_worker_invalid_response") });
51
- }
52
- });
53
- if (signal?.aborted)
54
- cancel();
55
- });
5
+ return JavaScriptVerifierResultSchema.parse(await executeJavaScriptIsolateInWorker({ ...input, exportName: input.exportName ?? "verify", maxResultBytes: 65_536, errorPrefix: "verifier" }));
56
6
  }