@alisio/core 0.5.2 → 0.6.0
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/analysis/janitor.d.ts +48 -0
- package/dist/analysis/janitor.js +170 -18
- package/dist/analysis/jobs.d.ts +11 -2
- package/dist/analysis/jobs.js +13 -0
- package/dist/analysis/oci.d.ts +11 -2
- package/dist/analysis/oci.js +20 -4
- package/dist/analysis/python/sources.js +2 -1
- package/dist/analysis/rerun.d.ts +1 -1
- package/dist/analysis/status.d.ts +3 -1
- package/dist/analysis/status.js +3 -0
- package/dist/application.d.ts +28 -2
- package/dist/application.js +189 -4
- package/dist/changelog/data.js +26 -0
- package/dist/config.d.ts +110 -2
- package/dist/config.js +188 -6
- package/dist/core/call-internals.d.ts +22 -0
- package/dist/core/call-internals.js +9 -0
- package/dist/core/contracts.d.ts +2 -2
- package/dist/core/runner.js +111 -38
- package/dist/forge/broker.d.ts +51 -0
- package/dist/forge/broker.js +244 -0
- package/dist/forge/budget.d.ts +27 -0
- package/dist/forge/budget.js +60 -0
- package/dist/forge/catalog-view.d.ts +24 -0
- package/dist/forge/catalog-view.js +43 -0
- package/dist/forge/errors.d.ts +26 -0
- package/dist/forge/errors.js +49 -0
- package/dist/forge/guides/javascript.d.ts +25 -0
- package/dist/forge/guides/javascript.js +90 -0
- package/dist/forge/guides/provider.d.ts +42 -0
- package/dist/forge/guides/provider.js +96 -0
- package/dist/forge/guides/python-environment.d.ts +15 -0
- package/dist/forge/guides/python-environment.js +81 -0
- package/dist/forge/guides/python.d.ts +38 -0
- package/dist/forge/guides/python.js +170 -0
- package/dist/forge/javascript/engine.d.ts +44 -0
- package/dist/forge/javascript/engine.js +229 -0
- package/dist/forge/javascript/hosts/factory.d.ts +25 -0
- package/dist/forge/javascript/hosts/factory.js +21 -0
- package/dist/forge/javascript/hosts/framing.d.ts +17 -0
- package/dist/forge/javascript/hosts/framing.js +49 -0
- package/dist/forge/javascript/hosts/isolate-main.d.ts +9 -0
- package/dist/forge/javascript/hosts/isolate-main.js +63 -0
- package/dist/forge/javascript/hosts/process-entry.d.ts +1 -0
- package/dist/forge/javascript/hosts/process-entry.js +3 -0
- package/dist/forge/javascript/hosts/process-host.d.ts +33 -0
- package/dist/forge/javascript/hosts/process-host.js +160 -0
- package/dist/forge/javascript/hosts/worker-entry.d.ts +1 -0
- package/dist/forge/javascript/hosts/worker-entry.js +3 -0
- package/dist/forge/javascript/hosts/worker-host.d.ts +18 -0
- package/dist/forge/javascript/hosts/worker-host.js +101 -0
- package/dist/forge/javascript/prelude.d.ts +2 -0
- package/dist/forge/javascript/prelude.js +333 -0
- package/dist/forge/javascript/probe.d.ts +2 -0
- package/dist/forge/javascript/probe.js +53 -0
- package/dist/forge/javascript/runtime-manifest.d.ts +14 -0
- package/dist/forge/javascript/runtime-manifest.js +14 -0
- package/dist/forge/javascript/vm-runner.d.ts +21 -0
- package/dist/forge/javascript/vm-runner.js +301 -0
- package/dist/forge/javascript/wasm-asset.d.ts +8 -0
- package/dist/forge/javascript/wasm-asset.js +8 -0
- package/dist/forge/javascript/wasm.d.ts +15 -0
- package/dist/forge/javascript/wasm.js +54 -0
- package/dist/forge/journal.d.ts +105 -0
- package/dist/forge/journal.js +174 -0
- package/dist/forge/kernel.d.ts +76 -0
- package/dist/forge/kernel.js +338 -0
- package/dist/forge/limits.d.ts +38 -0
- package/dist/forge/limits.js +73 -0
- package/dist/forge/nested.d.ts +96 -0
- package/dist/forge/nested.js +214 -0
- package/dist/forge/ports.d.ts +120 -0
- package/dist/forge/ports.js +1 -0
- package/dist/forge/progress.d.ts +24 -0
- package/dist/forge/progress.js +76 -0
- package/dist/forge/protocol.d.ts +110 -0
- package/dist/forge/protocol.js +171 -0
- package/dist/forge/python/backend-selector.d.ts +38 -0
- package/dist/forge/python/backend-selector.js +111 -0
- package/dist/forge/python/backend.d.ts +89 -0
- package/dist/forge/python/backend.js +1 -0
- package/dist/forge/python/engine.d.ts +84 -0
- package/dist/forge/python/engine.js +279 -0
- package/dist/forge/python/frame-parser.d.ts +24 -0
- package/dist/forge/python/frame-parser.js +107 -0
- package/dist/forge/python/frame-transport.d.ts +40 -0
- package/dist/forge/python/frame-transport.js +101 -0
- package/dist/forge/python/import-scan.d.ts +22 -0
- package/dist/forge/python/import-scan.js +133 -0
- package/dist/forge/python/managed-backend.d.ts +30 -0
- package/dist/forge/python/managed-backend.js +111 -0
- package/dist/forge/python/oci-backend.d.ts +27 -0
- package/dist/forge/python/oci-backend.js +84 -0
- package/dist/forge/python/pyodide-backend.d.ts +40 -0
- package/dist/forge/python/pyodide-backend.js +289 -0
- package/dist/forge/python/pyodide-install.d.ts +41 -0
- package/dist/forge/python/pyodide-install.js +167 -0
- package/dist/forge/python/pyodide-manifest.d.ts +42 -0
- package/dist/forge/python/pyodide-manifest.js +376 -0
- package/dist/forge/python/pyodide-runner-source.d.ts +2 -0
- package/dist/forge/python/pyodide-runner-source.js +3 -0
- package/dist/forge/python/rss.d.ts +2 -0
- package/dist/forge/python/rss.js +33 -0
- package/dist/forge/python/scratch-copy.d.ts +6 -0
- package/dist/forge/python/scratch-copy.js +61 -0
- package/dist/forge/python/settle.d.ts +6 -0
- package/dist/forge/python/settle.js +21 -0
- package/dist/forge/python/status.d.ts +8 -0
- package/dist/forge/python/status.js +15 -0
- package/dist/forge/python/wasm-node.d.ts +59 -0
- package/dist/forge/python/wasm-node.js +220 -0
- package/dist/forge/python/wiring.d.ts +39 -0
- package/dist/forge/python/wiring.js +47 -0
- package/dist/forge/registry.d.ts +26 -0
- package/dist/forge/registry.js +83 -0
- package/dist/forge/result.d.ts +58 -0
- package/dist/forge/result.js +86 -0
- package/dist/forge/routing-rule.d.ts +19 -0
- package/dist/forge/routing-rule.js +31 -0
- package/dist/forge/runtime.d.ts +64 -0
- package/dist/forge/runtime.js +88 -0
- package/dist/forge/scratch-files.d.ts +36 -0
- package/dist/forge/scratch-files.js +94 -0
- package/dist/forge/scratch-provider.d.ts +41 -0
- package/dist/forge/scratch-provider.js +79 -0
- package/dist/forge/scratch-quota.d.ts +8 -0
- package/dist/forge/scratch-quota.js +25 -0
- package/dist/forge/selector.d.ts +35 -0
- package/dist/forge/selector.js +51 -0
- package/dist/forge/session-dir.d.ts +59 -0
- package/dist/forge/session-dir.js +239 -0
- package/dist/forge/session-path.d.ts +31 -0
- package/dist/forge/session-path.js +61 -0
- package/dist/forge/slots.d.ts +16 -0
- package/dist/forge/slots.js +47 -0
- package/dist/forge/state-delta.d.ts +54 -0
- package/dist/forge/state-delta.js +99 -0
- package/dist/forge/state-machine.d.ts +20 -0
- package/dist/forge/state-machine.js +40 -0
- package/dist/forge/state-store.d.ts +25 -0
- package/dist/forge/state-store.js +50 -0
- package/dist/forge/task-log.d.ts +9 -0
- package/dist/forge/task-log.js +42 -0
- package/dist/forge/verdict.d.ts +20 -0
- package/dist/forge/verdict.js +28 -0
- package/dist/forge-isolate.d.ts +6 -0
- package/dist/forge-isolate.js +6 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +7 -0
- package/dist/permissions/decide.d.ts +42 -0
- package/dist/permissions/decide.js +24 -0
- package/dist/resources/context.d.ts +2 -1
- package/dist/resources/context.js +2 -1
- package/dist/runtime/access.d.ts +34 -0
- package/dist/runtime/access.js +80 -0
- package/dist/runtime/process.d.ts +2 -1
- package/dist/runtime/process.js +5 -5
- package/dist/runtime/store.d.ts +11 -0
- package/dist/runtime/store.js +103 -0
- package/dist/tools/analysis.d.ts +25 -2
- package/dist/tools/analysis.js +315 -267
- package/dist/tools/execute.js +6 -7
- package/dist/tools/forge-guide.d.ts +6 -0
- package/dist/tools/forge-guide.js +60 -0
- package/dist/tools/js-run.d.ts +19 -0
- package/dist/tools/js-run.js +126 -0
- package/dist/tools/standard.d.ts +7 -1
- package/dist/tools/standard.js +44 -43
- package/package.json +7 -2
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `WorkerIsolateHost` (spec §6.3, D-20, D-21): the VM runs in a `node:worker_threads` worker.
|
|
3
|
+
*
|
|
4
|
+
* Stopping a worker never depends on the guest listening: the parent sets the shared flag the
|
|
5
|
+
* guest's interrupt handler reads (value 1 = cancelled, 2 = deadline); if the worker has not left
|
|
6
|
+
* 50 ms later it is terminated, and the stop is only "confirmed" when the `exit` event is seen.
|
|
7
|
+
* `terminate()` stops a WASM loop on Node (~1 ms, measured); on Bun it does not, which is why Bun
|
|
8
|
+
* uses the process host.
|
|
9
|
+
*/
|
|
10
|
+
import { fileURLToPath } from "node:url";
|
|
11
|
+
import { Worker } from "node:worker_threads";
|
|
12
|
+
export const WORKER_TERMINATE_AFTER_MS = 50;
|
|
13
|
+
/** How long to wait for `exit` after `terminate()` before reporting the stop as unconfirmed. */
|
|
14
|
+
const EXIT_CONFIRM_MS = 2_000;
|
|
15
|
+
/** The worker entry next to this file: `.ts` when running from sources, `.js` from `dist`. */
|
|
16
|
+
export function workerEntryUrl(base = import.meta.url) {
|
|
17
|
+
const extension = base.endsWith(".ts") ? "ts" : "js";
|
|
18
|
+
return new URL(`./worker-entry.${extension}`, base);
|
|
19
|
+
}
|
|
20
|
+
export class WorkerIsolateHost {
|
|
21
|
+
kind = "worker";
|
|
22
|
+
#options;
|
|
23
|
+
constructor(options) {
|
|
24
|
+
this.#options = options;
|
|
25
|
+
}
|
|
26
|
+
start() {
|
|
27
|
+
const flagBuffer = new SharedArrayBuffer(4);
|
|
28
|
+
const flag = new Int32Array(flagBuffer);
|
|
29
|
+
const data = {
|
|
30
|
+
forgeIsolate: 1,
|
|
31
|
+
module: this.#options.module,
|
|
32
|
+
flag: flagBuffer,
|
|
33
|
+
};
|
|
34
|
+
const worker = new Worker(this.#options.entry ?? workerEntryUrl(), {
|
|
35
|
+
workerData: data,
|
|
36
|
+
env: {},
|
|
37
|
+
stdout: true,
|
|
38
|
+
stderr: true,
|
|
39
|
+
});
|
|
40
|
+
// Nothing the guest prints reaches the parent's streams.
|
|
41
|
+
worker.stdout.resume();
|
|
42
|
+
worker.stderr.resume();
|
|
43
|
+
let messageHandler = () => { };
|
|
44
|
+
let exitState;
|
|
45
|
+
let workerError;
|
|
46
|
+
const exited = new Promise((resolve) => {
|
|
47
|
+
worker.once("exit", (code) => {
|
|
48
|
+
exitState = { code, signal: null, ...(workerError ? { error: workerError } : {}) };
|
|
49
|
+
resolve(exitState);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
worker.on("message", (raw) => messageHandler(raw));
|
|
53
|
+
worker.on("error", (error) => {
|
|
54
|
+
workerError = error.message;
|
|
55
|
+
});
|
|
56
|
+
const terminateAfter = this.#options.terminateAfterMs ?? WORKER_TERMINATE_AFTER_MS;
|
|
57
|
+
return {
|
|
58
|
+
kind: "worker",
|
|
59
|
+
onMessage(handler) {
|
|
60
|
+
messageHandler = handler;
|
|
61
|
+
},
|
|
62
|
+
onStreamError() {
|
|
63
|
+
// Structured clone cannot deliver a malformed frame.
|
|
64
|
+
},
|
|
65
|
+
send(message) {
|
|
66
|
+
if (!exitState)
|
|
67
|
+
worker.postMessage(message);
|
|
68
|
+
},
|
|
69
|
+
exited,
|
|
70
|
+
async stop(reason) {
|
|
71
|
+
const started = performance.now();
|
|
72
|
+
if (exitState)
|
|
73
|
+
return { confirmed: true, method: "interrupt", elapsedMs: 0 };
|
|
74
|
+
Atomics.store(flag, 0, reason === "timeout" ? 2 : 1);
|
|
75
|
+
Atomics.notify(flag, 0);
|
|
76
|
+
let method = "interrupt";
|
|
77
|
+
const timer = setTimeout(() => {
|
|
78
|
+
method = "terminate";
|
|
79
|
+
void worker.terminate();
|
|
80
|
+
}, terminateAfter);
|
|
81
|
+
let confirmTimer;
|
|
82
|
+
const confirmed = await Promise.race([
|
|
83
|
+
exited.then(() => true),
|
|
84
|
+
new Promise((resolve) => {
|
|
85
|
+
confirmTimer = setTimeout(() => resolve(false), terminateAfter + EXIT_CONFIRM_MS);
|
|
86
|
+
}),
|
|
87
|
+
]);
|
|
88
|
+
clearTimeout(timer);
|
|
89
|
+
clearTimeout(confirmTimer);
|
|
90
|
+
return {
|
|
91
|
+
confirmed,
|
|
92
|
+
method,
|
|
93
|
+
elapsedMs: Math.round(performance.now() - started),
|
|
94
|
+
...(confirmed ? {} : { settled: exited.then(() => undefined) }),
|
|
95
|
+
};
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/** Path of the worker entry, for diagnostics. */
|
|
101
|
+
export const workerEntryPath = () => fileURLToPath(workerEntryUrl());
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The prelude: the JavaScript the guest VM evaluates before the model's code (spec §6.2, D-26). It
|
|
3
|
+
* installs the API globals (`tools`, `catalog`, `state`, `text`, `progress`, `parallel`, `console`,
|
|
4
|
+
* `callTool`), removes what a script must not reach and hands the isolate runner two functions.
|
|
5
|
+
*
|
|
6
|
+
* What it does and does not guarantee:
|
|
7
|
+
* - It captures the intrinsics it uses (the Array, String and Promise methods too, called through
|
|
8
|
+
* `uncurry`, so a script that rewrites a prototype cannot change what the API does), takes the
|
|
9
|
+
* bridge callback into a closure and deletes it from `globalThis`, freezes its API objects and
|
|
10
|
+
* removes `SharedArrayBuffer` and `InternalError`.
|
|
11
|
+
* - The script runs in the same VM, so it CAN tamper with the prelude's own state. None of the
|
|
12
|
+
* security invariants rely on it: credits, concurrency, sizes and the protocol are enforced by
|
|
13
|
+
* the host. What the prelude adds is ergonomics (local, early errors; queueing instead of
|
|
14
|
+
* failing) and the terminal `fatal` for a spent call budget.
|
|
15
|
+
* - It talks to the host only through `bridge(kind, jsonString)`; all values cross as JSON text.
|
|
16
|
+
*
|
|
17
|
+
* Erasable TypeScript only (D-33): a string constant, no logic here.
|
|
18
|
+
*/
|
|
19
|
+
import { PRELUDE_VERSION } from "./runtime-manifest.js";
|
|
20
|
+
import { sha256Hex } from "./wasm.js";
|
|
21
|
+
export const PRELUDE_SOURCE = String.raw `(function (bridge) {
|
|
22
|
+
"use strict";
|
|
23
|
+
var G = globalThis;
|
|
24
|
+
var JSONparse = JSON.parse, JSONstringify = JSON.stringify;
|
|
25
|
+
var ObjectFreeze = Object.freeze, ObjectDefineProperty = Object.defineProperty;
|
|
26
|
+
var ObjectCreate = Object.create, ObjectKeys = Object.keys;
|
|
27
|
+
var ArrayIsArray = Array.isArray;
|
|
28
|
+
var PromiseCtor = Promise;
|
|
29
|
+
var ErrorCtor = Error, TypeErrorCtor = TypeError;
|
|
30
|
+
var StringCtor = String, MathMin = Math.min, MathMax = Math.max;
|
|
31
|
+
var NumberIsInteger = Number.isInteger;
|
|
32
|
+
var ArrayCtor = Array;
|
|
33
|
+
var bindFn = Function.prototype.bind, callFn = Function.prototype.call;
|
|
34
|
+
// uncurry(String.prototype.slice)(text, 0, 5): the captured method, whatever the script does later.
|
|
35
|
+
function uncurry(fn) { return bindFn.call(callFn, fn); }
|
|
36
|
+
var ArrayShift = uncurry(Array.prototype.shift), ArrayJoin = uncurry(Array.prototype.join);
|
|
37
|
+
var StringSlice = uncurry(String.prototype.slice);
|
|
38
|
+
var StringCharCodeAt = uncurry(String.prototype.charCodeAt);
|
|
39
|
+
var StringIndexOf = uncurry(String.prototype.indexOf);
|
|
40
|
+
var PromiseThen = uncurry(Promise.prototype.then);
|
|
41
|
+
var PromiseResolve = bindFn.call(Promise.resolve, Promise);
|
|
42
|
+
var PromiseReject = bindFn.call(Promise.reject, Promise);
|
|
43
|
+
// The engine's own error type (a QuickJS limit). The constructor leaves the global scope so a
|
|
44
|
+
// script cannot build one; only the prototype is kept, to recognise errors the engine raised.
|
|
45
|
+
var ObjectGetPrototypeOf = Object.getPrototypeOf;
|
|
46
|
+
var InternalErrorProto = typeof InternalError === "function" ? InternalError.prototype : undefined;
|
|
47
|
+
var RangeErrorProto = RangeError.prototype;
|
|
48
|
+
// The codes a nested call (or the prelude on its behalf) can legitimately end a script with.
|
|
49
|
+
// Every other code a script writes on an error it throws is just text: SCRIPT_RUNTIME.
|
|
50
|
+
var PASS_CODES = ObjectCreate(null);
|
|
51
|
+
var passList = ["TOOL_UNAVAILABLE", "TOOL_INPUT_INVALID", "TOOL_FAILED", "PERMISSION_DENIED",
|
|
52
|
+
"APPROVAL_REQUIRED", "BUDGET_EXCEEDED", "OUTPUT_LIMIT", "STATE_CONFLICT"];
|
|
53
|
+
for (var p = 0; p < passList.length; p++) PASS_CODES[passList[p]] = true;
|
|
54
|
+
|
|
55
|
+
var limits = null;
|
|
56
|
+
var credits = 0, started = 0, nextCallId = 0;
|
|
57
|
+
var waiters = [];
|
|
58
|
+
var state = ObjectCreate(null);
|
|
59
|
+
var consoleBytes = 0, consoleCut = false;
|
|
60
|
+
var CONSOLE_QUOTA = 65536, CHUNK = 60000, MAX_RETURN = 200000;
|
|
61
|
+
|
|
62
|
+
function fail(code, message) {
|
|
63
|
+
var e = new ErrorCtor(message);
|
|
64
|
+
e.code = code;
|
|
65
|
+
return e;
|
|
66
|
+
}
|
|
67
|
+
// A terminal condition: tell the host and stop making progress. The host ends the execution.
|
|
68
|
+
function fatal(code, message) {
|
|
69
|
+
bridge("fatal", JSONstringify({ code: code, message: message }));
|
|
70
|
+
return new PromiseCtor(function () {});
|
|
71
|
+
}
|
|
72
|
+
function decodeError(text) {
|
|
73
|
+
var info;
|
|
74
|
+
try { info = JSONparse(text); } catch (e) { info = { code: "TOOL_FAILED", message: StringCtor(text) }; }
|
|
75
|
+
return fail(typeof info.code === "string" ? info.code : "TOOL_FAILED", StringCtor(info.message));
|
|
76
|
+
}
|
|
77
|
+
// UTF-8 size of a JSON text (JSON.stringify leaves no lone surrogate): the host measures bytes.
|
|
78
|
+
function utf8Length(text) {
|
|
79
|
+
var n = 0;
|
|
80
|
+
for (var i = 0; i < text.length; i++) {
|
|
81
|
+
var c = StringCharCodeAt(text, i);
|
|
82
|
+
if (c < 0x80) n += 1;
|
|
83
|
+
else if (c < 0x800) n += 2;
|
|
84
|
+
else if (c >= 0xd800 && c <= 0xdbff && i + 1 < text.length) {
|
|
85
|
+
var d = StringCharCodeAt(text, i + 1);
|
|
86
|
+
if (d >= 0xdc00 && d <= 0xdfff) { n += 4; i++; } else n += 3;
|
|
87
|
+
} else n += 3;
|
|
88
|
+
}
|
|
89
|
+
return n;
|
|
90
|
+
}
|
|
91
|
+
function encode(value, what) {
|
|
92
|
+
var json = JSONstringify(value);
|
|
93
|
+
if (json === undefined) return "null";
|
|
94
|
+
var max = limits.maxCallInputBytes;
|
|
95
|
+
if (json.length > max || (json.length * 3 > max && utf8Length(json) > max))
|
|
96
|
+
throw fail("TOOL_INPUT_INVALID", what + " is larger than " + max + " bytes");
|
|
97
|
+
return json;
|
|
98
|
+
}
|
|
99
|
+
// The registry's naming rule: [a-zA-Z0-9_-]{1,64}.
|
|
100
|
+
function validToolName(name) {
|
|
101
|
+
if (typeof name !== "string" || name.length < 1 || name.length > 64) return false;
|
|
102
|
+
for (var i = 0; i < name.length; i++) {
|
|
103
|
+
var c = StringCharCodeAt(name, i);
|
|
104
|
+
if (!((c >= 48 && c <= 57) || (c >= 65 && c <= 90) || (c >= 97 && c <= 122) || c === 95 || c === 45))
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function acquire() {
|
|
111
|
+
if (started >= limits.maxToolCalls)
|
|
112
|
+
return fatal("BUDGET_EXCEEDED", "Tool call limit reached (" + limits.maxToolCalls + ")");
|
|
113
|
+
if (credits > 0 && waiters.length === 0) {
|
|
114
|
+
credits--;
|
|
115
|
+
started++;
|
|
116
|
+
return PromiseResolve();
|
|
117
|
+
}
|
|
118
|
+
return new PromiseCtor(function (resolve) { waiters[waiters.length] = resolve; });
|
|
119
|
+
}
|
|
120
|
+
function grant(calls) {
|
|
121
|
+
credits += calls;
|
|
122
|
+
while (credits > 0 && waiters.length > 0) {
|
|
123
|
+
if (started >= limits.maxToolCalls) {
|
|
124
|
+
fatal("BUDGET_EXCEEDED", "Tool call limit reached (" + limits.maxToolCalls + ")");
|
|
125
|
+
waiters.length = 0;
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
credits--;
|
|
129
|
+
started++;
|
|
130
|
+
var resume = ArrayShift(waiters);
|
|
131
|
+
resume();
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function toolCall(name, input) {
|
|
136
|
+
if (!validToolName(name))
|
|
137
|
+
return PromiseReject(fail("TOOL_UNAVAILABLE", "tool name must be 1 to 64 characters of letters, digits, _ or -"));
|
|
138
|
+
var json;
|
|
139
|
+
try { json = encode(input === undefined ? {} : input, "tool input"); }
|
|
140
|
+
catch (e) { return PromiseReject(e.code ? e : fail("TOOL_INPUT_INVALID", "tool input must be JSON")); }
|
|
141
|
+
// Built by hand: returning a promise from a then-callback would look then up on it, and the
|
|
142
|
+
// script can replace Promise.prototype.then.
|
|
143
|
+
return new PromiseCtor(function (resolve, reject) {
|
|
144
|
+
PromiseThen(acquire(), function () {
|
|
145
|
+
try {
|
|
146
|
+
var id = ++nextCallId;
|
|
147
|
+
PromiseThen(bridge("call", JSONstringify({ callId: id, tool: name, input: json })), function (text) {
|
|
148
|
+
try { resolve(JSONparse(text)); } catch (e) { reject(e); }
|
|
149
|
+
}, function (error) {
|
|
150
|
+
reject(typeof error === "string" ? decodeError(error) : error);
|
|
151
|
+
});
|
|
152
|
+
} catch (e) { reject(e); }
|
|
153
|
+
}, reject);
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function catalogRequest(op, query, limit) {
|
|
158
|
+
return PromiseThen(bridge("catalog", JSONstringify({ op: op, query: query, limit: limit })),
|
|
159
|
+
function (text) { return JSONparse(text); },
|
|
160
|
+
function (error) { throw typeof error === "string" ? decodeError(error) : error; }
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function pool(items, fn, options, settle) {
|
|
165
|
+
if (!ArrayIsArray(items)) return PromiseReject(new TypeErrorCtor("items must be an array"));
|
|
166
|
+
if (typeof fn !== "function") return PromiseReject(new TypeErrorCtor("fn must be a function"));
|
|
167
|
+
var wanted = options && NumberIsInteger(options.concurrency) ? options.concurrency : limits.maxConcurrency;
|
|
168
|
+
var width = MathMax(1, MathMin(wanted, limits.maxConcurrency, items.length));
|
|
169
|
+
var results = new ArrayCtor(items.length);
|
|
170
|
+
var next = 0, failed = false, firstError;
|
|
171
|
+
function lane() {
|
|
172
|
+
return (async function () {
|
|
173
|
+
while (!failed) {
|
|
174
|
+
var i = next++;
|
|
175
|
+
if (i >= items.length) return;
|
|
176
|
+
try {
|
|
177
|
+
var value = await fn(items[i], i);
|
|
178
|
+
results[i] = settle ? { ok: true, value: value } : value;
|
|
179
|
+
} catch (e) {
|
|
180
|
+
if (settle) {
|
|
181
|
+
results[i] = { ok: false, error: { code: typeof e === "object" && e && typeof e.code === "string" ? e.code : "SCRIPT_RUNTIME", message: StringCtor(e && e.message !== undefined ? e.message : e) } };
|
|
182
|
+
} else if (!failed) {
|
|
183
|
+
failed = true;
|
|
184
|
+
firstError = e;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
})();
|
|
189
|
+
}
|
|
190
|
+
var lanes = [];
|
|
191
|
+
for (var n = 0; n < width; n++) lanes[lanes.length] = lane();
|
|
192
|
+
// Joined by hand: Promise.all would look up Promise.resolve, which the script can replace.
|
|
193
|
+
return new PromiseCtor(function (resolve, reject) {
|
|
194
|
+
var left = lanes.length;
|
|
195
|
+
function one() {
|
|
196
|
+
if (--left > 0) return;
|
|
197
|
+
if (failed) reject(firstError); else resolve(results);
|
|
198
|
+
}
|
|
199
|
+
for (var k = 0; k < lanes.length; k++) PromiseThen(lanes[k], one, one);
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function emitText(value) {
|
|
204
|
+
var text = typeof value === "string" ? value : JSONstringify(value);
|
|
205
|
+
if (text === undefined) text = StringCtor(value);
|
|
206
|
+
for (var at = 0; at < text.length || at === 0; at += CHUNK) {
|
|
207
|
+
bridge("output", JSONstringify({ type: "text", text: StringSlice(text, at, at + CHUNK) }));
|
|
208
|
+
if (text.length === 0) break;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
function consoleWrite(args) {
|
|
212
|
+
if (consoleCut) return;
|
|
213
|
+
var parts = [];
|
|
214
|
+
for (var i = 0; i < args.length; i++) {
|
|
215
|
+
var a = args[i];
|
|
216
|
+
if (typeof a === "string") parts[parts.length] = a;
|
|
217
|
+
else {
|
|
218
|
+
var j;
|
|
219
|
+
try { j = JSONstringify(a); } catch (e) { j = undefined; }
|
|
220
|
+
parts[parts.length] = j === undefined ? StringCtor(a) : j;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
var line = ArrayJoin(parts, " ") + "\n";
|
|
224
|
+
if (consoleBytes + line.length > CONSOLE_QUOTA) {
|
|
225
|
+
consoleCut = true;
|
|
226
|
+
emitText("[console output truncated]\n");
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
consoleBytes += line.length;
|
|
230
|
+
emitText(line);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function define(name, value) {
|
|
234
|
+
ObjectDefineProperty(G, name, { value: value, writable: false, configurable: false, enumerable: false });
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
var tools = ObjectFreeze({ call: toolCall });
|
|
238
|
+
var catalog = ObjectFreeze({
|
|
239
|
+
search: function (query, options) {
|
|
240
|
+
var limit = options && NumberIsInteger(options.limit) ? options.limit : 10;
|
|
241
|
+
return catalogRequest("search", StringCtor(query === undefined ? "" : query), limit);
|
|
242
|
+
},
|
|
243
|
+
describe: function (name) {
|
|
244
|
+
return PromiseThen(catalogRequest("describe", StringCtor(name), 1), function (entries) {
|
|
245
|
+
if (entries.length === 0) throw fail("TOOL_UNAVAILABLE", 'Tool "' + StringCtor(name) + '" is not available');
|
|
246
|
+
return entries[0];
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
var stateApi = ObjectFreeze({
|
|
251
|
+
get: function (key) { return state[key]; },
|
|
252
|
+
set: function (key, value) {
|
|
253
|
+
if (typeof key !== "string" || key === "" || key.length > 128)
|
|
254
|
+
throw fail("INVALID_INPUT", "state key must be 1 to 128 characters");
|
|
255
|
+
var json = JSONstringify(value);
|
|
256
|
+
if (json === undefined) throw fail("INVALID_INPUT", "state value must be JSON");
|
|
257
|
+
if (json.length > 65536) throw fail("INVALID_INPUT", "state value is larger than 65536 bytes");
|
|
258
|
+
state[key] = JSONparse(json);
|
|
259
|
+
bridge("state.set", JSONstringify({ key: key, value: json }));
|
|
260
|
+
},
|
|
261
|
+
delete: function (key) {
|
|
262
|
+
if (typeof key !== "string" || key === "" || key.length > 128)
|
|
263
|
+
throw fail("INVALID_INPUT", "state key must be 1 to 128 characters");
|
|
264
|
+
delete state[key];
|
|
265
|
+
bridge("state.delete", JSONstringify({ key: key }));
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
var parallel = ObjectFreeze({
|
|
269
|
+
map: function (items, fn, options) { return pool(items, fn, options, false); },
|
|
270
|
+
settle: function (items, fn, options) { return pool(items, fn, options, true); }
|
|
271
|
+
});
|
|
272
|
+
var consoleApi = ObjectFreeze({
|
|
273
|
+
log: function () { consoleWrite(arguments); },
|
|
274
|
+
warn: function () { consoleWrite(arguments); },
|
|
275
|
+
error: function () { consoleWrite(arguments); }
|
|
276
|
+
});
|
|
277
|
+
function progress(event) {
|
|
278
|
+
if (!event || typeof event.message !== "string") throw new TypeErrorCtor("progress needs a message");
|
|
279
|
+
bridge("progress", JSONstringify({ message: StringSlice(event.message, 0, 500), completed: event.completed, total: event.total }));
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function init(rawLimits, snapshot) {
|
|
283
|
+
limits = JSONparse(rawLimits);
|
|
284
|
+
var values = JSONparse(snapshot);
|
|
285
|
+
var keys = ObjectKeys(values);
|
|
286
|
+
for (var i = 0; i < keys.length; i++) state[keys[i]] = values[keys[i]];
|
|
287
|
+
define("tools", tools);
|
|
288
|
+
define("catalog", catalog);
|
|
289
|
+
define("state", stateApi);
|
|
290
|
+
define("text", emitText);
|
|
291
|
+
define("progress", progress);
|
|
292
|
+
define("parallel", parallel);
|
|
293
|
+
define("console", consoleApi);
|
|
294
|
+
define("callTool", toolCall);
|
|
295
|
+
delete G.SharedArrayBuffer;
|
|
296
|
+
delete G.InternalError;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
async function run(fn) {
|
|
300
|
+
try {
|
|
301
|
+
var value = await fn();
|
|
302
|
+
if (value === undefined) return '{"ok":true}';
|
|
303
|
+
var json = JSONstringify(value);
|
|
304
|
+
if (json === undefined) return '{"ok":true}';
|
|
305
|
+
if (json.length > MAX_RETURN)
|
|
306
|
+
throw fail("OUTPUT_LIMIT", "the returned value is larger than " + MAX_RETURN + " characters; return less (filter, aggregate or paginate)");
|
|
307
|
+
return '{"ok":true,"value":' + json + "}";
|
|
308
|
+
} catch (e) {
|
|
309
|
+
var info = { ok: false, code: "SCRIPT_RUNTIME", name: "Error", message: "" };
|
|
310
|
+
try {
|
|
311
|
+
if (e !== null && typeof e === "object") {
|
|
312
|
+
if (typeof e.code === "string" && PASS_CODES[e.code] === true) info.code = e.code;
|
|
313
|
+
if (typeof e.name === "string") info.name = e.name;
|
|
314
|
+
info.message = StringCtor(e.message);
|
|
315
|
+
// A limit counts only when the ENGINE raised it (its own error type), never by message text.
|
|
316
|
+
// Memory: QuickJS raises an InternalError a script cannot construct. Stack: QuickJS raises a
|
|
317
|
+
// RangeError, which a script can imitate; the worst that buys is that label on a failed run.
|
|
318
|
+
var proto = ObjectGetPrototypeOf(e);
|
|
319
|
+
if (InternalErrorProto !== undefined && proto === InternalErrorProto &&
|
|
320
|
+
StringIndexOf(info.message, "out of memory") >= 0) info.limit = "MEMORY_LIMIT";
|
|
321
|
+
else if (proto === RangeErrorProto && StringIndexOf(info.message, "call stack size") >= 0)
|
|
322
|
+
info.limit = "STACK_LIMIT";
|
|
323
|
+
} else info.message = StringCtor(e);
|
|
324
|
+
} catch (inner) { info.message = "unprintable error"; }
|
|
325
|
+
return JSONstringify(info);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function waiting() { return waiters.length; }
|
|
330
|
+
|
|
331
|
+
return { init: init, run: run, grant: grant, waiting: waiting };
|
|
332
|
+
})`;
|
|
333
|
+
export const PRELUDE_HASH = sha256Hex(`${PRELUDE_VERSION}:${PRELUDE_SOURCE}`);
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Full self-test of an isolate host (`alisio forge status`, spec D17): starts a real isolate, runs
|
|
3
|
+
* `return 1` through the whole protocol and checks the answer. Rejects with the reason otherwise.
|
|
4
|
+
*/
|
|
5
|
+
import { DEFAULT_FORGE_LIMITS } from "../limits.js";
|
|
6
|
+
import { PRELUDE_HASH } from "./prelude.js";
|
|
7
|
+
import { FORGE_PROTOCOL_VERSION } from "./runtime-manifest.js";
|
|
8
|
+
export async function probeIsolate(host, _module, timeoutMs = 10_000) {
|
|
9
|
+
const session = host.start();
|
|
10
|
+
try {
|
|
11
|
+
await new Promise((resolve, reject) => {
|
|
12
|
+
const timer = setTimeout(() => reject(new Error("the isolate did not answer in time")), timeoutMs);
|
|
13
|
+
const done = (error) => {
|
|
14
|
+
clearTimeout(timer);
|
|
15
|
+
if (error)
|
|
16
|
+
reject(error);
|
|
17
|
+
else
|
|
18
|
+
resolve();
|
|
19
|
+
};
|
|
20
|
+
session.onStreamError((reason) => done(new Error(reason)));
|
|
21
|
+
void session.exited.then((exit) => done(new Error(`the isolate exited early (${exit.error ?? `code ${String(exit.code)}`})`)));
|
|
22
|
+
session.onMessage((raw) => {
|
|
23
|
+
const message = raw;
|
|
24
|
+
if (message.type === "ready") {
|
|
25
|
+
if (message.payload?.preludeHash !== PRELUDE_HASH)
|
|
26
|
+
return done(new Error("the isolate runs an unexpected prelude"));
|
|
27
|
+
const { timeoutMs: _t, ...rest } = DEFAULT_FORGE_LIMITS;
|
|
28
|
+
session.send({
|
|
29
|
+
protocol: FORGE_PROTOCOL_VERSION,
|
|
30
|
+
type: "start",
|
|
31
|
+
seq: 0,
|
|
32
|
+
payload: {
|
|
33
|
+
source: "return 1",
|
|
34
|
+
state: {},
|
|
35
|
+
limits: { ...rest, timeoutMs },
|
|
36
|
+
catalogSize: 0,
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
else if (message.type === "complete") {
|
|
41
|
+
const value = message.payload.value;
|
|
42
|
+
done(value === 1 ? undefined : new Error("the isolate answered something else"));
|
|
43
|
+
}
|
|
44
|
+
else if (message.type === "fatal") {
|
|
45
|
+
done(new Error(String(message.payload?.message ?? "the isolate failed")));
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
finally {
|
|
51
|
+
await session.stop("limit_exceeded");
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the JavaScript engine is allowed to run (spec D17, D-22): the exact `quickjs-wasi` build
|
|
3
|
+
* and the hash of its WASM. A TypeScript constant on purpose (`tsc` does not copy JSON to `dist`).
|
|
4
|
+
* `wasm.ts` hashes the bytes it read before compiling anything and refuses a mismatch; the hash is
|
|
5
|
+
* checked again inside the isolate process.
|
|
6
|
+
*/
|
|
7
|
+
export declare const QUICKJS_WASI_VERSION = "3.6.2";
|
|
8
|
+
export declare const QUICKJS_WASM_SHA256 = "d4c9375f2b1ca4dc95f72c8aa2982a7a9951ac8011490d79c6582df732b4bbd9";
|
|
9
|
+
export declare const QUICKJS_WASM_BYTES = 637405;
|
|
10
|
+
/** Version of the host/guest message schema (`Envelope.protocol`). */
|
|
11
|
+
export declare const FORGE_PROTOCOL_VERSION = 1;
|
|
12
|
+
/** Bump whenever `prelude.ts` changes in a way that matters to a guest. */
|
|
13
|
+
export declare const PRELUDE_VERSION = 2;
|
|
14
|
+
export declare const JAVASCRIPT_ENGINE_VERSION = "quickjs-wasi@3.6.2";
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the JavaScript engine is allowed to run (spec D17, D-22): the exact `quickjs-wasi` build
|
|
3
|
+
* and the hash of its WASM. A TypeScript constant on purpose (`tsc` does not copy JSON to `dist`).
|
|
4
|
+
* `wasm.ts` hashes the bytes it read before compiling anything and refuses a mismatch; the hash is
|
|
5
|
+
* checked again inside the isolate process.
|
|
6
|
+
*/
|
|
7
|
+
export const QUICKJS_WASI_VERSION = "3.6.2";
|
|
8
|
+
export const QUICKJS_WASM_SHA256 = "d4c9375f2b1ca4dc95f72c8aa2982a7a9951ac8011490d79c6582df732b4bbd9";
|
|
9
|
+
export const QUICKJS_WASM_BYTES = 637_405;
|
|
10
|
+
/** Version of the host/guest message schema (`Envelope.protocol`). */
|
|
11
|
+
export const FORGE_PROTOCOL_VERSION = 1;
|
|
12
|
+
/** Bump whenever `prelude.ts` changes in a way that matters to a guest. */
|
|
13
|
+
export const PRELUDE_VERSION = 2;
|
|
14
|
+
export const JAVASCRIPT_ENGINE_VERSION = `quickjs-wasi@${QUICKJS_WASI_VERSION}`;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** How the isolate talks to its host. Messages are plain JSON-compatible objects. */
|
|
2
|
+
export interface GuestTransport {
|
|
3
|
+
send(message: unknown): void;
|
|
4
|
+
onMessage(handler: (message: unknown) => void): void;
|
|
5
|
+
}
|
|
6
|
+
export interface GuestOptions {
|
|
7
|
+
/** The pinned, already verified and compiled QuickJS module. */
|
|
8
|
+
module: WebAssembly.Module;
|
|
9
|
+
transport: GuestTransport;
|
|
10
|
+
/**
|
|
11
|
+
* The cancel flag shared with the parent (worker host only): 1 = cancelled, 2 = deadline. A
|
|
12
|
+
* process host has no shared memory and relies on the local deadline plus `SIGKILL`.
|
|
13
|
+
*/
|
|
14
|
+
flag?: Int32Array;
|
|
15
|
+
now?: () => number;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Runs one execution to its terminal message (`complete` or `fatal`) and returns. The caller closes
|
|
19
|
+
* the transport (and exits the process) afterwards.
|
|
20
|
+
*/
|
|
21
|
+
export declare function runGuest(options: GuestOptions): Promise<void>;
|