@nimbus-sh/core 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/runtime/bash-runner.d.ts +63 -0
- package/dist/runtime/bash-runner.d.ts.map +1 -0
- package/dist/runtime/bash-runner.generated.d.ts +14 -0
- package/dist/runtime/bash-runner.generated.d.ts.map +1 -0
- package/dist/runtime/bash-runner.generated.js +13 -0
- package/dist/runtime/bash-runner.js +290 -0
- package/dist/runtime/cpython-runner.d.ts +86 -0
- package/dist/runtime/cpython-runner.d.ts.map +1 -0
- package/dist/runtime/cpython-runner.js +425 -0
- package/dist/runtime/facet-host.d.ts +159 -0
- package/dist/runtime/facet-host.d.ts.map +1 -0
- package/dist/runtime/facet-host.js +22 -0
- package/dist/runtime/installed-runtimes.d.ts +99 -0
- package/dist/runtime/installed-runtimes.d.ts.map +1 -0
- package/dist/runtime/installed-runtimes.js +162 -0
- package/dist/runtime/local-facet-host.d.ts +38 -0
- package/dist/runtime/local-facet-host.d.ts.map +1 -0
- package/dist/runtime/local-facet-host.js +171 -0
- package/dist/runtime/python-pip.d.ts +38 -0
- package/dist/runtime/python-pip.d.ts.map +1 -0
- package/dist/runtime/python-pip.js +1063 -0
- package/dist/runtime/runtime-manifest.d.ts +86 -0
- package/dist/runtime/runtime-manifest.d.ts.map +1 -0
- package/dist/runtime/runtime-manifest.js +72 -0
- package/dist/runtime/runtime-registry.d.ts +161 -0
- package/dist/runtime/runtime-registry.d.ts.map +1 -0
- package/dist/runtime/runtime-registry.js +363 -0
- package/dist/runtime/vfs-snapshot.d.ts.map +1 -1
- package/dist/runtime/vfs-snapshot.js +15 -1
- package/dist/runtime/vfs-supervisor.d.ts +22 -0
- package/dist/runtime/vfs-supervisor.d.ts.map +1 -0
- package/dist/runtime/vfs-supervisor.js +65 -0
- package/dist/runtime/virtual-socket-kernel.generated.d.ts +14 -0
- package/dist/runtime/virtual-socket-kernel.generated.d.ts.map +1 -0
- package/dist/runtime/virtual-socket-kernel.generated.js +13 -0
- package/dist/runtime/wasm-runner.d.ts +80 -0
- package/dist/runtime/wasm-runner.d.ts.map +1 -0
- package/dist/runtime/wasm-runner.js +686 -0
- package/dist/workspace/nimbus-workspace.d.ts +16 -0
- package/dist/workspace/nimbus-workspace.d.ts.map +1 -1
- package/dist/workspace/nimbus-workspace.js +57 -2
- package/package.json +4 -2
- package/src/index.ts +9 -0
- package/src/runtime/bash-runner.generated.ts +14 -0
- package/src/runtime/bash-runner.ts +347 -0
- package/src/runtime/cpython-runner.ts +504 -0
- package/src/runtime/facet-host.ts +170 -0
- package/src/runtime/installed-runtimes.ts +235 -0
- package/src/runtime/local-facet-host.ts +205 -0
- package/src/runtime/python-pip.ts +1211 -0
- package/src/runtime/runtime-manifest.ts +155 -0
- package/src/runtime/runtime-registry.ts +510 -0
- package/src/runtime/vfs-snapshot.ts +15 -1
- package/src/runtime/vfs-supervisor.ts +67 -0
- package/src/runtime/virtual-socket-kernel.generated.ts +14 -0
- package/src/runtime/wasm-runner.ts +835 -0
- package/src/workspace/nimbus-workspace.ts +102 -3
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cpython-runner.ts — `python` / `python3` / `pip`, on CPython 3.13 built for
|
|
3
|
+
* wasm32-wasi.
|
|
4
|
+
*
|
|
5
|
+
* This replaces the Pyodide runner, and the reason is not the interpreter: it
|
|
6
|
+
* is the filesystem. Pyodide is CPython built with Emscripten, so it brings its
|
|
7
|
+
* own MEMFS, and every invocation had to copy the session's files in and diff
|
|
8
|
+
* them back out through vfs-snapshot.ts. That made Python the last runtime with
|
|
9
|
+
* a private, parallel filesystem. This build talks to runtime/wasi/preamble.ts
|
|
10
|
+
* like clang, bash and ruby do — `open()` in Python is the same syscall as
|
|
11
|
+
* `open()` in C — so there is nothing to copy and nothing to diff.
|
|
12
|
+
*
|
|
13
|
+
* What follows from that:
|
|
14
|
+
* - manifestVfs, not snapshotVfs: the facet is given sizes and modes and
|
|
15
|
+
* demand-loads the handful of files the program actually opens.
|
|
16
|
+
* - supervisorPid, not omitSupervisor: a pool without a supervisor can read
|
|
17
|
+
* the seeded manifest and can never write anything back. It looks like it
|
|
18
|
+
* works.
|
|
19
|
+
* - No Python-level socket shim. CPython's _socket is real here, over
|
|
20
|
+
* nimbus-net.c and the host's synthetic paths, so loopback is ordinary
|
|
21
|
+
* socket code rather than a monkey-patch.
|
|
22
|
+
*
|
|
23
|
+
* The interpreter is a WASI reactor (see packages/worker/wasm/python), because
|
|
24
|
+
* a command module's _start runs once and that only covers `python script.py`.
|
|
25
|
+
*
|
|
26
|
+
* FOUR THINGS THIS RUNTIME REDISCOVERED THE HARD WAY, ALL OF WHICH ruby-runner
|
|
27
|
+
* ALREADY KNEW. Read the list rather than finding a fifth:
|
|
28
|
+
* 1. Every entry into the VM goes through WebAssembly.promising, not only the
|
|
29
|
+
* calls known to park — a Suspending import traps on an unpromised stack
|
|
30
|
+
* even when it returns a plain integer.
|
|
31
|
+
* 2. The supervisor is adopted AFTER __wasiInitFS, which clears it on purpose,
|
|
32
|
+
* and the facet drains queued writes in a `finally`.
|
|
33
|
+
* 3. modes are seeded `{ '': 7, tmp: 7, home: 7 }` ahead of the manifest,
|
|
34
|
+
* because manifestVfs's walk skips the empty root — without it the preopen
|
|
35
|
+
* at '/' is mode 0 and every traversal under it is EACCES.
|
|
36
|
+
* 4. The loader pool is built per invocation, never cached: supervisorPid is
|
|
37
|
+
* baked into the SUPERVISOR binding at construction, so a held pool hands
|
|
38
|
+
* every later caller the first caller's write credential.
|
|
39
|
+
*
|
|
40
|
+
* One ordering in cpythonRunFacetFn is load-bearing and looks redundant: the
|
|
41
|
+
* supervisor stub is PUBLISHED on globalThis and only then adopted, because
|
|
42
|
+
* __wasiInitFS clears the adoption on purpose and the boot re-adopts it from
|
|
43
|
+
* there afterwards. Adopting once at the entry and deleting the Reflect.set
|
|
44
|
+
* leaves a guest that reads the seeded filesystem and silently writes nowhere —
|
|
45
|
+
* every write queued, none landed, no error anywhere. Ruby carries the same
|
|
46
|
+
* pair for the same reason. The drain in the `finally` is the other half: a
|
|
47
|
+
* program that wrote a file and then raised still wrote the file.
|
|
48
|
+
*/
|
|
49
|
+
import { resolveVfsPath } from '../vfs/path.js';
|
|
50
|
+
import { hasLeadingCliFlag } from './cli-flags.js';
|
|
51
|
+
import { CPYTHON_PREAMBLE_TAIL } from './cpython-preamble.js';
|
|
52
|
+
import { PYTHON_SERVER_ADAPTER } from './python-server-adapter.js';
|
|
53
|
+
import { requireVfsCred } from './os-contracts.js';
|
|
54
|
+
import { buildPipInvocation, PYTHON_SITE_PACKAGES_ROOT, sessionUsesSciVariant, } from './python-pip.js';
|
|
55
|
+
import { VIRTUAL_SOCKET_KERNEL_SRC } from './virtual-socket-kernel.generated.js';
|
|
56
|
+
import { WASI_INSTANCE_PREAMBLE_SRC } from './wasi-instance.js';
|
|
57
|
+
const PYTHON_VERSION_FLAGS = new Set(['--version', '-V']);
|
|
58
|
+
const PYTHON_HELP_FLAGS = new Set(['--help', '-h']);
|
|
59
|
+
/** Where `nimbus install python` stages the interpreter inside the session. */
|
|
60
|
+
const CPYTHON_WASM_REL = 'share/cpython/python.wasm';
|
|
61
|
+
/**
|
|
62
|
+
* The same interpreter with numpy and markupsafe's C speedups linked in, and
|
|
63
|
+
* their Python half. wasm32-wasi has no dlopen, so a compiled package is either
|
|
64
|
+
* in the binary or unavailable; EXTENSIONS.md has why that beat a runtime
|
|
65
|
+
* linker. Chosen per invocation from what the session has installed, so a
|
|
66
|
+
* session that installed none of it pays none of the 7.8 MiB.
|
|
67
|
+
*/
|
|
68
|
+
const CPYTHON_SCI_WASM_REL = 'share/cpython/python-sci.wasm';
|
|
69
|
+
const CPYTHON_SCI_PACKAGES_REL = 'lib/sci-packages.zip';
|
|
70
|
+
const CPYTHON_STDLIB_REL = 'lib/python313.zip';
|
|
71
|
+
const CPYTHON_CACERT_REL = 'etc/ssl/cert.pem';
|
|
72
|
+
/**
|
|
73
|
+
* The one canonical facet preamble. Composed in exactly one place: a
|
|
74
|
+
* hand-rolled second copy is how ruby-repl once drifted into booting a VM whose
|
|
75
|
+
* language prelude was missing.
|
|
76
|
+
*/
|
|
77
|
+
export function buildCPythonPreamble() {
|
|
78
|
+
return [
|
|
79
|
+
VIRTUAL_SOCKET_KERNEL_SRC,
|
|
80
|
+
WASI_INSTANCE_PREAMBLE_SRC,
|
|
81
|
+
CPYTHON_PREAMBLE_TAIL,
|
|
82
|
+
].join('\n');
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Python's CLI, as far as a sandbox needs it. Flags that only affect an
|
|
86
|
+
* interactive tty or bytecode caching are accepted and ignored; anything else
|
|
87
|
+
* is refused by name rather than silently doing something different.
|
|
88
|
+
*/
|
|
89
|
+
function parsePythonArgv(argv) {
|
|
90
|
+
const fail = (error) => ({ mode: 'inline', inlineCode: '', scriptPath: '', scriptArgs: [], exitCode: 2, error });
|
|
91
|
+
let i = 0;
|
|
92
|
+
while (i < argv.length) {
|
|
93
|
+
const a = argv[i];
|
|
94
|
+
if (a === '-c') {
|
|
95
|
+
const code = argv[i + 1];
|
|
96
|
+
if (code === undefined)
|
|
97
|
+
return fail('Argument expected for the -c option');
|
|
98
|
+
return { mode: 'inline', inlineCode: code, scriptPath: '', scriptArgs: argv.slice(i + 2), exitCode: 0 };
|
|
99
|
+
}
|
|
100
|
+
if (a === '-m') {
|
|
101
|
+
const mod = argv[i + 1];
|
|
102
|
+
if (mod === undefined)
|
|
103
|
+
return fail('Argument expected for the -m option');
|
|
104
|
+
const rest = argv.slice(i + 2);
|
|
105
|
+
const inlineCode = [
|
|
106
|
+
'import runpy, sys',
|
|
107
|
+
`sys.argv = ${JSON.stringify([mod, ...rest])}`,
|
|
108
|
+
`runpy.run_module(${JSON.stringify(mod)}, run_name='__main__', alter_sys=True)`,
|
|
109
|
+
].join('\n');
|
|
110
|
+
return { mode: 'inline', inlineCode, scriptPath: '', scriptArgs: rest, exitCode: 0 };
|
|
111
|
+
}
|
|
112
|
+
if (a === '-') {
|
|
113
|
+
return { mode: 'stdin', inlineCode: '', scriptPath: '-', scriptArgs: argv.slice(i + 1), exitCode: 0 };
|
|
114
|
+
}
|
|
115
|
+
if (!a.startsWith('-')) {
|
|
116
|
+
return { mode: 'script', inlineCode: '', scriptPath: a, scriptArgs: argv.slice(i + 1), exitCode: 0 };
|
|
117
|
+
}
|
|
118
|
+
if (/^-[OBuEItcsx]+$/.test(a)) {
|
|
119
|
+
i++;
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
return fail(`unknown option: ${a}`);
|
|
123
|
+
}
|
|
124
|
+
// No mode argument. init.ts routes a bare `python` to the REPL before it gets
|
|
125
|
+
// here, so reaching this point means flags without a program.
|
|
126
|
+
return fail("no program given. Use 'python -c \"code\"', 'python -m <module>' or 'python script.py'.");
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Whether this invocation should get a resident process rather than a one-shot.
|
|
130
|
+
* A script or `-m` can bind a port and keep serving; pip never does.
|
|
131
|
+
*/
|
|
132
|
+
function shouldRunAsResidentProcess(argv, parsed, pipMode) {
|
|
133
|
+
if (pipMode)
|
|
134
|
+
return false;
|
|
135
|
+
if (parsed.mode === 'script')
|
|
136
|
+
return true;
|
|
137
|
+
if (argv[0] === '-m' && argv[1] && argv[1] !== 'pip')
|
|
138
|
+
return true;
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
/** `python -m pip ...` is pip, reached the long way round. */
|
|
142
|
+
async function buildPythonModulePipInvocation(argv, cwd, vfs, runtimeContext) {
|
|
143
|
+
if (argv[0] !== '-m' || argv[1] !== 'pip')
|
|
144
|
+
return { mode: 'none', code: '', exitCode: 0 };
|
|
145
|
+
return await buildPipInvocation(argv.slice(2), 'pip', cwd, vfs, runtimeContext);
|
|
146
|
+
}
|
|
147
|
+
function toArrayBuffer(bytes) {
|
|
148
|
+
return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
|
|
149
|
+
}
|
|
150
|
+
function errorMessage(error) {
|
|
151
|
+
return error instanceof Error ? error.message : String(error);
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Facet-side entry. Serialized with fn.toString(), so it captures nothing and
|
|
155
|
+
* names no import: everything it needs is on globalThis, put there by the
|
|
156
|
+
* preamble.
|
|
157
|
+
*/
|
|
158
|
+
async function cpythonRunFacetFn(args, facetEnv) {
|
|
159
|
+
const run = Reflect.get(globalThis, '__cpythonRun');
|
|
160
|
+
if (typeof run !== 'function') {
|
|
161
|
+
return {
|
|
162
|
+
stdout: '', stderr: '', exitCode: 127,
|
|
163
|
+
error: 'cpython preamble missing: __cpythonRun not in scope',
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
const adopt = Reflect.get(globalThis, '__wasiAdoptSupervisor');
|
|
167
|
+
const drain = Reflect.get(globalThis, '__wasiDrainPersist');
|
|
168
|
+
const supervisor = facetEnv && facetEnv.SUPERVISOR;
|
|
169
|
+
// Published where the boot re-adopts it after the mount: adopting only here
|
|
170
|
+
// would be undone by __wasiInitFS, which clears it on purpose.
|
|
171
|
+
if (supervisor)
|
|
172
|
+
Reflect.set(globalThis, '__nimbusPySupervisor', supervisor);
|
|
173
|
+
adopt?.(supervisor);
|
|
174
|
+
try {
|
|
175
|
+
return await run(args);
|
|
176
|
+
}
|
|
177
|
+
finally {
|
|
178
|
+
// In `finally`, not on the success path: a program that wrote a file and
|
|
179
|
+
// then raised still wrote the file, and those bytes are the user's.
|
|
180
|
+
await drain?.();
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
export function makeCPythonRunnerFactory(deps) {
|
|
184
|
+
return function cpythonRunnerFactory(manifest, installRoot, binName, _binKind) {
|
|
185
|
+
const findFile = (rel) => {
|
|
186
|
+
const entry = manifest.files.find((f) => f.path === rel);
|
|
187
|
+
return entry ? `${installRoot}/${entry.path}` : null;
|
|
188
|
+
};
|
|
189
|
+
const baseWasmVfs = findFile(CPYTHON_WASM_REL);
|
|
190
|
+
const sciWasmVfs = findFile(CPYTHON_SCI_WASM_REL);
|
|
191
|
+
const sciPackagesVfs = findFile(CPYTHON_SCI_PACKAGES_REL);
|
|
192
|
+
const stdlibVfs = findFile(CPYTHON_STDLIB_REL);
|
|
193
|
+
let seedCache = null;
|
|
194
|
+
return async function cpythonBinHandler(ctx) {
|
|
195
|
+
const cred = requireVfsCred(ctx.cred, binName);
|
|
196
|
+
const credKey = `${cred.uid}:${cred.gid}:${cred.groups.join(',')}`;
|
|
197
|
+
const vfs = deps.vfs.as(cred);
|
|
198
|
+
const argv = ctx.args || [];
|
|
199
|
+
const cwd = ctx.cwd || '/home/user';
|
|
200
|
+
const pipRuntimeContext = {
|
|
201
|
+
// No Pyodide lockfile: there is no curated wheel index behind this
|
|
202
|
+
// interpreter, so pip resolves against PyPI like anywhere else.
|
|
203
|
+
pyodideLockfileText: null,
|
|
204
|
+
runtimeArtifacts: manifest.runtime_artifacts || [],
|
|
205
|
+
};
|
|
206
|
+
const isPipBin = _binKind === 'pip' || binName === 'pip' || binName === 'pip3';
|
|
207
|
+
const pipInvocation = isPipBin
|
|
208
|
+
? await buildPipInvocation(argv, binName, cwd, vfs, pipRuntimeContext)
|
|
209
|
+
: await buildPythonModulePipInvocation(argv, cwd, vfs, pipRuntimeContext);
|
|
210
|
+
if (pipInvocation.error) {
|
|
211
|
+
ctx.stderr.write(`${binName}: ${pipInvocation.error}\n`);
|
|
212
|
+
return pipInvocation.exitCode;
|
|
213
|
+
}
|
|
214
|
+
if (pipInvocation.mode !== 'pip' && hasLeadingCliFlag(argv, PYTHON_VERSION_FLAGS)) {
|
|
215
|
+
ctx.stdout.write('Python 3.13.14 (CPython, wasm32-wasi, Nimbus runtime)\n');
|
|
216
|
+
return 0;
|
|
217
|
+
}
|
|
218
|
+
if (pipInvocation.mode !== 'pip' && hasLeadingCliFlag(argv, PYTHON_HELP_FLAGS)) {
|
|
219
|
+
ctx.stdout.write(`usage: ${binName} [option] ... [-c cmd | -m mod | file | -] [arg] ...\n`);
|
|
220
|
+
ctx.stdout.write('Nimbus CPython 3.13 runtime (wasm32-wasi).\n');
|
|
221
|
+
ctx.stdout.write('Supported: -c <code>, -m <module>, <file.py>, stdin via -, and the session filesystem directly.\n');
|
|
222
|
+
ctx.stdout.write('zlib, lzma, bz2, hashlib, ssl, sqlite3 and sockets are built in; pure-Python wheels install with pip.\n');
|
|
223
|
+
return 0;
|
|
224
|
+
}
|
|
225
|
+
// Which interpreter this invocation gets. The sci variant is chosen from
|
|
226
|
+
// what the session has installed, never from a guess about what this
|
|
227
|
+
// program will import — the whole point of keying on state is that it is
|
|
228
|
+
// also right for `python -c` naming a module in a variable. Falling back
|
|
229
|
+
// when the variant is absent keeps a session installed before the variant
|
|
230
|
+
// shipped working, on the base interpreter, rather than failing to start.
|
|
231
|
+
const wantsSci = sessionUsesSciVariant(vfs)
|
|
232
|
+
&& sciWasmVfs !== null && vfs.exists(sciWasmVfs);
|
|
233
|
+
const wasmVfs = wantsSci ? sciWasmVfs : baseWasmVfs;
|
|
234
|
+
const sciPackagesPath = wantsSci && sciPackagesVfs && vfs.exists(sciPackagesVfs)
|
|
235
|
+
? sciPackagesVfs
|
|
236
|
+
: null;
|
|
237
|
+
if (!wasmVfs || !vfs.exists(wasmVfs)) {
|
|
238
|
+
ctx.stderr.write(`${binName}: python.wasm missing (re-run 'nimbus install python')\n`);
|
|
239
|
+
return 127;
|
|
240
|
+
}
|
|
241
|
+
if (!stdlibVfs || !vfs.exists(stdlibVfs)) {
|
|
242
|
+
ctx.stderr.write(`${binName}: python313.zip missing (re-run 'nimbus install python')\n`);
|
|
243
|
+
return 127;
|
|
244
|
+
}
|
|
245
|
+
const parsed = pipInvocation.mode === 'pip'
|
|
246
|
+
? { mode: 'inline', inlineCode: pipInvocation.code, scriptPath: '', scriptArgs: [], exitCode: 0 }
|
|
247
|
+
: parsePythonArgv(argv);
|
|
248
|
+
if (parsed.error) {
|
|
249
|
+
ctx.stderr.write(`${binName}: ${parsed.error}\n`);
|
|
250
|
+
return parsed.exitCode;
|
|
251
|
+
}
|
|
252
|
+
let userCode = '';
|
|
253
|
+
let progName = binName;
|
|
254
|
+
let pyArgv = [binName];
|
|
255
|
+
if (parsed.mode === 'inline') {
|
|
256
|
+
userCode = parsed.inlineCode;
|
|
257
|
+
pyArgv = ['-c', ...parsed.scriptArgs];
|
|
258
|
+
}
|
|
259
|
+
else if (parsed.mode === 'script') {
|
|
260
|
+
const absPath = resolveVfsPath(parsed.scriptPath, cwd);
|
|
261
|
+
try {
|
|
262
|
+
if (!vfs.exists(absPath)) {
|
|
263
|
+
ctx.stderr.write(`${binName}: can't open file '${parsed.scriptPath}': [Errno 2] No such file or directory\n`);
|
|
264
|
+
return 2;
|
|
265
|
+
}
|
|
266
|
+
userCode = new TextDecoder('utf-8').decode(vfs.readFile(absPath));
|
|
267
|
+
}
|
|
268
|
+
catch (e) {
|
|
269
|
+
ctx.stderr.write(`${binName}: ${parsed.scriptPath}: ${errorMessage(e)}\n`);
|
|
270
|
+
return 1;
|
|
271
|
+
}
|
|
272
|
+
progName = parsed.scriptPath;
|
|
273
|
+
pyArgv = [parsed.scriptPath, ...parsed.scriptArgs];
|
|
274
|
+
}
|
|
275
|
+
else {
|
|
276
|
+
const stdinReader = ctx.stdin;
|
|
277
|
+
userCode = (stdinReader && typeof stdinReader.read === 'function' ? await stdinReader.read() : '') ?? '';
|
|
278
|
+
pyArgv = ['-', ...parsed.scriptArgs];
|
|
279
|
+
}
|
|
280
|
+
// sys.argv is set from Python rather than from WASI argv: the reactor has
|
|
281
|
+
// no argv of its own, and this keeps the one place that decides what the
|
|
282
|
+
// program sees in TypeScript.
|
|
283
|
+
const prelude = [
|
|
284
|
+
PYTHON_SERVER_ADAPTER,
|
|
285
|
+
'import sys',
|
|
286
|
+
`sys.argv = ${JSON.stringify(pyArgv)}`,
|
|
287
|
+
// The variant's own packages. zipimport reads them straight out of the
|
|
288
|
+
// session filesystem, so they need no unpacking and no manifest entry
|
|
289
|
+
// beyond the archive itself.
|
|
290
|
+
...(sciPackagesPath
|
|
291
|
+
? [`sys.path.insert(0, ${JSON.stringify(`/${sciPackagesPath.replace(/^\/+/, '')}`)})`]
|
|
292
|
+
: []),
|
|
293
|
+
`sys.path.insert(0, ${JSON.stringify(`/${PYTHON_SITE_PACKAGES_ROOT}`)})`,
|
|
294
|
+
`sys.path.insert(0, ${JSON.stringify(cwd)})`,
|
|
295
|
+
// WASI has no process cwd, so wasi-libc starts every guest at '/'.
|
|
296
|
+
// Leaving it there silently reroutes every relative path a program
|
|
297
|
+
// opens — the shell says the user is in /home/user and Python resolves
|
|
298
|
+
// against the root.
|
|
299
|
+
'import os',
|
|
300
|
+
'try:',
|
|
301
|
+
` os.chdir(${JSON.stringify(cwd)})`,
|
|
302
|
+
'except OSError:',
|
|
303
|
+
' pass',
|
|
304
|
+
].join('\n');
|
|
305
|
+
const cacertVfs = findFile(CPYTHON_CACERT_REL);
|
|
306
|
+
const userEnv = { ...(ctx.env || {}) };
|
|
307
|
+
if (!userEnv.HOME)
|
|
308
|
+
userEnv.HOME = '/home/user';
|
|
309
|
+
if (!userEnv.PYTHONUNBUFFERED)
|
|
310
|
+
userEnv.PYTHONUNBUFFERED = '1';
|
|
311
|
+
// Without this OpenSSL has no trust anchors at all — there is no
|
|
312
|
+
// /etc/ssl on a Nimbus session — and every HTTPS request fails
|
|
313
|
+
// verification with a message about a missing local issuer rather than
|
|
314
|
+
// about a missing bundle.
|
|
315
|
+
if (!userEnv.SSL_CERT_FILE && cacertVfs)
|
|
316
|
+
userEnv.SSL_CERT_FILE = `/${cacertVfs.replace(/^\/+/, '')}`;
|
|
317
|
+
// A manifest, not a copy: sizes and modes only, with the facet demand-
|
|
318
|
+
// loading whatever the program opens. The stdlib zip is covered by it
|
|
319
|
+
// like any other file, which is the whole point of not having a private
|
|
320
|
+
// filesystem any more.
|
|
321
|
+
const stdlibDir = stdlibVfs.replace(/\/[^/]+$/, '');
|
|
322
|
+
// Every runtime file the interpreter is told about has to be a root of
|
|
323
|
+
// its own. The trust store was reachable only while the cwd happened to
|
|
324
|
+
// be an ancestor of the install — from ~ the walk swept the whole runtime
|
|
325
|
+
// tree in — so `cd` into any subdirectory and SSL_CERT_FILE pointed at a
|
|
326
|
+
// path the facet could not see, and every pip install failed
|
|
327
|
+
// CERTIFICATE_VERIFY_FAILED with the bundle sitting right there.
|
|
328
|
+
const cacertDir = cacertVfs ? cacertVfs.replace(/\/[^/]+$/, '') : null;
|
|
329
|
+
const revision = Math.max(vfs.revision(cwd), vfs.revision(PYTHON_SITE_PACKAGES_ROOT), vfs.revision(stdlibVfs));
|
|
330
|
+
let fsSeed = seedCache && seedCache.cred === credKey
|
|
331
|
+
&& seedCache.cwd === cwd && seedCache.revision === revision
|
|
332
|
+
? seedCache.result
|
|
333
|
+
: null;
|
|
334
|
+
if (!fsSeed) {
|
|
335
|
+
// The host decides what "seed" means: a manifest the facet demand-loads
|
|
336
|
+
// against, or the bytes themselves. Which one it is follows from
|
|
337
|
+
// whether the host can park a guest mid-syscall, and nothing here
|
|
338
|
+
// depends on the answer.
|
|
339
|
+
fsSeed = deps.facets.seedFilesystem(vfs, cwd, {
|
|
340
|
+
extraRoots: [PYTHON_SITE_PACKAGES_ROOT, stdlibDir, ...(cacertDir ? [cacertDir] : [])],
|
|
341
|
+
revision,
|
|
342
|
+
});
|
|
343
|
+
seedCache = { cred: credKey, cwd, revision, result: fsSeed };
|
|
344
|
+
}
|
|
345
|
+
if ('error' in fsSeed) {
|
|
346
|
+
ctx.stderr.write(`${binName}: ${fsSeed.error}\n`);
|
|
347
|
+
return 1;
|
|
348
|
+
}
|
|
349
|
+
const snapshot = fsSeed.snapshot;
|
|
350
|
+
// Opened per invocation, not cached: the supervisor capability is bound
|
|
351
|
+
// to this process's pid when the facet opens, so one held across calls
|
|
352
|
+
// would hand every later caller the first caller's write credential.
|
|
353
|
+
const facet = deps.facets.open({
|
|
354
|
+
// The variant is in the tag because a host's constructor-time wasm
|
|
355
|
+
// fingerprint is name:length:first-byte:last-byte, not a content hash.
|
|
356
|
+
// Two variants differ by megabytes so they would not collide today, but
|
|
357
|
+
// a warm slot serving the wrong interpreter is not a failure worth
|
|
358
|
+
// leaving to a size coincidence.
|
|
359
|
+
tag: wantsSci ? 'cpython-runner:sci' : 'cpython-runner',
|
|
360
|
+
concurrency: 1,
|
|
361
|
+
// Never absent. Without the capability the facet reads its seed and can
|
|
362
|
+
// never write anything back — the program appears to run and its output
|
|
363
|
+
// never reaches the session.
|
|
364
|
+
syscalls: { vfs, pid: ctx.pid },
|
|
365
|
+
preamble: buildCPythonPreamble(),
|
|
366
|
+
wasmModules: { 'python.wasm': toArrayBuffer(vfs.readFile(wasmVfs)) },
|
|
367
|
+
});
|
|
368
|
+
const facetArgs = {
|
|
369
|
+
userCode: `${prelude}\n${userCode}`,
|
|
370
|
+
pyArgv,
|
|
371
|
+
userEnv,
|
|
372
|
+
progName,
|
|
373
|
+
cwd,
|
|
374
|
+
pythonHome: `/${installRoot.replace(/^\/+/, '')}`,
|
|
375
|
+
supervisorPid: ctx.pid,
|
|
376
|
+
fsSnapshot: snapshot,
|
|
377
|
+
};
|
|
378
|
+
// A script or `-m` can bind a port and keep serving, and such a program
|
|
379
|
+
// is not finished when it stops producing output — it is finished when it
|
|
380
|
+
// stops running. Pyodide gave those a dedicated socket process; that
|
|
381
|
+
// spawn is not ported yet, so for now they get a one-shot facet with a
|
|
382
|
+
// budget long enough not to cut a server off mid-request. Porting the
|
|
383
|
+
// resident process is the last piece, and until it lands a server holds
|
|
384
|
+
// its facet rather than being driven by inbound requests.
|
|
385
|
+
const resident = shouldRunAsResidentProcess(argv, parsed, pipInvocation.mode === 'pip');
|
|
386
|
+
if (resident) {
|
|
387
|
+
facet.dispose();
|
|
388
|
+
if (!deps.startResident) {
|
|
389
|
+
ctx.stderr.write(`${binName}: this program keeps running after it starts, and this host has no `
|
|
390
|
+
+ 'process substrate to keep it on\n');
|
|
391
|
+
return 1;
|
|
392
|
+
}
|
|
393
|
+
const command = [binName, ...argv].map((part) => (/^[A-Za-z0-9_./:=@+-]+$/.test(part) ? part : JSON.stringify(part))).join(' ');
|
|
394
|
+
const spawnResult = await deps.startResident({ wasmVfsPath: wasmVfs, startArgs: facetArgs, cwd, command });
|
|
395
|
+
if (spawnResult.stdout)
|
|
396
|
+
ctx.stdout.write(spawnResult.stdout);
|
|
397
|
+
if (spawnResult.stderr)
|
|
398
|
+
ctx.stderr.write(spawnResult.stderr);
|
|
399
|
+
return spawnResult.exitCode;
|
|
400
|
+
}
|
|
401
|
+
let result;
|
|
402
|
+
try {
|
|
403
|
+
result = await facet.submit(cpythonRunFacetFn, facetArgs, {
|
|
404
|
+
timeoutMs: 120_000,
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
catch (e) {
|
|
408
|
+
ctx.stderr.write(`${binName}: ${errorMessage(e)}\n`);
|
|
409
|
+
return 1;
|
|
410
|
+
}
|
|
411
|
+
finally {
|
|
412
|
+
facet.dispose();
|
|
413
|
+
}
|
|
414
|
+
if (result.stdout)
|
|
415
|
+
ctx.stdout.write(result.stdout);
|
|
416
|
+
if (result.stderr)
|
|
417
|
+
ctx.stderr.write(result.stderr);
|
|
418
|
+
if (result.error) {
|
|
419
|
+
ctx.stderr.write(`${binName}: ${result.error}\n`);
|
|
420
|
+
return result.exitCode || 1;
|
|
421
|
+
}
|
|
422
|
+
return result.exitCode;
|
|
423
|
+
};
|
|
424
|
+
};
|
|
425
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* facet-host.ts — where a program carrying compiled WebAssembly actually runs.
|
|
3
|
+
*
|
|
4
|
+
* Every WASM runtime Nimbus ships (bash, CPython, Ruby, clang, the generic
|
|
5
|
+
* `wasm-runner`) has the same shape: a self-contained scheduler shipped as
|
|
6
|
+
* SOURCE, a table of wasm modules that must be compiled before the scheduler
|
|
7
|
+
* can reach them, and a function submitted into that scope with plain data and
|
|
8
|
+
* back. Only the substrate underneath differs.
|
|
9
|
+
*
|
|
10
|
+
* On workerd that substrate is a dynamic worker: request-time
|
|
11
|
+
* `WebAssembly.instantiate(bytes)` is refused by CSP, so the bytes ride inside
|
|
12
|
+
* the loader's modules map and are compiled during the inner worker's
|
|
13
|
+
* module-load phase — the one phase where wasm code generation is permitted.
|
|
14
|
+
* Off workerd there is no such rule and the same scope is built in place
|
|
15
|
+
* ({@link ./local-facet-host.ts}).
|
|
16
|
+
*
|
|
17
|
+
* NOT named after either. `Facet` is what Nimbus has always called this — one
|
|
18
|
+
* program, its own module scope, its own wasm table — and the port is the
|
|
19
|
+
* whole of what a runtime needs from it, so a runner cannot tell which
|
|
20
|
+
* substrate answered and never asks.
|
|
21
|
+
*/
|
|
22
|
+
import type { CredentialedVfs } from '../vfs/sqlite-vfs.js';
|
|
23
|
+
import type { WasiFsSnapshot } from './wasi-instance.js';
|
|
24
|
+
import type { WasiParking } from './wasi/types.js';
|
|
25
|
+
/**
|
|
26
|
+
* A function submitted into a facet.
|
|
27
|
+
*
|
|
28
|
+
* It is SERIALIZED on any host that runs it elsewhere, so it must be
|
|
29
|
+
* self-contained: closure references do not survive the crossing, and neither
|
|
30
|
+
* do module imports. Names the spec's `preamble` declares ARE in scope — that
|
|
31
|
+
* is what the preamble is for — and everything else travels as `args`.
|
|
32
|
+
*
|
|
33
|
+
* `bindings` carries whatever capabilities the host minted for this facet;
|
|
34
|
+
* `SUPERVISOR` is the session syscall capability, present only when the spec
|
|
35
|
+
* named a pid to act as.
|
|
36
|
+
*/
|
|
37
|
+
export type FacetFn<A, R> = (args: A, bindings: FacetBindings) => R | Promise<R>;
|
|
38
|
+
/** Capabilities handed to the facet's function as its second argument. */
|
|
39
|
+
export interface FacetBindings {
|
|
40
|
+
/** The session's syscall capability, bound to {@link FacetSpec.supervisorPid}. */
|
|
41
|
+
readonly SUPERVISOR?: unknown;
|
|
42
|
+
}
|
|
43
|
+
export interface FacetSpec {
|
|
44
|
+
/** Names the facet in diagnostics and in the host's own reuse key. */
|
|
45
|
+
tag: string;
|
|
46
|
+
/**
|
|
47
|
+
* Source evaluated once, before any function is submitted, in the scope those
|
|
48
|
+
* functions are evaluated in. This is how a runtime's scheduler and its WASI
|
|
49
|
+
* layer get there: they are far too large to travel per call, and they hold
|
|
50
|
+
* the state a session needs between calls (bash's process tree, CPython's
|
|
51
|
+
* `__main__`).
|
|
52
|
+
*/
|
|
53
|
+
preamble?: string;
|
|
54
|
+
/**
|
|
55
|
+
* Wasm images the facet's scope exposes as compiled `WebAssembly.Module`s on
|
|
56
|
+
* `globalThis.__NIMBUS_WASM[<key>]`. Compiled by the host, because on workerd
|
|
57
|
+
* the caller is not allowed to.
|
|
58
|
+
*/
|
|
59
|
+
wasmModules?: Record<string, ArrayBuffer>;
|
|
60
|
+
/**
|
|
61
|
+
* The session filesystem this facet's syscalls act on, and the process they
|
|
62
|
+
* act as. Absent for a facet that makes no syscall back into the session.
|
|
63
|
+
*
|
|
64
|
+
* Both halves, because the two hosts reach the same authority differently: a
|
|
65
|
+
* dynamic worker is a different isolate, so it is handed a capability minted
|
|
66
|
+
* for the PID and routed back to the session; a facet in the caller's own
|
|
67
|
+
* isolate is handed the credentialed VIEW. Naming only the pid would leave
|
|
68
|
+
* the second host nothing to serve from, and naming only the view would
|
|
69
|
+
* leave the first nothing to mint.
|
|
70
|
+
*
|
|
71
|
+
* Not a boolean: the supervisor derives the WRITE credential from the pid,
|
|
72
|
+
* so a facet given the capability without one can read the filesystem it was
|
|
73
|
+
* seeded with and silently write nowhere.
|
|
74
|
+
*/
|
|
75
|
+
syscalls?: FacetSyscalls;
|
|
76
|
+
/** Facets the host may keep warm for this spec. Default 1. */
|
|
77
|
+
concurrency?: number;
|
|
78
|
+
}
|
|
79
|
+
/** The session a facet's syscalls reach, and who they reach it as. */
|
|
80
|
+
export interface FacetSyscalls {
|
|
81
|
+
readonly vfs: CredentialedVfs;
|
|
82
|
+
readonly pid: number;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* A seeded WASI filesystem: what {@link FacetHost.seedFilesystem} produced, and
|
|
86
|
+
* how much of the session it had to carry to produce it.
|
|
87
|
+
*/
|
|
88
|
+
export interface FacetFilesystemSeed {
|
|
89
|
+
snapshot: WasiFsSnapshot;
|
|
90
|
+
files: number;
|
|
91
|
+
bytes: number;
|
|
92
|
+
}
|
|
93
|
+
/** What a runner knows about the subtree its guest should see. */
|
|
94
|
+
export interface FacetFilesystemOptions {
|
|
95
|
+
/** Directories outside `root` the program must also reach. */
|
|
96
|
+
extraRoots?: Iterable<string>;
|
|
97
|
+
/**
|
|
98
|
+
* The session revision the seed describes, when the caller has computed one.
|
|
99
|
+
* A host that serves reads back stamps it, which is what marks the seed a
|
|
100
|
+
* CACHE rather than the whole world; one that cannot has no use for it.
|
|
101
|
+
*/
|
|
102
|
+
revision?: number;
|
|
103
|
+
}
|
|
104
|
+
export interface FacetSubmitOptions {
|
|
105
|
+
/**
|
|
106
|
+
* Deadline for this call, honoured by hosts that can abandon a facet.
|
|
107
|
+
*
|
|
108
|
+
* A host sharing the caller's thread cannot: a wasm guest in a synchronous
|
|
109
|
+
* loop holds the only thread there is, and nothing observes a timer until it
|
|
110
|
+
* yields. Such a host says so ({@link ./local-facet-host.ts}) rather than
|
|
111
|
+
* racing a timer and returning while the guest runs on.
|
|
112
|
+
*/
|
|
113
|
+
timeoutMs?: number;
|
|
114
|
+
/** Wasm images for this call alone, merged over {@link FacetSpec.wasmModules}. */
|
|
115
|
+
wasmModules?: Record<string, ArrayBuffer>;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* One facet's scope, for as long as a runtime needs it.
|
|
119
|
+
*
|
|
120
|
+
* Held rather than per-call because the scope IS the session: bash boots on the
|
|
121
|
+
* first submit and is fed on every one after, and a scope rebuilt between them
|
|
122
|
+
* would hand the second call a shell that had never run.
|
|
123
|
+
*/
|
|
124
|
+
export interface Facet {
|
|
125
|
+
submit<A, R>(fn: FacetFn<A, R>, args: A, options?: FacetSubmitOptions): Promise<Awaited<R>>;
|
|
126
|
+
/** Idempotent. The scope and everything it holds are dropped. */
|
|
127
|
+
dispose(): void;
|
|
128
|
+
}
|
|
129
|
+
export interface FacetHost {
|
|
130
|
+
/**
|
|
131
|
+
* Whether a guest in this host can be SUSPENDED in the middle of a syscall.
|
|
132
|
+
*
|
|
133
|
+
* The one place the substrates are not interchangeable, so it is stated
|
|
134
|
+
* rather than smoothed over — the same posture as `ProcessImageDelivery` in
|
|
135
|
+
* the process fabric. Everything else about running a wasm program is the
|
|
136
|
+
* same code either way; this is not, and it decides two things at once:
|
|
137
|
+
* which import table the guest gets ({@link WasiParking}), and how much
|
|
138
|
+
* filesystem it must be handed before it starts.
|
|
139
|
+
*
|
|
140
|
+
* `jspi` — the host can park the guest on a promise, so a syscall may go
|
|
141
|
+
* back to the session mid-instruction and the seed can be a manifest.
|
|
142
|
+
* `none` — it cannot; V8 traps any call into a suspending import off a
|
|
143
|
+
* stack `WebAssembly.promising` did not enter. Every syscall must answer
|
|
144
|
+
* synchronously, so the seed has to BE the filesystem.
|
|
145
|
+
*/
|
|
146
|
+
readonly parking: WasiParking;
|
|
147
|
+
/**
|
|
148
|
+
* Hand a facet the part of the session filesystem its program needs.
|
|
149
|
+
*
|
|
150
|
+
* The host decides the strategy, because the strategy IS the consequence of
|
|
151
|
+
* {@link FacetHost.parking} and nothing about the program bears on it. A
|
|
152
|
+
* runner names the roots and gets a seed; it never learns which kind it got.
|
|
153
|
+
*/
|
|
154
|
+
seedFilesystem(vfs: CredentialedVfs, root: string, options?: FacetFilesystemOptions): FacetFilesystemSeed | {
|
|
155
|
+
error: string;
|
|
156
|
+
};
|
|
157
|
+
open(spec: FacetSpec): Facet;
|
|
158
|
+
}
|
|
159
|
+
//# sourceMappingURL=facet-host.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"facet-host.d.ts","sourceRoot":"","sources":["../../src/runtime/facet-host.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAC5D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACzD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAEnD;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,OAAO,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,EAAE,QAAQ,EAAE,aAAa,KAAK,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;AAEjF,0EAA0E;AAC1E,MAAM,WAAW,aAAa;IAC5B,kFAAkF;IAClF,QAAQ,CAAC,UAAU,CAAC,EAAE,OAAO,CAAC;CAC/B;AAED,MAAM,WAAW,SAAS;IACxB,sEAAsE;IACtE,GAAG,EAAE,MAAM,CAAC;IACZ;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;IAC1C;;;;;;;;;;;;;;OAcG;IACH,QAAQ,CAAC,EAAE,aAAa,CAAC;IACzB,8DAA8D;IAC9D,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,sEAAsE;AACtE,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,GAAG,EAAE,eAAe,CAAC;IAC9B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;CACtB;AAED;;;GAGG;AACH,MAAM,WAAW,mBAAmB;IAClC,QAAQ,EAAE,cAAc,CAAC;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;CACf;AAED,kEAAkE;AAClE,MAAM,WAAW,sBAAsB;IACrC,8DAA8D;IAC9D,UAAU,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;IAC9B;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,kBAAkB;IACjC;;;;;;;OAOG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,kFAAkF;IAClF,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;CAC3C;AAED;;;;;;GAMG;AACH,MAAM,WAAW,KAAK;IACpB,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,EAAE,OAAO,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,OAAO,CAAC,EAAE,kBAAkB,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC;IAC5F,iEAAiE;IACjE,OAAO,IAAI,IAAI,CAAC;CACjB;AAED,MAAM,WAAW,SAAS;IACxB;;;;;;;;;;;;;;;OAeG;IACH,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAC;IAC9B;;;;;;OAMG;IACH,cAAc,CACZ,GAAG,EAAE,eAAe,EACpB,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE,sBAAsB,GAC/B,mBAAmB,GAAG;QAAE,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;IAC3C,IAAI,CAAC,IAAI,EAAE,SAAS,GAAG,KAAK,CAAC;CAC9B"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* facet-host.ts — where a program carrying compiled WebAssembly actually runs.
|
|
3
|
+
*
|
|
4
|
+
* Every WASM runtime Nimbus ships (bash, CPython, Ruby, clang, the generic
|
|
5
|
+
* `wasm-runner`) has the same shape: a self-contained scheduler shipped as
|
|
6
|
+
* SOURCE, a table of wasm modules that must be compiled before the scheduler
|
|
7
|
+
* can reach them, and a function submitted into that scope with plain data and
|
|
8
|
+
* back. Only the substrate underneath differs.
|
|
9
|
+
*
|
|
10
|
+
* On workerd that substrate is a dynamic worker: request-time
|
|
11
|
+
* `WebAssembly.instantiate(bytes)` is refused by CSP, so the bytes ride inside
|
|
12
|
+
* the loader's modules map and are compiled during the inner worker's
|
|
13
|
+
* module-load phase — the one phase where wasm code generation is permitted.
|
|
14
|
+
* Off workerd there is no such rule and the same scope is built in place
|
|
15
|
+
* ({@link ./local-facet-host.ts}).
|
|
16
|
+
*
|
|
17
|
+
* NOT named after either. `Facet` is what Nimbus has always called this — one
|
|
18
|
+
* program, its own module scope, its own wasm table — and the port is the
|
|
19
|
+
* whole of what a runtime needs from it, so a runner cannot tell which
|
|
20
|
+
* substrate answered and never asks.
|
|
21
|
+
*/
|
|
22
|
+
export {};
|