@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,33 @@
1
+ import type { IsolateHost, IsolateSession } from "../../ports.ts";
2
+ export declare const PROCESS_KILL_BACKSTOP_MS = 500;
3
+ /**
4
+ * The isolate's whole environment: the marker, and on Windows `SystemRoot` and nothing else (the
5
+ * runtime cannot start without it). No other variable of the parent ever reaches the guest process.
6
+ */
7
+ export declare function isolateEnv(platform?: NodeJS.Platform, parent?: NodeJS.ProcessEnv): Record<string, string>;
8
+ export interface IsolateCommand {
9
+ execPath: string;
10
+ args: string[];
11
+ }
12
+ /** Whether this process is a binary compiled by `bun build --compile`. */
13
+ export declare function isBunCompiled(argv1?: string | undefined): boolean;
14
+ /**
15
+ * How to start an isolate process. A compiled binary re-runs itself with the hidden
16
+ * `__forge-isolate` command (its virtual files cannot be spawned); otherwise the process entry file
17
+ * next to this one runs directly (`.ts` through type stripping, `.js` from `dist`).
18
+ */
19
+ export declare function resolveIsolateCommand(options?: {
20
+ compiled?: boolean;
21
+ base?: string;
22
+ execPath?: string;
23
+ }): IsolateCommand;
24
+ export interface ProcessHostOptions {
25
+ command?: IsolateCommand;
26
+ backstopMs?: number;
27
+ }
28
+ export declare class ProcessIsolateHost implements IsolateHost {
29
+ #private;
30
+ readonly kind: "process";
31
+ constructor(options?: ProcessHostOptions);
32
+ start(): IsolateSession;
33
+ }
@@ -0,0 +1,160 @@
1
+ /**
2
+ * `ProcessIsolateHost` (spec §6.3, D-20, D-21): the VM runs in a child process of the same
3
+ * executable (Bun, where `worker.terminate()` cannot be trusted to stop WASM).
4
+ *
5
+ * The child gets an EMPTY environment but `ALISIO_FORGE_ISOLATE=1` (see `isolateEnv`), no inherited descriptors
6
+ * (stdin and stdout are the frame channel, stderr is dropped) and a private working directory.
7
+ * There is no shared memory with it, so:
8
+ * - a user cancellation is `SIGKILL` at once;
9
+ * - a deadline relies on the guest's own interrupt handler, with `SIGKILL` as a backstop 500 ms
10
+ * after the deadline passed.
11
+ * The stop is only "confirmed" when the child's `exit` is seen.
12
+ */
13
+ import { spawn } from "node:child_process";
14
+ import { mkdtempSync, rmSync } from "node:fs";
15
+ import { tmpdir } from "node:os";
16
+ import { join } from "node:path";
17
+ import { fileURLToPath } from "node:url";
18
+ import { MAX_MESSAGE_BYTES } from "../../protocol.js";
19
+ import { encodeFrame, FrameDecoder, FrameStreamError } from "./framing.js";
20
+ export const PROCESS_KILL_BACKSTOP_MS = 500;
21
+ const EXIT_CONFIRM_MS = 2_000;
22
+ /**
23
+ * The isolate's whole environment: the marker, and on Windows `SystemRoot` and nothing else (the
24
+ * runtime cannot start without it). No other variable of the parent ever reaches the guest process.
25
+ */
26
+ export function isolateEnv(platform = process.platform, parent = process.env) {
27
+ const env = { ALISIO_FORGE_ISOLATE: "1" };
28
+ if (platform === "win32" && parent.SystemRoot)
29
+ env.SystemRoot = parent.SystemRoot;
30
+ return env;
31
+ }
32
+ /** Whether this process is a binary compiled by `bun build --compile`. */
33
+ export function isBunCompiled(argv1 = process.argv[1]) {
34
+ return !!process.versions.bun && /^(\/\$bunfs\/|[A-Za-z]:[\\/]+~BUN[\\/])/.test(argv1 ?? "");
35
+ }
36
+ /**
37
+ * How to start an isolate process. A compiled binary re-runs itself with the hidden
38
+ * `__forge-isolate` command (its virtual files cannot be spawned); otherwise the process entry file
39
+ * next to this one runs directly (`.ts` through type stripping, `.js` from `dist`).
40
+ */
41
+ export function resolveIsolateCommand(options = {}) {
42
+ const execPath = options.execPath ?? process.execPath;
43
+ if (options.compiled ?? isBunCompiled())
44
+ return { execPath, args: ["__forge-isolate"] };
45
+ const base = options.base ?? import.meta.url;
46
+ const typescript = base.endsWith(".ts");
47
+ const entry = fileURLToPath(new URL(`./process-entry.${typescript ? "ts" : "js"}`, base));
48
+ const stripFlags = typescript && !process.versions.bun
49
+ ? ["--experimental-strip-types", "--disable-warning=ExperimentalWarning"]
50
+ : [];
51
+ return { execPath, args: [...stripFlags, entry] };
52
+ }
53
+ export class ProcessIsolateHost {
54
+ kind = "process";
55
+ #options;
56
+ constructor(options = {}) {
57
+ this.#options = options;
58
+ }
59
+ start() {
60
+ const command = this.#options.command ?? resolveIsolateCommand();
61
+ const backstopMs = this.#options.backstopMs ?? PROCESS_KILL_BACKSTOP_MS;
62
+ const cwd = mkdtempSync(join(tmpdir(), "alisio-forge-"));
63
+ const child = spawn(command.execPath, command.args, {
64
+ cwd,
65
+ env: isolateEnv(),
66
+ stdio: ["pipe", "pipe", "ignore"],
67
+ windowsHide: true,
68
+ });
69
+ let messageHandler = () => { };
70
+ let streamErrorHandler = () => { };
71
+ let exitState;
72
+ let spawnError;
73
+ const exited = new Promise((resolve) => {
74
+ let settled = false;
75
+ const finish = (exit) => {
76
+ if (settled)
77
+ return;
78
+ settled = true;
79
+ exitState = exit;
80
+ try {
81
+ rmSync(cwd, { recursive: true, force: true });
82
+ }
83
+ catch {
84
+ /* A leftover empty directory in the temporary folder is harmless. */
85
+ }
86
+ resolve(exit);
87
+ };
88
+ child.once("error", (error) => {
89
+ spawnError = error.message;
90
+ finish({ code: null, signal: null, error: error.message });
91
+ });
92
+ child.once("exit", (code, signal) => finish({ code, signal, ...(spawnError ? { error: spawnError } : {}) }));
93
+ });
94
+ const decoder = new FrameDecoder(MAX_MESSAGE_BYTES + 4096);
95
+ child.stdout?.on("data", (chunk) => {
96
+ try {
97
+ for (const message of decoder.push(chunk))
98
+ messageHandler(message);
99
+ }
100
+ catch (error) {
101
+ if (error instanceof FrameStreamError) {
102
+ streamErrorHandler(error.message);
103
+ child.kill("SIGKILL");
104
+ return;
105
+ }
106
+ throw error;
107
+ }
108
+ });
109
+ // The child may be killed while a frame is being written.
110
+ child.stdin?.on("error", () => { });
111
+ child.stdout?.on("error", () => { });
112
+ const waitExit = async () => {
113
+ let timer;
114
+ const confirmed = await Promise.race([
115
+ exited.then(() => true),
116
+ new Promise((resolve) => {
117
+ timer = setTimeout(() => resolve(false), EXIT_CONFIRM_MS);
118
+ }),
119
+ ]);
120
+ clearTimeout(timer);
121
+ return confirmed;
122
+ };
123
+ return {
124
+ kind: "process",
125
+ onMessage(handler) {
126
+ messageHandler = handler;
127
+ },
128
+ onStreamError(handler) {
129
+ streamErrorHandler = handler;
130
+ },
131
+ send(message) {
132
+ if (!exitState && child.stdin?.writable)
133
+ child.stdin.write(encodeFrame(message));
134
+ },
135
+ exited,
136
+ async stop(reason) {
137
+ const started = performance.now();
138
+ if (exitState)
139
+ return { confirmed: true, method: "kill", elapsedMs: 0 };
140
+ let backstop;
141
+ if (reason === "timeout") {
142
+ // The guest's own deadline should end it first; this is the backstop.
143
+ backstop = setTimeout(() => child.kill("SIGKILL"), backstopMs);
144
+ }
145
+ else
146
+ child.kill("SIGKILL");
147
+ const confirmed = await waitExit();
148
+ clearTimeout(backstop);
149
+ if (!confirmed)
150
+ child.kill("SIGKILL");
151
+ return {
152
+ confirmed,
153
+ method: "kill",
154
+ elapsedMs: Math.round(performance.now() - started),
155
+ ...(confirmed ? {} : { settled: exited.then(() => undefined) }),
156
+ };
157
+ },
158
+ };
159
+ }
160
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,3 @@
1
+ /** Entry file of the worker thread (spec D-33: erasable TypeScript only). */
2
+ import { runWorkerIsolate } from "./isolate-main.js";
3
+ await runWorkerIsolate();
@@ -0,0 +1,18 @@
1
+ import type { IsolateHost, IsolateSession } from "../../ports.ts";
2
+ export declare const WORKER_TERMINATE_AFTER_MS = 50;
3
+ /** The worker entry next to this file: `.ts` when running from sources, `.js` from `dist`. */
4
+ export declare function workerEntryUrl(base?: string): URL;
5
+ export interface WorkerHostOptions {
6
+ module: WebAssembly.Module;
7
+ /** Overrides the entry (tests of failure paths). */
8
+ entry?: URL;
9
+ terminateAfterMs?: number;
10
+ }
11
+ export declare class WorkerIsolateHost implements IsolateHost {
12
+ #private;
13
+ readonly kind: "worker";
14
+ constructor(options: WorkerHostOptions);
15
+ start(): IsolateSession;
16
+ }
17
+ /** Path of the worker entry, for diagnostics. */
18
+ export declare const workerEntryPath: () => string;
@@ -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,2 @@
1
+ export declare const PRELUDE_SOURCE: string;
2
+ export declare const PRELUDE_HASH: string;
@@ -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,2 @@
1
+ import type { IsolateHost } from "../ports.ts";
2
+ export declare function probeIsolate(host: IsolateHost, _module: WebAssembly.Module, timeoutMs?: number): Promise<void>;