@alisio/core 0.5.1 → 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.
Files changed (169) hide show
  1. package/dist/analysis/janitor.d.ts +48 -0
  2. package/dist/analysis/janitor.js +170 -18
  3. package/dist/analysis/jobs.d.ts +11 -2
  4. package/dist/analysis/jobs.js +13 -0
  5. package/dist/analysis/oci.d.ts +11 -2
  6. package/dist/analysis/oci.js +20 -4
  7. package/dist/analysis/python/sources.js +2 -1
  8. package/dist/analysis/rerun.d.ts +1 -1
  9. package/dist/analysis/status.d.ts +3 -1
  10. package/dist/analysis/status.js +3 -0
  11. package/dist/application.d.ts +38 -2
  12. package/dist/application.js +213 -4
  13. package/dist/changelog/data.js +64 -0
  14. package/dist/config.d.ts +110 -2
  15. package/dist/config.js +188 -6
  16. package/dist/core/call-internals.d.ts +22 -0
  17. package/dist/core/call-internals.js +9 -0
  18. package/dist/core/contracts.d.ts +2 -2
  19. package/dist/core/runner.js +111 -38
  20. package/dist/forge/broker.d.ts +51 -0
  21. package/dist/forge/broker.js +244 -0
  22. package/dist/forge/budget.d.ts +27 -0
  23. package/dist/forge/budget.js +60 -0
  24. package/dist/forge/catalog-view.d.ts +24 -0
  25. package/dist/forge/catalog-view.js +43 -0
  26. package/dist/forge/errors.d.ts +26 -0
  27. package/dist/forge/errors.js +49 -0
  28. package/dist/forge/guides/javascript.d.ts +25 -0
  29. package/dist/forge/guides/javascript.js +90 -0
  30. package/dist/forge/guides/provider.d.ts +42 -0
  31. package/dist/forge/guides/provider.js +96 -0
  32. package/dist/forge/guides/python-environment.d.ts +15 -0
  33. package/dist/forge/guides/python-environment.js +81 -0
  34. package/dist/forge/guides/python.d.ts +38 -0
  35. package/dist/forge/guides/python.js +170 -0
  36. package/dist/forge/javascript/engine.d.ts +44 -0
  37. package/dist/forge/javascript/engine.js +229 -0
  38. package/dist/forge/javascript/hosts/factory.d.ts +25 -0
  39. package/dist/forge/javascript/hosts/factory.js +21 -0
  40. package/dist/forge/javascript/hosts/framing.d.ts +17 -0
  41. package/dist/forge/javascript/hosts/framing.js +49 -0
  42. package/dist/forge/javascript/hosts/isolate-main.d.ts +9 -0
  43. package/dist/forge/javascript/hosts/isolate-main.js +63 -0
  44. package/dist/forge/javascript/hosts/process-entry.d.ts +1 -0
  45. package/dist/forge/javascript/hosts/process-entry.js +3 -0
  46. package/dist/forge/javascript/hosts/process-host.d.ts +33 -0
  47. package/dist/forge/javascript/hosts/process-host.js +160 -0
  48. package/dist/forge/javascript/hosts/worker-entry.d.ts +1 -0
  49. package/dist/forge/javascript/hosts/worker-entry.js +3 -0
  50. package/dist/forge/javascript/hosts/worker-host.d.ts +18 -0
  51. package/dist/forge/javascript/hosts/worker-host.js +101 -0
  52. package/dist/forge/javascript/prelude.d.ts +2 -0
  53. package/dist/forge/javascript/prelude.js +333 -0
  54. package/dist/forge/javascript/probe.d.ts +2 -0
  55. package/dist/forge/javascript/probe.js +53 -0
  56. package/dist/forge/javascript/runtime-manifest.d.ts +14 -0
  57. package/dist/forge/javascript/runtime-manifest.js +14 -0
  58. package/dist/forge/javascript/vm-runner.d.ts +21 -0
  59. package/dist/forge/javascript/vm-runner.js +301 -0
  60. package/dist/forge/javascript/wasm-asset.d.ts +8 -0
  61. package/dist/forge/javascript/wasm-asset.js +8 -0
  62. package/dist/forge/javascript/wasm.d.ts +15 -0
  63. package/dist/forge/javascript/wasm.js +54 -0
  64. package/dist/forge/journal.d.ts +105 -0
  65. package/dist/forge/journal.js +174 -0
  66. package/dist/forge/kernel.d.ts +76 -0
  67. package/dist/forge/kernel.js +338 -0
  68. package/dist/forge/limits.d.ts +38 -0
  69. package/dist/forge/limits.js +73 -0
  70. package/dist/forge/nested.d.ts +96 -0
  71. package/dist/forge/nested.js +214 -0
  72. package/dist/forge/ports.d.ts +120 -0
  73. package/dist/forge/ports.js +1 -0
  74. package/dist/forge/progress.d.ts +24 -0
  75. package/dist/forge/progress.js +76 -0
  76. package/dist/forge/protocol.d.ts +110 -0
  77. package/dist/forge/protocol.js +171 -0
  78. package/dist/forge/python/backend-selector.d.ts +38 -0
  79. package/dist/forge/python/backend-selector.js +111 -0
  80. package/dist/forge/python/backend.d.ts +89 -0
  81. package/dist/forge/python/backend.js +1 -0
  82. package/dist/forge/python/engine.d.ts +84 -0
  83. package/dist/forge/python/engine.js +279 -0
  84. package/dist/forge/python/frame-parser.d.ts +24 -0
  85. package/dist/forge/python/frame-parser.js +107 -0
  86. package/dist/forge/python/frame-transport.d.ts +40 -0
  87. package/dist/forge/python/frame-transport.js +101 -0
  88. package/dist/forge/python/import-scan.d.ts +22 -0
  89. package/dist/forge/python/import-scan.js +133 -0
  90. package/dist/forge/python/managed-backend.d.ts +30 -0
  91. package/dist/forge/python/managed-backend.js +111 -0
  92. package/dist/forge/python/oci-backend.d.ts +27 -0
  93. package/dist/forge/python/oci-backend.js +84 -0
  94. package/dist/forge/python/pyodide-backend.d.ts +40 -0
  95. package/dist/forge/python/pyodide-backend.js +289 -0
  96. package/dist/forge/python/pyodide-install.d.ts +41 -0
  97. package/dist/forge/python/pyodide-install.js +167 -0
  98. package/dist/forge/python/pyodide-manifest.d.ts +42 -0
  99. package/dist/forge/python/pyodide-manifest.js +376 -0
  100. package/dist/forge/python/pyodide-runner-source.d.ts +2 -0
  101. package/dist/forge/python/pyodide-runner-source.js +3 -0
  102. package/dist/forge/python/rss.d.ts +2 -0
  103. package/dist/forge/python/rss.js +33 -0
  104. package/dist/forge/python/scratch-copy.d.ts +6 -0
  105. package/dist/forge/python/scratch-copy.js +61 -0
  106. package/dist/forge/python/settle.d.ts +6 -0
  107. package/dist/forge/python/settle.js +21 -0
  108. package/dist/forge/python/status.d.ts +8 -0
  109. package/dist/forge/python/status.js +15 -0
  110. package/dist/forge/python/wasm-node.d.ts +59 -0
  111. package/dist/forge/python/wasm-node.js +220 -0
  112. package/dist/forge/python/wiring.d.ts +39 -0
  113. package/dist/forge/python/wiring.js +47 -0
  114. package/dist/forge/registry.d.ts +26 -0
  115. package/dist/forge/registry.js +83 -0
  116. package/dist/forge/result.d.ts +58 -0
  117. package/dist/forge/result.js +86 -0
  118. package/dist/forge/routing-rule.d.ts +19 -0
  119. package/dist/forge/routing-rule.js +31 -0
  120. package/dist/forge/runtime.d.ts +64 -0
  121. package/dist/forge/runtime.js +88 -0
  122. package/dist/forge/scratch-files.d.ts +36 -0
  123. package/dist/forge/scratch-files.js +94 -0
  124. package/dist/forge/scratch-provider.d.ts +41 -0
  125. package/dist/forge/scratch-provider.js +79 -0
  126. package/dist/forge/scratch-quota.d.ts +8 -0
  127. package/dist/forge/scratch-quota.js +25 -0
  128. package/dist/forge/selector.d.ts +35 -0
  129. package/dist/forge/selector.js +51 -0
  130. package/dist/forge/session-dir.d.ts +59 -0
  131. package/dist/forge/session-dir.js +239 -0
  132. package/dist/forge/session-path.d.ts +31 -0
  133. package/dist/forge/session-path.js +61 -0
  134. package/dist/forge/slots.d.ts +16 -0
  135. package/dist/forge/slots.js +47 -0
  136. package/dist/forge/state-delta.d.ts +54 -0
  137. package/dist/forge/state-delta.js +99 -0
  138. package/dist/forge/state-machine.d.ts +20 -0
  139. package/dist/forge/state-machine.js +40 -0
  140. package/dist/forge/state-store.d.ts +25 -0
  141. package/dist/forge/state-store.js +50 -0
  142. package/dist/forge/task-log.d.ts +9 -0
  143. package/dist/forge/task-log.js +42 -0
  144. package/dist/forge/verdict.d.ts +20 -0
  145. package/dist/forge/verdict.js +28 -0
  146. package/dist/forge-isolate.d.ts +6 -0
  147. package/dist/forge-isolate.js +6 -0
  148. package/dist/index.d.ts +7 -0
  149. package/dist/index.js +7 -0
  150. package/dist/permissions/decide.d.ts +42 -0
  151. package/dist/permissions/decide.js +24 -0
  152. package/dist/resources/context.d.ts +2 -1
  153. package/dist/resources/context.js +2 -1
  154. package/dist/runtime/access.d.ts +34 -0
  155. package/dist/runtime/access.js +80 -0
  156. package/dist/runtime/process.d.ts +2 -1
  157. package/dist/runtime/process.js +5 -5
  158. package/dist/runtime/store.d.ts +11 -0
  159. package/dist/runtime/store.js +103 -0
  160. package/dist/tools/analysis.d.ts +25 -2
  161. package/dist/tools/analysis.js +315 -267
  162. package/dist/tools/execute.js +6 -7
  163. package/dist/tools/forge-guide.d.ts +6 -0
  164. package/dist/tools/forge-guide.js +60 -0
  165. package/dist/tools/js-run.d.ts +19 -0
  166. package/dist/tools/js-run.js +126 -0
  167. package/dist/tools/standard.d.ts +7 -1
  168. package/dist/tools/standard.js +44 -43
  169. package/package.json +7 -2
