@nimbus-sh/worker 0.7.0 → 0.8.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 (74) hide show
  1. package/dist/facets/manager.d.ts.map +1 -1
  2. package/dist/facets/manager.js +16 -5
  3. package/dist/facets/real-vite-hmr.d.ts +3 -1
  4. package/dist/facets/real-vite-hmr.d.ts.map +1 -1
  5. package/dist/facets/real-vite-hmr.js +18 -8
  6. package/dist/facets/ws-terminal.d.ts +9 -31
  7. package/dist/facets/ws-terminal.d.ts.map +1 -1
  8. package/dist/facets/ws-terminal.js +58 -51
  9. package/dist/hosted/commands.d.ts +12 -0
  10. package/dist/hosted/commands.d.ts.map +1 -0
  11. package/dist/hosted/commands.js +1658 -0
  12. package/dist/hosted/runtime.d.ts +114 -0
  13. package/dist/hosted/runtime.d.ts.map +1 -0
  14. package/dist/hosted/runtime.js +288 -0
  15. package/dist/hosted/services.d.ts +40 -0
  16. package/dist/hosted/services.d.ts.map +1 -0
  17. package/dist/hosted/services.js +512 -0
  18. package/dist/runtime/bash-repl.d.ts +1 -1
  19. package/dist/runtime/bash-repl.d.ts.map +1 -1
  20. package/dist/runtime/bash-repl.js +38 -4
  21. package/dist/runtime/bun-runner.js +1 -1
  22. package/dist/runtime/node-runner.d.ts +1 -1
  23. package/dist/runtime/node-runner.d.ts.map +1 -1
  24. package/dist/runtime/node-runner.js +2 -2
  25. package/dist/runtime/opencode-facet-runner.js +14 -14
  26. package/dist/runtime/package-manager.d.ts +35 -122
  27. package/dist/runtime/package-manager.d.ts.map +1 -1
  28. package/dist/runtime/package-manager.js +61 -596
  29. package/dist/runtime/process-logs-api.d.ts +1 -1
  30. package/dist/runtime/process-logs-api.d.ts.map +1 -1
  31. package/dist/runtime/process-logs-api.js +1 -1
  32. package/dist/runtime/python-repl.d.ts +2 -1
  33. package/dist/runtime/python-repl.d.ts.map +1 -1
  34. package/dist/runtime/python-repl.js +82 -59
  35. package/dist/runtime/repl-session.d.ts +9 -47
  36. package/dist/runtime/repl-session.d.ts.map +1 -1
  37. package/dist/runtime/repl-session.js +105 -105
  38. package/dist/runtime/runtime-catalog.d.ts +13 -0
  39. package/dist/runtime/runtime-catalog.d.ts.map +1 -1
  40. package/dist/runtime/runtime-catalog.js +88 -0
  41. package/dist/session/agent.d.ts +1 -21
  42. package/dist/session/agent.d.ts.map +1 -1
  43. package/dist/session/ai.d.ts +5 -1
  44. package/dist/session/ai.d.ts.map +1 -1
  45. package/dist/session/ai.js +7 -0
  46. package/dist/session/hibernation.d.ts +2 -1
  47. package/dist/session/hibernation.d.ts.map +1 -1
  48. package/dist/session/hibernation.js +7 -4
  49. package/dist/session/init.d.ts +1 -36
  50. package/dist/session/init.d.ts.map +1 -1
  51. package/dist/session/init.js +17 -1773
  52. package/dist/session/loopback.d.ts +2 -29
  53. package/dist/session/loopback.d.ts.map +1 -1
  54. package/dist/session/loopback.js +3 -27
  55. package/dist/session/nimbus-session.d.ts +22 -13
  56. package/dist/session/nimbus-session.d.ts.map +1 -1
  57. package/dist/session/nimbus-session.js +67 -484
  58. package/dist/session/npm-install-port.d.ts +3 -1
  59. package/dist/session/npm-install-port.d.ts.map +1 -1
  60. package/dist/session/programmatic.d.ts +5 -38
  61. package/dist/session/programmatic.d.ts.map +1 -1
  62. package/dist/session/programmatic.js +16 -48
  63. package/dist/session/rpc.d.ts.map +1 -1
  64. package/dist/session/rpc.js +1 -2
  65. package/dist/session/supervisor-op.d.ts +2 -2
  66. package/dist/session/supervisor-op.d.ts.map +1 -1
  67. package/dist/session/supervisor-op.js +11 -3
  68. package/dist/workspace-host.d.ts +6 -0
  69. package/dist/workspace-host.d.ts.map +1 -0
  70. package/dist/workspace-host.js +5 -0
  71. package/dist/workspace.d.ts +2 -0
  72. package/dist/workspace.d.ts.map +1 -0
  73. package/dist/workspace.js +1 -0
  74. package/package.json +16 -11
@@ -1,22 +1,10 @@
1
- /**
2
- * nimbus-session.ts — NimbusSession Durable Object (v2.0).
3
- *
4
- * The supervisor DO that owns the VFS, shell, and all commands.
5
- * `node` execution is delegated to dynamic workers via LOADER.load().
6
- * IPC between facets and the supervisor flows through SupervisorRPC.
7
- */
8
- import { staticStdinReader } from '@nimbus-sh/core/shell/stdin-adapter.js';
1
+ import * as runtimeServices from '../hosted/services.js';
9
2
  import { DurableObject as CloudflareDurableObject } from 'cloudflare:workers';
