@eryx-org/eryx 0.0.0 → 0.15.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/README.md +281 -4
- package/eryx-sandbox.conditional-host-import-0.wasm +0 -0
- package/eryx-sandbox.conditional-host-import-1.wasm +0 -0
- package/eryx-sandbox.conditional-suspending-1-i32-to-i32.wasm +0 -0
- package/eryx-sandbox.core.wasm +0 -0
- package/eryx-sandbox.core10.wasm +0 -0
- package/eryx-sandbox.core11.wasm +0 -0
- package/eryx-sandbox.core12.wasm +0 -0
- package/eryx-sandbox.core13.wasm +0 -0
- package/eryx-sandbox.core2.wasm +0 -0
- package/eryx-sandbox.core3.wasm +0 -0
- package/eryx-sandbox.core4.wasm +0 -0
- package/eryx-sandbox.core5.wasm +0 -0
- package/eryx-sandbox.core6.wasm +0 -0
- package/eryx-sandbox.core7.wasm +0 -0
- package/eryx-sandbox.core8.wasm +0 -0
- package/eryx-sandbox.core9.wasm +0 -0
- package/eryx-sandbox.d.ts +163 -0
- package/eryx-sandbox.js +43541 -0
- package/index.d.ts +331 -0
- package/index.js +698 -0
- package/instance.js +174 -0
- package/interfaces/eryx-net-tcp.d.ts +30 -0
- package/interfaces/eryx-net-tls.d.ts +24 -0
- package/interfaces/wasi-cli-environment.d.ts +3 -0
- package/interfaces/wasi-cli-exit.d.ts +3 -0
- package/interfaces/wasi-cli-stderr.d.ts +3 -0
- package/interfaces/wasi-cli-stdin.d.ts +3 -0
- package/interfaces/wasi-cli-stdout.d.ts +3 -0
- package/interfaces/wasi-cli-terminal-input.d.ts +8 -0
- package/interfaces/wasi-cli-terminal-output.d.ts +8 -0
- package/interfaces/wasi-cli-terminal-stderr.d.ts +3 -0
- package/interfaces/wasi-cli-terminal-stdin.d.ts +3 -0
- package/interfaces/wasi-cli-terminal-stdout.d.ts +3 -0
- package/interfaces/wasi-clocks-monotonic-clock.d.ts +8 -0
- package/interfaces/wasi-clocks-wall-clock.d.ts +7 -0
- package/interfaces/wasi-filesystem-preopens.d.ts +3 -0
- package/interfaces/wasi-filesystem-types.d.ts +207 -0
- package/interfaces/wasi-io-error.d.ts +8 -0
- package/interfaces/wasi-io-poll.d.ts +10 -0
- package/interfaces/wasi-io-streams.d.ts +33 -0
- package/interfaces/wasi-random-random.d.ts +2 -0
- package/interfaces/wasi-sockets-instance-network.d.ts +3 -0
- package/interfaces/wasi-sockets-ip-name-lookup.d.ts +15 -0
- package/interfaces/wasi-sockets-network.d.ts +92 -0
- package/interfaces/wasi-sockets-tcp-create-socket.d.ts +5 -0
- package/interfaces/wasi-sockets-tcp.d.ts +52 -0
- package/interfaces/wasi-sockets-udp-create-socket.d.ts +5 -0
- package/interfaces/wasi-sockets-udp.d.ts +51 -0
- package/package.json +69 -3
- package/python-stdlib.tar.gz +0 -0
- package/shims/callbacks.d.ts +45 -0
- package/shims/callbacks.js +469 -0
- package/shims/ip.js +46 -0
- package/shims/net-fetch.js +264 -0
- package/shims/net-node.js +236 -0
- package/shims/net.d.ts +84 -0
- package/shims/net.js +255 -0
- package/shims/preview2-node.js +35 -0
- package/shims/raw-json.js +111 -0
- package/shims/sockets.js +53 -0
- package/stdlib-loader.js +174 -0
- package/worker.js +100 -0
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Callback shims for the eryx sandbox.
|
|
3
|
+
*
|
|
4
|
+
* These provide the host-side implementations of the sandbox's callback imports:
|
|
5
|
+
* - invoke: call a registered callback by name with JSON arguments
|
|
6
|
+
* - sleep: back the sandbox event loop's timers (asyncio.sleep, wait_for)
|
|
7
|
+
* - listCallbacks: list all registered callbacks
|
|
8
|
+
* - getExecutionOptions: return per-execution behavior options
|
|
9
|
+
* - reportTrace: receive trace events from the Python runtime
|
|
10
|
+
* - reportOutput: receive streaming stdout/stderr output from the Python runtime
|
|
11
|
+
*
|
|
12
|
+
* Each sandbox instance gets its own set from createCallbackHost(), with its
|
|
13
|
+
* own replay state and output decoders. Its callbacks come from that
|
|
14
|
+
* sandbox's `callbacks` option; trace and output handlers from its options,
|
|
15
|
+
* falling back to the module-wide defaults set by setTraceHandler() and
|
|
16
|
+
* setOutputHandler().
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { parseJson } from "./raw-json.js";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Module-wide handlers, used by every sandbox that doesn't set its own.
|
|
23
|
+
* @type {{traceHandler: ((lineno: number, eventJson: string, contextJson: string) => void) | null, outputHandler: ((stream: number, data: string) => void) | null}}
|
|
24
|
+
*/
|
|
25
|
+
const defaults = {
|
|
26
|
+
traceHandler: null,
|
|
27
|
+
outputHandler: null,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/** A JSON.stringify replacer writing BigInts as exact integers */
|
|
31
|
+
const exactBigInts = (_key, value) =>
|
|
32
|
+
typeof value === "bigint" && JSON.rawJSON ? JSON.rawJSON(value.toString()) : value;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* A sandbox's callbacks, from the `callbacks` option: what `list_callbacks()`
|
|
36
|
+
* shows the code, and the handler the host calls with each call's name and
|
|
37
|
+
* arguments JSON, which answers with the result's JSON.
|
|
38
|
+
* @param {import("@eryx-org/eryx-run/shared").Callbacks} callbacks
|
|
39
|
+
*/
|
|
40
|
+
function fromCallbacks(callbacks) {
|
|
41
|
+
if (Array.isArray(callbacks) || typeof callbacks !== "object" || callbacks === null) {
|
|
42
|
+
throw new TypeError(
|
|
43
|
+
"callbacks is an object of functions by name, e.g. { get_user: async ({ id }) => ... } " +
|
|
44
|
+
"(the list of {name, description} with a callbackHandler is gone)",
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
const handlers = new Map();
|
|
48
|
+
const infos = [];
|
|
49
|
+
for (const [name, callback] of Object.entries(callbacks)) {
|
|
50
|
+
const fn = typeof callback === "function" ? callback : callback?.handler;
|
|
51
|
+
if (typeof fn !== "function") {
|
|
52
|
+
throw new TypeError(`callback ${JSON.stringify(name)} isn't a function or { handler }`);
|
|
53
|
+
}
|
|
54
|
+
handlers.set(name, fn);
|
|
55
|
+
const description = typeof callback === "function" ? "" : (callback.description ?? "");
|
|
56
|
+
infos.push({ name, description, parametersSchemaJson: "{}" });
|
|
57
|
+
}
|
|
58
|
+
/** @param {string} name @param {string} argsJson */
|
|
59
|
+
const handler = async (name, argsJson) => {
|
|
60
|
+
try {
|
|
61
|
+
const fn = handlers.get(name);
|
|
62
|
+
if (!fn) throw new Error(`no callback ${name}`);
|
|
63
|
+
// undefined isn't JSON. A BigInt is written exactly, and so is what a
|
|
64
|
+
// handler wraps in JSON.rawJSON (e.g. "1.0", to stay a float)
|
|
65
|
+
return JSON.stringify((await fn(JSON.parse(argsJson))) ?? null, exactBigInts);
|
|
66
|
+
} catch (e) {
|
|
67
|
+
if (e instanceof SuspendCallback) throw e;
|
|
68
|
+
// What the handler threw, or a result JSON can't hold (a cycle), is an
|
|
69
|
+
// exception in Python, as on eryx.run: `RuntimeError("RangeError: ...")`
|
|
70
|
+
return { tag: "err", val: e instanceof Error ? `${e.name}: ${e.message}` : String(e) };
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
return { infos, handler };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Set the default handler for trace events from the Python runtime.
|
|
78
|
+
*
|
|
79
|
+
* @param {((lineno: number, eventJson: string, contextJson: string) => void) | null} handler
|
|
80
|
+
*/
|
|
81
|
+
export function setTraceHandler(handler) {
|
|
82
|
+
defaults.traceHandler = handler;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Set the default handler for streaming output (stdout/stderr) from the Python
|
|
87
|
+
* runtime.
|
|
88
|
+
*
|
|
89
|
+
* The handler is called in real-time as Python code writes to stdout or stderr,
|
|
90
|
+
* rather than waiting for execution to complete.
|
|
91
|
+
*
|
|
92
|
+
* @param {((stream: number, data: string) => void) | null} handler
|
|
93
|
+
* stream: 0 = stdout, 1 = stderr
|
|
94
|
+
* data: the text written
|
|
95
|
+
*/
|
|
96
|
+
export function setOutputHandler(handler) {
|
|
97
|
+
defaults.outputHandler = handler;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Thrown by a callback handler to suspend execution ("retry later").
|
|
102
|
+
*
|
|
103
|
+
* The guest halts immediately: no further Python runs and no further callbacks
|
|
104
|
+
* dispatch. Under executeWithJournal() the suspension is surfaced as
|
|
105
|
+
* `outcome.suspended` and the call is not journaled, so it re-runs live when the
|
|
106
|
+
* recorded journal is replayed. Under plain execute() it rejects like any error.
|
|
107
|
+
*/
|
|
108
|
+
export class SuspendCallback extends Error {
|
|
109
|
+
/** @param {string} reason - Opaque reason, surfaced to the caller */
|
|
110
|
+
constructor(reason) {
|
|
111
|
+
super(reason);
|
|
112
|
+
this.name = "SuspendCallback";
|
|
113
|
+
this.reason = reason;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* A recorded callback outcome. `ok` is the handler's exact JSON result text (not
|
|
119
|
+
* re-parsed, so replay returns byte-for-byte what Python saw live); `err` is a
|
|
120
|
+
* Python-visible error message, or the message of a thrown Error when `thrown`.
|
|
121
|
+
* @typedef {{ok: string} | {err: string, thrown?: boolean}} RecordedResult
|
|
122
|
+
*/
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Replay state for an executeWithJournal() run. Mirrors the Rust `ReplayState`
|
|
126
|
+
* (crates/eryx/src/replay.rs): cached results bucketed by (name, canonical
|
|
127
|
+
* args) as a FIFO multiset, a sticky divergence guard, and a gate that rejects
|
|
128
|
+
* every call after a suspension.
|
|
129
|
+
* @typedef {{cached: Map<string, RecordedResult[]>, liveMode: boolean, nextSeq: number, entries: Array<{name: string, argsJson: string, result: RecordedResult}|undefined>, suspended: Object|null, suspendSeq: number|null, replayedCount: number}} ReplayState
|
|
130
|
+
*/
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Parse a serialized journal into its entries.
|
|
134
|
+
*
|
|
135
|
+
* Parsed with {@link parseJson} rather than `JSON.parse` so each `Ok` value
|
|
136
|
+
* keeps its exact text (`1.0` stays a float, large integers stay exact).
|
|
137
|
+
* @param {string} journal
|
|
138
|
+
* @returns {Array<{name: string, argsJson: string, result: RecordedResult}>}
|
|
139
|
+
*/
|
|
140
|
+
function _parseJournal(journal) {
|
|
141
|
+
if (typeof journal !== "string") {
|
|
142
|
+
throw new TypeError(
|
|
143
|
+
"journal must be the JSON string from a previous executeWithJournal() outcome",
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
const invalid = () =>
|
|
147
|
+
new TypeError("journal is not a valid callback journal");
|
|
148
|
+
const field = (node, key, kind) => {
|
|
149
|
+
const value = node?.kind === "object" ? node.value.get(key) : undefined;
|
|
150
|
+
if (value?.kind !== kind) throw invalid();
|
|
151
|
+
return value;
|
|
152
|
+
};
|
|
153
|
+
const entries = field(parseJson(journal), "entries", "array").value;
|
|
154
|
+
return entries.map((entry) => {
|
|
155
|
+
const result = field(entry, "result", "object").value;
|
|
156
|
+
const ok = result.get("Ok");
|
|
157
|
+
const err = result.get("Err");
|
|
158
|
+
if (!(ok || err?.kind === "string")) throw invalid();
|
|
159
|
+
return {
|
|
160
|
+
name: field(entry, "name", "string").value,
|
|
161
|
+
argsJson: field(entry, "args_json", "string").value,
|
|
162
|
+
result: ok
|
|
163
|
+
? { ok: ok.raw }
|
|
164
|
+
: { err: err.value, thrown: entry.value.get("thrown")?.raw === "true" },
|
|
165
|
+
};
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Build the replay state for a run, replaying results from `journal` if given.
|
|
171
|
+
* @param {string} [journal] - A journal string from a previous outcome
|
|
172
|
+
* @returns {ReplayState}
|
|
173
|
+
*/
|
|
174
|
+
function _newReplay(journal) {
|
|
175
|
+
const cached = new Map();
|
|
176
|
+
for (const entry of journal === undefined ? [] : _parseJournal(journal)) {
|
|
177
|
+
const key = `${entry.name}\0${entry.argsJson}`;
|
|
178
|
+
if (!cached.has(key)) cached.set(key, []);
|
|
179
|
+
cached.get(key).push(entry.result);
|
|
180
|
+
}
|
|
181
|
+
return {
|
|
182
|
+
cached,
|
|
183
|
+
liveMode: false,
|
|
184
|
+
nextSeq: 0,
|
|
185
|
+
entries: [],
|
|
186
|
+
suspended: null,
|
|
187
|
+
suspendSeq: null,
|
|
188
|
+
replayedCount: 0,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** 64-bit FNV-1a, matching the Rust journal's `args_hash`. */
|
|
193
|
+
function _fnv1a64(text) {
|
|
194
|
+
let hash = 0xcbf29ce484222325n;
|
|
195
|
+
for (const byte of new TextEncoder().encode(text)) {
|
|
196
|
+
hash = BigInt.asUintN(64, (hash ^ BigInt(byte)) * 0x100000001b3n);
|
|
197
|
+
}
|
|
198
|
+
return hash;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Serialize what a run recorded.
|
|
203
|
+
*
|
|
204
|
+
* The journal is serialized in the Rust `CallbackJournal` serde shape. Entries
|
|
205
|
+
* keep dispatch order and, if a callback suspended, everything dispatched at or
|
|
206
|
+
* after the suspending call is dropped so the journal is a clean prefix ending
|
|
207
|
+
* before the suspension point.
|
|
208
|
+
* @param {ReplayState} state
|
|
209
|
+
* @param {string} code - The script that produced the journal
|
|
210
|
+
*/
|
|
211
|
+
function _finishReplay(state, code) {
|
|
212
|
+
const entries = [];
|
|
213
|
+
state.entries.forEach((entry, index) => {
|
|
214
|
+
if (entry === undefined) return;
|
|
215
|
+
if (state.suspendSeq !== null && index >= state.suspendSeq) return;
|
|
216
|
+
const { name, argsJson, result } = entry;
|
|
217
|
+
const recorded =
|
|
218
|
+
"ok" in result
|
|
219
|
+
? `{"Ok":${result.ok}}`
|
|
220
|
+
: `{"Err":${JSON.stringify(result.err)}}`;
|
|
221
|
+
// `thrown` is a JS-only extension (serde ignores unknown fields): the Error
|
|
222
|
+
// halted the guest instead of reaching Python, so replay throws it again.
|
|
223
|
+
const thrown = result.thrown ? `,"thrown":true` : "";
|
|
224
|
+
entries.push(
|
|
225
|
+
`{"index":${index},"name":${JSON.stringify(name)},"args_hash":${_fnv1a64(argsJson)},` +
|
|
226
|
+
`"args_json":${JSON.stringify(argsJson)},"result":${recorded}${thrown}}`,
|
|
227
|
+
);
|
|
228
|
+
});
|
|
229
|
+
return {
|
|
230
|
+
journal: `{"code":${JSON.stringify(code)},"entries":[${entries.join(",")}]}`,
|
|
231
|
+
replayedCallbacks: state.replayedCount,
|
|
232
|
+
suspended: state.suspended ?? undefined,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** Deliver a recorded result to the guest exactly as it was delivered live. */
|
|
237
|
+
function _replayResult(result) {
|
|
238
|
+
if ("ok" in result) return result.ok;
|
|
239
|
+
if (result.thrown) throw new Error(result.err);
|
|
240
|
+
return { tag: "err", val: result.err };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Run a live callback, recording its outcome into `state` at `seq`.
|
|
245
|
+
*
|
|
246
|
+
* Every outcome of a call that actually ran is journaled, so replay never runs
|
|
247
|
+
* it twice: a JSON result as `Ok` (its exact text), an `{tag: "err"}` result or
|
|
248
|
+
* thrown non-Error as `Err`, and a thrown Error as `Err` marked `thrown` (it
|
|
249
|
+
* halts the guest; replay halts it the same way). Only a SuspendCallback is not
|
|
250
|
+
* journaled: the call re-runs live on resume, as in Rust.
|
|
251
|
+
*/
|
|
252
|
+
async function _invokeLive(handler, state, seq, name, argsJson) {
|
|
253
|
+
const record = (result) => {
|
|
254
|
+
state.entries[seq] = { name, argsJson, result };
|
|
255
|
+
};
|
|
256
|
+
let ret;
|
|
257
|
+
try {
|
|
258
|
+
ret = await handler(name, argsJson);
|
|
259
|
+
} catch (e) {
|
|
260
|
+
if (e instanceof SuspendCallback) {
|
|
261
|
+
if (state.suspended === null) {
|
|
262
|
+
state.suspended = { name, argsJson, reason: e.reason };
|
|
263
|
+
state.suspendSeq = seq;
|
|
264
|
+
}
|
|
265
|
+
} else if (e instanceof Error) {
|
|
266
|
+
record({ err: e.message, thrown: true });
|
|
267
|
+
} else {
|
|
268
|
+
record({ err: String(e) });
|
|
269
|
+
}
|
|
270
|
+
throw e;
|
|
271
|
+
}
|
|
272
|
+
if (ret !== null && typeof ret === "object" && ret.tag === "err") {
|
|
273
|
+
record({ err: String(ret.val) });
|
|
274
|
+
} else {
|
|
275
|
+
const json = ret !== null && typeof ret === "object" ? ret.val : ret;
|
|
276
|
+
try {
|
|
277
|
+
parseJson(json);
|
|
278
|
+
record({ ok: json });
|
|
279
|
+
} catch {
|
|
280
|
+
// ponytail: a non-JSON result breaks the handler contract and can't be
|
|
281
|
+
// stored as an `Ok` value, so it stays unjournaled (re-runs on replay).
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return ret;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/** Longest delay setTimeout accepts; larger values fire immediately. */
|
|
288
|
+
const MAX_TIMEOUT_MS = 2 ** 31 - 1;
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Complete after the given number of nanoseconds.
|
|
292
|
+
* This backs the sandbox event loop's timers (asyncio.sleep, wait_for, ...).
|
|
293
|
+
*
|
|
294
|
+
* @param {bigint} durationNs - Delay in nanoseconds
|
|
295
|
+
* @returns {Promise<void>}
|
|
296
|
+
*/
|
|
297
|
+
export function sleep(durationNs) {
|
|
298
|
+
// Round up so a timer never fires early.
|
|
299
|
+
const ms = Number((BigInt(durationNs) + 999_999n) / 1_000_000n);
|
|
300
|
+
return new Promise((resolve) =>
|
|
301
|
+
setTimeout(resolve, Math.min(ms, MAX_TIMEOUT_MS)),
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const _decoderOpts = { fatal: false, ignoreBOM: true };
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Create the callback imports for one sandbox instance.
|
|
309
|
+
*
|
|
310
|
+
* Each handler option overrides the module-wide default from the matching
|
|
311
|
+
* setter; an omitted (undefined) option follows the default, including later
|
|
312
|
+
* changes to it. Replay state and streaming output decoders belong to the
|
|
313
|
+
* returned host alone.
|
|
314
|
+
*
|
|
315
|
+
* @internal Used by index.js for each Sandbox.
|
|
316
|
+
* @param {{callbacks?: import("@eryx-org/eryx-run/shared").Callbacks, traceHandler?: ((lineno: number, eventJson: string, contextJson: string) => void) | null, outputHandler?: ((stream: number, data: string) => void) | null}} [options]
|
|
317
|
+
*/
|
|
318
|
+
export function createCallbackHost(options = {}) {
|
|
319
|
+
const { infos: callbacks, handler } = fromCallbacks(options.callbacks ?? {});
|
|
320
|
+
/** Calls the code made since takeInvocations() last ran */
|
|
321
|
+
let invocations = 0;
|
|
322
|
+
const pick = (key) => (options[key] === undefined ? defaults[key] : options[key]);
|
|
323
|
+
|
|
324
|
+
/** @type {ReplayState | null} */
|
|
325
|
+
let replay = null;
|
|
326
|
+
|
|
327
|
+
// Streaming decoders, tied to the output handler they were created under so
|
|
328
|
+
// an incomplete multi-byte sequence written under one handler isn't carried
|
|
329
|
+
// into the next.
|
|
330
|
+
let decodersFor = null;
|
|
331
|
+
let stdoutDecoder = new TextDecoder("utf-8", _decoderOpts);
|
|
332
|
+
let stderrDecoder = new TextDecoder("utf-8", _decoderOpts);
|
|
333
|
+
const outputHandler = () => {
|
|
334
|
+
const handler = pick("outputHandler");
|
|
335
|
+
if (handler !== decodersFor) {
|
|
336
|
+
decodersFor = handler;
|
|
337
|
+
stdoutDecoder = new TextDecoder("utf-8", _decoderOpts);
|
|
338
|
+
stderrDecoder = new TextDecoder("utf-8", _decoderOpts);
|
|
339
|
+
}
|
|
340
|
+
return handler;
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Invoke a callback by name with JSON arguments.
|
|
345
|
+
* This is called by the sandbox runtime when Python code calls invoke().
|
|
346
|
+
*
|
|
347
|
+
* @param {string} name - Callback name
|
|
348
|
+
* @param {string} argumentsJson - JSON-encoded arguments
|
|
349
|
+
* @returns {Promise<string>} JSON-encoded result
|
|
350
|
+
*/
|
|
351
|
+
// Always async: jco 1.41 doesn't turn a synchronous throw from an async-lowered
|
|
352
|
+
// import into an `err` result, leaving the guest stuck mid-invoke so the next
|
|
353
|
+
// execute panics ("RefCell already borrowed"). A rejection is handled correctly.
|
|
354
|
+
async function invoke(name, argumentsJson) {
|
|
355
|
+
invocations++;
|
|
356
|
+
const state = replay;
|
|
357
|
+
if (!state) {
|
|
358
|
+
return handler(name, argumentsJson);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Decide synchronously, before any await, exactly as the Rust ReplayCallback.
|
|
362
|
+
if (state.suspended !== null) {
|
|
363
|
+
throw new SuspendCallback(
|
|
364
|
+
"execution already suspended by a previous callback",
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
const seq = state.nextSeq++;
|
|
368
|
+
if (!state.liveMode) {
|
|
369
|
+
// Key on the guest's canonical args text verbatim, as the Rust host does.
|
|
370
|
+
const result = state.cached.get(`${name}\0${argumentsJson}`)?.shift();
|
|
371
|
+
if (result !== undefined) {
|
|
372
|
+
state.replayedCount++;
|
|
373
|
+
state.entries[seq] = { name, argsJson: argumentsJson, result };
|
|
374
|
+
return _replayResult(result);
|
|
375
|
+
}
|
|
376
|
+
// First miss: the run has diverged from the journal, so this call and every
|
|
377
|
+
// later one runs live (prevents replaying a now-stale cached result).
|
|
378
|
+
state.liveMode = true;
|
|
379
|
+
}
|
|
380
|
+
return _invokeLive(handler, state, seq, name, argumentsJson);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
return {
|
|
384
|
+
imports: {
|
|
385
|
+
invoke,
|
|
386
|
+
sleep,
|
|
387
|
+
/**
|
|
388
|
+
* List all registered callbacks.
|
|
389
|
+
* This is called by the sandbox runtime when Python code calls list_callbacks().
|
|
390
|
+
*/
|
|
391
|
+
listCallbacks: () => callbacks,
|
|
392
|
+
/**
|
|
393
|
+
* Return execution behavior options for the JavaScript host.
|
|
394
|
+
*
|
|
395
|
+
* JavaScript instances may execute repeatedly with different callbacks, so
|
|
396
|
+
* callback setup is not reused. Tracing is enabled only while a trace
|
|
397
|
+
* handler is registered.
|
|
398
|
+
*/
|
|
399
|
+
getExecutionOptions: () => ({
|
|
400
|
+
pythonTracing: pick("traceHandler") !== null,
|
|
401
|
+
reuseEmptyCallbacks: false,
|
|
402
|
+
}),
|
|
403
|
+
/** Report a trace event from the Python runtime's sys.settrace hook. */
|
|
404
|
+
reportTrace(lineno, eventJson, contextJson) {
|
|
405
|
+
pick("traceHandler")?.(lineno, eventJson, contextJson);
|
|
406
|
+
},
|
|
407
|
+
/**
|
|
408
|
+
* Report streaming output from the Python runtime.
|
|
409
|
+
* This is called by the sandbox runtime on every sys.stdout/stderr.write().
|
|
410
|
+
*
|
|
411
|
+
* @param {number} stream - 0 = stdout, 1 = stderr
|
|
412
|
+
* @param {Uint8Array} data - The raw bytes written
|
|
413
|
+
*/
|
|
414
|
+
reportOutput(stream, data) {
|
|
415
|
+
const handler = outputHandler();
|
|
416
|
+
if (handler) {
|
|
417
|
+
const decoder = stream === 0 ? stdoutDecoder : stderrDecoder;
|
|
418
|
+
handler(stream, decoder.decode(data, { stream: true }));
|
|
419
|
+
}
|
|
420
|
+
},
|
|
421
|
+
},
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Start journaling callbacks, replaying results from `journal` if given.
|
|
425
|
+
* @param {string} [journal] - A journal string from a previous outcome
|
|
426
|
+
*/
|
|
427
|
+
beginReplay(journal) {
|
|
428
|
+
replay = _newReplay(journal);
|
|
429
|
+
},
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Stop journaling and return what was recorded.
|
|
433
|
+
* @param {string} code - The script that produced the journal
|
|
434
|
+
*/
|
|
435
|
+
endReplay(code) {
|
|
436
|
+
const state = replay;
|
|
437
|
+
replay = null;
|
|
438
|
+
return _finishReplay(state, code);
|
|
439
|
+
},
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Deliver any incomplete multi-byte sequence left at the end of an
|
|
443
|
+
* execution (as U+FFFD) and reset the decoders, so it can't merge with the
|
|
444
|
+
* next execution's output. Only call it between executions: mid-run it
|
|
445
|
+
* would cut a character the guest is still writing.
|
|
446
|
+
*/
|
|
447
|
+
/** How many callback calls the code made since this was last called. */
|
|
448
|
+
takeInvocations() {
|
|
449
|
+
const count = invocations;
|
|
450
|
+
invocations = 0;
|
|
451
|
+
return count;
|
|
452
|
+
},
|
|
453
|
+
|
|
454
|
+
flushOutput() {
|
|
455
|
+
const handler = outputHandler();
|
|
456
|
+
const stdoutTail = stdoutDecoder.decode();
|
|
457
|
+
const stderrTail = stderrDecoder.decode();
|
|
458
|
+
if (!handler) return;
|
|
459
|
+
if (stdoutTail) handler(0, stdoutTail);
|
|
460
|
+
if (stderrTail) handler(1, stderrTail);
|
|
461
|
+
},
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Imports bound to the module-wide defaults, for embedders that instantiate the
|
|
466
|
+
// low-level component themselves.
|
|
467
|
+
const _defaultHost = createCallbackHost();
|
|
468
|
+
export const { invoke, listCallbacks, getExecutionOptions, reportTrace, reportOutput } =
|
|
469
|
+
_defaultHost.imports;
|
package/shims/ip.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IP address parsing shared by the network transports.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/** Bytes of an IPv4 address in dotted decimal, or null. */
|
|
6
|
+
export function ipv4Bytes(addr) {
|
|
7
|
+
const parts = addr.split(".");
|
|
8
|
+
if (parts.length !== 4 || !parts.every((p) => /^\d{1,3}$/.test(p) && Number(p) <= 255)) return null;
|
|
9
|
+
return parts.map(Number);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Bytes of an IPv6 address (with an optional dotted IPv4 tail), or null. */
|
|
13
|
+
export function ipv6Bytes(addr) {
|
|
14
|
+
const tail = /^(.*:)(\d+\.\d+\.\d+\.\d+)$/.exec(addr);
|
|
15
|
+
if (tail) {
|
|
16
|
+
const b = ipv4Bytes(tail[2]);
|
|
17
|
+
if (!b) return null;
|
|
18
|
+
addr = `${tail[1]}${((b[0] << 8) | b[1]).toString(16)}:${((b[2] << 8) | b[3]).toString(16)}`;
|
|
19
|
+
}
|
|
20
|
+
const halves = addr.split("::");
|
|
21
|
+
if (halves.length > 2) return null;
|
|
22
|
+
const groups = (s) => (s ? s.split(":") : []);
|
|
23
|
+
const head = groups(halves[0]);
|
|
24
|
+
const rest = halves.length === 2 ? groups(halves[1]) : [];
|
|
25
|
+
const fill = 8 - head.length - rest.length;
|
|
26
|
+
if (halves.length === 2 ? fill < 1 : fill !== 0) return null;
|
|
27
|
+
const all = [...head, ...Array(halves.length === 2 ? fill : 0).fill("0"), ...rest];
|
|
28
|
+
if (!all.every((g) => /^[0-9a-f]{1,4}$/i.test(g))) return null;
|
|
29
|
+
return all.flatMap((g) => [parseInt(g, 16) >> 8, parseInt(g, 16) & 255]);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The bytes of the IPv4 address that IPv6 address bytes `b` stand for on the
|
|
34
|
+
* wire, or null, like the Rust crate's `embedded_ipv4`: IPv4-mapped
|
|
35
|
+
* (`::ffff:a.b.c.d`), IPv4-compatible (`::a.b.c.d`), well-known-prefix NAT64
|
|
36
|
+
* (`64:ff9b::a.b.c.d`, RFC 6052), or the client address of a Teredo address
|
|
37
|
+
* (`2001::/32`, RFC 4380: the last 32 bits, inverted).
|
|
38
|
+
*/
|
|
39
|
+
export function embeddedIPv4(b) {
|
|
40
|
+
const zero = (from, to) => b.slice(from, to).every((x) => x === 0);
|
|
41
|
+
if (zero(0, 10) && b[10] === 255 && b[11] === 255) return b.slice(12);
|
|
42
|
+
const nat64 = b[0] === 0 && b[1] === 0x64 && b[2] === 0xff && b[3] === 0x9b;
|
|
43
|
+
if ((nat64 || zero(0, 4)) && zero(4, 12)) return b.slice(12);
|
|
44
|
+
if (b[0] === 0x20 && b[1] === 0x01 && b[2] === 0 && b[3] === 0) return b.slice(12).map((x) => ~x & 255);
|
|
45
|
+
return null;
|
|
46
|
+
}
|