@nimbus-sh/worker 0.2.3 → 0.4.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 (146) hide show
  1. package/dist/_shared/session-router.d.ts +8 -0
  2. package/dist/_shared/session-router.d.ts.map +1 -1
  3. package/dist/_shared/session-router.js +8 -0
  4. package/dist/esbuild-wasm-bundle.generated.d.ts +1 -1
  5. package/dist/esbuild-wasm-bundle.generated.js +1 -1
  6. package/dist/facets/cirrus-real.js +1 -1
  7. package/dist/facets/manager.d.ts +43 -129
  8. package/dist/facets/manager.d.ts.map +1 -1
  9. package/dist/facets/manager.js +107 -318
  10. package/dist/facets/opencode-staging.d.ts +1 -1
  11. package/dist/facets/opencode-staging.d.ts.map +1 -1
  12. package/dist/facets/opencode-staging.js +3 -0
  13. package/dist/facets/real-vite-hmr.js +1 -1
  14. package/dist/facets/vite-dev-server.d.ts +4 -4
  15. package/dist/facets/vite-dev-server.js +5 -5
  16. package/dist/git/commands.d.ts +8 -0
  17. package/dist/git/commands.d.ts.map +1 -1
  18. package/dist/git/commands.js +41 -9
  19. package/dist/git/network-facet.d.ts +1 -1
  20. package/dist/git/network-facet.d.ts.map +1 -1
  21. package/dist/git/network-facet.js +39 -15
  22. package/dist/git-bundle.generated.d.ts +2 -2
  23. package/dist/git-bundle.generated.d.ts.map +1 -1
  24. package/dist/git-bundle.generated.js +3 -3
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +9 -1
  27. package/dist/loaders/child-process/spawn-facet.js +1 -1
  28. package/dist/loaders/child-process/spawn-pool.d.ts.map +1 -1
  29. package/dist/loaders/child-process/spawn-pool.js +2 -2
  30. package/dist/loaders/generated-workers.d.ts +2 -2
  31. package/dist/loaders/generated-workers.d.ts.map +1 -1
  32. package/dist/loaders/generated-workers.js +3 -3
  33. package/dist/loaders/npm-resolve-preamble.d.ts +3 -3
  34. package/dist/loaders/npm-resolve-preamble.js +3 -3
  35. package/dist/loaders/pre-bundle-preamble.d.ts +5 -5
  36. package/dist/loaders/pre-bundle-preamble.js +6 -6
  37. package/dist/loaders/process-host.d.ts +6 -107
  38. package/dist/loaders/process-host.d.ts.map +1 -1
  39. package/dist/loaders/process-host.js +6 -405
  40. package/dist/npm/install-batch-facet.d.ts +1 -1
  41. package/dist/npm/install-batch-facet.js +1 -1
  42. package/dist/npm/installer.d.ts +6 -6
  43. package/dist/npm/installer.d.ts.map +1 -1
  44. package/dist/npm/installer.js +28 -27
  45. package/dist/npm/pre-bundle-facet.d.ts +2 -2
  46. package/dist/npm/pre-bundle-facet.js +5 -5
  47. package/dist/npm/r2-cache.d.ts +1 -1
  48. package/dist/npm/resolve-facet.d.ts +1 -1
  49. package/dist/npm/resolve-facet.js +1 -1
  50. package/dist/npm/resolve-one-facet.d.ts +5 -5
  51. package/dist/npm/resolve-one-facet.js +5 -5
  52. package/dist/router/index.js +2 -2
  53. package/dist/router/remote-api.js +9 -1
  54. package/dist/runtime/bun-repl.d.ts +1 -1
  55. package/dist/runtime/bun-repl.js +3 -3
  56. package/dist/runtime/esbuild-wasm-bytes.js +1 -1
  57. package/dist/runtime/facet-loader-host.d.ts +5 -8
  58. package/dist/runtime/facet-loader-host.d.ts.map +1 -1
  59. package/dist/runtime/facet-loader-host.js +7 -11
  60. package/dist/runtime/node-repl.js +2 -2
  61. package/dist/runtime/node-shims-artifact.js +1 -1
  62. package/dist/runtime/node-shims.js +2 -1
  63. package/dist/runtime/opencode-artifact.js +1 -1
  64. package/dist/runtime/opentui-wasm-bytes.js +1 -1
  65. package/dist/runtime/python-repl.js +3 -3
  66. package/dist/runtime/ruby-repl.js +2 -2
  67. package/dist/runtime/ruby-resident.js +1 -1
  68. package/dist/runtime/sqlite-wasm-bytes.js +1 -1
  69. package/dist/session/diag.js +1 -1
  70. package/dist/session/hibernation.d.ts +20 -53
  71. package/dist/session/hibernation.d.ts.map +1 -1
  72. package/dist/session/hibernation.js +57 -194
  73. package/dist/session/init-phases.d.ts +2 -3
  74. package/dist/session/init-phases.d.ts.map +1 -1
  75. package/dist/session/init-phases.js +3 -2
  76. package/dist/session/init.d.ts.map +1 -1
  77. package/dist/session/init.js +8 -4
  78. package/dist/session/keys.d.ts +14 -37
  79. package/dist/session/keys.d.ts.map +1 -1
  80. package/dist/session/keys.js +18 -37
  81. package/dist/session/nimbus-session.d.ts +10 -15
  82. package/dist/session/nimbus-session.d.ts.map +1 -1
  83. package/dist/session/nimbus-session.js +28 -31
  84. package/dist/session/port-capability.d.ts +43 -0
  85. package/dist/session/port-capability.d.ts.map +1 -0
  86. package/dist/session/port-capability.js +49 -0
  87. package/dist/session/programmatic.d.ts +39 -5
  88. package/dist/session/programmatic.d.ts.map +1 -1
  89. package/dist/session/programmatic.js +150 -19
  90. package/dist/session/routes.d.ts +2 -0
  91. package/dist/session/routes.d.ts.map +1 -1
  92. package/dist/session/routes.js +96 -20
  93. package/dist/session/rpc.d.ts +36 -9
  94. package/dist/session/rpc.d.ts.map +1 -1
  95. package/dist/session/rpc.js +84 -24
  96. package/dist/session/start-real-vite.d.ts.map +1 -1
  97. package/dist/session/start-real-vite.js +3 -1
  98. package/dist/session/state-store.d.ts +3 -2
  99. package/dist/session/state-store.d.ts.map +1 -1
  100. package/dist/session/state-store.js +3 -2
  101. package/dist/session/supervisor-rpc.d.ts.map +1 -1
  102. package/dist/session/supervisor-rpc.js +7 -4
  103. package/dist/session/ws.d.ts +1 -3
  104. package/dist/session/ws.d.ts.map +1 -1
  105. package/dist/session/ws.js +4 -3
  106. package/dist/wrangler/nimbus-wrangler.js +1 -1
  107. package/package.json +4 -2
  108. package/scripts/bundle-esbuild-wasm.mjs +2 -2
  109. package/scripts/bundle-facet-workers.mjs +5 -7
  110. package/scripts/bundle-sqlite-wasm.mjs +1 -1
  111. package/dist/facets/inner-do-registry.d.ts +0 -41
  112. package/dist/facets/inner-do-registry.d.ts.map +0 -1
  113. package/dist/facets/inner-do-registry.js +0 -51
  114. package/dist/facets/launch-pacer.d.ts +0 -111
  115. package/dist/facets/launch-pacer.d.ts.map +0 -1
  116. package/dist/facets/launch-pacer.js +0 -130
  117. package/dist/loaders/fanout-pool.d.ts +0 -207
  118. package/dist/loaders/fanout-pool.d.ts.map +0 -1
  119. package/dist/loaders/fanout-pool.js +0 -363
  120. package/dist/loaders/loader-pool.d.ts +0 -294
  121. package/dist/loaders/loader-pool.d.ts.map +0 -1
  122. package/dist/loaders/loader-pool.js +0 -642
  123. package/dist/loaders/process-fabric.d.ts +0 -509
  124. package/dist/loaders/process-fabric.d.ts.map +0 -1
  125. package/dist/loaders/process-fabric.js +0 -360
  126. package/dist/loaders/vendor/errors.d.ts +0 -24
  127. package/dist/loaders/vendor/errors.d.ts.map +0 -1
  128. package/dist/loaders/vendor/errors.js +0 -46
  129. package/dist/loaders/vendor/serialize.d.ts +0 -3
  130. package/dist/loaders/vendor/serialize.d.ts.map +0 -1
  131. package/dist/loaders/vendor/serialize.js +0 -25
  132. package/dist/loaders/vendor/types.d.ts +0 -69
  133. package/dist/loaders/vendor/types.d.ts.map +0 -1
  134. package/dist/loaders/vendor/types.js +0 -4
  135. package/dist/loaders/workerd-facet-host.d.ts +0 -141
  136. package/dist/loaders/workerd-facet-host.d.ts.map +0 -1
  137. package/dist/loaders/workerd-facet-host.js +0 -284
  138. package/dist/session/bindings.d.ts +0 -209
  139. package/dist/session/bindings.d.ts.map +0 -1
  140. package/dist/session/bindings.js +0 -680
  141. package/dist/session/ctx-exports.d.ts +0 -16
  142. package/dist/session/ctx-exports.d.ts.map +0 -1
  143. package/dist/session/ctx-exports.js +0 -22
  144. package/dist/session/ws-hibernation-config.d.ts +0 -64
  145. package/dist/session/ws-hibernation-config.d.ts.map +0 -1
  146. package/dist/session/ws-hibernation-config.js +0 -89