10
3
  import { SqliteVFS } from '@nimbus-sh/core/vfs/sqlite-vfs.js';
11
- import { composeFacetManager } from '../facets/compose.js';
12
- import { FacetProcessManager, textBytes } from '../facets/process.js';
13
- import { ChildProcessSpawnPool } from '../loaders/child-process/spawn-pool.js';
14
4
  import { SessionProcessSupervisor } from '@nimbus-sh/core/runtime/session-process-supervisor.js';
15
5
  import { PID_GEN_STRIDE } from '@nimbus-sh/core/runtime/process-table.js';
16
- import { CRED_KERNEL, CRED_SESSION_USER, } from '@nimbus-sh/core/runtime/os-contracts.js';
6
+ import { CRED_KERNEL, CRED_SESSION_USER } from '@nimbus-sh/core/runtime/os-contracts.js';
17
7
  import { PortRegistry } from '@nimbus-sh/core/runtime/port-registry.js';
18
- import { EsbuildBundlePool } from '../facets/esbuild-bundle-pool.js';
19
- import { EsbuildService } from '@nimbus-sh/core/runtime/esbuild-service.js';
20
8
  import { registerAllocObserver } from '@nimbus-sh/platform/heavy-alloc-coord.js';
21
9
  // S10: oom-discriminator helpers (recordFailure, getFailures,
22
10
  // getLastRpcFrame, getLastFacetId, snapshotForStorage, rehydrateFromStorage)
@@ -30,58 +18,63 @@ import { registerAllocObserver } from '@nimbus-sh/platform/heavy-alloc-coord.js'
30
18
  // longer references them directly. Phase 2 A'.5 renamed the function
31
19
  // (was getEsbuildWasmBytes; cached) to fetchEsbuildWasmBytes (no
32
20
  // supervisor cache; goes through env.ASSETS on demand).
21
+ // S10: oom-discriminator helpers (recordFailure, getFailures,
22
+ // getLastRpcFrame, getLastFacetId, snapshotForStorage, rehydrateFromStorage)
23
+ // moved to sibling modules (-rpc uses recordFailure for _reportExternalExit;
24
+ // -ws uses recordFailure for webSocketError; -diag uses getFailures +
25
+ // snapshotForStorage + rehydrateFromStorage; -routes uses getFailures +
26
+ // getLastRpcFrame + getLastFacetId for /api/_diag/memory). Class file
27
+ // no longer references any of them directly.
28
+ // S10: classifyError, LRU_MAX_ENTRIES, fetchEsbuildWasmBytes moved to
29
+ // sibling modules (-rpc, -routes, esbuild-wasm-bytes); class file no
30
+ // longer references them directly. Phase 2 A'.5 renamed the function
31
+ // (was getEsbuildWasmBytes; cached) to fetchEsbuildWasmBytes (no
32
+ // supervisor cache; goes through env.ASSETS on demand).
33
33
  import { adoptCtxExports } from '@nimbus-sh/fabric/composition.js';
34
- import { NIMBUS_VERSION, DEFAULT_MOUNT_POINTS, CF_COMPAT_DATE } from '@nimbus-sh/core/constants.js';
34
+ import { NIMBUS_VERSION, DEFAULT_MOUNT_POINTS } from '@nimbus-sh/core/constants.js';
35
35
  import { seedBaseFilesystem } from '@nimbus-sh/core/workspace';
36
+ import { routeSessionLoopback } from './ai.js';
36
37
  import { seedProject, SEED_PROJECT_NAME } from '@nimbus-sh/core/vfs/seed-project.js';
37
38
  import { BASE_PATH_HEADER } from '../_shared/session-router.js';
38
39
  import { ATTACH_BOOTSTRAP_JTI_KEY_PREFIX, SESSION_DESTROYED_KEY } from './keys.js';
39
40
  import { appendScrollback } from './state-store.js';
40
- import { notifyTerminalEvent, wireProcessLogSocketBroadcast } from '../runtime/process-logs-api.js';
41
+ import { wireProcessLogSocketBroadcast } from '../runtime/process-logs-api.js';
41
42
  // S3: tryEnableReplicas + getReplicaState extracted to ./nimbus-session-replica.ts.
42
- import { wireReplicasOnConstruct as _w12WireReplicasOnConstruct, getReplicaState as _w12GetReplicaState, } from './replica-routes.js';
43
+ // S3: tryEnableReplicas + getReplicaState extracted to ./nimbus-session-replica.ts.
44
+ import { wireReplicasOnConstruct as _w12WireReplicasOnConstruct, getReplicaState as _w12GetReplicaState } from './replica-routes.js';
43
45
  // S5: storage-key constants moved to ./nimbus-session-keys.ts; consumed by
44
46
  // sibling modules (-hib, -diag, -ws). The class file itself no longer
45
47
  // references any storage key directly.
46
48
  // S4: W9 hibernation surface extracted.
