@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.
Files changed (60) hide show
  1. package/dist/index.d.ts +2 -0
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +1 -0
  4. package/dist/runtime/bash-runner.d.ts +63 -0
  5. package/dist/runtime/bash-runner.d.ts.map +1 -0
  6. package/dist/runtime/bash-runner.generated.d.ts +14 -0
  7. package/dist/runtime/bash-runner.generated.d.ts.map +1 -0
  8. package/dist/runtime/bash-runner.generated.js +13 -0
  9. package/dist/runtime/bash-runner.js +290 -0
  10. package/dist/runtime/cpython-runner.d.ts +86 -0
  11. package/dist/runtime/cpython-runner.d.ts.map +1 -0
  12. package/dist/runtime/cpython-runner.js +425 -0
  13. package/dist/runtime/facet-host.d.ts +159 -0
  14. package/dist/runtime/facet-host.d.ts.map +1 -0
  15. package/dist/runtime/facet-host.js +22 -0
  16. package/dist/runtime/installed-runtimes.d.ts +99 -0
  17. package/dist/runtime/installed-runtimes.d.ts.map +1 -0
  18. package/dist/runtime/installed-runtimes.js +162 -0
  19. package/dist/runtime/local-facet-host.d.ts +38 -0
  20. package/dist/runtime/local-facet-host.d.ts.map +1 -0
  21. package/dist/runtime/local-facet-host.js +171 -0
  22. package/dist/runtime/python-pip.d.ts +38 -0
  23. package/dist/runtime/python-pip.d.ts.map +1 -0
  24. package/dist/runtime/python-pip.js +1063 -0
  25. package/dist/runtime/runtime-manifest.d.ts +86 -0
  26. package/dist/runtime/runtime-manifest.d.ts.map +1 -0
  27. package/dist/runtime/runtime-manifest.js +72 -0
  28. package/dist/runtime/runtime-registry.d.ts +161 -0
  29. package/dist/runtime/runtime-registry.d.ts.map +1 -0
  30. package/dist/runtime/runtime-registry.js +363 -0
  31. package/dist/runtime/vfs-snapshot.d.ts.map +1 -1
  32. package/dist/runtime/vfs-snapshot.js +15 -1
  33. package/dist/runtime/vfs-supervisor.d.ts +22 -0
  34. package/dist/runtime/vfs-supervisor.d.ts.map +1 -0
  35. package/dist/runtime/vfs-supervisor.js +65 -0
  36. package/dist/runtime/virtual-socket-kernel.generated.d.ts +14 -0
  37. package/dist/runtime/virtual-socket-kernel.generated.d.ts.map +1 -0
  38. package/dist/runtime/virtual-socket-kernel.generated.js +13 -0
  39. package/dist/runtime/wasm-runner.d.ts +80 -0
  40. package/dist/runtime/wasm-runner.d.ts.map +1 -0
  41. package/dist/runtime/wasm-runner.js +686 -0
  42. package/dist/workspace/nimbus-workspace.d.ts +16 -0
  43. package/dist/workspace/nimbus-workspace.d.ts.map +1 -1
  44. package/dist/workspace/nimbus-workspace.js +57 -2
  45. package/package.json +4 -2
  46. package/src/index.ts +9 -0
  47. package/src/runtime/bash-runner.generated.ts +14 -0
  48. package/src/runtime/bash-runner.ts +347 -0
  49. package/src/runtime/cpython-runner.ts +504 -0
  50. package/src/runtime/facet-host.ts +170 -0
  51. package/src/runtime/installed-runtimes.ts +235 -0
  52. package/src/runtime/local-facet-host.ts +205 -0
  53. package/src/runtime/python-pip.ts +1211 -0
  54. package/src/runtime/runtime-manifest.ts +155 -0
  55. package/src/runtime/runtime-registry.ts +510 -0
  56. package/src/runtime/vfs-snapshot.ts +15 -1
  57. package/src/runtime/vfs-supervisor.ts +67 -0
  58. package/src/runtime/virtual-socket-kernel.generated.ts +14 -0
  59. package/src/runtime/wasm-runner.ts +835 -0
  60. package/src/workspace/nimbus-workspace.ts +102 -3
