@openpond/evals 0.7.5 → 0.7.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.
Files changed (46) hide show
  1. package/JAVASCRIPT_ENVIRONMENTS.md +47 -0
  2. package/dist/environment-execution-services-contract.js +27 -0
  3. package/dist/environment-execution-services.js +100 -0
  4. package/dist/javascript-environment-contract.js +13 -0
  5. package/dist/javascript-environment-node.js +4 -2
  6. package/dist/javascript-environment.js +4 -2
  7. package/dist/javascript-isolate-process-entry.js +2 -2
  8. package/dist/javascript-isolate-process-source.js +1 -1
  9. package/dist/javascript-isolate-process.js +6 -4
  10. package/dist/javascript-isolate.js +39 -30
  11. package/dist/javascript-verifier-worker-source.js +1 -1
  12. package/dist/sql-execution-contract.js +52 -0
  13. package/dist/sql-execution-engine.js +135 -0
  14. package/dist/sql-execution-node.js +58 -0
  15. package/dist/sql-execution-process-entry.js +3 -0
  16. package/dist/sql-execution-process-source.js +1 -0
  17. package/dist/sql-wasm-binary.js +1 -0
  18. package/dist/types/environment-execution-services-contract.d.ts +74 -0
  19. package/dist/types/environment-execution-services-contract.d.ts.map +1 -0
  20. package/dist/types/environment-execution-services.d.ts +17 -0
  21. package/dist/types/environment-execution-services.d.ts.map +1 -0
  22. package/dist/types/evidence/conformance.d.ts +4 -4
  23. package/dist/types/evidence/contracts.d.ts +4 -4
  24. package/dist/types/javascript-environment-contract.d.ts +121 -0
  25. package/dist/types/javascript-environment-contract.d.ts.map +1 -1
  26. package/dist/types/javascript-environment-node.d.ts.map +1 -1
  27. package/dist/types/javascript-environment.d.ts +36 -0
  28. package/dist/types/javascript-environment.d.ts.map +1 -1
  29. package/dist/types/javascript-isolate-process.d.ts.map +1 -1
  30. package/dist/types/javascript-isolate.d.ts +4 -1
  31. package/dist/types/javascript-isolate.d.ts.map +1 -1
  32. package/dist/types/learning/contracts.d.ts +11 -11
  33. package/dist/types/learning/operations.d.ts +6 -6
  34. package/dist/types/learning/repository.d.ts +5 -5
  35. package/dist/types/learning/transport.d.ts +27 -27
  36. package/dist/types/sql-execution-contract.d.ts +72 -0
  37. package/dist/types/sql-execution-contract.d.ts.map +1 -0
  38. package/dist/types/sql-execution-engine.d.ts +4 -0
  39. package/dist/types/sql-execution-engine.d.ts.map +1 -0
  40. package/dist/types/sql-execution-node.d.ts +9 -0
  41. package/dist/types/sql-execution-node.d.ts.map +1 -0
  42. package/dist/types/sql-execution-process-entry.d.ts +2 -0
  43. package/dist/types/sql-execution-process-entry.d.ts.map +1 -0
  44. package/dist/types/sql-execution-process-source.d.ts +1 -0
  45. package/dist/types/sql-wasm-binary.d.ts +1 -0
  46. package/package.json +10 -1
@@ -63,3 +63,50 @@ is cancelled, waits for worker termination, and fails explicitly when that bound
63
63
  operation deadline is exceeded.
64
64
 
65
65
  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.
