@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.
- package/JAVASCRIPT_ENVIRONMENTS.md +47 -0
- package/dist/environment-execution-services-contract.js +27 -0
- package/dist/environment-execution-services.js +100 -0
- package/dist/javascript-environment-contract.js +13 -0
- package/dist/javascript-environment-node.js +4 -2
- package/dist/javascript-environment.js +4 -2
- package/dist/javascript-isolate-process-entry.js +2 -2
- package/dist/javascript-isolate-process-source.js +1 -1
- package/dist/javascript-isolate-process.js +6 -4
- package/dist/javascript-isolate.js +39 -30
- package/dist/javascript-verifier-worker-source.js +1 -1
- package/dist/sql-execution-contract.js +52 -0
- package/dist/sql-execution-engine.js +135 -0
- package/dist/sql-execution-node.js +58 -0
- package/dist/sql-execution-process-entry.js +3 -0
- package/dist/sql-execution-process-source.js +1 -0
- package/dist/sql-wasm-binary.js +1 -0
- package/dist/types/environment-execution-services-contract.d.ts +74 -0
- package/dist/types/environment-execution-services-contract.d.ts.map +1 -0
- package/dist/types/environment-execution-services.d.ts +17 -0
- package/dist/types/environment-execution-services.d.ts.map +1 -0
- package/dist/types/evidence/conformance.d.ts +4 -4
- package/dist/types/evidence/contracts.d.ts +4 -4
- package/dist/types/javascript-environment-contract.d.ts +121 -0
- package/dist/types/javascript-environment-contract.d.ts.map +1 -1
- package/dist/types/javascript-environment-node.d.ts.map +1 -1
- package/dist/types/javascript-environment.d.ts +36 -0
- package/dist/types/javascript-environment.d.ts.map +1 -1
- package/dist/types/javascript-isolate-process.d.ts.map +1 -1
- package/dist/types/javascript-isolate.d.ts +4 -1
- package/dist/types/javascript-isolate.d.ts.map +1 -1
- package/dist/types/learning/contracts.d.ts +11 -11
- package/dist/types/learning/operations.d.ts +6 -6
- package/dist/types/learning/repository.d.ts +5 -5
- package/dist/types/learning/transport.d.ts +27 -27
- package/dist/types/sql-execution-contract.d.ts +72 -0
- package/dist/types/sql-execution-contract.d.ts.map +1 -0
- package/dist/types/sql-execution-engine.d.ts +4 -0
- package/dist/types/sql-execution-engine.d.ts.map +1 -0
- package/dist/types/sql-execution-node.d.ts +9 -0
- package/dist/types/sql-execution-node.d.ts.map +1 -0
- package/dist/types/sql-execution-process-entry.d.ts +2 -0
- package/dist/types/sql-execution-process-entry.d.ts.map +1 -0
- package/dist/types/sql-execution-process-source.d.ts +1 -0
- package/dist/types/sql-wasm-binary.d.ts +1 -0
- package/package.json +10 -1
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { assertBoundedTaskJson } from "./task-schema.js";
|
|
3
|
+
const integer = z.string().regex(/^(0|-?[1-9][0-9]*)$/).max(20).refine(value => {
|
|
4
|
+
if (value.length > 20 || !/^(0|-?[1-9][0-9]*)$/.test(value))
|
|
5
|
+
return false;
|
|
6
|
+
const parsed = BigInt(value);
|
|
7
|
+
return parsed >= -(1n << 63n) && parsed < (1n << 63n);
|
|
8
|
+
}, "SQL integers must fit signed 64-bit storage.");
|
|
9
|
+
export const SqlValueSchema = z.union([
|
|
10
|
+
z.null(), z.string().max(262_144),
|
|
11
|
+
z.number().finite(),
|
|
12
|
+
z.object({ integer }).strict(),
|
|
13
|
+
z.object({ blobBase64: z.string().max(349_528).regex(/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/) }).strict(),
|
|
14
|
+
]);
|
|
15
|
+
const identifier = z.string().min(1).max(128).refine(value => !value.includes("\0"));
|
|
16
|
+
export const SqlSnapshotSchema = z.object({
|
|
17
|
+
tables: z.array(z.object({
|
|
18
|
+
name: identifier,
|
|
19
|
+
columns: z.array(z.object({ name: identifier, type: z.enum(["INTEGER", "REAL", "TEXT", "BLOB"]) }).strict()).min(1).max(128),
|
|
20
|
+
rows: z.array(z.array(SqlValueSchema).max(128)).max(4_096),
|
|
21
|
+
}).strict()).max(64),
|
|
22
|
+
}).strict().superRefine((snapshot, context) => {
|
|
23
|
+
const names = new Set();
|
|
24
|
+
for (const [index, table] of snapshot.tables.entries()) {
|
|
25
|
+
const name = table.name.toLowerCase();
|
|
26
|
+
if (names.has(name) || name.startsWith("sqlite_"))
|
|
27
|
+
context.addIssue({ code: "custom", path: ["tables", index, "name"], message: "Table names must be unique and outside SQLite's reserved namespace." });
|
|
28
|
+
names.add(name);
|
|
29
|
+
if (new Set(table.columns.map(column => column.name.toLowerCase())).size !== table.columns.length)
|
|
30
|
+
context.addIssue({ code: "custom", path: ["tables", index, "columns"], message: "Column names must be unique." });
|
|
31
|
+
if (table.rows.some(row => row.length !== table.columns.length))
|
|
32
|
+
context.addIssue({ code: "custom", path: ["tables", index, "rows"], message: "Each row must match the declared columns." });
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
export const SqlExecutionRequestSchema = z.object({
|
|
36
|
+
schemaVersion: z.literal("openpond.sqlExecution.v1"),
|
|
37
|
+
snapshot: SqlSnapshotSchema,
|
|
38
|
+
sql: z.string().min(1).max(65_536).refine(value => !value.includes("\0")),
|
|
39
|
+
maxRows: z.number().int().min(1).max(4_096),
|
|
40
|
+
maxResultBytes: z.number().int().min(256).max(262_144),
|
|
41
|
+
}).strict();
|
|
42
|
+
export const SqlExecutionResultSchema = z.discriminatedUnion("status", [
|
|
43
|
+
z.object({ status: z.literal("completed"), columns: z.array(z.string()).max(128), rows: z.array(z.array(SqlValueSchema).max(128)).max(4_096) }).strict(),
|
|
44
|
+
z.object({ status: z.literal("rejected"), code: z.enum(["invalid_query", "multiple_statements", "result_too_large", "memory_limit"]) }).strict(),
|
|
45
|
+
]);
|
|
46
|
+
export function assertSqlExecutionRequest(value) {
|
|
47
|
+
assertBoundedTaskJson(value, 1_048_576);
|
|
48
|
+
const parsed = SqlExecutionRequestSchema.parse(value);
|
|
49
|
+
if (new TextEncoder().encode(parsed.sql).byteLength > 65_536)
|
|
50
|
+
throw new Error("sql_source_too_large");
|
|
51
|
+
return parsed;
|
|
52
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import sqlite3InitModule from "@sqlite.org/sqlite-wasm";
|
|
2
|
+
import { sqlWasmBinary } from "./sql-wasm-binary.js";
|
|
3
|
+
import { assertSqlExecutionRequest } from "./sql-execution-contract.js";
|
|
4
|
+
class SqlQueryRejectedError extends Error {
|
|
5
|
+
}
|
|
6
|
+
const functions = new Set([
|
|
7
|
+
"abs", "avg", "char", "coalesce", "concat", "concat_ws", "count", "format", "glob", "group_concat", "hex", "if", "ifnull", "iif", "instr", "length", "like", "likelihood", "likely", "lower", "ltrim", "max", "min", "nullif", "octet_length", "printf", "quote", "replace", "round", "rtrim", "sign", "string_agg", "substr", "substring", "sum", "total", "trim", "typeof", "unhex", "unicode", "unlikely", "upper", "zeroblob",
|
|
8
|
+
"row_number", "rank", "dense_rank", "percent_rank", "cume_dist", "ntile", "lag", "lead", "first_value", "last_value", "nth_value",
|
|
9
|
+
"json", "json_array", "json_array_length", "json_error_position", "json_extract", "json_group_array", "json_group_object", "json_insert", "json_object", "json_patch", "json_quote", "json_remove", "json_replace", "json_set", "json_type", "json_valid", "->", "->>",
|
|
10
|
+
]);
|
|
11
|
+
const quoteIdentifier = (value) => `"${value.replaceAll('"', '""')}"`;
|
|
12
|
+
const decode = (value) => typeof value !== "object" || value === null ? value : "integer" in value ? BigInt(value.integer) : Buffer.from(value.blobBase64, "base64");
|
|
13
|
+
function encode(value) {
|
|
14
|
+
if (typeof value === "bigint")
|
|
15
|
+
return value <= BigInt(Number.MAX_SAFE_INTEGER) && value >= BigInt(Number.MIN_SAFE_INTEGER) ? Number(value) : { integer: value.toString() };
|
|
16
|
+
if (value instanceof Uint8Array)
|
|
17
|
+
return { blobBase64: Buffer.from(value).toString("base64") };
|
|
18
|
+
if (value === null || typeof value === "string" || (typeof value === "number" && Number.isFinite(value)))
|
|
19
|
+
return value;
|
|
20
|
+
throw new SqlQueryRejectedError("sql_result_unsupported_value");
|
|
21
|
+
}
|
|
22
|
+
/** Internal child entry only: synchronous SQLite never runs in the application owner. */
|
|
23
|
+
export async function executeSqlInChild(raw) {
|
|
24
|
+
const input = assertSqlExecutionRequest(raw);
|
|
25
|
+
// The upstream runtime accepts Emscripten options; its declaration omits them.
|
|
26
|
+
const initialize = sqlite3InitModule;
|
|
27
|
+
const sqlite = await initialize({ wasmBinary: sqlWasmBinary, wasmMemory: new WebAssembly.Memory({ initial: 128, maximum: 512 }), print: () => { }, printErr: () => { } });
|
|
28
|
+
const { capi } = sqlite;
|
|
29
|
+
const db = new sqlite.oo1.DB(":memory:", "c");
|
|
30
|
+
try {
|
|
31
|
+
db.exec("PRAGMA temp_store=MEMORY; PRAGMA trusted_schema=OFF;");
|
|
32
|
+
for (const table of input.snapshot.tables) {
|
|
33
|
+
db.exec(`CREATE TABLE ${quoteIdentifier(table.name)} (${table.columns.map(column => `${quoteIdentifier(column.name)} ${column.type}`).join(",")})`);
|
|
34
|
+
const statement = db.prepare(`INSERT INTO ${quoteIdentifier(table.name)} VALUES (${table.columns.map(() => "?").join(",")})`);
|
|
35
|
+
try {
|
|
36
|
+
for (const row of table.rows) {
|
|
37
|
+
statement.bind(row.map(decode));
|
|
38
|
+
statement.step();
|
|
39
|
+
statement.reset(true);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
finally {
|
|
43
|
+
statement.finalize();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
db.exec("PRAGMA query_only=ON");
|
|
47
|
+
for (const [id, limit] of [[capi.SQLITE_LIMIT_LENGTH, 1_048_576], [capi.SQLITE_LIMIT_SQL_LENGTH, 65_536], [capi.SQLITE_LIMIT_COLUMN, 128], [capi.SQLITE_LIMIT_EXPR_DEPTH, 100], [capi.SQLITE_LIMIT_COMPOUND_SELECT, 50], [capi.SQLITE_LIMIT_VDBE_OP, 100_000], [capi.SQLITE_LIMIT_ATTACHED, 0], [capi.SQLITE_LIMIT_WORKER_THREADS, 0]])
|
|
48
|
+
capi.sqlite3_limit(db.pointer, id, limit);
|
|
49
|
+
const tables = new Set(input.snapshot.tables.map(table => table.name.toLowerCase()));
|
|
50
|
+
const rc = capi.sqlite3_set_authorizer(db.pointer, (_, action, first, second, database) => {
|
|
51
|
+
if (action === capi.SQLITE_SELECT || action === capi.SQLITE_RECURSIVE)
|
|
52
|
+
return capi.SQLITE_OK;
|
|
53
|
+
// SQLite's count(*) optimization reports a null database and empty column.
|
|
54
|
+
if (action === capi.SQLITE_READ && (database === "main" || (database === 0 && second === "")) && typeof first === "string" && tables.has(first.toLowerCase()))
|
|
55
|
+
return capi.SQLITE_OK;
|
|
56
|
+
if (action === capi.SQLITE_FUNCTION && typeof second === "string" && functions.has(second.toLowerCase()))
|
|
57
|
+
return capi.SQLITE_OK;
|
|
58
|
+
return capi.SQLITE_DENY;
|
|
59
|
+
}, 0);
|
|
60
|
+
if (rc !== capi.SQLITE_OK)
|
|
61
|
+
throw new Error("sql_authorizer_install_failed");
|
|
62
|
+
try {
|
|
63
|
+
const count = countStatements(sqlite, db.pointer, input.sql);
|
|
64
|
+
if (count > 1)
|
|
65
|
+
return { status: "rejected", code: "multiple_statements" };
|
|
66
|
+
if (count !== 1)
|
|
67
|
+
return { status: "rejected", code: "invalid_query" };
|
|
68
|
+
const statement = db.prepare(input.sql);
|
|
69
|
+
try {
|
|
70
|
+
if (!statement.columnCount || statement.parameterCount)
|
|
71
|
+
return { status: "rejected", code: "invalid_query" };
|
|
72
|
+
const columns = statement.getColumnNames();
|
|
73
|
+
const rows = [];
|
|
74
|
+
let bytes = Buffer.byteLength(JSON.stringify({ status: "completed", columns, rows }));
|
|
75
|
+
if (bytes > input.maxResultBytes)
|
|
76
|
+
return { status: "rejected", code: "result_too_large" };
|
|
77
|
+
while (statement.step()) {
|
|
78
|
+
if (rows.length >= input.maxRows)
|
|
79
|
+
return { status: "rejected", code: "result_too_large" };
|
|
80
|
+
const row = statement.get([]).map(encode);
|
|
81
|
+
bytes += Buffer.byteLength(JSON.stringify(row)) + (rows.length ? 1 : 0);
|
|
82
|
+
if (bytes > input.maxResultBytes)
|
|
83
|
+
return { status: "rejected", code: "result_too_large" };
|
|
84
|
+
rows.push(row);
|
|
85
|
+
}
|
|
86
|
+
return { status: "completed", columns, rows };
|
|
87
|
+
}
|
|
88
|
+
finally {
|
|
89
|
+
statement.finalize();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
const code = error && typeof error === "object" && "resultCode" in error ? error.resultCode : undefined;
|
|
94
|
+
if (typeof code !== "number" && !(error instanceof SqlQueryRejectedError))
|
|
95
|
+
throw error;
|
|
96
|
+
return { status: "rejected", code: code === capi.SQLITE_NOMEM ? "memory_limit" : code === capi.SQLITE_TOOBIG ? "result_too_large" : "invalid_query" };
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
finally {
|
|
100
|
+
db.close();
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function countStatements(sqlite, db, sql) {
|
|
104
|
+
const { wasm, capi } = sqlite;
|
|
105
|
+
const scope = wasm.scopedAllocPush();
|
|
106
|
+
try {
|
|
107
|
+
const [start, bytes] = wasm.scopedAllocCString(sql, true);
|
|
108
|
+
const [statementOut, tailOut] = wasm.scopedAllocPtr(2);
|
|
109
|
+
let cursor = start, count = 0;
|
|
110
|
+
while (cursor < start + bytes) {
|
|
111
|
+
wasm.pokePtr(statementOut, 0);
|
|
112
|
+
wasm.pokePtr(tailOut, 0);
|
|
113
|
+
const rc = capi.sqlite3_prepare_v3(db, cursor, start + bytes - cursor, 0, statementOut, tailOut);
|
|
114
|
+
const statement = wasm.peekPtr(statementOut);
|
|
115
|
+
try {
|
|
116
|
+
if (rc !== capi.SQLITE_OK)
|
|
117
|
+
throw Object.assign(new Error("sql_prepare_failed"), { resultCode: rc });
|
|
118
|
+
if (statement && ++count > 1)
|
|
119
|
+
return count;
|
|
120
|
+
}
|
|
121
|
+
finally {
|
|
122
|
+
if (statement)
|
|
123
|
+
capi.sqlite3_finalize(statement);
|
|
124
|
+
}
|
|
125
|
+
const tail = wasm.peekPtr(tailOut);
|
|
126
|
+
if (tail <= cursor)
|
|
127
|
+
throw new Error("sql_parser_did_not_advance");
|
|
128
|
+
cursor = tail;
|
|
129
|
+
}
|
|
130
|
+
return count;
|
|
131
|
+
}
|
|
132
|
+
finally {
|
|
133
|
+
wasm.scopedAllocPop(scope);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { sqlExecutionProcessSource } from "./sql-execution-process-source.js";
|
|
3
|
+
import { assertBoundedTaskJson } from "./task-schema.js";
|
|
4
|
+
import { assertSqlExecutionRequest, SqlExecutionResultSchema } from "./sql-execution-contract.js";
|
|
5
|
+
export * from "./sql-execution-contract.js";
|
|
6
|
+
/** Every result, cancellation and timeout settles after the owned child exits. */
|
|
7
|
+
export async function executeSqlInProcess(input) {
|
|
8
|
+
input.signal?.throwIfAborted();
|
|
9
|
+
if (!Number.isSafeInteger(input.timeoutMs) || input.timeoutMs < 1 || input.timeoutMs > 30_000)
|
|
10
|
+
throw new Error("sql_timeout_invalid");
|
|
11
|
+
const request = assertSqlExecutionRequest(input.request);
|
|
12
|
+
const { signal } = input;
|
|
13
|
+
const program = `globalThis.__openpondSqlInput = JSON.parse(${JSON.stringify(JSON.stringify(request))});\n${sqlExecutionProcessSource}`;
|
|
14
|
+
return new Promise((resolve, reject) => {
|
|
15
|
+
const child = spawn("node", ["--max-old-space-size=64", "--input-type=module", "-"], { stdio: ["pipe", "pipe", "pipe"], env: { PATH: process.env.PATH }, windowsHide: true });
|
|
16
|
+
let failure, output = "", outputBytes = 0, stderrBytes = 0;
|
|
17
|
+
const stop = (error) => { failure ??= error; child.kill("SIGKILL"); };
|
|
18
|
+
const cancel = () => stop(signal?.reason instanceof Error ? signal.reason : new Error("sql_cancelled"));
|
|
19
|
+
const timer = setTimeout(() => stop(new Error("sql_timeout")), input.timeoutMs);
|
|
20
|
+
signal?.addEventListener("abort", cancel, { once: true });
|
|
21
|
+
child.stdout.setEncoding("utf8");
|
|
22
|
+
child.stdout.on("data", (chunk) => {
|
|
23
|
+
outputBytes += Buffer.byteLength(chunk);
|
|
24
|
+
if (outputBytes > request.maxResultBytes + 65_536)
|
|
25
|
+
stop(new Error("sql_process_response_too_large"));
|
|
26
|
+
else
|
|
27
|
+
output += chunk;
|
|
28
|
+
});
|
|
29
|
+
child.stderr.on("data", (chunk) => { stderrBytes += chunk.byteLength; if (stderrBytes > 65_536)
|
|
30
|
+
stop(new Error("sql_process_stderr_too_large")); });
|
|
31
|
+
child.once("error", error => { failure ??= error; });
|
|
32
|
+
child.stdin.on("error", error => stop(error));
|
|
33
|
+
child.once("close", code => {
|
|
34
|
+
clearTimeout(timer);
|
|
35
|
+
signal?.removeEventListener("abort", cancel);
|
|
36
|
+
if (failure) {
|
|
37
|
+
reject(failure);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
if (code !== 0)
|
|
42
|
+
throw new Error("sql_process_exited_without_result");
|
|
43
|
+
const message = JSON.parse(output);
|
|
44
|
+
if (!message || typeof message !== "object" || !("ok" in message) || message.ok !== true || !("result" in message))
|
|
45
|
+
throw new Error("sql_environment_failed");
|
|
46
|
+
const result = SqlExecutionResultSchema.parse(message.result);
|
|
47
|
+
assertBoundedTaskJson(result, request.maxResultBytes);
|
|
48
|
+
resolve(result);
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
reject(error);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
child.stdin.end(program);
|
|
55
|
+
if (signal?.aborted)
|
|
56
|
+
cancel();
|
|
57
|
+
});
|
|
58
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { executeSqlInChild } from "./sql-execution-engine.js";
|
|
2
|
+
const input = globalThis.__openpondSqlInput;
|
|
3
|
+
void executeSqlInChild(input).then(result => process.stdout.write(JSON.stringify({ ok: true, result })), () => process.stdout.write(JSON.stringify({ ok: false, error: "sql_environment_failed" })));
|