@@ -0,0 +1,347 @@
1
+ /**
2
+ * bash-runner — real GNU bash 5.2.37 (wasm32-wasi, asyncified) in a
3
+ * dedicated facet.
4
+ *
5
+ * bash CANNOT run through the stock JSPI wasm-runner: the binary is
6
+ * asyncify-instrumented (fork/setjmp/blocking-pipe unwinds) and needs
7
+ * 15 `nimbus_proc` imports plus MULTIPLE instances per facet (fork).
8
+ * This runner embeds the proven multi-instance fork/pipe/exec/setjmp
9
+ * scheduler (packages/worker/wasm/bash/run-bash-fork.mjs — the local
10
+ * acid-test driver, itself a port of the PROVEN-LIVE fork M1/M2/M3
11
+ * mechanisms) as a facet preamble.
12
+ *
13
+ * Architecture (mirrors ruby-runner's facet dispatch):
14
+ * - bash.async.wasm + the coreutil exec targets ship through the facet
15
+ * host, which compiles them and exposes them on
16
+ * globalThis.__NIMBUS_WASM.
17
+ * - The preamble defines __bashBoot / __bashFeed. Boot instantiates
18
+ * bash, pumps the scheduler until the process tree exits or the
19
+ * root parks on a terminal stdin read; each feed delivers stdin
20
+ * bytes and pumps again. Facet state persists across submits on
21
+ * the warm isolate (same mechanism as __rubyInstance caching).
22
+ * - stdout/stderr accumulate per pump slice and stream back to the
23
+ * CommandContext; VFS writes come back as a WasiFsDiff on exit.
24
+ */
25
+ import type { RuntimeManifest } from './runtime-manifest.js';
26
+ import type { CredentialedVfs, SqliteVFS } from '../vfs/sqlite-vfs.js';
27
+ import type { Facet, FacetHost } from './facet-host.js';
28
+ import type { Command, CommandContext, CommandInputStream } from '../substrate/lifo/commands/types.js';
29
+ import { z } from 'zod';
30
+ import type { WasiFsDiff } from './vfs-snapshot.js';
31
+ import type { BashBootArgs, BashFeedArgs, BashSlice } from './bash/types.js';
32
+ import { BASH_RUNNER_BODY_SRC } from './bash-runner.generated.js';
33
+ import { flushVfsDiff, snapshotVfs } from './vfs-snapshot.js';
34
+ import { requireVfsCred } from './os-contracts.js';
35
+ import { resolveVfsPath } from '../vfs/path.js';
36
+
37
+ type BashRunnerFactory = (
38
+ manifest: RuntimeManifest,
39
+ installRoot: string,
40
+ binName: string,
41
+ binKind: string | undefined,
42
+ ) => Command;
43
+
44
+ type BashStepArgs = BashBootArgs | BashFeedArgs;
45
+
46
+ const BashSliceSchema = z.object({
47
+ state: z.enum(['need-input', 'exited', 'error']),
48
+ exitCode: z.number().optional(),
49
+ stdout: z.string().optional(),
50
+ stderr: z.string().optional(),
51
+ error: z.string().optional(),
52
+ fsDiff: z.custom<WasiFsDiff>().optional(),
53
+ stats: z.record(z.string(), z.unknown()).optional(),
54
+ }).passthrough();
55
+
56
+ function normalizeSlice(raw: unknown): BashSlice | null {
57
+ const parsed = BashSliceSchema.safeParse(raw);
58
+ if (!parsed.success) return null;
59
+ return {
60
+ state: parsed.data.state,
61
+ exitCode: Number(parsed.data.exitCode ?? 0),
62
+ stdout: parsed.data.stdout || '',
63
+ stderr: parsed.data.stderr || '',
64
+ error: parsed.data.error,
65
+ fsDiff: parsed.data.fsDiff,
66
+ stats: parsed.data.stats,
67
+ };
68
+ }
69
+
70
+ function toArrayBuffer(bytes: Uint8Array): ArrayBuffer {
71
+ const out = new ArrayBuffer(bytes.byteLength);
72
+ new Uint8Array(out).set(bytes);
73
+ return out;
74
+ }
75
+
76
+ function errorMessage(error: unknown): string {
77
+ if (error instanceof Error) return error.message;
78
+ return String(error);
79
+ }
80
+
81
+ async function bashFacetStep(args: BashStepArgs): Promise<unknown> {
82
+ const boot = Reflect.get(globalThis, '__bashBoot');
83
+ const feed = Reflect.get(globalThis, '__bashFeed');
84
+ if (typeof boot !== 'function' || typeof feed !== 'function') {
85
+ return {
86
+ state: 'error',
87
+ exitCode: 127,
88
+ stdout: '',
89
+ stderr: '',
90
+ error: 'bash-runner preamble missing (__bashBoot/__bashFeed not in scope)',
91
+ };
92
+ }
93
+ return args.op === 'boot' ? boot(args) : feed(args);
94
+ }
95
+
96
+ export interface BashFacetSession {
97
+ readonly initial: BashSlice;
98
+ push(data: string, eof?: boolean): Promise<BashSlice>;
99
+ close(): Promise<void>;
100
+ }
101
+
102
+ export async function createBashFacetSession(deps: {
103
+ facets: FacetHost;
104
+ vfs: CredentialedVfs;
105
+ manifest: RuntimeManifest;
106
+ installRoot: string;
107
+ argv: string[];
108
+ env: Record<string, string>;
109
+ cwd: string;
110
+ stdinData?: string;
111
+ stdinClosed: boolean;
112
+ stdinTty: boolean;
113
+ extraRoots?: string[];
114
+ }): Promise<BashFacetSession> {
115
+ const findFile = (relativePath: string): string | null => {
116
+ const entry = deps.manifest.files.find((file) => file.path === relativePath);
117
+ return entry ? `${deps.installRoot}/${entry.path}` : null;
118
+ };
119
+ const bashWasmPath = findFile('share/bash/bash.async.wasm');
120
+ if (!bashWasmPath || !deps.vfs.exists(bashWasmPath)) {
121
+ throw new Error("bash.async.wasm missing (re-run 'nimbus install bash')");
122
+ }
123
+
124
+ const userEnv: Record<string, string> = { ...deps.env };
125
+ userEnv.HOME ||= '/home/user';
126
+ userEnv.PATH ||= '/bin:/usr/bin';
127
+ userEnv.TERM ||= 'dumb';
128
+ userEnv.NIMBUS_PWD = deps.cwd;
129
+ userEnv.BASH_ENV ||= '/etc/nimbus.bashrc';
130
+ userEnv.PWD = deps.cwd;
131
+
132
+ const extraRoots = [...(deps.extraRoots ?? [])];
133
+ if (userEnv.HOME !== '/home/user') extraRoots.push(userEnv.HOME);
134
+ const fsSnapshot = snapshotVfs(deps.vfs, deps.cwd, { extraRoots });
135
+ if ('error' in fsSnapshot) throw new Error(fsSnapshot.error);
136
+
137
+ const wasmModules: Record<string, ArrayBuffer> = {
138
+ 'bash.async.wasm': toArrayBuffer(deps.vfs.readFile(bashWasmPath)),
139
+ };
140
+ for (const file of deps.manifest.files) {
141
+ const prefix = 'share/bash/coreutils/';
142
+ if (!file.path.startsWith(prefix) || !file.path.endsWith('.wasm')) continue;
143
+ const name = file.path.slice(prefix.length, -'.wasm'.length);
144
+ const vfsPath = `${deps.installRoot}/${file.path}`;
145
+ if (deps.vfs.exists(vfsPath)) {
146
+ wasmModules[`cu_${name}.wasm`] = toArrayBuffer(deps.vfs.readFile(vfsPath));
147
+ }
148
+ }
149
+
150
+ const appletsPath = findFile('share/bash/coreutils/busybox.applets');
151
+ const busyboxApplets = appletsPath && deps.vfs.exists(appletsPath)
152
+ ? new TextDecoder().decode(deps.vfs.readFile(appletsPath))
153
+ .split('\n')
154
+ .map((line) => line.trim())
155
+ .filter(Boolean)
156
+ : [];
157
+
158
+ const facet: Facet = deps.facets.open({
159
+ tag: 'bash-runner',
160
+ concurrency: 1,
161
+ // No supervisor capability: bash is seeded with its subtree by value and
162
+ // hands the whole mutation back as a diff on exit, so it makes no syscall
163
+ // into the session while it runs.
164
+ preamble: BASH_RUNNER_PREAMBLE,
165
+ wasmModules,
166
+ });
167
+
168
+ let active = true;
169
+ let closed = false;
170
+ const submit = async (args: BashStepArgs): Promise<BashSlice> => {
171
+ const slice = normalizeSlice(
172
+ await facet.submit<BashStepArgs, unknown>(bashFacetStep, args, { timeoutMs: 300_000 }),
173
+ );
174
+ if (!slice) throw new Error('facet returned an invalid payload');
175
+ if (slice.state === 'exited') {
176
+ if (slice.fsDiff) flushVfsDiff(deps.vfs, slice.fsDiff);
177
+ active = false;
178
+ } else if (slice.state === 'error') {
179
+ active = false;
180
+ }
181
+ return slice;
182
+ };
183
+
184
+ try {
185
+ const initial = await submit({
186
+ op: 'boot',
187
+ argv: deps.argv,
188
+ environ: Object.entries(userEnv).map(([key, value]) => `${key}=${value}`),
189
+ cwd: deps.cwd,
190
+ fsSnapshot: fsSnapshot.snapshot,
191
+ stdinData: deps.stdinData ?? '',
192
+ stdinClosed: deps.stdinClosed,
193
+ stdinTty: deps.stdinTty,
194
+ busyboxApplets,
195
+ });
196
+ return {
197
+ initial,
198
+ push(data, eof = false) {
199
+ if (closed) throw new Error('bash facet session is closed');
200
+ return submit({ op: 'feed', data, eof });
201
+ },
202
+ async close() {
203
+ if (closed) return;
204
+ try {
205
+ if (active) await submit({ op: 'feed', data: '', eof: true });
206
+ } catch {
207
+ // Session teardown is best-effort; the owning command already
208
+ // reports dispatch failures from boot/push.
209
+ } finally {
210
+ closed = true;
211
+ facet.dispose();
212
+ }
213
+ },
214
+ };
215
+ } catch (error: unknown) {
216
+ facet.dispose();
217
+ throw error;
218
+ }
219
+ }
220
+
221
+ /** bash flags that consume the following argv element. */
222
+ const BASH_OPT_WITH_ARG = new Set(['-c', '-o', '+o', '--rcfile', '--init-file']);
223
+
224
+ /**
225
+ * Locate the script-path argv element (first non-flag arg when -c is
226
+ * absent) so the handler can resolve it against the session cwd —
227
+ * bash's own cwd inside the facet starts at '/' until the BASH_ENV
228
+ * chdir runs, so relative script paths must be made absolute host-side.
229
+ * Returns the argv index or -1 (interactive / -c / stdin modes).
230
+ */
231
+ function findScriptArgIndex(argv: string[]): number {
232
+ for (let i = 0; i < argv.length; i++) {
233
+ const a = argv[i];
234
+ if (a === '--') return i + 1 < argv.length ? i + 1 : -1;
235
+ if (a === '-' ) return -1; // read from stdin
236
+ if (a.startsWith('-') || a.startsWith('+')) {
237
+ if (BASH_OPT_WITH_ARG.has(a)) {
238
+ if (a === '-c') return -1; // command string mode
239
+ i++; // skip the option's argument
240
+ }
241
+ continue;
242
+ }
243
+ return i;
244
+ }
245
+ return -1;
246
+ }
247
+
248
+ export function makeBashRunnerFactory(deps: {
249
+ facets: FacetHost;
250
+ vfs: SqliteVFS;
251
+ }): BashRunnerFactory {
252
+ return function bashRunnerFactory(manifest, installRoot, binName, _binKind) {
253
+ return async function bashBinHandler(ctx: CommandContext): Promise<number> {
254
+ // All VFS access (runtime wasm reads, script probes, snapshot,
255
+ // fsDiff writeback) runs as the INVOKING process credential —
256
+ // S2a enforcement applies to bash exactly as to ruby/python.
257
+ const cred = requireVfsCred('cred' in ctx ? ctx.cred : undefined, binName);
258
+ const vfs = deps.vfs.as(cred);
259
+ const argv = [...(ctx.args ?? [])];
260
+ const cwd = ctx.cwd || '/home/user';
261
+
262
+ // Resolve a relative script path against the session cwd.
263
+ const scriptIdx = findScriptArgIndex(argv);
264
+ const extraRoots: string[] = [];
265
+ if (scriptIdx >= 0) {
266
+ const abs = resolveVfsPath(argv[scriptIdx], cwd);
267
+ if (!vfs.exists(abs)) {
268
+ ctx.stderr.write(`${binName}: ${argv[scriptIdx]}: No such file or directory\n`);
269
+ return 127;
270
+ }
271
+ // Pass bash an ABSOLUTE path: the facet chdir's to the session
272
+ // cwd via BASH_ENV before opening the script, so a relative arg
273
+ // would resolve against cwd twice. resolveVfsPath returns a
274
+ // slash-less canonical key; re-anchor it at root.
275
+ argv[scriptIdx] = '/' + abs;
276
+ const dir = abs.replace(/\/[^/]*$/, '');
277
+ if (dir) extraRoots.push(dir);
278
+ }
279
+
280
+ // stdin plumbing. A terminal-backed fd 0 feeds incrementally
281
+ // (interactive bash, `read` builtins); a piped stdin is drained
282
+ // upfront and closed so the scheduler never parks on it.
283
+ const stdinIsTty = typeof ctx.isFdTerminal === 'function' ? ctx.isFdTerminal(0) : !ctx.stdin;
284
+ const feedStream: CommandInputStream | undefined = ctx.terminalStdin ?? ctx.stdin;
285
+ let stdinData = '';
286
+ let stdinClosed = true;
287
+ if (stdinIsTty && feedStream) {
288
+ stdinClosed = false;
289
+ } else if (ctx.stdin) {
290
+ stdinData = await ctx.stdin.readAll();
291
+ }
292
+
293
+ let session: BashFacetSession | null = null;
294
+ try {
295
+ session = await createBashFacetSession({
296
+ facets: deps.facets,
297
+ vfs,
298
+ manifest,
299
+ installRoot,
300
+ argv: [binName, ...argv],
301
+ env: ctx.env || {},
302
+ cwd,
303
+ stdinData,
304
+ stdinClosed,
305
+ stdinTty: stdinIsTty,
306
+ extraRoots,
307
+ });
308
+ let slice = session.initial;
309
+
310
+ for (;;) {
311
+ if (slice.stdout) ctx.stdout.write(slice.stdout);
312
+ if (slice.stderr) ctx.stderr.write(slice.stderr);
313
+ if (slice.state === 'exited') {
314
+ return slice.exitCode;
315
+ }
316
+ if (slice.state === 'error') {
317
+ ctx.stderr.write(`${binName}: ${slice.error || 'bash facet error'}\n`);
318
+ return slice.exitCode || 1;
319
+ }
320
+ // need-input: pull the next chunk from the terminal.
321
+ let data = '';
322
+ let eof = true;
323
+ if (!ctx.signal.aborted && feedStream) {
324
+ const chunk = await feedStream.read();
325
+ if (!ctx.signal.aborted) { data = chunk === null ? '' : chunk.replace(/\r\n?/g, '\n'); eof = chunk === null; }
326
+ }
327
+ slice = await session.push(data, eof);
328
+ }
329
+ } catch (e: unknown) {
330
+ ctx.stderr.write(`${binName}: dispatch failed: ${errorMessage(e)}\n`);
331
+ return 1;
332
+ } finally {
333
+ await session?.close();
334
+ }
335
+ };
336
+ };
337
+ }
338
+
339
+ /**
340
+ * Source string injected as the facet `preamble`. The facet's scope evaluates
341
+ * it verbatim so `__bashBoot` / `__bashFeed` are in scope when the user fn
342
+ * runs. Self-contained — no closure captures, no imports.
343
+ *
344
+ * The scheduler itself lives in `bash/preamble.ts` as real TypeScript; the build
345
+ * bundles it into `bash-runner.generated.ts`.
346
+ */
347
+ export const BASH_RUNNER_PREAMBLE: string = BASH_RUNNER_BODY_SRC;