47
- import { wireHibernationOnConstruct as _w9WireHibernationOnConstruct, wireProcessLogPersist as _w9DoWireProcessLogPersist, ensureHibSchema as _w9DoEnsureHibSchema, scheduleHibFlush as _w9DoScheduleHibFlush, clearDestroyedTombstone as _w1ClearDestroyedTombstone, dispatchAlarm as _w9DoDispatchAlarm, flushOnClose as _w9DoFlushOnClose, } from './hibernation.js';
49
+ // S5: storage-key constants moved to ./nimbus-session-keys.ts; consumed by
50
+ // sibling modules (-hib, -diag, -ws). The class file itself no longer
51
+ // references any storage key directly.
52
+ // S4: W9 hibernation surface extracted.
53
+ import { wireHibernationOnConstruct as _w9WireHibernationOnConstruct, wireProcessLogPersist as _w9DoWireProcessLogPersist, ensureHibSchema as _w9DoEnsureHibSchema, scheduleHibFlush as _w9DoScheduleHibFlush, clearDestroyedTombstone as _w1ClearDestroyedTombstone, dispatchAlarm as _w9DoDispatchAlarm, flushOnClose as _w9DoFlushOnClose } from './hibernation.js';
48
54
  import { timers } from '@nimbus-sh/fabric/timers.js';
49
55
  import { adoptGeneration, generation } from '@nimbus-sh/fabric/generation.js';
50
56
  // S6: initSession (1875 LOC of cmd registrations + boot wiring) extracted.
57
+ // S6: initSession (1875 LOC of cmd registrations + boot wiring) extracted.
51
58
  import { initSession as _w11InitSession } from './init.js';
52
59
  // S7: webSocket lifecycle (message, close, error, F1 discriminator,
53
60
  // _w5SafePersistRing) extracted.
54
- import { wsMessage as _wsDoMessage, wsClose as _wsDoClose, wsError as _wsDoError, safePersistRing as _wsDoSafePersistRing, } from './ws.js';
61
+ // S7: webSocket lifecycle (message, close, error, F1 discriminator,
62
+ // _w5SafePersistRing) extracted.
63
+ import { wsMessage as _wsDoMessage, wsClose as _wsDoClose, wsError as _wsDoError, safePersistRing as _wsDoSafePersistRing } from './ws.js';
64
+ // S8: Supervisor RPC + W8 cp* + legacy VFS impls extracted.
55
65
  // S8: Supervisor RPC + W8 cp* + legacy VFS impls extracted.
56
66
  import * as _rpc from './rpc.js';
57
67
  import { buildSessionSupervisorOps } from './supervisor-op.js';
58
- // The supervisor terminates a facet's outbound sockets so inbound frames
59
- // arrive as supervisor replies (VFS coherence witness 3).
60
- import { WebSocketRelay } from './ws-relay.js';
68
+ // S9: HTTP fetch routing extracted (combined S9a + S9b).
61
69
  // S9: HTTP fetch routing extracted (combined S9a + S9b).
62
70
  import * as _routes from './routes.js';
63
71
  import * as _portCapability from './port-capability.js';
64
72
  // Programmatic SDK RPC surface.
73
+ // Programmatic SDK RPC surface.
65
74
  import * as _programmatic from './programmatic.js';
66
75
  // S10: heap probe + W5 OOM-ring persistence extracted.
76
+ // S10: heap probe + W5 OOM-ring persistence extracted.
67
77
  import * as _diag from './diag.js';
68
- // ── Pure helpers extracted to ./nimbus-session-helpers.ts (S1) ────────
69
- //
70
- // renderNoDevServerHtml, BUNDLER_BIN_PREFIXES, NIMBUS_UNSUPPORTED_BINS,
71
- // WRANGLER_IGNORED_FLAGS{,_WITH_VALUE}, WRANGLER_UNSUPPORTED_CONFIG_FIELDS,
72
- // filterWranglerFlags, detectUnsupportedWranglerConfig, _CP_FACET_DIRECT,
73
- // _CP_PURE_BUILTIN, _classifyCommand, detectBundlerBin, checkNodeModulesGuard
74
- // all live in the helpers module now.
75
- //
76
- // They are imported here (so call sites in this file work unchanged) and
77
- // re-exported (so external callers importing them from
78
- // `./nimbus-session.js` keep working — back-compat).
79
- //
80
- // (esbuild wasm bytes are fetched from env.ASSETS by
81
- // src/esbuild-wasm-bytes.ts at pool-construction time; A'.5 dropped
82
- // the supervisor-resident cache + the SUPERVISOR.getEsbuildWasm RPC.)
83
- // Helpers needed by this class file's own logic (not just re-export).
84
- import { _classifyCommand, } from './helpers.js';
85
78
  import { z } from 'zod/v4';