@@ -14,7 +14,96 @@ import { PortRegistry } from '@nimbus-sh/core/runtime/port-registry.js';
14
14
  import { CRED_KERNEL } from '@nimbus-sh/core/runtime/os-contracts.js';
15
15
  import { endProcessInput, resizeProcess, signalProcess, writeProcessInput, } from '@nimbus-sh/core/runtime/process-input-routing.js';
16
16
  import { z } from 'zod/v4';
17
- import { SESSION_DESTROYED_KEY, VITE_CONFIG_KEY, W9_ISOLATE_GEN_KEY } from './keys.js';
17
+ import { SESSION_DESTROYED_KEY, SHELL_STATE_KEY_PREFIX, VITE_CONFIG_KEY } from './keys.js';
18
+ import { clearPortCapability, persistPortCapability, restorePortCapability, } from './port-capability.js';
19
+ import { GENERATION_KEY, assumeGeneration, generation } from '@nimbus-sh/fabric/generation.js';
20
+ import { HeadlessTerminal, Shell } from '@nimbus-sh/core/substrate/lifo/index.js';
21
+ const ShellIdSchema = z.string().min(1).max(160).regex(/^[A-Za-z0-9][A-Za-z0-9._:-]*$/);
22
+ const ShellStateSchema = z.object({
23
+ cwd: z.string().startsWith('/'),
24
+ env: z.record(z.string(), z.string()),
25
+ }).strict();
26
+ /**
27
+ * A second Shell over the session's own kernel, filesystem and command
28
+ * registry — the same objects the interactive shell uses, so a named shell is
29
+ * not a second filesystem or a second process table. Only cwd and environment
30
+ * are its own, which is exactly what makes `cd` stick between calls.
31
+ */
32
+ export function createProgrammaticShell(self, pid, state) {
33
+ const parent = self.shell;
34
+ if (!parent)
35
+ throw new Error('Nimbus shell did not initialize');
36
+ const shell = new Shell(new HeadlessTerminal(), parent.getVfs(), parent.getRegistry(), { ...parent.getEnv(), ...state.env, $: String(pid) }, parent.getProcessRegistry(), {
37
+ pid,
38
+ get cred() { return self.processes.cred(pid); },
39
+ setUmask: (mask) => self.processes.setUmask(pid, mask),
40
+ runAs: parent.getRunAsHost(),
41
+ });
42
+ shell.setCwd(state.cwd);
43
+ return shell;
44
+ }
45
+ /**
46
+ * Run `body` against a named shell's durable state, or against nothing when no
47
+ * `shellId` was given.
48
+ *
49
+ * Serialized per id: two concurrent calls naming one shell would otherwise
50
+ * read the same cwd and race to write it back, and the loser's `cd` would
51
+ * vanish. A background job reads the state but does not write it back — its
52
+ * shell outlives the call, so what it would persist is a snapshot of a moment
53
+ * nobody asked about.
54
+ */
55
+ async function withShellState(self, options, background, run) {
56
+ if (options.shellId === undefined)
57
+ return run(null);
58
+ const id = ShellIdSchema.parse(options.shellId);
59
+ self._programmaticShellQueues ??= new Map();
60
+ const queues = self._programmaticShellQueues;
61
+ const previous = queues.get(id) ?? Promise.resolve();
62
+ let release = () => { };
63
+ const gate = new Promise((resolve) => { release = resolve; });
64
+ const tail = previous.catch(() => undefined).then(() => gate);
65
+ queues.set(id, tail);
66
+ await previous.catch(() => undefined);
67
+ try {
68
+ const key = `${SHELL_STATE_KEY_PREFIX}${id}`;
69
+ const stored = await self.ctx.storage.get(key);
70
+ const root = typeof options.shellRoot === 'string' && options.shellRoot.startsWith('/')
71
+ ? options.shellRoot
72
+ : getHome(self);
73
+ const state = stored === undefined
74
+ ? { cwd: root, env: {} }
75
+ : ShellStateSchema.parse(stored);
76
+ let shell = null;
77
+ try {
78
+ return await run({
79
+ cwd: state.cwd,
80
+ create: (pid) => {
81
+ shell = createProgrammaticShell(self, pid, state);
82
+ return shell;
83
+ },
84
+ });
85
+ }
86
+ finally {
87
+ const captured = background || shell === null ? state : capturedShellState(shell);
88
+ await self.ctx.storage.put(key, captured);
89
+ }
90
+ }
91
+ finally {
92
+ release();
93
+ if (queues.get(id) === tail)
94
+ queues.delete(id);
95
+ }
96
+ }
97
+ function capturedShellState(shell) {
98
+ const env = { ...shell.getEnv() };
99
+ // `$` is the pid of the call that just ended, not state of the shell.
100
+ delete env.$;
101
+ return { cwd: shell.getCwd(), env };
102
+ }
103
+ /**
104
+ * The durable half of the port capability lives in `./port-capability.js`, so
105
+ * the facet manager can retire a stale one without importing this module.
106
+ */
18
107
  const ProcessLogsOptionsSchema = z.object({
19
108
  cursor: z.number().int().nonnegative().optional(),
20
109
  lines: z.number().int().nonnegative().optional(),
@@ -101,13 +190,19 @@ export async function ensureProgrammaticReady(self, options = {}) {
101
190
  }
102
191
  return { ok: true, preinstalled: preinstall };
103
192
  }
104
- function startShellJob(self, command, options, job) {
105
- const shell = self.shell;
106
- if (!shell)
193
+ function startShellJob(self, command, options, job, scoped) {
194
+ const parentShell = self.shell;
195
+ if (!parentShell)
107
196
  throw new Error('Nimbus shell did not initialize');
108
197
  const line = String(command);
109
- const cwd = options.cwd ?? shell.getCwd?.() ?? '/home/user';
110
- const entry = self.processes.spawn(line, [line], cwd, { longRunning: job.background });
198
+ const cwd = options.cwd ?? scoped?.cwd ?? parentShell.getCwd?.() ?? '/home/user';
199
+ const entry = self.processes.spawn(line, [line], cwd, {
200
+ longRunning: job.background,
201
+ cred: options.cred,
202
+ });
203
+ // The scoped shell is built around the pid, so its `$` and its credential
204
+ // are the ones this command actually runs under.
205
+ const shell = scoped?.create(entry.pid) ?? parentShell;
111
206
  const pid = entry.pid;
112
207
  if (job.background)
113
208
  self.processes.openInput(pid);
@@ -129,8 +224,11 @@ function startShellJob(self, command, options, job) {
129
224
  sink?.(text);
130
225
  };
131
226
  const run = shell.execute(line, {
132
- cwd,
133
- env: { ...(shell.getEnv?.() ?? {}), ...(options.env ?? {}) },
227
+ // A named shell already holds its own cwd and env; passing them again
228
+ // would pin it to the values this call started with and `cd` would not
229
+ // survive the call, which is the whole point of naming one.
230
+ cwd: scoped ? options.cwd : cwd,
231
+ env: scoped ? options.env : { ...(shell.getEnv?.() ?? {}), ...(options.env ?? {}) },
134
232
  onStdout: emit('stdout', job.onStdout),
135
233
  onStderr: emit('stderr', job.onStderr),
136
234
  signal: controller.signal,
@@ -161,6 +259,9 @@ function startShellJob(self, command, options, job) {
161
259
  }
162
260
  export async function rpcExec(self, command, options = {}) {
163
261
  await ensureProgrammaticReady(self, options);
262
+ return withShellState(self, options, false, (scoped) => execOnShell(self, command, options, scoped));
263
+ }
264
+ async function execOnShell(self, command, options, scoped) {
164
265
  const stdout = [];
165
266
  const stderr = [];
166
267
  const started = Date.now();
@@ -170,7 +271,7 @@ export async function rpcExec(self, command, options = {}) {
170
271
  background: false,
171
272
  onStdout: (d) => stdout.push(d),
172
273
  onStderr: (d) => stderr.push(d),
173
- });
274
+ }, scoped);
174
275
  let result;
175
276
  try {
176
277
  result = options.timeoutMs && options.timeoutMs > 0
@@ -249,7 +350,10 @@ function collectJobOutput(self, pid) {
249
350
  */
250
351
  export async function rpcStartProcess(self, command, options = {}) {
251
352
  await ensureProgrammaticReady(self, options);
252
- const job = startShellJob(self, command, options, { background: true });
353
+ return withShellState(self, options, true, (scoped) => startOnShell(self, command, options, scoped));
354
+ }
355
+ async function startOnShell(self, command, options, scoped) {
356
+ const job = startShellJob(self, command, options, { background: true }, scoped);
253
357
  const line = String(command);
254
358
  notifyTerminalEvent(self.terminal ?? null, {
255
359
  type: 'spawn', pid: job.pid, command: line, longRunning: true, attachedTty: false,
@@ -411,24 +515,51 @@ export async function rpcProcessLogs(self, pid, options = {}) {
411
515
  }
412
516
  export async function rpcListPorts(self) {
413
517
  await ensureProgrammaticReady(self);
414
- return self.portRegistry.getAll().map(serializePort);
518
+ const entries = self.portRegistry.getAll();
519
+ // Persisted at the moment the embedder is told the value, not at
520
+ // registration: a capability nobody has been handed does not need to
521
+ // survive anything.
522
+ await Promise.all(entries.map((entry) => persistPortCapability(self, entry.port, entry.capability)));
523
+ return entries.map(serializePort);
415
524
  }
416
525
  export async function rpcExposePort(self, port) {
417
526
  await ensureProgrammaticReady(self);
418
527
  const n = Number(port);
419
528
  const entry = self.portRegistry.get(n);
529
+ if (entry)
530
+ await persistPortCapability(self, n, entry.capability);
420
531
  return {
421
532
  port: n,
422
533
  listening: !!entry,
423
534
  pid: entry?.pid ?? null,
424
535
  registeredAt: entry?.registeredAt ?? null,
536
+ capability: entry?.capability ?? null,
425
537
  };
426
538
  }
427
539
  export async function rpcUnexposePort(self, port) {
428
540
  await ensureProgrammaticReady(self);
429
541
  const n = Number(port);
542
+ // Before the unregister, so a crash between the two leaves a dead token
543
+ // rather than a live one.
544
+ await clearPortCapability(self, n);
430
545
  return { port: n, ok: self.portRegistry.unregister(n) };
431
546
  }
547
+ /**
548
+ * Route an embedder request that carries a port capability. The embedder has
549
+ * authenticated the capability at its edge and stripped its own credentials,
550
+ * so the guest's `Authorization` is preserved through this path and no other.
551
+ */
552
+ export async function rpcRouteCapabilityPort(self, port, capability, request, pathname) {
553
+ await ensureProgrammaticReady(self);
554
+ await restorePortCapability(self, port);
555
+ if (request.headers.get('upgrade')?.toLowerCase() === 'websocket') {
556
+ // A 101 cannot cross the DO RPC boundary this entrypoint is reached
557
+ // through; the session fetch route is the one that keeps fetch semantics.
558
+ return new Response('WebSocket upgrades must use the session fetch route', { status: 409 });
559
+ }
560
+ const routed = await self.portRegistry.routeCapabilityRequest(Number(port), String(capability), request, pathname);
561
+ return routed ?? new Response('Not found', { status: 404 });
562
+ }
432
563
  export async function rpcDeleteFile(self, path, options = {}) {
433
564
  await ensureProgrammaticReady(self);
434
565
  const p = String(path).replace(/^\/+/, '');
@@ -546,7 +677,7 @@ export async function rpcDestroy(self, options = {}) {
546
677
  // current-generation (pid > pidBase) — landing its output on the
547
678
  // destroyed/recreated session. Keep {tombstone, isolateGen} consistent.
548
679
  try {
549
- await self.ctx.storage.put(W9_ISOLATE_GEN_KEY, self._w9IsolateGen ?? 0);
680
+ await self.ctx.storage.put(GENERATION_KEY, generation(self.ctx));
550
681
  }
551
682
  catch { /* best-effort */ }
552
683
  resetInMemorySessionState(self);
@@ -582,10 +713,10 @@ async function quiesceInMemorySessionState(self) {
582
713
  * The generation the NEXT boot of this session will run as.
583
714
  *
584
715
  * rpcDestroy re-persists the pre-destroy generation after wiping storage,
585
- * so `maybeBumpIsolateGen` reads it back and bumps once — landing here.
716
+ * so `adoptGeneration` reads it back and bumps once — landing here.
586
717
  */
587
718
  function successorGeneration(self) {
588
- return (self._w9IsolateGen ?? 0) + 1;
719
+ return generation(self.ctx) + 1;
589
720
  }
590
721
  /**
591
722
  * Install the empty process/port state a destroyed session leaves behind.
@@ -679,12 +810,11 @@ function resetInMemorySessionState(self) {
679
810
  // Adopt the generation the next boot will derive from the counter
680
811
  // rpcDestroy just re-persisted, so the in-memory pid floor and the
681
812
  // persisted one agree. Deliberately left unpersisted: storage keeps the
682
- // pre-destroy value, and maybeBumpIsolateGen re-derives this one from it.
683
- const generation = successorGeneration(self);
684
- installEmptyProcessState(self, generation);
813
+ // pre-destroy value, and adoptGeneration re-derives this one from it.
814
+ const successor = successorGeneration(self);
815
+ installEmptyProcessState(self, successor);
685
816
  self._w9SchemaInit = false;
686
- self._w9IsolateGen = generation;
687
- self._w9IsolateGenPersisted = false;
817
+ assumeGeneration(self.ctx, successor);
688
818
  try {
689
819
  self._w9WireProcessLogPersist?.();
690
820
  }
@@ -714,6 +844,7 @@ function serializePort(p) {
714
844
  port: Number(p.port),
715
845
  pid: Number(p.pid),
716
846
  registeredAt: Number(p.registeredAt),
847
+ capability: String(p.capability),
717
848
  };
718
849
  }
719
850
  function shellQuote(s) {
@@ -24,6 +24,8 @@
24
24
  * (~30 sites). RoutesHost = any pragmatic deviation, like InitHost in S6.
25
25
  */
26
26
  type RoutesHost = any;
27
+ /** Route a capability-authenticated embedder request to a guest HTTP server. */
28
+ export declare function routeCapabilityPort(self: RoutesHost, port: number, capability: string, request: Request, innerPath: string): Promise<Response>;
27
29
  export declare function handleFetch(self: RoutesHost, request: Request): Promise<Response>;
28
30
  export {};
29
31
  //# sourceMappingURL=routes.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../src/session/routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAsCH,KAAK,UAAU,GAAG,GAAG,CAAC;AAkQtB,wBAAsB,WAAW,CAAC,IAAI,EAAE,UAAU,EAAE,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAi8BvF"}
1
+ {"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../src/session/routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAqDH,KAAK,UAAU,GAAG,GAAG,CAAC;AA6LtB,gFAAgF;AAChF,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,UAAU,EAChB,IAAI,EAAE,MAAM,EACZ,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,OAAO,EAChB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,QAAQ,CAAC,CAEnB;AAyGD,wBAAsB,WAAW,CAAC,IAAI,EAAE,UAAU,EAAE,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CA8+BvF"}
@@ -27,12 +27,12 @@ import { handleReplicaPreflight as _w12HandleReplicaPreflight } from '../replica
27
27
  import { replicasSuspended as _w12ReplicasSuspended } from '../replica/suspension.js';
28
28
  import { sanitizeUntrustedRequest } from '@nimbus-sh/core/_shared/untrusted-request.js';
29
29
  import { matchLogsPath, handleLogsWebSocketRequest, handleProcessesListRequest, } from '../runtime/process-logs-api.js';
30
- import { readDiagCounters } from '@nimbus-sh/core/observability/diag-counters.js';
31
- import { getFailures, getLastRpcFrame, getLastFacetId, getRecoveryEvents, recordRecoveryEvent, resetRecoveryEvents, } from '@nimbus-sh/core/observability/oom-discriminator.js';
30
+ import { readDiagCounters } from '@nimbus-sh/platform/diag-counters.js';
31
+ import { getFailures, getLastRpcFrame, getLastFacetId, getRecoveryEvents, recordRecoveryEvent, resetRecoveryEvents, } from '@nimbus-sh/platform/oom-discriminator.js';
32
32
  import { DEFAULT_VITE_PORT, LRU_MAX_ENTRIES } from '@nimbus-sh/core/constants.js';
33
33
  import { BASE_PATH_HEADER } from '../_shared/session-router.js';
34
34
  import { VITE_CONFIG_KEY } from './keys.js';
35
- import { estimateSupervisorHeap, WORKERD_EVICTION_LABELS } from '@nimbus-sh/core/observability/heap-estimate.js';
35
+ import { estimateSupervisorHeap, WORKERD_EVICTION_LABELS } from '@nimbus-sh/platform/heap-estimate.js';
36
36
  import { loadShellState, loadKernelMounts, getScrollbackStats, clearSessionState, appendScrollback, loadScrollback } from './state-store.js';
37
37
  import { classifyWsUpgrade, joinExistingSession } from './init-phases.js';
38
38
  import { EsbuildService } from '@nimbus-sh/core/runtime/esbuild-service.js';
@@ -40,7 +40,14 @@ import { ViteDevServer } from '../facets/vite-dev-server.js';
40
40
  import { notifyTerminalEvent, wireProcessLogSocketBroadcast } from '../runtime/process-logs-api.js';
41
41
  import { makeLongRunningPortStub } from '@nimbus-sh/core/runtime/long-running-handle.js';
42
42
  import { startRealVite } from './start-real-vite.js';
43
- import { getLoadedCodesStats } from './bindings.js';
43
+ import { getLoadedCodesStats } from '@nimbus-sh/fabric/bindings.js';
44
+ import { generation } from '@nimbus-sh/fabric/generation.js';
45
+ import { facetIdBudget } from '@nimbus-sh/fabric/budgets.js';
46
+ import { loaderLedgerStats } from '@nimbus-sh/fabric/budgets.js';
47
+ import { HOSTED_WEBSOCKET_CAPABILITY_HEADER, HOSTED_WEBSOCKET_KEY_HEADER, } from '@nimbus-sh/fabric/process-host.js';
48
+ import { routeHostedWebSocket } from './rpc.js';
49
+ import { clearPortCapability, persistPortCapability, readPortCapability, restorePortCapability, } from './port-capability.js';
50
+ import { PREVIEW_CAPABILITY_HEADER } from '../_shared/session-router.js';
44
51
  import { renderNoDevServerHtml } from './helpers.js';
45
52
  import { handleAgentRequest } from './agent.js';
46
53
  import { captureSessionAiCredential } from './ai.js';
@@ -50,7 +57,7 @@ import { CRED_KERNEL } from '@nimbus-sh/core/runtime/os-contracts.js';
50
57
  // uses (they did: the packument purge used a stale `/p/` segment).
51
58
  import { R2CacheClient, packumentL2Url, tarballL2Url, parseTarballAddress } from '../npm/r2-cache.js';
52
59
  import { fetchEsbuildWasmBytes, ESBUILD_WASM_L2_KEY } from '../runtime/esbuild-wasm-bytes.js';
53
- import { NimbusFanoutPool, IN_DO_THRESHOLD, MAX_PEER_FANOUT } from '../loaders/fanout-pool.js';
60
+ import { Fanout, IN_DO_THRESHOLD, MAX_PEER_FANOUT } from '@nimbus-sh/fabric/fanout.js';
54
61
  import { z } from 'zod/v4';
55
62
  const TestSpawnEmitterBodySchema = z.object({
56
63
  lines: z.coerce.number().optional(),
@@ -105,6 +112,9 @@ async function restorePersistedDevServer(self, onlyPort) {
105
112
  const port = (config.port && Number.isFinite(config.port)) ? config.port : DEFAULT_VITE_PORT;
106
113
  if (onlyPort != null && onlyPort !== port)
107
114
  return;
115
+ // Read BEFORE the restore: bringing the server back re-registers the port,
116
+ // and a fresh registration mints a capability nobody was ever handed.
117
+ const persistedCapability = await readPortCapability(self, port);
108
118
  // A cirrus-real session rebuilds the real-vite facet, not the Cirrus shim —
109
119
  // same key, same starter the `vite` builtin uses, so a woken real-vite
110
120
  // session comes back as real-vite rather than degrading to the shim.
@@ -125,6 +135,7 @@ async function restorePersistedDevServer(self, onlyPort) {
125
135
  configDir: config.configDir || config.root,
126
136
  }).finally(() => { self._realViteRestore = null; });
127
137
  await self._realViteRestore;
138
+ await readoptCapability(self, port, persistedCapability);
128
139
  return;
129
140
  }
130
141
  self.ensureSqliteFs();
@@ -155,7 +166,9 @@ async function restorePersistedDevServer(self, onlyPort) {
155
166
  // restored server across hibernation cycles.
156
167
  try {
157
168
  self.portRegistry.bindFacetStub(entry.pid, makeLongRunningPortStub(self.viteDevServer));
169
+ await clearPortCapability(self, port);
158
170
  self.portRegistry.register(port, entry.pid);
171
+ await readoptCapability(self, port, persistedCapability);
159
172
  self._viteShimPid = entry.pid;
160
173
  self._viteShimPort = port;
161
174
  }
@@ -179,8 +192,18 @@ async function restorePersistedDevServer(self, onlyPort) {
179
192
  * the untrusted-code boundary and cannot carry it, and a plain user server on
180
193
  * any other port is mounted at root and needs no rewriting.
181
194
  */
182
- async function routeToSessionPort(self, port, request, innerPath, mountBase) {
195
+ async function routeToSessionPort(self, port, request, innerPath, mountBase, capability) {
183
196
  await restorePersistedDevServer(self, port);
197
+ if (capability !== undefined) {
198
+ // A rebuilt supervisor holds a capability nobody was handed; the durable
199
+ // one is the value in circulation, so it wins before the check.
200
+ await restorePortCapability(self, port);
201
+ if (!self.portRegistry.hasCapability(port, capability)) {
202
+ // 404, not 403: a wrong capability must not confirm that the port is
203
+ // listening at all.
204
+ return new Response('Not found', { status: 404 });
205
+ }
206
+ }
184
207
  if (port === self._viteShimPort) {
185
208
  // The preview HMR WebSocket can't cross the port-registry RPC, so it is
186
209
  // accepted in-DO for both dev servers — the same handling `/preview/` uses.
@@ -193,11 +216,28 @@ async function routeToSessionPort(self, port, request, innerPath, mountBase) {
193
216
  return self.viteDevServer.handleRequest(request, innerPath, mountBase);
194
217
  }
195
218
  }
196
- const proxied = await self.portRegistry.routeRequest(port, request, innerPath);
219
+ const proxied = capability === undefined
220
+ ? await self.portRegistry.routeRequest(port, request, innerPath)
221
+ : await self.portRegistry.routeCapabilityRequest(port, capability, request, innerPath);
197
222
  if (proxied)
198
223
  return proxied;
199
224
  return new Response(`No process listening on port ${port}`, { status: 502 });
200
225
  }
226
+ /**
227
+ * Re-adopt a preview capability the embedder already holds, after a restore
228
+ * re-registered the port under a freshly minted one.
229
+ */
230
+ async function readoptCapability(self, port, capability) {
231
+ if (!capability)
232
+ return;
233
+ if (self.portRegistry.restoreCapability(port, capability)) {
234
+ await persistPortCapability(self, port, capability);
235
+ }
236
+ }
237
+ /** Route a capability-authenticated embedder request to a guest HTTP server. */
238
+ export function routeCapabilityPort(self, port, capability, request, innerPath) {
239
+ return routeToSessionPort(self, port, request, normalizeForwardedHttpPath(innerPath), '', capability);
240
+ }
201
241
  /**
202
242
  * The public URL prefix a port-routed request is mounted at. `<port>--<sid>`
203
243
  * hosts arrive with an empty base header (mounted at the origin root); the
@@ -294,6 +334,23 @@ async function parseJsonBody(request, schema) {
294
334
  }
295
335
  export async function handleFetch(self, request) {
296
336
  const url = new URL(request.url);
337
+ // The peer end of the fetch-semantic WebSocket hop, before anything else:
338
+ // this request is a sibling coordinator's, not a browser's, and it names
339
+ // the hosted process rather than a route on this session. Both headers are
340
+ // stripped so the process never sees the transport that carried it.
341
+ const hostedWebSocket = request.headers.get(HOSTED_WEBSOCKET_KEY_HEADER);
342
+ if (hostedWebSocket) {
343
+ if (request.headers.get('upgrade')?.toLowerCase() !== 'websocket') {
344
+ return new Response('Expected WebSocket', { status: 426 });
345
+ }
346
+ const capability = request.headers.get(HOSTED_WEBSOCKET_CAPABILITY_HEADER);
347
+ if (!capability)
348
+ return new Response('Not found', { status: 404 });
349
+ const headers = new Headers(request.headers);
350
+ headers.delete(HOSTED_WEBSOCKET_KEY_HEADER);
351
+ headers.delete(HOSTED_WEBSOCKET_CAPABILITY_HEADER);
352
+ return routeHostedWebSocket(self, hostedWebSocket, capability, new Request(request.url, { method: request.method, headers }));
353
+ }
297
354
  // Capture session basePath from the routing header (if forwarded by the
298
355
  // Worker's session-router). Threaded through to ViteDevServer so the
299
356
  // served app's module URLs, HMR paths, <base href>, and router basename
@@ -584,7 +641,7 @@ export async function handleFetch(self, request) {
584
641
  // process.memoryUsage() and they were always zero — useless.
585
642
  //
586
643
  // C'.1 replaces the zero-everywhere readout with a deterministic
587
- // estimator (src/observability/heap-estimate.ts) that sums known
644
+ // estimator (src/@nimbus-sh/platform/heap-estimate.js) that sums known
588
645
  // supervisor allocations from runtime counters. Every byte has a
589
646
  // named contributor.
590
647
  //
@@ -666,11 +723,19 @@ export async function handleFetch(self, request) {
666
723
  // (32 entries) with FIFO eviction. The counters here let
667
724
  // ops dashboards visualise the bound + the eviction rate.
668
725
  loadedCodes: getLoadedCodesStats(),
726
+ // Per-DO Worker Loader accounting: distinct loader ids ever gotten
727
+ // (each permanently holds one of the ~5-6 dynamic-worker slots) and
728
+ // live/peak concurrent Loader fetches.
729
+ loader: loaderLedgerStats(self.ctx),
669
730
  rpc: {
670
731
  lastFrame: getLastRpcFrame(),
671
732
  },
672
733
  facet: {
673
734
  lastDispatch: getLastFacetId(),
735
+ // Facet IDs consumed over this DO's LIFETIME against the 65,536 the
736
+ // platform will ever grant it. Append-only and never reclaimed;
737
+ // crossing the wall is unrecoverable for the object.
738
+ idBudget: await facetIdBudget(self.ctx),
674
739
  },
675
740
  // ── v3 / C' observability foundation ──────────────────────
676
741
  heap,
@@ -685,7 +750,7 @@ export async function handleFetch(self, request) {
685
750
  // `autoResponseConfigured` reports the runtime's actual
686
751
  // capability (older workerd builds report false).
687
752
  hib: {
688
- isolateGen: self._w9IsolateGen,
753
+ isolateGen: generation(self.ctx),
689
754
  autoResponseConfigured: self._w9WsConfig?.autoResponseConfigured ?? false,
690
755
  autoResponseError: self._w9WsConfig?.autoResponseError ?? null,
691
756
  hibernationEventTimeoutMs: self._w9WsConfig?.timeoutSetMs ?? null,
@@ -852,7 +917,7 @@ export async function handleFetch(self, request) {
852
917
  fromState: body.fromState,
853
918
  toState: body.toState,
854
919
  trigger: body.trigger,
855
- isolateGen: body.isolateGen || self._w9IsolateGen,
920
+ isolateGen: body.isolateGen || generation(self.ctx),
856
921
  dataLoss: body.dataLoss === true,
857
922
  snapshotKeysRehydrated: body.snapshotKeysRehydrated || 0,
858
923
  notes: body.notes,
@@ -890,7 +955,7 @@ export async function handleFetch(self, request) {
890
955
  }
891
956
  // ── two-tier-fanout primitive probe ──────────────────────────────
892
957
  // in-DO fanout in-DO and peer-DO fanout peer-DO speedups via the
893
- // NimbusFanoutPool primitive. Independent of any specific
958
+ // Fanout primitive. Independent of any specific
894
959
  // production site (install-batch, pre-bundle, etc.) so the
895
960
  // primitive's behavior can be measured cleanly without
896
961
  // confounders.
@@ -1016,6 +1081,7 @@ export async function handleFetch(self, request) {
1016
1081
  try {
1017
1082
  const apiViteStub = makeLongRunningPortStub(self.viteDevServer);
1018
1083
  self.portRegistry.bindFacetStub(apiViteEntry.pid, apiViteStub);
1084
+ await clearPortCapability(self, apiVitePort);
1019
1085
  self.portRegistry.register(apiVitePort, apiViteEntry.pid);
1020
1086
  self._viteShimPid = apiViteEntry.pid;
1021
1087
  self._viteShimPort = apiVitePort;
@@ -1220,7 +1286,10 @@ export async function handleFetch(self, request) {
1220
1286
  if (portMatch) {
1221
1287
  const port = parseInt(portMatch[1]);
1222
1288
  const path = normalizeForwardedHttpPath(portMatch[2] || '/');
1223
- return routeToSessionPort(self, port, request, path, portRouteMountBase(request, port));
1289
+ // An embedder that authenticated a preview capability at its own edge
1290
+ // forwards it here; the guest's Authorization then survives the hop.
1291
+ const capability = request.headers.get(PREVIEW_CAPABILITY_HEADER);
1292
+ return routeToSessionPort(self, port, request, path, portRouteMountBase(request, port), capability ?? undefined);
1224
1293
  }
1225
1294
  // ── /api/_diag/cache — per-tier cache observability ───────────────
1226
1295
  //
@@ -1240,7 +1309,12 @@ export async function handleFetch(self, request) {
1240
1309
  const { readExecTelemetry } = await import('../facets/exec-telemetry.js');
1241
1310
  return Response.json({ records: readExecTelemetry() });
1242
1311
  }
1312
+ // Write surfaces gated like /api/_test/*: zeroing telemetry from prod
1313
+ // would erase the evidence an investigation is standing on, so without
1314
+ // NIMBUS_DEBUG these 404 rather than being a free reset vector.
1243
1315
  if (url.pathname === '/api/_diag/exec/reset' && request.method === 'POST') {
1316
+ if (!self.nimbusDebug)
1317
+ return new Response('not found', { status: 404 });
1244
1318
  const { resetExecTelemetry } = await import('../facets/exec-telemetry.js');
1245
1319
  resetExecTelemetry();
1246
1320
  return new Response(null, { status: 204 });
@@ -1251,6 +1325,8 @@ export async function handleFetch(self, request) {
1251
1325
  return Response.json(snap);
1252
1326
  }
1253
1327
  if (url.pathname === '/api/_diag/cache/reset' && request.method === 'POST') {
1328
+ if (!self.nimbusDebug)
1329
+ return new Response('not found', { status: 404 });
1254
1330
  const { reset } = await import('@nimbus-sh/core/_shared/cache-stats.js');
1255
1331
  reset();
1256
1332
  return new Response(null, { status: 204 });
@@ -1396,7 +1472,7 @@ async function handleCacheTestEndpoint(self, url, request) {
1396
1472
  }
1397
1473
  // ── two-tier-fanout primitive benchmark endpoint ────────────────────────
1398
1474
  //
1399
- // Routes under /api/_test/fanout/* exercise NimbusFanoutPool's two
1475
+ // Routes under /api/_test/fanout/* exercise Fanout's two
1400
1476
  // topologies (in-DO fanout in-DO + peer-DO fanout peer-DO) via a synthetic workload
1401
1477
  // that's independent of the production install-batch / pre-bundle
1402
1478
  // sites. The probe measures speedup, peer-DO routing determinism,
@@ -1425,7 +1501,7 @@ async function handleFanoutTestEndpoint(self, url, request) {
1425
1501
  const path = url.pathname;
1426
1502
  if (path === '/api/_test/fanout/topology' && request.method === 'GET') {
1427
1503
  const n = Math.max(0, parseInt(url.searchParams.get('n') || '0', 10));
1428
- const pool = new NimbusFanoutPool(env, self.ctx, {
1504
+ const pool = new Fanout(env, self.ctx, {
1429
1505
  tag: 'fanout-bench',
1430
1506
  timeoutMs: 60_000,
1431
1507
  });
@@ -1440,7 +1516,7 @@ async function handleFanoutTestEndpoint(self, url, request) {
1440
1516
  const keysRaw = url.searchParams.get('keys') || '';
1441
1517
  const keys = keysRaw.split(',').map((k) => k.trim()).filter(Boolean);
1442
1518
  const peerCount = Math.max(1, Math.min(parseInt(url.searchParams.get('n') || String(keys.length), 10), MAX_PEER_FANOUT));
1443
- const pool = new NimbusFanoutPool(env, self.ctx, {
1519
+ const pool = new Fanout(env, self.ctx, {
1444
1520
  tag: 'fanout-bench',
1445
1521
  timeoutMs: 60_000,
1446
1522
  });
@@ -1454,7 +1530,7 @@ async function handleFanoutTestEndpoint(self, url, request) {
1454
1530
  const body = await parseJsonBody(request, FanoutBenchBodySchema);
1455
1531
  const n = Math.max(1, Math.min(64, body.n || 8));
1456
1532
  const sleepMs = Math.max(0, Math.min(2000, body.sleepMs || 100));
1457
- const pool = new NimbusFanoutPool(env, self.ctx, {
1533
+ const pool = new Fanout(env, self.ctx, {
1458
1534
  tag: 'fanout-bench',
1459
1535
  timeoutMs: 60_000,
1460
1536
  });
@@ -1470,7 +1546,7 @@ async function handleFanoutTestEndpoint(self, url, request) {
1470
1546
  const results = await pool.submitMany(tasks, async (item, env) => {
1471
1547
  const startMs = Date.now();
1472
1548
  // Identify which env we're running in. SUPERVISOR is the
1473
- // RPC stub auto-injected by NimbusLoaderPool; its presence
1549
+ // RPC stub auto-injected by IsolatePool; its presence
1474
1550
  // tells us we're inside a loader isolate (not the supervisor).
1475
1551
  const loaderEnvKeys = Object.keys(env || {}).sort();
1476
1552
  // Sleep entirely inside the isolate — no external network.
@@ -1510,11 +1586,11 @@ async function handleFanoutTestEndpoint(self, url, request) {
1510
1586
  const n = Math.max(1, Math.min(64, body.n || 8));
1511
1587
  const sleepMs = Math.max(0, Math.min(2000, body.sleepMs || 100));
1512
1588
  // Same workload, but FORCE serial dispatch by using a single
1513
- // NimbusLoaderPool with concurrency=1 and submitting one task
1589
+ // IsolatePool with concurrency=1 and submitting one task
1514
1590
  // at a time. This is the T_serial reference for the 5× speedup
1515
1591
  // assertion.
1516
- const { NimbusLoaderPool } = await import('../loaders/loader-pool.js');
1517
- const pool = new NimbusLoaderPool(env, self.ctx, {
1592
+ const { IsolatePool } = await import('@nimbus-sh/fabric/isolate-pool.js');
1593
+ const pool = new IsolatePool(env, self.ctx, {
1518
1594
  concurrency: 1,
1519
1595
  timeoutMs: 60_000,
1520
1596
  tag: 'fanout-serial',