@nimbus-sh/worker 0.2.0 → 0.2.2

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 (83) hide show
  1. package/dist/facets/manager.d.ts +47 -2
  2. package/dist/facets/manager.d.ts.map +1 -1
  3. package/dist/facets/manager.js +139 -15
  4. package/dist/loaders/generated-workers.d.ts +2 -2
  5. package/dist/loaders/generated-workers.d.ts.map +1 -1
  6. package/dist/loaders/generated-workers.js +3 -3
  7. package/dist/runtime/bash-repl.d.ts +1 -1
  8. package/dist/runtime/bash-repl.d.ts.map +1 -1
  9. package/dist/runtime/bash-repl.js +3 -2
  10. package/dist/runtime/cpython-resident.d.ts +27 -0
  11. package/dist/runtime/cpython-resident.d.ts.map +1 -0
  12. package/dist/runtime/cpython-resident.js +133 -0
  13. package/dist/runtime/facet-loader-host.d.ts +16 -1
  14. package/dist/runtime/facet-loader-host.d.ts.map +1 -1
  15. package/dist/runtime/facet-loader-host.js +42 -0
  16. package/dist/runtime/package-manager.d.ts +3 -58
  17. package/dist/runtime/package-manager.d.ts.map +1 -1
  18. package/dist/runtime/package-manager.js +6 -143
  19. package/dist/runtime/python-repl.d.ts +1 -1
  20. package/dist/runtime/python-repl.d.ts.map +1 -1
  21. package/dist/runtime/python-repl.js +2 -2
  22. package/dist/runtime/ruby-repl.js +1 -1
  23. package/dist/runtime/ruby-resident.d.ts +20 -0
  24. package/dist/runtime/ruby-resident.d.ts.map +1 -0
  25. package/dist/runtime/ruby-resident.js +278 -0
  26. package/dist/runtime/runtime-catalog.d.ts +4 -80
  27. package/dist/runtime/runtime-catalog.d.ts.map +1 -1
  28. package/dist/runtime/runtime-catalog.js +4 -67
  29. package/dist/session/init.d.ts +16 -7
  30. package/dist/session/init.d.ts.map +1 -1
  31. package/dist/session/init.js +210 -249
  32. package/dist/session/keys.d.ts +13 -0
  33. package/dist/session/keys.d.ts.map +1 -1
  34. package/dist/session/keys.js +13 -0
  35. package/dist/session/nimbus-session.d.ts +30 -2
  36. package/dist/session/nimbus-session.d.ts.map +1 -1
  37. package/dist/session/nimbus-session.js +49 -58
  38. package/dist/session/programmatic.d.ts +3 -3
  39. package/dist/session/programmatic.d.ts.map +1 -1
  40. package/dist/session/programmatic.js +3 -2
  41. package/dist/session/routes.js +1 -1
  42. package/dist/session/supervisor-rpc.d.ts +6 -13
  43. package/dist/session/supervisor-rpc.d.ts.map +1 -1
  44. package/dist/session/supervisor-rpc.js +7 -17
  45. package/package.json +2 -2
  46. package/scripts/bundle-facet-workers.mjs +9 -8
  47. package/scripts/bundle-runtime.mjs +276 -44
  48. package/dist/npm/tarball-stream.d.ts +0 -93
  49. package/dist/npm/tarball-stream.d.ts.map +0 -1
  50. package/dist/npm/tarball-stream.js +0 -235
  51. package/dist/npm/tarball.d.ts +0 -34
  52. package/dist/npm/tarball.d.ts.map +0 -1
  53. package/dist/npm/tarball.js +0 -97
  54. package/dist/runtime/bash-runner.d.ts +0 -63
  55. package/dist/runtime/bash-runner.d.ts.map +0 -1
  56. package/dist/runtime/bash-runner.generated.d.ts +0 -14
  57. package/dist/runtime/bash-runner.generated.d.ts.map +0 -1
  58. package/dist/runtime/bash-runner.generated.js +0 -13
  59. package/dist/runtime/bash-runner.js +0 -291
  60. package/dist/runtime/clang-runner.d.ts +0 -37
  61. package/dist/runtime/clang-runner.d.ts.map +0 -1
  62. package/dist/runtime/clang-runner.js +0 -865
  63. package/dist/runtime/cpython-runner.d.ts +0 -71
  64. package/dist/runtime/cpython-runner.d.ts.map +0 -1
  65. package/dist/runtime/cpython-runner.js +0 -535
  66. package/dist/runtime/python-pip.d.ts +0 -38
  67. package/dist/runtime/python-pip.d.ts.map +0 -1
  68. package/dist/runtime/python-pip.js +0 -1063
  69. package/dist/runtime/ruby-gems.d.ts +0 -30
  70. package/dist/runtime/ruby-gems.d.ts.map +0 -1
  71. package/dist/runtime/ruby-gems.js +0 -636
  72. package/dist/runtime/ruby-runner.d.ts +0 -75
  73. package/dist/runtime/ruby-runner.d.ts.map +0 -1
  74. package/dist/runtime/ruby-runner.js +0 -1577
  75. package/dist/runtime/runtime-registry.d.ts +0 -162
  76. package/dist/runtime/runtime-registry.d.ts.map +0 -1
  77. package/dist/runtime/runtime-registry.js +0 -364
  78. package/dist/runtime/virtual-socket-kernel.generated.d.ts +0 -14
  79. package/dist/runtime/virtual-socket-kernel.generated.d.ts.map +0 -1
  80. package/dist/runtime/virtual-socket-kernel.generated.js +0 -13
  81. package/dist/runtime/wasm-runner.d.ts +0 -70
  82. package/dist/runtime/wasm-runner.d.ts.map +0 -1
  83. package/dist/runtime/wasm-runner.js +0 -664