86
79
  const CpFacetDirectPayloadSchema = z.object({
87
80
  command: z.unknown().optional().transform((value) => value == null ? '' : String(value)),
@@ -241,7 +234,26 @@ The editor opens this file in Markdown preview mode by default. Use
241
234
  `;
242
235
  }
243
236
  export class NimbusSession extends CloudflareDurableObject {
237
+ #runtimeServices;
244
238
  // this.ctx and this.env are provided by the DurableObject base class
239
+ runtimeWorkspace = null;
240
+ runtimeReady = null;
241
+ get runtimeManager() {
242
+ if (!this.runtimeWorkspace)
243
+ throw new Error('Nimbus runtime is not initialized');
244
+ return this.runtimeWorkspace.runtimes;
245
+ }
246
+ async routeLoopback(port, request) {
247
+ return routeSessionLoopback({ ctx: this.ctx, env: this.env, portRegistry: this.portRegistry }, port, request);
248
+ }
249
+ async ensureRuntimeReady() {
250
+ if (this.shell)
251
+ return;
252
+ this.runtimeReady ??= this.initSession(null).then(() => {
253
+ this._b4Phase = 'drained';
254
+ }).finally(() => { this.runtimeReady = null; });
255
+ await this.runtimeReady;
256
+ }
245
257
  sqliteFs = null;
246
258
  kernel = null;
247
259
  shell = null;
@@ -414,7 +426,13 @@ export class NimbusSession extends CloudflareDurableObject {
414
426
  */
415
427
  wranglerAliasBannerShown = false;
416
428
  constructor(ctx, env) {
417
- super(ctx, env); // enables RPC on the DO
429
+ super(ctx, env);
430
+ this.#runtimeServices = runtimeServices.bindRuntimeServices(this, {
431
+ ctx,
432
+ env,
433
+ notify: (line) => this._notifySession(line),
434
+ requestLaunchTurn: (at) => this._scheduleLaunchTurn(at),
435
+ });
418
436
  // In `wrangler dev`, the outer Worker and this DO share a single
419
437
  // workerd process, so the `adoptCtxExports(ctx.exports)` call in the
420
438
  // outer fetch handler (src/index.ts) is visible here via the
@@ -989,11 +1007,7 @@ export class NimbusSession extends CloudflareDurableObject {
989
1007
  return _diag.persistRing(this, this.ctx);
990
1008
  }
991
1009
  /** The session's esbuild facet pool provider; constructing it does no work. */
992
- ensureBundlePool() {
993
- if (!this.bundlePool)
994
- this.bundlePool = new EsbuildBundlePool(this.env, this.ctx);
995
- return this.bundlePool;
996
- }
1010
+ ensureBundlePool() { return this.#runtimeServices.ensureBundlePool(); }
997
1011
  /**
998
1012
  * The session's FacetManager, composed through the one factory every host
999
1013
  * uses (`facets/compose.ts`). What is wired here is only what is the
@@ -1002,295 +1016,19 @@ export class NimbusSession extends CloudflareDurableObject {
1002
1016
  * report that keeps the process table honest. The isolated esbuild
1003
1017
  * transform and the durable image-store fallback are the factory's.
1004
1018
  */
1005
- ensureFacetManager() {
1006
- if (!this.facetManagerComposed) {
1007
- // The manager is composed over the filesystem, so the filesystem comes
1008
- // first. Cheap and idempotent; every caller already stood it up or is
1009
- // about to.
1010
- this.ensureSqliteFs();
1011
- this.facetManagerComposed = composeFacetManager({
1012
- ctx: this.ctx,
1013
- env: this.env,
1014
- processes: this.processes,
1015
- portRegistry: this.portRegistry,
1016
- vfs: this.sqliteFs,
1017
- ...(this.esbuildService ? { esbuild: this.esbuildService } : {}),
1018
- hooks: {
1019
- onExternalExit: (pid, code, reason) => this._reportExternalExit(pid, code, reason),
1020
- requestLaunchTurn: (notBefore) => { void this._scheduleLaunchTurn(notBefore); },
1021
- notify: (line) => this._notifySession(line),
1022
- onSpawn: (pid, command, longRunning) => {
1023
- const attachedTty = this.processes.get(pid)?.attachedTty === true;
1024
- if (longRunning) {
1025
- try {
1026
- this.processes.openInput(pid);
1027
- }
1028
- catch { }
1029
- }
1030
- // Only surface long-running / user-visible spawns to keep
1031
- // the terminal uncluttered. Short `node <file>` evals also
1032
- // get a line because users want the pid for `logs`/`kill`.
1033
- if (!this.terminal)
1034
- return;
1035
- const label = longRunning ? 'started (long-running)' : 'started';
1036
- this.terminal.write(`\x1b[2m[facet ${label}: pid=${pid} cmd="${command}"]\x1b[0m\r\n`);
1037
- // Structured event so the tabs UI can auto-open a log tab
1038
- // for long-running processes (vite, wrangler dev, etc.).
1039
- notifyTerminalEvent(this.terminal, {
1040
- type: 'spawn', pid, command, longRunning, attachedTty,
1041
- });
1042
- },
1043
- },
1044
- });
1045
- }
1046
- const composed = this.facetManagerComposed;
1047
- // W3.5 Fix B: share the session's lazy esbuildService with the
1048
- // FacetManager so the bundle's ESM→CJS pre-pass doesn't pay
1049
- // wasm-init twice. The session may construct it after the manager
1050
- // exists, so the share is re-offered on every call; FacetManager
1051
- // otherwise lazy-creates its own on first exec — same wasm bytes,
1052
- // same ~10ms init cost, just paid once per surface.
1053
- if (this.esbuildService) {
1054
- composed.manager.setEsbuildService(this.esbuildService);
1055
- }
1056
- return composed;
1057
- }
1019
+ ensureFacetManager() { return this.#runtimeServices.ensureFacetManager(); }
1058
1020
  /**
1059
1021
  * The supervisor-owned WebSocket relay. Lazy, because most sessions never
1060
1022
  * open a socket and the sockets it holds are live objects that must not
1061
1023
  * outlive the session.
1062
1024
  */
1063
- _ensureWebSocketRelay() {
1064
- if (!this.webSocketRelay)
1065
- this.webSocketRelay = new WebSocketRelay();
1066
- return this.webSocketRelay;
1067
- }
1025
+ _ensureWebSocketRelay() { return this.#runtimeServices._ensureWebSocketRelay(); }
1068
1026
  /**
1069
1027
  * W8: lazily construct the FacetProcessManager when the first cp* RPC
1070
1028
  * arrives. Wired with adapters that bridge the Nimbus shell command
1071
1029
  * registry to the FacetProcessManager's CommandRegistryLike contract.
1072
1030
  */
1073
- _ensureFacetProcessManager() {
1074
- if (this.facetProcessManager)
1075
- return this.facetProcessManager;
1076
- this.ensureSqliteFs();
1077
- this.ensureFacetManager();
1078
- // FacetProcessManager is statically imported at top-of-file (W8).
1079
- // No lazy-import: workerd doesn't ship CJS require, and the dynamic
1080
- // import would be async — making _ensureFacetProcessManager async
1081
- // would force every cp* RPC entry point to also be async on the
1082
- // promise-resolution path, which is fine but uglier. Compile-time
1083
- // tree-shaking handles unused-when-no-cp-RPC paths.
1084
- // Adapter for FacetManagerLike — wraps the existing FacetManager.exec
1085
- // with a streaming surface. Phase 1 simplification: facet-direct
1086
- // commands are dispatched through the shell registry the same way
1087
- // shell.execute does, but with the per-PID hooks routed.
1088
- const facetMgrAdapter = {
1089
- execStream: async (codeJson, opts, hooks) => {
1090
- // codeJson is a payload from FacetProcessManager._dispatch facet-direct
1091
- // path: {command, args, env, cwd, stdin}. We dispatch through the
1092
- // existing shell registry by resolving the command and invoking
1093
- // it with synthesized output streams that route to hooks.
1094
- let payload;
1095
- try {
1096
- const parsed = CpFacetDirectPayloadSchema.safeParse(JSON.parse(codeJson));
1097
- if (!parsed.success) {
1098
- hooks.onStderr(textBytes('child_process: facet dispatch requires a broker-assigned process pid\n'));
1099
- return 1;
1100
- }
1101
- payload = parsed.data;
1102
- }
1103
- catch {
1104
- hooks.onStderr(textBytes('child_process: invalid facet dispatch payload\n'));
1105
- return 1;
1106
- }
1107
- const registry = this._cpRegistry;
1108
- if (!registry) {
1109
- hooks.onStderr(textBytes('child_process: command registry unavailable\n'));
1110
- return 127;
1111
- }
1112
- const commandName = normalizeCpCommandName(payload.command);
1113
- const cmd = await registry.resolve(commandName);
1114
- if (!cmd) {
1115
- hooks.onStderr(textBytes(`${payload.command}: command not found\n`));
1116
- return 127;
1117
- }
1118
- // Synthesize a CommandContext for the internal shell substrate.
1119
- const stdoutStream = { write: (d) => hooks.onStdout(textBytes(String(d))) };
1120
- const stderrStream = { write: (d) => hooks.onStderr(textBytes(String(d))) };
1121
- const ac = new AbortController();
1122
- const cred = this.processes.cred(payload.processPid);
1123
- const ctx = {
1124
- pid: payload.processPid,
1125
- cred,
1126
- args: payload.args || [],
1127
- env: payload.env || {},
1128
- cwd: payload.cwd || '/home/user',
1129
- vfs: this.sqliteFs.as(cred),
1130
- stdout: stdoutStream,
1131
- stderr: stderrStream,
1132
- signal: ac.signal,
1133
- // For commands that need stdin we pass a tiny adapter.
1134
- stdin: staticStdinReader(payload.stdin || ''),
1135
- setUmask: (mask) => { this.processes.setUmask(payload.processPid, mask); },
1136
- runAs: async (targetCred, argv) => {
1137
- if (argv.length === 0)
1138
- return 0;
1139
- const child = this.processes.spawn(argv.join(' '), argv, payload.cwd, { parentPid: payload.processPid, cred: targetCred });
1140
- let exitCode = 1;
1141
- try {
1142
- exitCode = await cmdRegistryAdapter.runPureBuiltin(child.pid, argv[0], argv.slice(1), payload.env, payload.cwd, payload.stdin, hooks);
1143
- return exitCode;
1144
- }
1145
- finally {
1146
- this.processes.exit(child.pid, exitCode);
1147
- }
1148
- },
1149
- __nimbusCaptureOutput: true,
1150
- };
1151
- try {
1152
- const code = await cmd(ctx);
1153
- return typeof code === 'number' ? code : 0;
1154
- }
1155
- catch (e) {
1156
- hooks.onStderr(textBytes(`${payload.command}: ${e?.message || String(e)}\n`));
1157
- return 1;
1158
- }
1159
- },
1160
- abort: (facetName) => {
1161
- // Best-effort: relay to ctx.facets.abort, mirroring FacetManager.kill.
1162
- try {
1163
- this.ctx.facets?.abort?.(facetName, new Error('SIGKILL'));
1164
- }
1165
- catch { }
1166
- return true;
1167
- },
1168
- };
1169
- // Adapter for CommandRegistryLike. The shared shell registry is
1170
- // attached to `this._cpRegistry` by the shell-init path (see
1171
- // construction near line 2058 — registry passed as ctor arg there).
1172
- const cmdRegistryAdapter = {
1173
- // Consult the live shell registry FIRST so dynamically-registered
1174
- // commands (registerUnixCommands / git / npm / wrangler etc.) are seen
1175
- // even if they're not in the static _CP_PURE_BUILTIN allow-list. Falls
1176
- // back to the static facet-direct table for known facet-only commands.
1177
- // Returns null
1178
- // (→ exit 127) for everything unknown.
1179
- resolve: (name) => {
1180
- const commandName = normalizeCpCommandName(name);
1181
- const registry = this._cpRegistry;
1182
- if (registry && typeof registry.has === 'function' && registry.has(commandName)) {
1183
- // Registered — classify by name. Reuse the static table so
1184
- // facet-direct commands (node/npm/git/...) keep their kind
1185
- // even when they ALSO happen to be registry entries.
1186
- return _classifyCommand(commandName) || { kind: 'pure-builtin' };
1187
- }
1188
- return _classifyCommand(commandName);
1189
- },
1190
- runPureBuiltin: async (pid, name, args, env, cwd, stdin, hooks) => {
1191
- const registry = this._cpRegistry;
1192
- if (!registry) {
1193
- hooks.onStderr(textBytes('cp: registry unavailable\n'));
1194
- return 127;
1195
- }
1196
- const commandName = normalizeCpCommandName(name);
1197
- const cmd = await registry.resolve(commandName);
1198
- if (!cmd) {
1199
- hooks.onStderr(textBytes(`${name}: command not found\n`));
1200
- return 127;
1201
- }
1202
- const cred = this.processes.cred(pid);
1203
- const ac = new AbortController();
1204
- const ctx = {
1205
- pid,
1206
- cred,
1207
- args, env, cwd,
1208
- vfs: this.sqliteFs.as(cred),
1209
- stdout: { write: (d) => hooks.onStdout(textBytes(String(d))) },
1210
- stderr: { write: (d) => hooks.onStderr(textBytes(String(d))) },
1211
- signal: ac.signal,
1212
- stdin: staticStdinReader(stdin),
1213
- setUmask: (mask) => { this.processes.setUmask(pid, mask); },
1214
- runAs: async (targetCred, argv) => {
1215
- if (argv.length === 0)
1216
- return 0;
1217
- const child = this.processes.spawn(argv.join(' '), argv, cwd, { parentPid: pid, cred: targetCred });
1218
- let exitCode = 1;
1219
- try {
1220
- exitCode = await cmdRegistryAdapter.runPureBuiltin(child.pid, argv[0], argv.slice(1), env, cwd, stdin, hooks);
1221
- return exitCode;
1222
- }
1223
- finally {
1224
- this.processes.exit(child.pid, exitCode);
1225
- }
1226
- },
1227
- };
1228
- try {
1229
- const code = await cmd(ctx);
1230
- return typeof code === 'number' ? code : 0;
1231
- }
1232
- catch (e) {
1233
- hooks.onStderr(textBytes(`${name}: ${e?.message || String(e)}\n`));
1234
- return 1;
1235
- }
1236
- },
1237
- };
1238
- // Construct the child-process Loader pool when the binding is available.
1239
- // Unit-test hosts without LOADER continue through direct dispatch.
1240
- let spawnPool;
1241
- try {
1242
- const envAny = this.env;
1243
- if (envAny?.LOADER && typeof envAny.LOADER.get === 'function') {
1244
- spawnPool = new ChildProcessSpawnPool(this.env, this.ctx);
1245
- }
1246
- }
1247
- catch {
1248
- spawnPool = undefined;
1249
- }
1250
- this.facetProcessManager = new FacetProcessManager({
1251
- facetMgr: facetMgrAdapter,
1252
- processes: this.processes,
1253
- vfsForProcess: (pid) => this.sqliteFs.as(this.processes.cred(pid)),
1254
- commandRegistry: cmdRegistryAdapter,
1255
- shellExecutor: {
1256
- execute: async (pid, commandLine, env, cwd, stdin, hooks) => {
1257
- if (!this.shell) {
1258
- hooks.onStderr(textBytes('sh: shell unavailable\n'));
1259
- return 127;
1260
- }
1261
- const cred = this.processes.cred(pid);
1262
- const setUmask = (mask) => { this.processes.setUmask(pid, mask); };
1263
- const runAs = async (_parent, targetCred, argv) => {
1264
- if (argv.length === 0)
1265
- return 0;
1266
- const child = this.processes.spawn(argv.join(' '), argv, cwd, { parentPid: pid, cred: targetCred });
1267
- let exitCode = 1;
1268
- try {
1269
- exitCode = await cmdRegistryAdapter.runPureBuiltin(child.pid, argv[0], argv.slice(1), env, cwd, stdin, hooks);
1270
- return exitCode;
1271
- }
1272
- finally {
1273
- this.processes.exit(child.pid, exitCode);
1274
- }
1275
- };
1276
- const result = await this.shell.execute(String(commandLine), {
1277
- cwd: cwd || '/home/user',
1278
- env: { ...this.shell.env, ...(env || {}) },
1279
- onStdout: hooks.onStdout,
1280
- onStderr: hooks.onStderr,
1281
- stdin,
1282
- isolateShellState: true,
1283
- commandContext: { pid, cred, setUmask },
1284
- runAs,
1285
- });
1286
- return typeof result?.exitCode === 'number' ? result.exitCode : 0;
1287
- },
1288
- },
1289
- ctx: this.ctx,
1290
- spawnPool,
1291
- });
1292
- return this.facetProcessManager;
1293
- }
1031
+ _ensureFacetProcessManager() { return this.#runtimeServices._ensureFacetProcessManager(); }
1294
1032
  /**
1295
1033
  * Set the shell command registry for the W8 broker to dispatch
1296
1034
  * resolved commands. Called from the shell-init path right after
@@ -1304,148 +1042,13 @@ export class NimbusSession extends CloudflareDurableObject {
1304
1042
  * fetch calls across the lifetime of this DO instance. This prevents
1305
1043
  * ephemeral port exhaustion from creating a new worker per fetch.
1306
1044
  */
1307
- ensureFetchProxy(log) {
1308
- if (this.fetchProxyEntrypoint)
1309
- return this.fetchProxyEntrypoint;
1310
- try {
1311
- const env = this.env;
1312
- if (!env?.LOADER?.load) {
1313
- log?.('LOADER.load not available — using global fetch');
1314
- return null;
1315
- }
1316
- // Buffered proxy: reads the entire response body into an ArrayBuffer
1317
- // and returns it in ONE message instead of forwarding a ReadableStream.
1318
- // In workerd local dev, streaming responses across a service-binding
1319
- // RPC fabric opens a separate loopback socket PER chunk (~16KB), which
1320
- // exhausts ephemeral ports for larger installs (npm registry packuments
1321
- // are 500KB-3MB, tarballs up to 5MB). Buffering to arrayBuffer means
1322
- // 1 stub call = 1 loopback connection, not N connections.
1323
- //
1324
- // 32MB cap prevents a malformed giant response from OOMing the proxy
1325
- // isolate. Packages with tarballs larger than 32MB will fail to install
1326
- // cleanly (returned as 413 → caller treats as failed fetch).
1327
- const proxyCode = [
1328
- 'const MAX_BYTES = 32 * 1024 * 1024;',
1329
- 'export default {',
1330
- ' async fetch(request, workerEnv) {',
1331
- ' try {',
1332
- ' const body = await request.json();',
1333
- ' const resp = await fetch(body.url, {',
1334
- ' method: body.method || "GET",',
1335
- ' headers: body.headers || {},',
1336
- ' });',
1337
- ' // Check advertised Content-Length before buffering',
1338
- ' const clStr = resp.headers.get("content-length");',
1339
- ' if (clStr) {',
1340
- ' const cl = parseInt(clStr, 10);',
1341
- ' if (cl > MAX_BYTES) {',
1342
- ' return new Response(',
1343
- ' JSON.stringify({ error: "response too large: " + cl + " bytes (cap " + MAX_BYTES + ")" }),',
1344
- ' { status: 413, headers: { "Content-Type": "application/json" } }',
1345
- ' );',
1346
- ' }',
1347
- ' }',
1348
- ' // Buffer entire body — ONE message, not streamed chunks',
1349
- ' const buf = await resp.arrayBuffer();',
1350
- ' if (buf.byteLength > MAX_BYTES) {',
1351
- ' return new Response(',
1352
- ' JSON.stringify({ error: "response exceeded cap: " + buf.byteLength + " bytes" }),',
1353
- ' { status: 413, headers: { "Content-Type": "application/json" } }',
1354
- ' );',
1355
- ' }',
1356
- ' return new Response(buf, {',
1357
- ' status: resp.status,',
1358
- ' statusText: resp.statusText,',
1359
- ' headers: Object.fromEntries(resp.headers.entries()),',
1360
- ' });',
1361
- ' } catch (e) {',
1362
- ' return new Response(JSON.stringify({ error: e.message }), {',
1363
- ' status: 502,',
1364
- ' headers: { "Content-Type": "application/json" },',
1365
- ' });',
1366
- ' }',
1367
- ' }',
1368
- '};',
1369
- ].join('\n');
1370
- const worker = env.LOADER.load({
1371
- compatibilityDate: CF_COMPAT_DATE,
1372
- compatibilityFlags: ['nodejs_compat'],
1373
- mainModule: 'fetch-proxy.js',
1374
- modules: { 'fetch-proxy.js': proxyCode },
1375
- });
1376
- this.fetchProxyEntrypoint = worker.getEntrypoint();
1377
- log?.('Fetch proxy worker created (singleton)');
1378
- return this.fetchProxyEntrypoint;
1379
- }
1380
- catch (e) {
1381
- log?.(`Fetch proxy creation failed: ${e?.message}`);
1382
- return null;
1383
- }
1384
- }
1045
+ ensureFetchProxy(log) { return this.#runtimeServices.ensureFetchProxy(log); }
1385
1046
  /**
1386
1047
  * Build a FetchFn that routes through the singleton proxy entrypoint.
1387
1048
  * All concurrent fetches share ONE worker — no port exhaustion.
1388
1049
  */
1389
- buildFetchFn(log) {
1390
- const entrypoint = this.ensureFetchProxy(log);
1391
- if (!entrypoint)
1392
- return undefined;
1393
- return async (url, init) => {
1394
- const headers = {};
1395
- if (init?.headers) {
1396
- if (init.headers instanceof Headers) {
1397
- init.headers.forEach((v, k) => { headers[k] = v; });
1398
- }
1399
- else if (typeof init.headers === 'object') {
1400
- Object.assign(headers, init.headers);
1401
- }
1402
- }
1403
- return entrypoint.fetch(new Request('http://fetch-proxy/do-fetch', {
1404
- method: 'POST',
1405
- body: JSON.stringify({ url, method: init?.method || 'GET', headers }),
1406
- }));
1407
- };
1408
- }
1409
- async ensureNpmInstaller(onProgress) {
1410
- this.ensureSqliteFs();
1411
- if (!this.esbuildService) {
1412
- if (!this.sqliteFs)
1413
- throw new Error('Session VFS is not initialized');
1414
- this.esbuildService = new EsbuildService(this.sqliteFs.as(CRED_KERNEL));
1415
- }
1416
- // Lazy-load the installer (+ its ~216 KB resolver/facet/loader-pool
1417
- // subgraph) on first npm use so it stays out of the cold script-eval
1418
- // graph. The install command paths that call this are already async.
1419
- const { NpmInstaller } = await import('../npm/installer.js');
1420
- // ── Lazy fetch-proxy ────────────────────────────────────────────
1421
- // The fetch-proxy is a singleton dynamic worker (LOADER.load) that
1422
- // buffers registry responses to dodge wrangler-local-dev port
1423
- // exhaustion. It is only needed for the in-supervisor npm paths.
1424
- // When the resolver and install paths run in facets (default-on),
1425
- // they use bare globalThis.fetch and need no proxy.
1426
- //
1427
- // workerd has a per-DO cap on concurrent dynamic workers (~5-6
1428
- // empirically). A permanent live proxy worker eats one of those
1429
- // slots for the entire DO lifetime, so the proxy is built only when
1430
- // any facet path is disabled via its env flag.
1431
- const useFacetResolver = this._envFlagDefaultOn('NIMBUS_FACET_RESOLVER');
1432
- const useFacetInstall = this._envFlagDefaultOn('NIMBUS_FACET_NPM_INSTALL');
1433
- const useBatchFacet = this._envFlagDefaultOn('NIMBUS_FACET_NPM_INSTALL_BATCH');
1434
- const needProxy = !(useFacetResolver && useFacetInstall && useBatchFacet);
1435
- const fetchFn = needProxy ? this.buildFetchFn(onProgress) : undefined;
1436
- if (!needProxy) {
1437
- onProgress?.(`[npm] Lazy fetch-proxy: skipped (all facet paths default-on)`);
1438
- }
1439
- this.npmInstaller = new NpmInstaller(this.sqliteFs, this.ctx.storage.sql, {
1440
- esbuild: this.esbuildService,
1441
- bundlePool: this.ensureBundlePool(),
1442
- ctx: this.ctx,
1443
- env: this.env,
1444
- onProgress,
1445
- fetchFn,
1446
- });
1447
- return this.npmInstaller;
1448
- }
1050
+ buildFetchFn(log) { return this.#runtimeServices.buildFetchFn(log); }
1051
+ async ensureNpmInstaller(onProgress) { return this.#runtimeServices.ensureNpmInstaller(onProgress); }
1449
1052
  /**
1450
1053
  * Read an environment flag with default-on semantics. Mirrors the
1451
1054
  * shouldUseFacetPool / shouldUseFacetResolver / shouldUseBatchFacet
@@ -1453,15 +1056,7 @@ export class NimbusSession extends CloudflareDurableObject {
1453
1056
  * lazy-proxy decision uses identical semantics without leaking that
1454
1057
  * private API across modules.
1455
1058
  */
1456
- _envFlagDefaultOn(name) {
1457
- const raw = this.env?.[name];
1458
- if (raw === undefined || raw === null)
1459
- return true;
1460
- const s = String(raw).toLowerCase();
1461
- if (s === '0' || s === '' || s === 'false' || s === 'off' || s === 'no')
1462
- return false;
1463
- return true;
1464
- }
1059
+ _envFlagDefaultOn(name) { return this.#runtimeServices._envFlagDefaultOn(name); }
1465
1060
  // ── Session initialization ────────────────────────────────────────────
1466
1061
  // ── Session initialization ────────────────────────────────────────────
1467
1062
  //
@@ -1487,19 +1082,7 @@ export class NimbusSession extends CloudflareDurableObject {
1487
1082
  */
1488
1083
  _wakeRebuild = null;
1489
1084
  // ── Filesystem seeding ────────────────────────────────────────────────
1490
- ensureGlobalPrefixDirs(prefix) {
1491
- const fs = this.sqliteFs.as(CRED_SESSION_USER);
1492
- const dirs = [
1493
- prefix,
1494
- `${prefix}/lib`,
1495
- `${prefix}/lib/node_modules`,
1496
- `${prefix}/bin`,
1497
- ];
1498
- for (const dir of dirs) {
1499
- if (!fs.exists(dir))
1500
- fs.mkdir(dir, { recursive: true });
1501
- }
1502
- }
1085
+ ensureGlobalPrefixDirs(prefix) { return this.#runtimeServices.ensureGlobalPrefixDirs(prefix); }
1503
1086
  /**
1504
1087
  * The starter content a fresh Nimbus session shows a user: the banner, the
1505
1088
  * sample files, and the Vite starter app.
@@ -12,5 +12,7 @@
12
12
  */
13
13
  import type { NpmInstallPort } from '@nimbus-sh/core/substrate/lifo/commands/system/npm.js';
14
14
  import type { SessionInternal } from './internal.js';
15
- export declare function createNpmInstallPort(self: SessionInternal): NpmInstallPort;
15
+ type InstallHost = Pick<SessionInternal, 'ensureSqliteFs' | 'ensureNpmInstaller' | 'ensureGlobalPrefixDirs'>;
16
+ export declare function createNpmInstallPort(self: InstallHost): NpmInstallPort;
17
+ export {};
16
18
  //# sourceMappingURL=npm-install-port.d.ts.map