66
+
67
+ ## Submitted SQL and JavaScript
68
+
69
+ An optional `executionServices` array on the immutable environment definition binds
70
+ an owner-controlled service to a named `step` tool or to `collect`. Omission preserves
71
+ existing definition hashes. Each service declares an identity, deadline and explicit
72
+ value references: `{ scope: "input" | "initialState" | "state" | "arguments", path: [...] }`.
73
+ References traverse own JSON properties only. The controller receives service results
74
+ as `services[id]`; candidates cannot supply that field.
75
+
76
+ A `sqlite.v1` binding declares `sql`, `snapshot`, `maxRows` and `maxResultBytes`.
77
+ Snapshots contain named tables, typed columns (`INTEGER`, `REAL`, `TEXT`, `BLOB`) and
78
+ array rows. SQLite executes exactly one statement in a fresh in-memory database.
79
+ The Node adapter enforces read-only authorization, a deterministic function allowlist,
80
+ a 32 MiB WASM memory maximum, value/SQL/result limits and a parent-owned deadline.
81
+ Writes, attachment, pragma changes, extensions and nondeterministic functions are
82
+ rejected. Results retain ordered column names and array rows, including duplicates.
83
+ SQL null is JSON null; exact large integers use `{ integer: "9223372036854775807" }`
84
+ and blobs use `{ blobBase64: "AP8=" }`. JSON numbers retain their ordinary IEEE-754
85
+ semantics. Use tagged integers when exact 64-bit storage is required.
86
+
87
+ A `javascript.v1` binding declares `source`, `cases`, `exportName`, `maxCases` and
88
+ `maxResultBytes`. Cases are owner-held objects with an `input` property. Each case
89
+ runs in a fresh QuickJS interpreter, and only its `input` crosses into the candidate.
90
+ Other properties, including expected results, remain with the owner. Put hidden cases
91
+ and expectations in private `initialState`, not task `input`: task input is part of
92
+ the policy transcript. The authored controller grades returned case values against
93
+ its own expectations. No host functions, module loader, filesystem or network are
94
+ exposed to candidate JavaScript.
95
+
96
+ Service outcomes are either completed results or explicit rejected submissions.
97
+ Syntax, authorization, output and engine-memory failures cannot manufacture a passing
98
+ answer. A service timeout produces a rejected outcome only after child termination;
99
+ owner cancellation and infrastructure setup failures still fail the operation.
100
+ Service preparation and the authored controller share the operation's overall budget
101
+ and its tracked promise. Collection remains unable to mutate session state, and
102
+ `destroy()` aborts and awaits all active work before running authored cleanup.
103
+
104
+ `executeJavaScriptEnvironmentInProcess` and `executeJavaScriptEnvironmentInWorker`
105
+ provide these services on Node hosts; service children always use Node processes.
106
+ The portable in-process interpreter supports candidate JavaScript and rejects a SQL
107
+ binding without a Node SQL executor. The standalone Node API is
108
+ `executeSqlInProcess({ request, timeoutMs, signal })` from
109
+ `@openpond/evals/sql-execution/node`; request/result schemas are available from
110
+ `@openpond/evals/sql-execution`. The SQLite WASM bytes are embedded in the trusted
111
+ child program, so execution survives installation and host bundling without resolving
112
+ an application-relative WASM file or loading submitted code as host JavaScript.
@@ -0,0 +1,27 @@
1
+ import { z } from "zod";
2
+ import { SqlExecutionRequestSchema } from "./sql-execution-contract.js";
3
+ /** Explicit owner-side paths; resolution never evaluates code or traverses prototypes. */
4
+ export const EnvironmentValueReferenceSchema = z.object({
5
+ scope: z.enum(["input", "initialState", "state", "arguments"]),
6
+ path: z.array(z.string().min(1).max(256)).max(16),
7
+ }).strict();
8
+ const common = {
9
+ id: z.string().min(1).max(128),
10
+ operation: z.enum(["step", "collect"]),
11
+ toolName: z.string().min(1).max(500).optional(),
12
+ timeoutMs: z.number().int().min(1).max(20_000),
13
+ };
14
+ export const EnvironmentExecutionServiceSchema = z.discriminatedUnion("kind", [
15
+ z.object({ ...common, kind: z.literal("sqlite.v1"), sql: EnvironmentValueReferenceSchema, snapshot: EnvironmentValueReferenceSchema,
16
+ maxRows: SqlExecutionRequestSchema.shape.maxRows, maxResultBytes: SqlExecutionRequestSchema.shape.maxResultBytes }).strict(),
17
+ z.object({ ...common, kind: z.literal("javascript.v1"), source: EnvironmentValueReferenceSchema, cases: EnvironmentValueReferenceSchema,
18
+ exportName: z.string().regex(/^[A-Za-z_$][A-Za-z0-9_$]*$/).max(128), maxCases: z.number().int().min(1).max(32), maxResultBytes: z.number().int().min(256).max(262_144) }).strict(),
19
+ ]).superRefine((service, context) => {
20
+ if ((service.operation === "step") !== (service.toolName !== undefined))
21
+ context.addIssue({ code: "custom", path: ["toolName"], message: "Only step services require a declared tool name." });
22
+ if (service.operation === "collect") {
23
+ const references = service.kind === "sqlite.v1" ? [service.sql, service.snapshot] : [service.source, service.cases];
24
+ if (references.some(reference => reference.scope === "arguments"))
25
+ context.addIssue({ code: "custom", message: "Collection has no tool arguments." });
26
+ }
27
+ });
@@ -0,0 +1,100 @@
1
+ import { EnvironmentExecutionServiceSchema } from "./environment-execution-services-contract.js";
2
+ import { JavaScriptIsolateExecutionError } from "./javascript-isolate.js";
3
+ import { assertBoundedTaskJson } from "./task-schema.js";
4
+ import { assertSqlExecutionRequest } from "./sql-execution-contract.js";
5
+ function resolveValue(value, reference) {
6
+ let resolved = reference.scope === "arguments" ? value.action?.arguments : value[reference.scope];
7
+ for (const key of reference.path) {
8
+ if (!resolved || typeof resolved !== "object" || !Object.hasOwn(resolved, key))
9
+ throw new Error("environment_service_reference_missing");
10
+ resolved = resolved[key];
11
+ }
12
+ return resolved;
13
+ }
14
+ /** The single returned promise owns service work, controller execution and cancellation. */
15
+ export async function executeEnvironmentWithServices(input, runners) {
16
+ if (!input.executionServices?.length)
17
+ return runners.controller(input);
18
+ if (!Number.isSafeInteger(input.timeoutMs) || input.timeoutMs < 1 || input.timeoutMs > 30_000)
19
+ throw new Error("environment_timeout_invalid");
20
+ assertBoundedTaskJson(input.value, 4_194_304);
21
+ const services = input.executionServices.map(service => EnvironmentExecutionServiceSchema.parse(service));
22
+ if (services.length > 16)
23
+ throw new Error("environment_service_limit");
24
+ if (new Set(services.map(service => service.id)).size !== services.length)
25
+ throw new Error("environment_service_identity_collision");
26
+ const started = Date.now();
27
+ const controller = new AbortController();
28
+ const signal = input.signal ? AbortSignal.any([input.signal, controller.signal]) : controller.signal;
29
+ const timer = setTimeout(() => controller.abort(new Error("environment_timeout")), input.timeoutMs);
30
+ try {
31
+ const results = Object.create(null);
32
+ for (const service of services) {
33
+ signal.throwIfAborted();
34
+ if (service.operation !== input.operation || (service.operation === "step" && service.toolName !== input.value.action?.name))
35
+ throw new Error("environment_service_operation_mismatch");
36
+ const budget = Math.min(service.timeoutMs, input.timeoutMs - (Date.now() - started));
37
+ if (budget < 1)
38
+ throw new Error("environment_timeout");
39
+ const deadline = new AbortController();
40
+ const serviceSignal = AbortSignal.any([signal, deadline.signal]);
41
+ const serviceTimer = setTimeout(() => deadline.abort(new Error("environment_service_timeout")), budget);
42
+ const serviceStarted = Date.now();
43
+ try {
44
+ if (service.kind === "sqlite.v1") {
45
+ if (!runners.sql)
46
+ throw new Error("environment_sql_executor_required");
47
+ const sql = resolveValue(input.value, service.sql);
48
+ if (typeof sql !== "string" || !sql.length || new TextEncoder().encode(sql).byteLength > 65_536 || sql.includes("\0")) {
49
+ results[service.id] = { status: "rejected", code: "invalid_submission" };
50
+ continue;
51
+ }
52
+ const request = assertSqlExecutionRequest({ schemaVersion: "openpond.sqlExecution.v1", snapshot: resolveValue(input.value, service.snapshot), sql, maxRows: service.maxRows, maxResultBytes: service.maxResultBytes });
53
+ results[service.id] = await runners.sql({ request, timeoutMs: input.timeoutMs, signal: serviceSignal });
54
+ }
55
+ else {
56
+ const source = resolveValue(input.value, service.source), cases = resolveValue(input.value, service.cases);
57
+ if (typeof source !== "string" || new TextEncoder().encode(source).byteLength > 524_288) {
58
+ results[service.id] = { status: "rejected", code: "invalid_submission" };
59
+ continue;
60
+ }
61
+ if (!Array.isArray(cases) || !cases.length || cases.length > service.maxCases || cases.some(item => !item || typeof item !== "object" || !Object.hasOwn(item, "input")))
62
+ throw new Error("environment_service_cases_invalid");
63
+ const values = [];
64
+ for (const item of cases) {
65
+ const remaining = budget - (Date.now() - serviceStarted);
66
+ if (remaining < 1) {
67
+ deadline.abort(new Error("environment_service_timeout"));
68
+ serviceSignal.throwIfAborted();
69
+ }
70
+ // Hidden expected results and all other case fields remain with the owner.
71
+ values.push(await runners.candidate({ source, exportName: service.exportName, value: structuredClone(item.input), timeoutMs: Math.max(1, remaining), maxResultBytes: service.maxResultBytes, deterministic: true, errorPrefix: "candidate", signal: serviceSignal }));
72
+ if (new TextEncoder().encode(JSON.stringify({ status: "completed", cases: values })).byteLength > service.maxResultBytes)
73
+ throw new JavaScriptIsolateExecutionError("candidate_result_too_large");
74
+ }
75
+ results[service.id] = { status: "completed", cases: values };
76
+ }
77
+ }
78
+ catch (error) {
79
+ signal.throwIfAborted();
80
+ if (deadline.signal.aborted)
81
+ results[service.id] = { status: "rejected", code: "timeout" };
82
+ else if (error instanceof JavaScriptIsolateExecutionError)
83
+ results[service.id] = { status: "rejected", code: "execution_failed" };
84
+ else
85
+ throw error;
86
+ }
87
+ finally {
88
+ clearTimeout(serviceTimer);
89
+ }
90
+ }
91
+ signal.throwIfAborted();
92
+ const remaining = input.timeoutMs - (Date.now() - started);
93
+ if (remaining < 1)
94
+ throw new Error("environment_timeout");
95
+ return await runners.controller({ ...input, executionServices: undefined, value: { ...input.value, services: results }, timeoutMs: remaining, signal });
96
+ }
97
+ finally {
98
+ clearTimeout(timer);
99
+ }
100
+ }
@@ -1,6 +1,8 @@
1
1
  import { z } from "zod";