@@ -3,12 +3,21 @@
3
3
  *
4
4
  * Why this is one big function and not a class:
5
5
  * initSession runs once per /ws upgrade and walks the session
6
- * through Phase R (rehydrate from SQL), Phase B (build kernel +
7
- * shell + register commands), Phase W (attach terminal), and
8
- * (cold-only) Phase O (MOTD + framework hint). The phases share
9
- * lots of locals (vfs, kernel, registry, shell) and ordering
10
- * matters strictly — there's no interesting reuse boundary that a
11
- * class decomposition would expose.
6
+ * through Phase R (rehydrate from SQL), Phase B (compose the
7
+ * workspace + register the session's commands), Phase W (attach
8
+ * terminal), and (cold-only) Phase O (MOTD + framework hint). The
9
+ * phases share lots of locals (vfs, kernel, registry, shell) and
10
+ * ordering matters strictly — there's no interesting reuse boundary
11
+ * that a class decomposition would expose.
12
+ *
13
+ * What it no longer does is COMPOSE the operating system. The kernel,
14
+ * the provider mounts, the command registry, the coreutils, the exec
15
+ * resolver, the default environment and the shell come from
16
+ * `NimbusWorkspace` (@nimbus-sh/core/workspace), which an embedder off
17
+ * Cloudflare builds the same way. What stays here is everything that
18
+ * only makes sense with a socket, a Durable Object and a product behind
19
+ * it: the terminal and its scrollback, the phase machine, the persisted
20
+ * shell state, npm, git, vite, wrangler and the facet-backed runtimes.
12
21
  *
13
22
  * The function is intentionally written so that a reader sees:
14
23
  * 1. setPhase('rehydrate') ...
@@ -24,27 +33,25 @@
24
33
  * - acceptShellWebSocket → self.initSession(ws) (S7 will extract).
25
34
  * - The class still has `initSession(ws)` as a delegator method.
26
35
  */
27
- import { Kernel, Shell, createDefaultRegistry, ProcessRegistry, MemoryPersistenceBackend, createCurlCommand, createNpmCommand, NPM_VERSION, createTopCommand, createWatchCommand, createHelpCommand, rehydrateGlobalPackages, } from '@nimbus-sh/core/substrate/lifo/index.js';
36
+ import { Shell, createCurlCommand, createNpmCommand, NPM_VERSION, createTopCommand, createWatchCommand, createHelpCommand, rehydrateGlobalPackages, } from '@nimbus-sh/core/substrate/lifo/index.js';
28
37
  import { createKillCommand } from '@nimbus-sh/core/substrate/lifo/commands/system/kill.js';
29
- import { SqliteVFSProvider } from '@nimbus-sh/core/vfs/sqlite-vfs.js';
38
+ import { NimbusWorkspace } from '@nimbus-sh/core/workspace';
30
39
  import { CRED_KERNEL, requireVfsCred } from '@nimbus-sh/core/runtime/os-contracts.js';
31
40
  import { WebSocketTerminal } from '../facets/ws-terminal.js';
32
41
  import { EsbuildService } from '@nimbus-sh/core/runtime/esbuild-service.js';
33
42
  import { runFresh } from '../runtime/node-runner.js';
34
43
  import { runBunScript, BUN_VERSION } from '../runtime/bun-runner.js';
35
- import { buildRuntimeHandler, resolveRuntimeScriptPath, } from '../runtime/runtime-registry.js';
44
+ import { buildRuntimeHandler, resolveRuntimeScriptPath, } from '@nimbus-sh/core/runtime/runtime-registry.js';
36
45
  import { parseViteConfigSource } from '@nimbus-sh/core/runtime/vite-config-parser.js';
37
46
  import { startRealVite } from './start-real-vite.js';
38
47
  import { findHtmlScriptEntrypoint, rewriteViteBuildHtml } from '../runtime/html-entrypoint.js';
39
48
  import { normalizeVfsPath, parentVfsPath, resolveVfsPath, stripLeadingSlashes } from '@nimbus-sh/core/vfs/path.js';
40
- import { installPathExecResolver, } from '@nimbus-sh/core/shell/exec-dispatch.js';
41
49
  import { ViteDevServer } from '../facets/vite-dev-server.js';
42
50
  import { shouldUseRealVite } from '../facets/cirrus-real.js';
43
51
  import { makeLongRunningPortStub, resolveLongRunningPort, expandArgvShellDefaults, } from '@nimbus-sh/core/runtime/long-running-handle.js';
44
52
  import { NimbusWrangler } from '../wrangler/nimbus-wrangler.js';
45
53
  import { filterWranglerFlags, detectBundlerBin, checkNodeModulesGuard, detectUnsupportedWranglerConfig, } from './helpers.js';
46
54
  import { HeredocHandler, LineEditorExtender } from '@nimbus-sh/core/shell/features.js';
47
- import { registerUnixCommands } from '@nimbus-sh/core/shell/unix-commands.js';
48
55
  import { registerShellEntrypointCommands } from '@nimbus-sh/core/shell/shell-entrypoints.js';
49
56
  import { makeChshCommand } from '@nimbus-sh/core/substrate/lifo/shell/default-shell.js';
50
57
  import { installNpmBinFallbackResolver } from '../shell/npm-bin-entrypoints.js';