@@ -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>;
@@ -0,0 +1,301 @@
1
+ /**
2
+ * The guest side of the JavaScript engine (spec §6, D-26): runs INSIDE the isolate (a worker
3
+ * thread or a child process). It creates one QuickJS VM per execution, evaluates the prelude,
4
+ * bridges `tools.call`, `catalog.*`, `state.*`, `text`, `progress` to protocol messages and drives
5
+ * the VM's job queue until the root promise settles.
6
+ *
7
+ * The host never trusts anything produced here: every message is validated against the protocol,
8
+ * the call budget and the sizes again on the other side. This runner only has to be a faithful
9
+ * and well-behaved guest; if it is not, the host ends the execution.
10
+ *
11
+ * Erasable TypeScript only (D-33): the isolate entry loads this file under Node's type stripping.
12
+ */
13
+ import { JSValueHandle, QuickJS } from "quickjs-wasi";
14
+ import { PRELUDE_HASH, PRELUDE_SOURCE } from "./prelude.js";
15
+ import { FORGE_PROTOCOL_VERSION } from "./runtime-manifest.js";
16
+ /** What a failed script reports: a limit only when the engine's own error type said so (L7). */
17
+ const classify = (info) => {
18
+ const message = info.message ?? "";
19
+ if (info.limit === "MEMORY_LIMIT" || info.limit === "STACK_LIMIT")
20
+ return { code: info.limit, message };
21
+ return { code: info.code ?? "SCRIPT_RUNTIME", message };
22
+ };
23
+ /** The VM itself failed outside the script (the exception comes from the engine, not from guest text). */
24
+ const classifyEngineFailure = (message) => {
25
+ if (/out of memory/i.test(message))
26
+ return "MEMORY_LIMIT";
27
+ if (/call stack size/i.test(message))
28
+ return "STACK_LIMIT";
29
+ return "SCRIPT_RUNTIME";
30
+ };
31
+ /**
32
+ * Runs one execution to its terminal message (`complete` or `fatal`) and returns. The caller closes
33
+ * the transport (and exits the process) afterwards.
34
+ */
35
+ export function runGuest(options) {
36
+ const { transport, module } = options;
37
+ const now = options.now ?? (() => performance.now());
38
+ return new Promise((finish) => {
39
+ let seq = 0;
40
+ let started = false;
41
+ let done = false;
42
+ const send = (type, payload) => {
43
+ if (done && type !== "complete" && type !== "fatal")
44
+ return;
45
+ transport.send({ protocol: FORGE_PROTOCOL_VERSION, type, seq: seq++, payload });
46
+ };
47
+ let vm;
48
+ let api;
49
+ let root;
50
+ let deadline = Number.POSITIVE_INFINITY;
51
+ let interrupted;
52
+ const calls = new Map();
53
+ const catalogs = new Map();
54
+ let nextCatalogId = 0;
55
+ /** Credits that arrive while the VM is still being created. */
56
+ let earlyCredits = 0;
57
+ /** A terminal condition the prelude asked for (a spent call budget). */
58
+ let requested;
59
+ const end = (type, payload) => {
60
+ if (done)
61
+ return;
62
+ send(type, payload);
63
+ done = true;
64
+ try {
65
+ vm?.dispose();
66
+ }
67
+ catch {
68
+ /* The VM is going away with the isolate. */
69
+ }
70
+ finish();
71
+ };
72
+ const fatal = (code, message) => end("fatal", { code, message });
73
+ const interruptHandler = () => {
74
+ const flag = options.flag ? Atomics.load(options.flag, 0) : 0;
75
+ if (flag !== 0) {
76
+ interrupted = flag === 2 ? "timeout" : "cancelled";
77
+ return true;
78
+ }
79
+ if (now() >= deadline) {
80
+ interrupted = "timeout";
81
+ return true;
82
+ }
83
+ return false;
84
+ };
85
+ const bridge = (kind, data) => {
86
+ const machine = vm;
87
+ const body = JSON.parse(data);
88
+ switch (kind) {
89
+ case "call": {
90
+ const callId = Number(body.callId);
91
+ const deferred = machine.newPromise();
92
+ calls.set(callId, deferred);
93
+ send("call", { callId, tool: body.tool, input: JSON.parse(String(body.input)) });
94
+ return deferred.handle;
95
+ }
96
+ case "catalog": {
97
+ const requestId = nextCatalogId++;
98
+ const deferred = machine.newPromise();
99
+ catalogs.set(requestId, deferred);
100
+ send("catalog", {
101
+ requestId,
102
+ op: body.op,
103
+ query: body.query,
104
+ ...(typeof body.limit === "number" ? { limit: body.limit } : {}),
105
+ });
106
+ return deferred.handle;
107
+ }
108
+ case "output":
109
+ send("output", body);
110
+ return undefined;
111
+ case "progress":
112
+ send("progress", {
113
+ message: body.message,
114
+ ...(typeof body.completed === "number" ? { completed: body.completed } : {}),
115
+ ...(typeof body.total === "number" ? { total: body.total } : {}),
116
+ });
117
+ return undefined;
118
+ case "state.set":
119
+ send("state.set", { key: body.key, value: JSON.parse(String(body.value)) });
120
+ return undefined;
121
+ case "state.delete":
122
+ send("state.delete", { key: body.key });
123
+ return undefined;
124
+ case "fatal":
125
+ // Ends the execution once the VM is back on the host stack (`pump` looks, never here).
126
+ requested ??= { code: String(body.code), message: String(body.message) };
127
+ return undefined;
128
+ default:
129
+ throw new Error(`unknown bridge request ${kind}`);
130
+ }
131
+ };
132
+ const applyGrant = (credits) => {
133
+ const machine = vm;
134
+ const count = machine.newNumber(credits);
135
+ machine
136
+ .callFunction(api.grant, machine.undefined, count)
137
+ .dispose();
138
+ count.dispose();
139
+ };
140
+ /** Runs queued promise jobs, then checks how the root promise stands. */
141
+ const pump = () => {
142
+ if (done || !vm || !root)
143
+ return;
144
+ try {
145
+ vm.executePendingJobs();
146
+ }
147
+ catch (error) {
148
+ const message = error instanceof Error ? error.message : String(error);
149
+ if (interrupted)
150
+ return fatal(interrupted === "timeout" ? "TIMEOUT" : "CANCELLED", "interrupted");
151
+ if (requested)
152
+ return fatal(requested.code, requested.message);
153
+ return fatal(classifyEngineFailure(message), message);
154
+ }
155
+ if (done)
156
+ return;
157
+ if (requested)
158
+ return fatal(requested.code, requested.message);
159
+ const exports = vm._getExports();
160
+ const state = exports.qjs_promise_state(root.ptr);
161
+ if (state === 0) {
162
+ if (calls.size + catalogs.size > 0)
163
+ return;
164
+ const waiting = vm.callFunction(api?.waiting, vm.undefined);
165
+ const queued = Number(vm.dump(waiting));
166
+ waiting.dispose();
167
+ if (queued === 0)
168
+ fatal("UNRESOLVABLE_PROMISE", "the script is waiting on a promise that nothing can settle");
169
+ return;
170
+ }
171
+ const resultHandle = new JSValueHandle(vm, exports.qjs_promise_result(root.ptr));
172
+ const text = vm.dump(resultHandle);
173
+ resultHandle.dispose();
174
+ if (state === 2) {
175
+ // `run` never rejects; a rejection here means the VM itself failed.
176
+ return fatal("SCRIPT_RUNTIME", `script failed: ${String(text)}`);
177
+ }
178
+ const info = JSON.parse(String(text));
179
+ if (info.ok)
180
+ end("complete", info.value === undefined ? {} : { value: info.value });
181
+ else {
182
+ const { code, message } = classify(info);
183
+ fatal(code, message);
184
+ }
185
+ };
186
+ const start = async (payload) => {
187
+ try {
188
+ deadline = now() + payload.limits.timeoutMs;
189
+ vm = await QuickJS.create({
190
+ wasm: module,
191
+ memoryLimit: payload.limits.memoryBytes,
192
+ maxStackSize: payload.limits.stackBytes,
193
+ interruptHandler,
194
+ moduleLoader: {
195
+ load: () => {
196
+ throw new Error("modules are not available");
197
+ },
198
+ },
199
+ });
200
+ const machine = vm;
201
+ const bridgeFn = machine.newFunction("__forge_bridge", (kind, data) => {
202
+ const result = bridge(String(machine.dump(kind)), String(machine.dump(data)));
203
+ return result ?? machine.undefined;
204
+ });
205
+ const preludeFn = machine.evalCode(PRELUDE_SOURCE, "prelude.js");
206
+ const exported = machine.callFunction(preludeFn, machine.undefined, bridgeFn);
207
+ api = {
208
+ init: exported.getProp("init"),
209
+ run: exported.getProp("run"),
210
+ grant: exported.getProp("grant"),
211
+ waiting: exported.getProp("waiting"),
212
+ };
213
+ const limitsJson = machine.newString(JSON.stringify(payload.limits));
214
+ const stateJson = machine.newString(JSON.stringify(payload.state));
215
+ machine.callFunction(api.init, machine.undefined, limitsJson, stateJson).dispose();
216
+ limitsJson.dispose();
217
+ stateJson.dispose();
218
+ if (earlyCredits > 0)
219
+ applyGrant(earlyCredits);
220
+ earlyCredits = 0;
221
+ let body;
222
+ try {
223
+ body = machine.evalCode(`(async function () {\n${payload.source}\n})`, "js_run.js");
224
+ }
225
+ catch (error) {
226
+ const message = error instanceof Error ? error.message : String(error);
227
+ return fatal("SCRIPT_SYNTAX", message);
228
+ }
229
+ root = machine.callFunction(api.run, machine.undefined, body);
230
+ pump();
231
+ }
232
+ catch (error) {
233
+ if (done)
234
+ return;
235
+ if (interrupted)
236
+ return fatal(interrupted === "timeout" ? "TIMEOUT" : "CANCELLED", "interrupted");
237
+ fatal("ISOLATE_FAILURE", error instanceof Error ? error.message : String(error));
238
+ }
239
+ };
240
+ transport.onMessage((raw) => {
241
+ if (done)
242
+ return;
243
+ const message = raw;
244
+ try {
245
+ switch (message.type) {
246
+ case "start":
247
+ if (started)
248
+ return;
249
+ started = true;
250
+ void start(message.payload);
251
+ return;
252
+ case "result": {
253
+ const payload = message.payload;
254
+ const deferred = calls.get(payload.callId);
255
+ if (!deferred || !vm)
256
+ return;
257
+ calls.delete(payload.callId);
258
+ const value = vm.newString(JSON.stringify(payload.ok ? payload.value : payload.error));
259
+ if (payload.ok)
260
+ deferred.resolve(value);
261
+ else
262
+ deferred.reject(value);
263
+ value.dispose();
264
+ pump();
265
+ return;
266
+ }
267
+ case "catalog.result": {
268
+ const payload = message.payload;
269
+ const deferred = catalogs.get(payload.requestId);
270
+ if (!deferred || !vm)
271
+ return;
272
+ catalogs.delete(payload.requestId);
273
+ const value = vm.newString(JSON.stringify(payload.entries));
274
+ deferred.resolve(value);
275
+ value.dispose();
276
+ pump();
277
+ return;
278
+ }
279
+ case "grant": {
280
+ const payload = message.payload;
281
+ if (!vm || !api) {
282
+ earlyCredits += payload.calls;
283
+ return;
284
+ }
285
+ applyGrant(payload.calls);
286
+ pump();
287
+ return;
288
+ }
289
+ default:
290
+ return;
291
+ }
292
+ }
293
+ catch (error) {
294
+ if (interrupted)
295
+ return fatal(interrupted === "timeout" ? "TIMEOUT" : "CANCELLED", "interrupted");
296
+ fatal("ISOLATE_FAILURE", error instanceof Error ? error.message : String(error));
297
+ }
298
+ });
299
+ send("ready", { engine: "javascript-quickjs", preludeHash: PRELUDE_HASH });
300
+ });
301
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Bun-only: the WASM as an embedded asset. Imported dynamically, and only when `process.versions.bun`
3
+ * is set (`wasm.ts`), so Node never touches it. In a compiled binary the default export is a path
4
+ * into the binary's virtual file system that `node:fs` can read; running from sources it is the
5
+ * real file.
6
+ */
7
+ import path from "quickjs-wasi/quickjs.wasm";
8
+ export default path;
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Bun-only: the WASM as an embedded asset. Imported dynamically, and only when `process.versions.bun`
3
+ * is set (`wasm.ts`), so Node never touches it. In a compiled binary the default export is a path
4
+ * into the binary's virtual file system that `node:fs` can read; running from sources it is the
5
+ * real file.
6
+ */
7
+ import path from "quickjs-wasi/quickjs.wasm" with { type: "file" };
8
+ export default path;
@@ -0,0 +1,15 @@
1
+ export declare const sha256Hex: (data: Uint8Array | string) => string;
2
+ /** Why `bytes` is not the pinned WASM, or undefined when it is. */
3
+ export declare function wasmProblem(bytes: Uint8Array): string | undefined;
4
+ /** The path of the WASM: the Bun asset on Bun, the package file on Node. */
5
+ export declare function resolveWasmPath(): Promise<string>;
6
+ export type WasmBytesLoader = () => Promise<Uint8Array>;
7
+ /**
8
+ * Reads, verifies and compiles. Throws an `Error` whose message says why when the bytes are not the
9
+ * pinned build. Not cached: see `loadQuickJsModule`.
10
+ */
11
+ export declare function compileVerifiedModule(loader?: WasmBytesLoader): Promise<WebAssembly.Module>;
12
+ /** The process-wide compiled module. A failure is cached too: the engine stays not ready. */
13
+ export declare function loadQuickJsModule(): Promise<WebAssembly.Module>;
14
+ /** Test seam: forgets the cached module. */
15
+ export declare function resetQuickJsModuleCache(): void;
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Loads, verifies and compiles the QuickJS WASM (spec D3, D17, D-22).
3
+ *
4
+ * The bytes come from the Bun asset (guarded dynamic import) or, on Node, from
5
+ * `quickjs-wasi/quickjs.wasm`. Their SHA-256 is compared with the manifest BEFORE compiling: a
6
+ * mismatch means the engine is not ready. The compiled `WebAssembly.Module` is cached per process
7
+ * and reused by every execution (the memory and globals of a VM never are).
8
+ *
9
+ * Erasable TypeScript only (D-33): the isolate entry imports this file under Node's type stripping.
10
+ */
11
+ import { createHash } from "node:crypto";
12
+ import { readFile } from "node:fs/promises";
13
+ import { createRequire } from "node:module";
14
+ import { QUICKJS_WASM_BYTES, QUICKJS_WASM_SHA256 } from "./runtime-manifest.js";
15
+ export const sha256Hex = (data) => createHash("sha256").update(data).digest("hex");
16
+ /** Why `bytes` is not the pinned WASM, or undefined when it is. */
17
+ export function wasmProblem(bytes) {
18
+ if (bytes.byteLength !== QUICKJS_WASM_BYTES)
19
+ return `quickjs.wasm has ${bytes.byteLength} bytes, expected ${QUICKJS_WASM_BYTES}`;
20
+ const actual = sha256Hex(bytes);
21
+ if (actual !== QUICKJS_WASM_SHA256)
22
+ return `quickjs.wasm hash ${actual} does not match the pinned ${QUICKJS_WASM_SHA256}`;
23
+ return undefined;
24
+ }
25
+ /** The path of the WASM: the Bun asset on Bun, the package file on Node. */
26
+ export async function resolveWasmPath() {
27
+ if (process.versions.bun) {
28
+ const asset = await import("./wasm-asset.js");
29
+ return asset.default;
30
+ }
31
+ return createRequire(import.meta.url).resolve("quickjs-wasi/quickjs.wasm");
32
+ }
33
+ const defaultLoader = async () => readFile(await resolveWasmPath());
34
+ /**
35
+ * Reads, verifies and compiles. Throws an `Error` whose message says why when the bytes are not the
36
+ * pinned build. Not cached: see `loadQuickJsModule`.
37
+ */
38
+ export async function compileVerifiedModule(loader = defaultLoader) {
39
+ const bytes = await loader();
40
+ const problem = wasmProblem(bytes);
41
+ if (problem)
42
+ throw new Error(problem);
43
+ return WebAssembly.compile(bytes);
44
+ }
45
+ let cached;
46
+ /** The process-wide compiled module. A failure is cached too: the engine stays not ready. */
47
+ export function loadQuickJsModule() {
48
+ cached ??= compileVerifiedModule();
49
+ return cached;
50
+ }
51
+ /** Test seam: forgets the cached module. */
52
+ export function resetQuickJsModuleCache() {
53
+ cached = undefined;
54
+ }
@@ -0,0 +1,105 @@
1
+ /**
2
+ * The Forja execution journal (spec §9.1, §9.3) over the shared SQLite store.
3
+ *
4
+ * One `forge_executions` row per execution whose status follows the Phase 1 state machine, one
5
+ * `forge_calls` row per nested call, and a restart reconciliation for rows a dead process left
6
+ * non-terminal. Only hashes of inputs are stored, never inputs or results. Every status change is
7
+ * a compare-and-set on the previous status, so a terminal state is reached exactly once even with
8
+ * several processes sharing the database.
9
+ */
10
+ import type { EngineId, IsolationLevel, SqlDatabase } from "@alisio/sdk";
11
+ import type { ForgeErrorCode } from "./errors.ts";
12
+ import { type ExecutionState } from "./state-machine.ts";
13
+ export type TerminalExecutionState = "succeeded" | "failed" | "cancelled" | "timed_out";
14
+ /** Effect class of a nested call, as the gate classifies it. */
15
+ export type CallEffect = "read" | "write" | "process" | "external" | "internal";
16
+ export type CallStatus = "started" | "succeeded" | "failed";
17
+ export interface OpenExecutionInput {
18
+ id: string;
19
+ rootSessionId: string;
20
+ sessionId: string;
21
+ toolCallId: string;
22
+ engine: EngineId;
23
+ isolation: IsolationLevel;
24
+ selectionReason: string;
25
+ codeSha256: string;
26
+ analysisExecutionId?: string | undefined;
27
+ }
28
+ export interface ExecutionRecord extends Omit<OpenExecutionInput, "analysisExecutionId"> {
29
+ status: ExecutionState;
30
+ startedAt: number;
31
+ endedAt?: number;
32
+ metrics?: Record<string, unknown>;
33
+ errorCode?: ForgeErrorCode;
34
+ terminationUnconfirmed: boolean;
35
+ analysisExecutionId?: string;
36
+ ownerPid?: number;
37
+ }
38
+ export interface CloseOutcome {
39
+ status: TerminalExecutionState;
40
+ errorCode?: ForgeErrorCode;
41
+ metrics?: Record<string, unknown>;
42
+ /** The host could not verify that the guest stopped (D-21). */
43
+ terminationUnconfirmed?: boolean;
44
+ }
45
+ export interface StartCallInput {
46
+ executionId: string;
47
+ /** 1-based position of the call within its execution. */
48
+ n: number;
49
+ toolName: string;
50
+ effect: CallEffect;
51
+ inputSha256: string;
52
+ /** Only kept for the `write` effect (D-31). */
53
+ paths?: string[];
54
+ }
55
+ export interface CallRecord {
56
+ executionId: string;
57
+ callId: string;
58
+ toolName: string;
59
+ effect: CallEffect;
60
+ status: CallStatus;
61
+ durationMs?: number;
62
+ inputSha256: string;
63
+ errorCode?: ForgeErrorCode;
64
+ paths?: string[];
65
+ }
66
+ export interface EndCallOutcome {
67
+ status: "succeeded" | "failed";
68
+ durationMs?: number;
69
+ errorCode?: ForgeErrorCode;
70
+ }
71
+ /** The id of the n-th nested call of a tool call (D-24). It never enters `tool_calls`. */
72
+ export declare const forgeCallId: (toolCallId: string, n: number) => string;
73
+ export declare class SqliteExecutionJournal {
74
+ #private;
75
+ constructor(db: SqlDatabase, options?: {
76
+ now?: () => number;
77
+ ownerPid?: number;
78
+ });
79
+ /** Records a new execution in `validating`. */
80
+ open(input: OpenExecutionInput): ExecutionRecord;
81
+ /** Links the execution to the `analysis_executions` row its engine created while preparing. */
82
+ linkAnalysisExecution(id: string, analysisExecutionId: string): void;
83
+ get(id: string): ExecutionRecord | undefined;
84
+ listByRoot(rootSessionId: string): ExecutionRecord[];
85
+ /** Moves to a non-terminal state the diagram allows. Terminal states go through `close()`. */
86
+ transition(id: string, to: ExecutionState): void;
87
+ /** Reaches a terminal state, once. A second close throws and changes nothing. */
88
+ close(id: string, outcome: CloseOutcome): void;
89
+ /** Records the start of a nested call and returns its id. */
90
+ startCall(input: StartCallInput): string;
91
+ /** Ends a call that is still `started`; false when it already ended (or does not exist). */
92
+ endCall(executionId: string, callId: string, outcome: EndCallOutcome): boolean;
93
+ calls(executionId: string): CallRecord[];
94
+ }
95
+ /**
96
+ * Restart reconciliation (§9.3, FJ-AC-25): every non-terminal execution whose owner process is
97
+ * gone (or unknown) becomes `failed` with `ISOLATE_FAILURE` and reason `interrupted`. Nothing is
98
+ * relaunched. Executions of another live process are left alone. Returns the rows reconciled.
99
+ *
100
+ * Call it once when the application opens the store (Phase 3 wires it next to the runner).
101
+ */
102
+ export declare function reconcileInterruptedExecutions(db: SqlDatabase, options?: {
103
+ now?: () => number;
104
+ isAlive?: (pid: number) => boolean;
105
+ }): number;