2
2
  import { ImmutableAssetRefSchema, ToolDeclarationSchema, contentHash } from "@openpond/harness";
3
3
  import { assertBoundedTaskJson, validateTaskSchema } from "./task-schema-validation.js";
4
+ import { EnvironmentExecutionServiceSchema } from "./environment-execution-services-contract.js";
5
+ export * from "./environment-execution-services-contract.js";
4
6
  export const JavaScriptEnvironmentDefinitionContentSchema = z.object({
5
7
  schemaVersion: z.literal("openpond.javascriptEnvironment.v1"),
6
8
  id: z.string().trim().min(1).max(500),
@@ -11,6 +13,7 @@ export const JavaScriptEnvironmentDefinitionContentSchema = z.object({
11
13
  maxStateBytes: z.number().int().min(1).max(1_048_576),
12
14
  maxObservationBytes: z.number().int().min(1).max(262_144),
13
15
  operationTimeoutMs: z.number().int().min(1).max(30_000),
16
+ executionServices: z.array(EnvironmentExecutionServiceSchema).min(1).max(16).optional(),
14
17
  }).strict().superRefine((value, context) => {
15
18
  if (new Set(value.tools.map(tool => tool.name)).size !== value.tools.length)
16
19
  context.addIssue({ code: "custom", path: ["tools"], message: "Environment tool names must be unique." });
@@ -20,6 +23,16 @@ export const JavaScriptEnvironmentDefinitionContentSchema = z.object({
20
23
  if (tool.inputSchemaHash !== contentHash(tool.inputSchema))
21
24
  context.addIssue({ code: "custom", path: ["tools", index, "inputSchemaHash"], message: "Tool schema identity differs from its declared schema." });
22
25
  });
26
+ const services = value.executionServices ?? [];
27
+ if (new Set(services.map(service => service.id)).size !== services.length)
28
+ context.addIssue({ code: "custom", path: ["executionServices"], message: "Execution service identities must be unique." });
29
+ services.forEach((service, index) => {
30
+ const tool = value.tools.find(tool => tool.name === service.toolName);
31
+ if (service.operation === "step" && !tool)
32
+ context.addIssue({ code: "custom", path: ["executionServices", index, "toolName"], message: "Execution services must target a declared tool." });
33
+ if (service.timeoutMs >= Math.min(value.operationTimeoutMs, tool?.timeoutMs ?? value.operationTimeoutMs))
34
+ context.addIssue({ code: "custom", path: ["executionServices", index, "timeoutMs"], message: "Service budgets must leave time for the authored controller." });
35
+ });
23
36
  });
24
37
  export const JavaScriptEnvironmentDefinitionSchema = JavaScriptEnvironmentDefinitionContentSchema.safeExtend({ contentHash: z.string().regex(/^[a-f0-9]{64}$/) });
25
38
  export const JavaScriptEnvironmentOperationSchema = z.enum(["create", "reset", "step", "collect", "destroy"]);
@@ -1,13 +1,15 @@
1
1
  import { JavaScriptEnvironmentOperationSchema, JavaScriptEnvironmentResultSchema } from "./javascript-environment-contract.js";
2
2
  import { executeJavaScriptIsolateInWorker } from "./javascript-isolate-node.js";
3
3
  import { executeJavaScriptIsolateInProcess } from "./javascript-isolate-process.js";
4
+ import { executeEnvironmentWithServices } from "./environment-execution-services.js";
5
+ import { executeSqlInProcess } from "./sql-execution-node.js";
4
6
  /** The execution owner awaits termination before recording a step or cleanup. */
5
7
  export async function executeJavaScriptEnvironmentInWorker(input) {
6
8
  const operation = JavaScriptEnvironmentOperationSchema.parse(input.operation);
7
- return JavaScriptEnvironmentResultSchema.parse(await executeJavaScriptIsolateInWorker({ ...input, exportName: operation, maxResultBytes: 1_572_864, deterministic: true, errorPrefix: "environment" }));
9
+ return executeEnvironmentWithServices(input, { sql: executeSqlInProcess, candidate: executeJavaScriptIsolateInProcess, controller: async (prepared) => JavaScriptEnvironmentResultSchema.parse(await executeJavaScriptIsolateInWorker({ ...prepared, exportName: operation, maxResultBytes: 1_572_864, deterministic: true, errorPrefix: "environment" })) });
8
10
  }
9
11
  /** Uses a Node child process independently of the execution owner's runtime. */
10
12
  export async function executeJavaScriptEnvironmentInProcess(input) {
11
13
  const operation = JavaScriptEnvironmentOperationSchema.parse(input.operation);
12
- return JavaScriptEnvironmentResultSchema.parse(await executeJavaScriptIsolateInProcess({ ...input, exportName: operation, maxResultBytes: 1_572_864, deterministic: true, errorPrefix: "environment" }));
14
+ return executeEnvironmentWithServices(input, { sql: executeSqlInProcess, candidate: executeJavaScriptIsolateInProcess, controller: async (prepared) => JavaScriptEnvironmentResultSchema.parse(await executeJavaScriptIsolateInProcess({ ...prepared, exportName: operation, maxResultBytes: 1_572_864, deterministic: true, errorPrefix: "environment" })) });
13
15
  }
@@ -2,12 +2,13 @@ import { contentHash } from "@openpond/harness";
2
2
  import { verifyLearningTextAsset } from "./learning/assets.js";
3
3
  import { assertBoundedTaskJson, validateTaskValue } from "./task-schema.js";
4
4
  import { executeJavaScriptIsolate } from "./javascript-isolate.js";
5
+ import { executeEnvironmentWithServices } from "./environment-execution-services.js";
5
6
  import { assertJavaScriptEnvironmentDefinition, JavaScriptEnvironmentOperationSchema, JavaScriptEnvironmentResultSchema } from "./javascript-environment-contract.js";
6
7
  export * from "./javascript-environment-contract.js";
7
8
  /** Pure, bounded module execution. Server hosts should provide the worker runner. */
8
9
  export async function executeJavaScriptEnvironment(input) {
9
10
  const operation = JavaScriptEnvironmentOperationSchema.parse(input.operation);
10
- return JavaScriptEnvironmentResultSchema.parse(await executeJavaScriptIsolate({ ...input, exportName: operation, maxResultBytes: 1_572_864, deterministic: true, errorPrefix: "environment" }));
11
+ return executeEnvironmentWithServices(input, { candidate: executeJavaScriptIsolate, controller: async (prepared) => JavaScriptEnvironmentResultSchema.parse(await executeJavaScriptIsolate({ ...prepared, exportName: operation, maxResultBytes: 1_572_864, deterministic: true, errorPrefix: "environment" })) });
11
12
  }
12
13
  export class JavaScriptEnvironmentActionError extends Error {
13
14
  code;
@@ -59,7 +60,8 @@ export async function createJavaScriptEnvironmentSession(input) {
59
60
  throw new JavaScriptEnvironmentActionError("unknown_tool");
60
61
  if (action && tool && !validateTaskValue(tool.inputSchema, action.arguments).valid)
61
62
  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
+ const executionServices = definition.executionServices?.filter(service => service.operation === operation && (operation !== "step" || service.toolName === action?.name));
64
+ 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, ...(executionServices?.length ? { executionServices } : {}) });
63
65
  active = promise;
64
66
  const result = JavaScriptEnvironmentResultSchema.parse(await promise);
65
67
  signal.throwIfAborted();
@@ -1,8 +1,8 @@
1
- import { executeJavaScriptIsolate } from "./javascript-isolate.js";
1
+ import { executeJavaScriptIsolate, JavaScriptIsolateExecutionError } from "./javascript-isolate.js";
2
2
  // Only this bundled, trusted program runs in Node. Authored source is interpreter data.
3
3
  const owner = globalThis;
4
4
  const input = owner.__openpondIsolateInput;
5
5
  delete owner.__openpondIsolateInput;
6
6
  if (!input)
7
7
  throw new Error("JavaScript process requires its execution owner.");
8
- void executeJavaScriptIsolate(input).then((result) => process.stdout.write(JSON.stringify({ ok: true, result })), (error) => process.stdout.write(JSON.stringify({ ok: false, error: (error instanceof Error ? error.message : "JavaScript execution failed.").slice(0, 8_192) })));
8
+ void executeJavaScriptIsolate(input).then((result) => process.stdout.write(JSON.stringify({ ok: true, result })), (error) => process.stdout.write(JSON.stringify({ ok: false, executionError: error instanceof JavaScriptIsolateExecutionError, error: (error instanceof Error ? error.message : "JavaScript execution failed.").slice(0, 8_192) })));