@@ -52,7 +59,8 @@ import { parseNpmInstallInvocation } from '../npm/install-args.js';
52
59
  import { npmLogEnabled } from '../npm/npm-log.js';
53
60
  import { materializeNpmBinShims } from '../npm/bin-links.js';
54
61
  import { registerGitCommands } from '../git/commands.js';
55
- import { makeNimbusVerbHandler, createRuntimeCommandHintResolver, listInstalledRuntimes, rehydrateInstalledRuntimes, registerRunnerFactory, } from '../runtime/package-manager.js';
62
+ import { makeNimbusVerbHandler, createRuntimeCommandHintResolver, } from '../runtime/package-manager.js';
63
+ import { listInstalledRuntimes, rehydrateInstalledRuntimes, registerRunnerFactory, } from '@nimbus-sh/core/runtime/installed-runtimes.js';
56
64
  // Runtime factories (clang/python/ruby/bash/wasm) are imported lazily at
57
65
  // first-use inside their registered handlers — see the registrations below.
58
66
  // Keeping them off the top-level import graph shaves their module-eval cost
@@ -61,7 +69,7 @@ import { makeNimbusVerbHandler, createRuntimeCommandHintResolver, listInstalledR
61
69
  import { hasSeededProject, SEED_PROJECT_DIR } from '@nimbus-sh/core/vfs/seed-project.js';
62
70
  import { notifyTerminalEvent } from '../runtime/process-logs-api.js';
63
71
  import { stripAnsi } from '@nimbus-sh/core/runtime/process-logs.js';
64
- import { NIMBUS_VERSION, DEFAULT_HOSTNAME, DEFAULT_MOUNT_POINTS, NODE_VERSION, DEFAULT_PATH, } from '@nimbus-sh/core/constants.js';
72
+ import { DEFAULT_MOUNT_POINTS, NODE_VERSION, } from '@nimbus-sh/core/constants.js';
65
73
  import { ensureSessionStateSchema, loadShellState, stampHydratedAt, countSessionStateKeys, loadKernelMounts, persistKernelMounts, appendScrollback, loadScrollback, } from './state-store.js';
66
74
  import { recordRecoveryEvent } from '@nimbus-sh/core/observability/oom-discriminator.js';
67
75
  import { sessionAiEnv } from './ai.js';
@@ -76,7 +84,7 @@ function resolveNpmPrefix(prefix, cwd) {
76
84
  function quoteShellArgument(value) {
77
85
  return `'${value.replace(/'/g, `'\\''`)}'`;
78
86
  }
79
- export function initSession(self, ws) {
87
+ export async function initSession(self, ws) {
80
88
  self.ensureSqliteFs();
81
89
  const kernelFs = self.sqliteFs.as(CRED_KERNEL);
82
90
  self.ensureFacetManager();
@@ -156,43 +164,147 @@ export function initSession(self, ws) {
156
164
  self.terminal.write('\x1b[33m[nimbus] this session resumed on a new instance. Anything still '
157
165
  + 'running was lost with the old one — re-run it if it had not finished.\x1b[0m\r\n');
158
166
  }
159
- // [B'.4] Phase B — Build. Construct Kernel + Shell + registry +
160
- // install all commands. CPU-intensive phase. Spans from here
161
- // through ~line 1925 (just before Phase O).
167
+ // [B'.4] Phase B — Build. Compose the workspace (kernel + mounts +
168
+ // shell + the OS command set), then install the session's own
169
+ // commands and wiring on top. CPU-intensive phase. Spans from here
170
+ // through Phase O.
162
171
  setPhase(self, 'build', 'init-session');
163
- // ── Boot kernel with in-memory VFS (mounts delegate to SqliteFS) ──
164
- self.kernel = new Kernel(new MemoryPersistenceBackend());
165
- self.kernel.initFilesystem();
166
- // ── Mount SqliteFSProvider at all top-level directories [B'.2] ──
172
+ // ── Mount list = DEFAULT_MOUNT_POINTS ∪ persisted-mounts [B'.2] ──
167
173
  //
168
- // Mount list = DEFAULT_MOUNT_POINTS ∪ persisted-mounts. The
169
- // defaults are always present (they're platform invariants);
174
+ // The defaults are always present (they're platform invariants);
170
175
  // any extras a future custom-mount feature might add survive
171
- // reconnect via the nimbus_kernel_mounts table. The persist
172
- // step at the end writes the merged list back so the table
173
- // tracks the live mount tree.
176
+ // reconnect via the nimbus_kernel_mounts table. The persist step
177
+ // below writes the merged list back so the table tracks the live
178
+ // mount tree — today the same 7 rows every initSession.
174
179
  const persistedMounts = loadKernelMounts(self.ctx);
175
180
  const mountPoints = Array.from(new Set([
176
181
  ...DEFAULT_MOUNT_POINTS,
177
182
  ...persistedMounts,
178
183
  ]));
179
- for (const mp of mountPoints) {
180
- const provider = new SqliteVFSProvider(self.sqliteFs, mp);
181
- self.kernel.vfs.mount('/' + mp, provider);
184
+ // ── What the session adds to the workspace's environment [B'.1] ──
185
+ //
186
+ // The platform defaults — PATH, PS1, HOME, PORT, HOST and the rest —
187
+ // belong to the workspace (core/workspace/nimbus-workspace.ts) because
188
+ // they are the OS's, not this transport's. What is genuinely the
189
+ // session's layers here:
190
+ //
191
+ // NIMBUS_SESSION_ID — derived from sessionBasePath = "/s/<id>". Set
192
+ // here as a placeholder ("") and patched below right
193
+ // after the shell exists, so the user's first command
194
+ // sees the real id. Sentry / Datadog / any ops
195
+ // integration that wants a session-stable token reads it.
196
+ //
197
+ // sessionAiEnv() — the session AI gateway (session/ai.ts). A coding
198
+ // agent, a user's own script or curl reaches the
199
+ // session's models from these without being configured:
200
+ // by OPENAI_BASE_URL if it reads one, and otherwise by
201
+ // CLOUDFLARE_API_KEY, this session's capability token,
202
+ // which mediates the tool's own egress back to the
203
+ // gateway (_shared/ai-egress.ts).
204
+ //
205
+ // persisted.env — the user's own `export FOO=bar`, which survives
206
+ // reconnect and wins over everything above. A user who
207
+ // exports their own OPENAI_BASE_URL or CLOUDFLARE_API_KEY
208
+ // still wins; their key is not this session's token, so
209
+ // their request goes to their own account.
210
+ const envOverlay = {
211
+ NIMBUS_SESSION_ID: '',
212
+ ...sessionAiEnv(),
213
+ ...(persisted.env || {}),
214
+ };
215
+ // ── The identity the shell acts under ──
216
+ //
217
+ // Every command the shell runs is credentialed by a live entry in the
218
+ // session's process table, which is what makes `sudo`, `chown` and the
219
+ // per-process umask mean anything. A workspace with no host process
220
+ // table falls back to a bare uid-1000 identity; this one has one.
221
+ if (self.shellProcessPid !== null) {
222
+ self.processes.exit(self.shellProcessPid, 0);
182
223
  }
183
- // Persist the mount-tree. Today this writes the same
184
- // DEFAULT_MOUNT_POINTS list every initSession (idempotent — the
185
- // table just keeps the same 7 rows). Future custom mounts will
186
- // flow through the same code path.
224
+ const shellProcess = self.processes.spawn('sh', ['sh'], persisted.cwd || '/home/user');
225
+ self.shellProcessPid = shellProcess.pid;
226
+ const runAsProcess = async (parent, cred, argv) => {
227
+ if (argv.length === 0)
228
+ return 0;
229
+ const child = self.processes.spawn(argv.join(' '), argv, parent.cwd, { parentPid: parent.pid, cred });
230
+ const activeShell = self.shell;
231
+ if (!activeShell) {
232
+ self.processes.exit(child.pid, 1);
233
+ throw new Error('shell is not initialized');
234
+ }
235
+ const identity = commandIdentityFor(child.pid);
236
+ let exitCode = 1;
237
+ try {
238
+ const stdin = parent.stdin && parent.stdin !== parent.terminalStdin
239
+ ? await parent.stdin.readAll()
240
+ : undefined;
241
+ const result = await activeShell.execute(argv.map(quoteShellArgument).join(' '), {
242
+ cwd: parent.cwd,
243
+ env: parent.env,
244
+ stdin,
245
+ terminalStdin: parent.terminalStdin,
246
+ signal: parent.signal,
247
+ isolateShellState: true,
248
+ terminalFds: {
249
+ stdin: parent.isFdTerminal?.(0) ?? false,
250
+ stdout: parent.isFdTerminal?.(1) ?? false,
251
+ stderr: parent.isFdTerminal?.(2) ?? false,
252
+ },
253
+ onStdout: (data) => parent.stdout.write(data),
254
+ onStderr: (data) => parent.stderr.write(data),
255
+ commandContext: {
256
+ pid: identity.pid,
257
+ cred: identity.cred,
258
+ setUmask: identity.setUmask,
259
+ },
260
+ runAs: runAsProcess,
261
+ });
262
+ exitCode = result.exitCode;
263
+ return exitCode;
264
+ }
265
+ finally {
266
+ self.processes.exit(child.pid, exitCode);
267
+ }
268
+ };
269
+ const commandIdentityFor = (pid) => ({
270
+ pid,
271
+ get cred() {
272
+ return self.processes.cred(pid);
273
+ },
274
+ setUmask(mask) {
275
+ self.processes.setUmask(pid, mask);
276
+ },
277
+ runAs: runAsProcess,
278
+ });
279
+ // ── The workspace: one recipe for kernel + mounts + shell + coreutils ──
280
+ //
281
+ // No `facets`: the session registers its own runtime factories below,
282
+ // because the ones it needs carry REPLs, a resident-process substrate
283
+ // and clang, none of which a bare facet host reaches. Everything the
284
+ // workspace does register is the OS, and is identical either way.
285
+ const workspace = await NimbusWorkspace.create({
286
+ sql: self.ctx.storage.sql,
287
+ // The filesystem this DO already opened. `ensureSqliteFs` above may
288
+ // have been called many requests ago; a second SqliteVFS over the
289
+ // same rows would be a second cache serving stale reads.
290
+ vfs: self.sqliteFs,
291
+ mounts: mountPoints,
292
+ env: envOverlay,
293
+ terminal: self.terminal,
294
+ identity: commandIdentityFor(shellProcess.pid),
295
+ });
296
+ self.kernel = workspace.kernel;
297
+ self.shell = workspace.shell;
298
+ const kernel = workspace.kernel;
299
+ const registry = workspace.registry;
300
+ const processRegistry = kernel.processRegistry;
301
+ const env = workspace.env;
302
+ const sqliteFs = self.sqliteFs;
303
+ const facetMgr = self.facetManager;
187
304
  try {
188
305
  persistKernelMounts(self.ctx, mountPoints);
189
306
  }
190
307
  catch { /* fail-soft */ }
191
- // ── Create command registry ──
192
- const registry = createDefaultRegistry();
193
- const kernel = self.kernel;
194
- const sqliteFs = self.sqliteFs;
195
- const facetMgr = self.facetManager;
196
308
  // ── editor/monaco (2026-05-13): editor-pane fs bridge ──
197
309
  //
198
310
  // The terminal hosts the WS that the editor pane reuses for
@@ -320,24 +432,10 @@ export function initSession(self, ws) {
320
432
  });
321
433
  }
322
434
  });
323
- // ── WASI Stage 1: exec dispatch for path-shaped invocations ──
324
- // `./x`, `/abs/x`, `../x` go through POSIX execve semantics
325
- // (shell/exec-dispatch.ts): exec-bit check (with the wasm-magic
326
- // grandfather rule for pre-chmod modes), then format dispatch —
327
- // `\0asm` → wasm-runner, `#!` → the named interpreter, binary
328
- // junk → honest ENOEXEC, plain text → sh (the POSIX ENOEXEC
329
- // fallback). Bare-word "X" still must come from the registered set.
330
- //
331
- // Implementation: monkey-patch registry.resolve. cwd and file
332
- // state are read at every resolve call (not cached) so `cd` and
333
- // recompiles between invocations are honoured.
334
- installPathExecResolver(registry, kernelFs, () => self.shell?.getCwd() || '/home/user');
335
435
  // W8: hand the registry to the cp broker so child_process.spawn from
336
436
  // a parent facet can resolve and dispatch commands the same way the
337
- // shell does. Done AFTER all registrations are complete (below).
437
+ // shell does.
338
438
  self._setCpRegistry(registry);
339
- // ── Unix commands (30+ real implementations) ──
340
- registerUnixCommands(registry, sqliteFs);
341
439
  registry.register('chsh', makeChshCommand({
342
440
  isBashInstalled: (home) => listInstalledRuntimes(sqliteFs, home).some((runtime) => runtime.name === 'bash'),
343
441
  }));
@@ -354,8 +452,9 @@ export function initSession(self, ws) {
354
452
  // 3. Rehydrate any previously-installed runtimes from VFS so their
355
453
  // bins reappear in the registry after DO eviction or WS reconnect.
356
454
  registerRunnerFactory('clang-runner', (manifest, installRoot, binName, binKind) => async (ctx) => {
357
- const { makeClangRunnerFactory } = await import('../runtime/clang-runner.js');
358
- return await makeClangRunnerFactory({ facetMgr, vfs: sqliteFs })(manifest, installRoot, binName, binKind)(ctx);
455
+ const { makeClangRunnerFactory } = await import('@nimbus-sh/core/runtime/clang-runner.js');
456
+ const { facetHostForManager } = await import('../runtime/facet-loader-host.js');
457
+ return await makeClangRunnerFactory({ facets: facetHostForManager(facetMgr), vfs: sqliteFs })(manifest, installRoot, binName, binKind)(ctx);
359
458
  });
360
459
  // Pyodide v1 — Python 3.13 via the same R2-package-manager
361
460
  // substrate that ships clang. Manifest entrypoints `python` and
@@ -391,8 +490,14 @@ export function initSession(self, ws) {
391
490
  }
392
491
  // Args present (one-shot mode: -c, script, -m, -). Fall through
393
492
  // to the canonical handler (imported lazily on first use).
394
- const { makeCPythonRunnerFactory } = await import('../runtime/cpython-runner.js');
395
- return await makeCPythonRunnerFactory({ facetMgr, vfs: sqliteFs })(manifest, installRoot, binName, binKind)(ctx);
493
+ const { makeCPythonRunnerFactory } = await import('@nimbus-sh/core/runtime/cpython-runner.js');
494
+ const { facetHostForManager } = await import('../runtime/facet-loader-host.js');
495
+ const { cpythonResidentStart } = await import('../runtime/cpython-resident.js');
496
+ return await makeCPythonRunnerFactory({
497
+ facets: facetHostForManager(facetMgr),
498
+ vfs: sqliteFs,
499
+ startResident: cpythonResidentStart(facetMgr),
500
+ })(manifest, installRoot, binName, binKind)(ctx);
396
501
  });
397
502
  // Ruby v1 — Ruby 3.3.4 via ruby.wasm 2.9.3-2.9.4. Same architecture
398
503
  // as python-runner: ruby+stdlib.wasm rides via LOADER modules-map,
@@ -414,8 +519,15 @@ export function initSession(self, ws) {
414
519
  installRoot,
415
520
  });
416
521
  }
417
- const { makeRubyRunnerFactory } = await import('../runtime/ruby-runner.js');
418
- return await makeRubyRunnerFactory({ facetMgr, vfs: sqliteFs, registry })(manifest, installRoot, binName, binKind)(ctx);
522
+ const { makeRubyRunnerFactory } = await import('@nimbus-sh/core/runtime/ruby-runner.js');
523
+ const { facetHostForManager } = await import('../runtime/facet-loader-host.js');
524
+ const { rubyResidentStart } = await import('../runtime/ruby-resident.js');
525
+ return await makeRubyRunnerFactory({
526
+ facets: facetHostForManager(facetMgr),
527
+ vfs: sqliteFs,
528
+ registry,
529
+ startResident: rubyResidentStart(facetMgr),
530
+ })(manifest, installRoot, binName, binKind)(ctx);
419
531
  });
420
532
  // GNU bash 5.2.37 (wasm32-wasi, asyncified) — dedicated facet
421
533
  // runner driving the fork/pipe/exec/setjmp scheduler (fork M1-M3
@@ -440,8 +552,9 @@ export function initSession(self, ws) {
440
552
  shell: self.shell ?? undefined,
441
553
  });
442
554
  }
443
- const { makeBashRunnerFactory } = await import('../runtime/bash-runner.js');
444
- return await makeBashRunnerFactory({ facetMgr, vfs: sqliteFs })(manifest, installRoot, binName, binKind)(ctx);
555
+ const { makeBashRunnerFactory } = await import('@nimbus-sh/core/runtime/bash-runner.js');
556
+ const { facetHostForManager } = await import('../runtime/facet-loader-host.js');
557
+ return await makeBashRunnerFactory({ facets: facetHostForManager(facetMgr), vfs: sqliteFs })(manifest, installRoot, binName, binKind)(ctx);
445
558
  });
446
559
  {
447
560
  // Cast registry to the minimal package-manager shape. CommandRegistry
@@ -554,13 +667,12 @@ export function initSession(self, ws) {
554
667
  ' -v, --version Print version\n' +
555
668
  ' -h, --help Print help\n' +
556
669
  '\nExecution via DO Facets (isolated V8 isolate)',
557
- run: runFresh,
670
+ run: (code, opts) => runFresh(facetMgr, code, opts),
558
671
  supportsBinSpawn: true,
559
672
  };
560
673
  {
561
674
  const oneShotNode = buildRuntimeHandler(nodeSpec, {
562
675
  vfs: sqliteFs,
563
- facetMgr,
564
676
  getEsbuild: () => {
565
677
  if (!self.esbuildService) {
566
678
  self.ensureSqliteFs();
@@ -616,7 +728,7 @@ export function initSession(self, ws) {
616
728
  'Bun.spawn/Bun.password/Bun.gunzip backed by Workers-native\n' +
617
729
  'primitives. Bun.serve / Bun.sql / Bun.S3 throw with supported alternatives.\n' +
618
730
  'Execution via DO Facets (isolated V8 isolate per call).',
619
- run: runBunScript,
731
+ run: (code, opts) => runBunScript(facetMgr, code, opts),
620
732
  subcommands: {
621
733
  // bun install / i / add → npm install (same VFS, same R2 caches).
622
734
  install: async (ctx, reg) => {
@@ -710,7 +822,6 @@ export function initSession(self, ws) {
710
822
  {
711
823
  const oneShotBun = buildRuntimeHandler(bunSpec, {
712
824
  vfs: sqliteFs,
713
- facetMgr,
714
825
  getEsbuild: () => {
715
826
  if (!self.esbuildService) {
716
827
  self.ensureSqliteFs();
@@ -730,20 +841,15 @@ export function initSession(self, ws) {
730
841
  return await oneShotBun(ctx);
731
842
  });
732
843
  }
733
- // ── wasm-runner: native WebAssembly runtime via LOADER-modules transport ──
844
+ // ── wasm-runner: native WebAssembly, on this session's facet host ──
734
845
  //
735
- // Re-introduced after the runtime registry refactor's revert. Bytes
736
- // ride INSIDE the worker code blob (LOADER's modules map, the
737
- // one phase where wasm code generation IS allowed); request-time
846
+ // Bytes ride INSIDE the inner worker's code blob, the one phase where
847
+ // workerd permits wasm code generation; request-time
738
848
  // WebAssembly.instantiate(bytes) is CSP-blocked and avoided.
739
849
  //
740
850
  // wasm-csp/findings.md — add(3,4)===7 in 11ms warm against the
741
851
  // deployed Cloudflare fleet.
742
852
  //
743
- // bypassesScriptRead: the registry skips the read-source/
744
- // shebang-strip/esbuild-transform flow. wasm-runner reads bytes
745
- // itself in spec.run() and ships via NimbusLoaderPool.
746
- //
747
853
  // Lazy: the wasm-runner module (WASI instance preamble + snapshot
748
854
  // machinery) is imported on first `wasm-runner` invocation and the
749
855
  // built handler memoized, so its module-eval cost stays off the cold
@@ -752,31 +858,18 @@ export function initSession(self, ws) {
752
858
  let wasmHandler = null;
753
859
  registry.register('wasm-runner', async (ctx) => {
754
860
  if (!wasmHandler) {
755
- const { makeWasmRunner, WASM_RUNNER_VERSION, WASM_RUNNER_HELP, formatWasmRunnerWasiInfo, } = await import('../runtime/wasm-runner.js');
756
- const wasmSpec = {
757
- name: 'wasm-runner',
758
- version: WASM_RUNNER_VERSION,
759
- helpText: WASM_RUNNER_HELP,
760
- subcommands: {
761
- '--wasi-info': async (c) => {
762
- c.stdout.write(formatWasmRunnerWasiInfo());
763
- return 0;
764
- },
765
- },
766
- run: makeWasmRunner({
767
- // filesystem WASI: extended VFS surface for WASI file-IO. The
768
- // wasm-runner snapshots a session subtree into the facet, flushes
769
- // the diff back via this surface after _start returns.
770
- vfs: sqliteFs,
771
- env: self.env,
772
- ctx: self.ctx,
773
- processes: self.processes,
774
- }),
775
- bypassesScriptRead: true,
776
- };
861
+ const { wasmRunnerSpec } = await import('@nimbus-sh/core/runtime/wasm-runner.js');
862
+ const { loaderFacetHost } = await import('../runtime/facet-loader-host.js');
863
+ const wasmSpec = wasmRunnerSpec({
864
+ // filesystem WASI: extended VFS surface for WASI file-IO. The
865
+ // wasm-runner snapshots a session subtree into the facet, flushes
866
+ // the diff back via this surface after _start returns.
867
+ vfs: sqliteFs,
868
+ facets: loaderFacetHost(self.env, self.ctx),
869
+ processes: self.processes,
870
+ });
777
871
  wasmHandler = buildRuntimeHandler(wasmSpec, {
778
872
  vfs: sqliteFs,
779
- facetMgr,
780
873
  getEsbuild: () => {
781
874
  if (!self.esbuildService) {
782
875
  self.ensureSqliteFs();
@@ -1578,158 +1671,28 @@ export function initSession(self, ws) {
1578
1671
  }
1579
1672
  return result.failed.length > 0 ? 1 : 0;
1580
1673
  });
1581
- // ── Set up environment [B'.1: rehydrate from SQL] ──
1582
- //
1583
- // Cold start: env is the platform default below.
1584
- // Silent re-init (persisted env present): the Shell's constructor
1585
- // does `this.env = { ...n }`, so we layer the persisted env over
1586
- // the defaults — defaults provide PATH/PS1/etc. (which the user
1587
- // never sets explicitly), persisted overlays whatever the user
1588
- // did set (NIMBUS_TEST=cool, etc.).
1589
- //
1590
- // Primitive #7 (runtime primitive support): PORT/HOST and
1591
- // NIMBUS_SESSION_ID are part of the standard contract.
1592
- //
1593
- // PORT=3000 — the same default Markflow's `${PORT:-3000}` shell
1594
- // expansion targets, and what every Express/Hono/
1595
- // fastify/Bun.serve script reads when the user
1596
- // doesn't set it explicitly. Long-running spawns
1597
- // still pull from `--port` argv first (see
1598
- // runtime/long-running-handle.ts:resolveLongRunningPort);
1599
- // this default is the SOURCE for that fall-through.
1600
- //
1601
- // HOST=0.0.0.0 — Cloudflare Workers / DO have no localhost vs.
1602
- // external distinction (the supervisor never opens
1603
- // a real socket); 0.0.0.0 is what every tutorial
1604
- // tells users to bind to and matches CF docs.
1605
- //
1606
- // NIMBUS_SESSION_ID — derived from sessionBasePath = "/s/<id>".
1607
- // Set lazily here as a placeholder ("") and patched
1608
- // below right after Shell construction so the user's
1609
- // first command sees the real id.
1610
- //
1611
- // Why these aren't optional: package.json scripts that hardcode
1612
- // process.env.PORT (Express's default app, every "create-vite"
1613
- // template) get `undefined` without this. Sentry / Datadog / any
1614
- // ops integration that wants a session-stable token uses
1615
- // NIMBUS_SESSION_ID.
1616
- const env = {
1617
- HOME: '/home/user',
1618
- USER: 'user',
1619
- SHELL: '/bin/sh',
1620
- HOSTNAME: DEFAULT_HOSTNAME,
1621
- TERM: 'xterm-256color',
1622
- PWD: '/home/user',
1623
- PATH: DEFAULT_PATH,
1624
- PS1: `\x1b[1;32muser@${DEFAULT_HOSTNAME}\x1b[0m:\x1b[1;34m\\w\x1b[0m$ `,
1625
- NODE_ENV: 'development',
1626
- LANG: 'en_US.UTF-8',
1627
- EDITOR: 'nano',
1628
- NIMBUS_VERSION: NIMBUS_VERSION,
1629
- TMPDIR: '/tmp',
1630
- XDG_CONFIG_HOME: '/home/user/.config',
1631
- XDG_DATA_HOME: '/home/user/.local/share',
1632
- npm_config_prefix: '/usr/local',
1633
- // Primitive #7 contract additions.
1634
- PORT: '3000',
1635
- HOST: '0.0.0.0',
1636
- NIMBUS_SESSION_ID: '', // patched after Shell ctor — see below.
1637
- // The session AI gateway (session/ai.ts). A coding agent, a user's own
1638
- // script or curl reaches the session's models from these without being
1639
- // configured: by OPENAI_BASE_URL if it reads one, and otherwise by
1640
- // CLOUDFLARE_API_KEY, this session's capability token, which mediates the
1641
- // tool's own egress back to the gateway (_shared/ai-egress.ts). A user
1642
- // who exports their own OPENAI_BASE_URL or CLOUDFLARE_API_KEY still wins,
1643
- // via the persisted spread — their key is not this session's token, so
1644
- // their request goes to their own account.
1645
- ...sessionAiEnv(),
1646
- // Persisted env keys win over defaults — the user's `export FOO=bar`
1647
- // survives reconnect.
1648
- ...(persisted.env || {}),
1649
- };
1650
- // ── Create shell ──
1651
- const processRegistry = new ProcessRegistry();
1652
- if (self.shellProcessPid !== null) {
1653
- self.processes.exit(self.shellProcessPid, 0);
1654
- }
1655
- const shellProcess = self.processes.spawn('sh', ['sh'], persisted.cwd || '/home/user');
1656
- self.shellProcessPid = shellProcess.pid;
1657
- const runAsProcess = async (parent, cred, argv) => {
1658
- if (argv.length === 0)
1659
- return 0;
1660
- const child = self.processes.spawn(argv.join(' '), argv, parent.cwd, { parentPid: parent.pid, cred });
1661
- const activeShell = self.shell;
1662
- if (!activeShell) {
1663
- self.processes.exit(child.pid, 1);
1664
- throw new Error('shell is not initialized');
1665
- }
1666
- const identity = commandIdentityFor(child.pid);
1667
- let exitCode = 1;
1668
- try {
1669
- const stdin = parent.stdin && parent.stdin !== parent.terminalStdin
1670
- ? await parent.stdin.readAll()
1671
- : undefined;
1672
- const result = await activeShell.execute(argv.map(quoteShellArgument).join(' '), {
1673
- cwd: parent.cwd,
1674
- env: parent.env,
1675
- stdin,
1676
- terminalStdin: parent.terminalStdin,
1677
- signal: parent.signal,
1678
- isolateShellState: true,
1679
- terminalFds: {
1680
- stdin: parent.isFdTerminal?.(0) ?? false,
1681
- stdout: parent.isFdTerminal?.(1) ?? false,
1682
- stderr: parent.isFdTerminal?.(2) ?? false,
1683
- },
1684
- onStdout: (data) => parent.stdout.write(data),
1685
- onStderr: (data) => parent.stderr.write(data),
1686
- commandContext: {
1687
- pid: identity.pid,
1688
- cred: identity.cred,
1689
- setUmask: identity.setUmask,
1690
- },
1691
- runAs: runAsProcess,
1692
- });
1693
- exitCode = result.exitCode;
1694
- return exitCode;
1695
- }
1696
- finally {
1697
- self.processes.exit(child.pid, exitCode);
1698
- }
1699
- };
1700
- const commandIdentityFor = (pid) => ({
1701
- pid,
1702
- get cred() {
1703
- return self.processes.cred(pid);
1704
- },
1705
- setUmask(mask) {
1706
- self.processes.setUmask(pid, mask);
1707
- },
1708
- runAs: runAsProcess,
1709
- });
1710
- self.shell = new Shell(self.terminal, self.kernel.vfs, registry, env, processRegistry, commandIdentityFor(shellProcess.pid));
1711
1674
  // Primitive #7: patch NIMBUS_SESSION_ID into the live shell env.
1712
1675
  // sessionBasePath is "/s/<sid>" set by the X-Nimbus-Base header on
1713
1676
  // the first /ws upgrade — by the time initSession runs (after the
1714
1677
  // ws handshake), it's populated. Older /ws-pre-base callers see
1715
1678
  // an empty string, which is the safe placeholder (no false id).
1716
1679
  //
1717
- // We patch the live env (not the local `env` map above) so persisted
1718
- // shell state on warm-rejoin still picks up the SAME session id —
1719
- // the DO's name is stable across hibernation cycles. Any user
1720
- // `export NIMBUS_SESSION_ID=...` would have been persisted to
1721
- // persisted.env and the spread above would have overridden the
1722
- // empty placeholder; we only set when the live env is empty
1723
- // (don't clobber a user-set value).
1680
+ // We patch the live env (not the overlay the workspace was built
1681
+ // from) so persisted shell state on warm-rejoin still picks up the
1682
+ // SAME session id — the DO's name is stable across hibernation
1683
+ // cycles. Any user `export NIMBUS_SESSION_ID=...` would have been
1684
+ // persisted to persisted.env and the overlay's spread would have
1685
+ // overridden the empty placeholder; we only set when the live env
1686
+ // is empty (don't clobber a user-set value).
1724
1687
  const sessionIdFromBase = (self.sessionBasePath || '').replace(/^\/s\//, '');
1725
1688
  if (sessionIdFromBase) {
1726
1689
  // Shell.env is declared private but mutable at runtime — there's
1727
1690
  // no public setter. We `any`-cast deliberately; the alternative
1728
- // (replacing the whole Shell after ctor) would lose the kernel +
1729
- // registry wiring. Anti-req note: this is NOT a defensive cast,
1730
- // it's a deliberate single-write operation to plug the contract
1731
- // gap that env-construction couldn't fill (sessionBasePath
1732
- // wasn't yet hydrated at ctor time).
1691
+ // (replacing the whole Shell after construction) would lose the
1692
+ // kernel + registry wiring. Anti-req note: this is NOT a defensive
1693
+ // cast, it's a deliberate single-write operation to plug the
1694
+ // contract gap that env-construction couldn't fill (sessionBasePath
1695
+ // wasn't yet hydrated when the workspace was composed).
1733
1696
  const shellAny = self.shell;
1734
1697
  if (!shellAny.env.NIMBUS_SESSION_ID) {
1735
1698
  shellAny.env.NIMBUS_SESSION_ID = sessionIdFromBase;
@@ -2745,17 +2708,15 @@ export function initSession(self, ws) {
2745
2708
  catch { /* non-critical */ }
2746
2709
  }
2747
2710
  // ── Start shell ──
2748
- self.shell.start();
2749
- (async () => {
2750
- try {
2751
- await self.shell.sourceFile('/etc/profile');
2752
- }
2753
- catch { }
2754
- try {
2755
- await self.shell.sourceFile('/home/user/.nimbusrc');
2756
- }
2757
- catch { }
2758
- })();
2711
+ //
2712
+ // Now, and not inside the workspace, because the login files are the
2713
+ // user's and may name any of the commands registered above. Not awaited:
2714
+ // `shell.start()` runs synchronously and the rc files apply as they
2715
+ // finish, which is what the terminal has always done. A user's broken
2716
+ // rc file must not take the socket down with it.
2717
+ workspace.start().catch((e) => {
2718
+ console.warn('[nimbus] shell start failed:', e?.message || e);
2719
+ });
2759
2720
  ws.send(JSON.stringify({ type: 'ready' }));
2760
2721
  }
2761
2722
  function installShellExecutionFeatures(shell, terminal) {