@nimbus-sh/worker 0.2.1 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -2
- package/dist/facets/manager.d.ts +24 -0
- package/dist/facets/manager.d.ts.map +1 -1
- package/dist/facets/manager.js +73 -14
- package/dist/loaders/generated-workers.d.ts +2 -2
- package/dist/loaders/generated-workers.d.ts.map +1 -1
- package/dist/loaders/generated-workers.js +3 -3
- package/dist/runtime/facet-loader-host.d.ts +3 -2
- package/dist/runtime/facet-loader-host.d.ts.map +1 -1
- package/dist/runtime/facet-loader-host.js +1 -0
- package/dist/runtime/ruby-repl.js +1 -1
- package/dist/runtime/ruby-resident.d.ts +20 -0
- package/dist/runtime/ruby-resident.d.ts.map +1 -0
- package/dist/runtime/ruby-resident.js +278 -0
- package/dist/session/init.d.ts +16 -7
- package/dist/session/init.d.ts.map +1 -1
- package/dist/session/init.js +192 -208
- package/dist/session/keys.d.ts +11 -9
- package/dist/session/keys.d.ts.map +1 -1
- package/dist/session/keys.js +11 -9
- package/dist/session/nimbus-session.d.ts +11 -1
- package/dist/session/nimbus-session.d.ts.map +1 -1
- package/dist/session/nimbus-session.js +13 -57
- package/dist/session/programmatic.d.ts +1 -1
- package/dist/session/programmatic.d.ts.map +1 -1
- package/dist/session/programmatic.js +1 -1
- package/dist/session/routes.js +1 -1
- package/package.json +2 -2
- package/scripts/bundle-facet-workers.mjs +4 -3
- package/scripts/bundle-runtime.mjs +279 -44
- package/dist/npm/tarball-stream.d.ts +0 -93
- package/dist/npm/tarball-stream.d.ts.map +0 -1
- package/dist/npm/tarball-stream.js +0 -235
- package/dist/npm/tarball.d.ts +0 -34
- package/dist/npm/tarball.d.ts.map +0 -1
- package/dist/npm/tarball.js +0 -97
- package/dist/runtime/clang-runner.d.ts +0 -37
- package/dist/runtime/clang-runner.d.ts.map +0 -1
- package/dist/runtime/clang-runner.js +0 -865
- package/dist/runtime/ruby-gems.d.ts +0 -30
- package/dist/runtime/ruby-gems.d.ts.map +0 -1
- package/dist/runtime/ruby-gems.js +0 -636
- package/dist/runtime/ruby-runner.d.ts +0 -75
- package/dist/runtime/ruby-runner.d.ts.map +0 -1
- package/dist/runtime/ruby-runner.js +0 -1577
package/dist/session/init.js
CHANGED
|
@@ -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 (
|
|
7
|
-
*
|
|
8
|
-
* (cold-only) Phase O (MOTD + framework hint). The
|
|
9
|
-
* lots of locals (vfs, kernel, registry, shell) and
|
|
10
|
-
* matters strictly — there's no interesting reuse boundary
|
|
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,9 +33,9 @@
|
|
|
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 {
|
|
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 {
|
|
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';
|
|
@@ -37,14 +46,12 @@ import { parseViteConfigSource } from '@nimbus-sh/core/runtime/vite-config-parse
|
|
|
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';
|
|
@@ -62,7 +69,7 @@ import { listInstalledRuntimes, rehydrateInstalledRuntimes, registerRunnerFactor
|
|
|
62
69
|
import { hasSeededProject, SEED_PROJECT_DIR } from '@nimbus-sh/core/vfs/seed-project.js';
|
|
63
70
|
import { notifyTerminalEvent } from '../runtime/process-logs-api.js';
|
|
64
71
|
import { stripAnsi } from '@nimbus-sh/core/runtime/process-logs.js';
|
|
65
|
-
import {
|
|
72
|
+
import { DEFAULT_MOUNT_POINTS, NODE_VERSION, } from '@nimbus-sh/core/constants.js';
|
|
66
73
|
import { ensureSessionStateSchema, loadShellState, stampHydratedAt, countSessionStateKeys, loadKernelMounts, persistKernelMounts, appendScrollback, loadScrollback, } from './state-store.js';
|
|
67
74
|
import { recordRecoveryEvent } from '@nimbus-sh/core/observability/oom-discriminator.js';
|
|
68
75
|
import { sessionAiEnv } from './ai.js';
|
|
@@ -77,7 +84,7 @@ function resolveNpmPrefix(prefix, cwd) {
|
|
|
77
84
|
function quoteShellArgument(value) {
|
|
78
85
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
79
86
|
}
|
|
80
|
-
export function initSession(self, ws) {
|
|
87
|
+
export async function initSession(self, ws) {
|
|
81
88
|
self.ensureSqliteFs();
|
|
82
89
|
const kernelFs = self.sqliteFs.as(CRED_KERNEL);
|
|
83
90
|
self.ensureFacetManager();
|
|
@@ -157,43 +164,158 @@ export function initSession(self, ws) {
|
|
|
157
164
|
self.terminal.write('\x1b[33m[nimbus] this session resumed on a new instance. Anything still '
|
|
158
165
|
+ 'running was lost with the old one — re-run it if it had not finished.\x1b[0m\r\n');
|
|
159
166
|
}
|
|
160
|
-
//
|
|
161
|
-
//
|
|
162
|
-
//
|
|
167
|
+
// A reset that killed a resident launch left its journal row behind
|
|
168
|
+
// (facets/manager.ts, _recoverInterruptedLaunches). The alarm the dying
|
|
169
|
+
// instance was using for launch turns is NOT a trigger recovery can rely
|
|
170
|
+
// on — measured live, a launch killed early in its first chunks rolls the
|
|
171
|
+
// alarm-map put back with the rest of the dying turn, so the replacement
|
|
172
|
+
// instance never fires an alarm at all. What always follows a dead session
|
|
173
|
+
// is this reconnect, so recovery runs here: after the terminal exists (the
|
|
174
|
+
// report lands in front of the user, live) and after the constructor's
|
|
175
|
+
// pidBase gate (the journal predicate needs this instance's generation).
|
|
176
|
+
// Idempotent per instance; a no-op whenever no launch was interrupted.
|
|
177
|
+
await self.facetManager.pumpResidentLaunches();
|
|
178
|
+
// [B'.4] Phase B — Build. Compose the workspace (kernel + mounts +
|
|
179
|
+
// shell + the OS command set), then install the session's own
|
|
180
|
+
// commands and wiring on top. CPU-intensive phase. Spans from here
|
|
181
|
+
// through Phase O.
|
|
163
182
|
setPhase(self, 'build', 'init-session');
|
|
164
|
-
// ──
|
|
165
|
-
self.kernel = new Kernel(new MemoryPersistenceBackend());
|
|
166
|
-
self.kernel.initFilesystem();
|
|
167
|
-
// ── Mount SqliteFSProvider at all top-level directories [B'.2] ──
|
|
183
|
+
// ── Mount list = DEFAULT_MOUNT_POINTS ∪ persisted-mounts [B'.2] ──
|
|
168
184
|
//
|
|
169
|
-
//
|
|
170
|
-
// defaults are always present (they're platform invariants);
|
|
185
|
+
// The defaults are always present (they're platform invariants);
|
|
171
186
|
// any extras a future custom-mount feature might add survive
|
|
172
|
-
// reconnect via the nimbus_kernel_mounts table. The persist
|
|
173
|
-
//
|
|
174
|
-
//
|
|
187
|
+
// reconnect via the nimbus_kernel_mounts table. The persist step
|
|
188
|
+
// below writes the merged list back so the table tracks the live
|
|
189
|
+
// mount tree — today the same 7 rows every initSession.
|
|
175
190
|
const persistedMounts = loadKernelMounts(self.ctx);
|
|
176
191
|
const mountPoints = Array.from(new Set([
|
|
177
192
|
...DEFAULT_MOUNT_POINTS,
|
|
178
193
|
...persistedMounts,
|
|
179
194
|
]));
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
195
|
+
// ── What the session adds to the workspace's environment [B'.1] ──
|
|
196
|
+
//
|
|
197
|
+
// The platform defaults — PATH, PS1, HOME, PORT, HOST and the rest —
|
|
198
|
+
// belong to the workspace (core/workspace/nimbus-workspace.ts) because
|
|
199
|
+
// they are the OS's, not this transport's. What is genuinely the
|
|
200
|
+
// session's layers here:
|
|
201
|
+
//
|
|
202
|
+
// NIMBUS_SESSION_ID — derived from sessionBasePath = "/s/<id>". Set
|
|
203
|
+
// here as a placeholder ("") and patched below right
|
|
204
|
+
// after the shell exists, so the user's first command
|
|
205
|
+
// sees the real id. Sentry / Datadog / any ops
|
|
206
|
+
// integration that wants a session-stable token reads it.
|
|
207
|
+
//
|
|
208
|
+
// sessionAiEnv() — the session AI gateway (session/ai.ts). A coding
|
|
209
|
+
// agent, a user's own script or curl reaches the
|
|
210
|
+
// session's models from these without being configured:
|
|
211
|
+
// by OPENAI_BASE_URL if it reads one, and otherwise by
|
|
212
|
+
// CLOUDFLARE_API_KEY, this session's capability token,
|
|
213
|
+
// which mediates the tool's own egress back to the
|
|
214
|
+
// gateway (_shared/ai-egress.ts).
|
|
215
|
+
//
|
|
216
|
+
// persisted.env — the user's own `export FOO=bar`, which survives
|
|
217
|
+
// reconnect and wins over everything above. A user who
|
|
218
|
+
// exports their own OPENAI_BASE_URL or CLOUDFLARE_API_KEY
|
|
219
|
+
// still wins; their key is not this session's token, so
|
|
220
|
+
// their request goes to their own account.
|
|
221
|
+
const envOverlay = {
|
|
222
|
+
NIMBUS_SESSION_ID: '',
|
|
223
|
+
...sessionAiEnv(),
|
|
224
|
+
...(persisted.env || {}),
|
|
225
|
+
};
|
|
226
|
+
// ── The identity the shell acts under ──
|
|
227
|
+
//
|
|
228
|
+
// Every command the shell runs is credentialed by a live entry in the
|
|
229
|
+
// session's process table, which is what makes `sudo`, `chown` and the
|
|
230
|
+
// per-process umask mean anything. A workspace with no host process
|
|
231
|
+
// table falls back to a bare uid-1000 identity; this one has one.
|
|
232
|
+
if (self.shellProcessPid !== null) {
|
|
233
|
+
self.processes.exit(self.shellProcessPid, 0);
|
|
183
234
|
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
235
|
+
const shellProcess = self.processes.spawn('sh', ['sh'], persisted.cwd || '/home/user');
|
|
236
|
+
self.shellProcessPid = shellProcess.pid;
|
|
237
|
+
const runAsProcess = async (parent, cred, argv) => {
|
|
238
|
+
if (argv.length === 0)
|
|
239
|
+
return 0;
|
|
240
|
+
const child = self.processes.spawn(argv.join(' '), argv, parent.cwd, { parentPid: parent.pid, cred });
|
|
241
|
+
const activeShell = self.shell;
|
|
242
|
+
if (!activeShell) {
|
|
243
|
+
self.processes.exit(child.pid, 1);
|
|
244
|
+
throw new Error('shell is not initialized');
|
|
245
|
+
}
|
|
246
|
+
const identity = commandIdentityFor(child.pid);
|
|
247
|
+
let exitCode = 1;
|
|
248
|
+
try {
|
|
249
|
+
const stdin = parent.stdin && parent.stdin !== parent.terminalStdin
|
|
250
|
+
? await parent.stdin.readAll()
|
|
251
|
+
: undefined;
|
|
252
|
+
const result = await activeShell.execute(argv.map(quoteShellArgument).join(' '), {
|
|
253
|
+
cwd: parent.cwd,
|
|
254
|
+
env: parent.env,
|
|
255
|
+
stdin,
|
|
256
|
+
terminalStdin: parent.terminalStdin,
|
|
257
|
+
signal: parent.signal,
|
|
258
|
+
isolateShellState: true,
|
|
259
|
+
terminalFds: {
|
|
260
|
+
stdin: parent.isFdTerminal?.(0) ?? false,
|
|
261
|
+
stdout: parent.isFdTerminal?.(1) ?? false,
|
|
262
|
+
stderr: parent.isFdTerminal?.(2) ?? false,
|
|
263
|
+
},
|
|
264
|
+
onStdout: (data) => parent.stdout.write(data),
|
|
265
|
+
onStderr: (data) => parent.stderr.write(data),
|
|
266
|
+
commandContext: {
|
|
267
|
+
pid: identity.pid,
|
|
268
|
+
cred: identity.cred,
|
|
269
|
+
setUmask: identity.setUmask,
|
|
270
|
+
},
|
|
271
|
+
runAs: runAsProcess,
|
|
272
|
+
});
|
|
273
|
+
exitCode = result.exitCode;
|
|
274
|
+
return exitCode;
|
|
275
|
+
}
|
|
276
|
+
finally {
|
|
277
|
+
self.processes.exit(child.pid, exitCode);
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
const commandIdentityFor = (pid) => ({
|
|
281
|
+
pid,
|
|
282
|
+
get cred() {
|
|
283
|
+
return self.processes.cred(pid);
|
|
284
|
+
},
|
|
285
|
+
setUmask(mask) {
|
|
286
|
+
self.processes.setUmask(pid, mask);
|
|
287
|
+
},
|
|
288
|
+
runAs: runAsProcess,
|
|
289
|
+
});
|
|
290
|
+
// ── The workspace: one recipe for kernel + mounts + shell + coreutils ──
|
|
291
|
+
//
|
|
292
|
+
// No `facets`: the session registers its own runtime factories below,
|
|
293
|
+
// because the ones it needs carry REPLs, a resident-process substrate
|
|
294
|
+
// and clang, none of which a bare facet host reaches. Everything the
|
|
295
|
+
// workspace does register is the OS, and is identical either way.
|
|
296
|
+
const workspace = await NimbusWorkspace.create({
|
|
297
|
+
sql: self.ctx.storage.sql,
|
|
298
|
+
// The filesystem this DO already opened. `ensureSqliteFs` above may
|
|
299
|
+
// have been called many requests ago; a second SqliteVFS over the
|
|
300
|
+
// same rows would be a second cache serving stale reads.
|
|
301
|
+
vfs: self.sqliteFs,
|
|
302
|
+
mounts: mountPoints,
|
|
303
|
+
env: envOverlay,
|
|
304
|
+
terminal: self.terminal,
|
|
305
|
+
identity: commandIdentityFor(shellProcess.pid),
|
|
306
|
+
});
|
|
307
|
+
self.kernel = workspace.kernel;
|
|
308
|
+
self.shell = workspace.shell;
|
|
309
|
+
const kernel = workspace.kernel;
|
|
310
|
+
const registry = workspace.registry;
|
|
311
|
+
const processRegistry = kernel.processRegistry;
|
|
312
|
+
const env = workspace.env;
|
|
313
|
+
const sqliteFs = self.sqliteFs;
|
|
314
|
+
const facetMgr = self.facetManager;
|
|
188
315
|
try {
|
|
189
316
|
persistKernelMounts(self.ctx, mountPoints);
|
|
190
317
|
}
|
|
191
318
|
catch { /* fail-soft */ }
|
|
192
|
-
// ── Create command registry ──
|
|
193
|
-
const registry = createDefaultRegistry();
|
|
194
|
-
const kernel = self.kernel;
|
|
195
|
-
const sqliteFs = self.sqliteFs;
|
|
196
|
-
const facetMgr = self.facetManager;
|
|
197
319
|
// ── editor/monaco (2026-05-13): editor-pane fs bridge ──
|
|
198
320
|
//
|
|
199
321
|
// The terminal hosts the WS that the editor pane reuses for
|
|
@@ -321,24 +443,10 @@ export function initSession(self, ws) {
|
|
|
321
443
|
});
|
|
322
444
|
}
|
|
323
445
|
});
|
|
324
|
-
// ── WASI Stage 1: exec dispatch for path-shaped invocations ──
|
|
325
|
-
// `./x`, `/abs/x`, `../x` go through POSIX execve semantics
|
|
326
|
-
// (shell/exec-dispatch.ts): exec-bit check (with the wasm-magic
|
|
327
|
-
// grandfather rule for pre-chmod modes), then format dispatch —
|
|
328
|
-
// `\0asm` → wasm-runner, `#!` → the named interpreter, binary
|
|
329
|
-
// junk → honest ENOEXEC, plain text → sh (the POSIX ENOEXEC
|
|
330
|
-
// fallback). Bare-word "X" still must come from the registered set.
|
|
331
|
-
//
|
|
332
|
-
// Implementation: monkey-patch registry.resolve. cwd and file
|
|
333
|
-
// state are read at every resolve call (not cached) so `cd` and
|
|
334
|
-
// recompiles between invocations are honoured.
|
|
335
|
-
installPathExecResolver(registry, kernelFs, () => self.shell?.getCwd() || '/home/user');
|
|
336
446
|
// W8: hand the registry to the cp broker so child_process.spawn from
|
|
337
447
|
// a parent facet can resolve and dispatch commands the same way the
|
|
338
|
-
// shell does.
|
|
448
|
+
// shell does.
|
|
339
449
|
self._setCpRegistry(registry);
|
|
340
|
-
// ── Unix commands (30+ real implementations) ──
|
|
341
|
-
registerUnixCommands(registry, sqliteFs);
|
|
342
450
|
registry.register('chsh', makeChshCommand({
|
|
343
451
|
isBashInstalled: (home) => listInstalledRuntimes(sqliteFs, home).some((runtime) => runtime.name === 'bash'),
|
|
344
452
|
}));
|
|
@@ -355,8 +463,9 @@ export function initSession(self, ws) {
|
|
|
355
463
|
// 3. Rehydrate any previously-installed runtimes from VFS so their
|
|
356
464
|
// bins reappear in the registry after DO eviction or WS reconnect.
|
|
357
465
|
registerRunnerFactory('clang-runner', (manifest, installRoot, binName, binKind) => async (ctx) => {
|
|
358
|
-
const { makeClangRunnerFactory } = await import('
|
|
359
|
-
|
|
466
|
+
const { makeClangRunnerFactory } = await import('@nimbus-sh/core/runtime/clang-runner.js');
|
|
467
|
+
const { facetHostForManager } = await import('../runtime/facet-loader-host.js');
|
|
468
|
+
return await makeClangRunnerFactory({ facets: facetHostForManager(facetMgr), vfs: sqliteFs })(manifest, installRoot, binName, binKind)(ctx);
|
|
360
469
|
});
|
|
361
470
|
// Pyodide v1 — Python 3.13 via the same R2-package-manager
|
|
362
471
|
// substrate that ships clang. Manifest entrypoints `python` and
|
|
@@ -421,8 +530,15 @@ export function initSession(self, ws) {
|
|
|
421
530
|
installRoot,
|
|
422
531
|
});
|
|
423
532
|
}
|
|
424
|
-
const { makeRubyRunnerFactory } = await import('
|
|
425
|
-
|
|
533
|
+
const { makeRubyRunnerFactory } = await import('@nimbus-sh/core/runtime/ruby-runner.js');
|
|
534
|
+
const { facetHostForManager } = await import('../runtime/facet-loader-host.js');
|
|
535
|
+
const { rubyResidentStart } = await import('../runtime/ruby-resident.js');
|
|
536
|
+
return await makeRubyRunnerFactory({
|
|
537
|
+
facets: facetHostForManager(facetMgr),
|
|
538
|
+
vfs: sqliteFs,
|
|
539
|
+
registry,
|
|
540
|
+
startResident: rubyResidentStart(facetMgr),
|
|
541
|
+
})(manifest, installRoot, binName, binKind)(ctx);
|
|
426
542
|
});
|
|
427
543
|
// GNU bash 5.2.37 (wasm32-wasi, asyncified) — dedicated facet
|
|
428
544
|
// runner driving the fork/pipe/exec/setjmp scheduler (fork M1-M3
|
|
@@ -1566,158 +1682,28 @@ export function initSession(self, ws) {
|
|
|
1566
1682
|
}
|
|
1567
1683
|
return result.failed.length > 0 ? 1 : 0;
|
|
1568
1684
|
});
|
|
1569
|
-
// ── Set up environment [B'.1: rehydrate from SQL] ──
|
|
1570
|
-
//
|
|
1571
|
-
// Cold start: env is the platform default below.
|
|
1572
|
-
// Silent re-init (persisted env present): the Shell's constructor
|
|
1573
|
-
// does `this.env = { ...n }`, so we layer the persisted env over
|
|
1574
|
-
// the defaults — defaults provide PATH/PS1/etc. (which the user
|
|
1575
|
-
// never sets explicitly), persisted overlays whatever the user
|
|
1576
|
-
// did set (NIMBUS_TEST=cool, etc.).
|
|
1577
|
-
//
|
|
1578
|
-
// Primitive #7 (runtime primitive support): PORT/HOST and
|
|
1579
|
-
// NIMBUS_SESSION_ID are part of the standard contract.
|
|
1580
|
-
//
|
|
1581
|
-
// PORT=3000 — the same default Markflow's `${PORT:-3000}` shell
|
|
1582
|
-
// expansion targets, and what every Express/Hono/
|
|
1583
|
-
// fastify/Bun.serve script reads when the user
|
|
1584
|
-
// doesn't set it explicitly. Long-running spawns
|
|
1585
|
-
// still pull from `--port` argv first (see
|
|
1586
|
-
// runtime/long-running-handle.ts:resolveLongRunningPort);
|
|
1587
|
-
// this default is the SOURCE for that fall-through.
|
|
1588
|
-
//
|
|
1589
|
-
// HOST=0.0.0.0 — Cloudflare Workers / DO have no localhost vs.
|
|
1590
|
-
// external distinction (the supervisor never opens
|
|
1591
|
-
// a real socket); 0.0.0.0 is what every tutorial
|
|
1592
|
-
// tells users to bind to and matches CF docs.
|
|
1593
|
-
//
|
|
1594
|
-
// NIMBUS_SESSION_ID — derived from sessionBasePath = "/s/<id>".
|
|
1595
|
-
// Set lazily here as a placeholder ("") and patched
|
|
1596
|
-
// below right after Shell construction so the user's
|
|
1597
|
-
// first command sees the real id.
|
|
1598
|
-
//
|
|
1599
|
-
// Why these aren't optional: package.json scripts that hardcode
|
|
1600
|
-
// process.env.PORT (Express's default app, every "create-vite"
|
|
1601
|
-
// template) get `undefined` without this. Sentry / Datadog / any
|
|
1602
|
-
// ops integration that wants a session-stable token uses
|
|
1603
|
-
// NIMBUS_SESSION_ID.
|
|
1604
|
-
const env = {
|
|
1605
|
-
HOME: '/home/user',
|
|
1606
|
-
USER: 'user',
|
|
1607
|
-
SHELL: '/bin/sh',
|
|
1608
|
-
HOSTNAME: DEFAULT_HOSTNAME,
|
|
1609
|
-
TERM: 'xterm-256color',
|
|
1610
|
-
PWD: '/home/user',
|
|
1611
|
-
PATH: DEFAULT_PATH,
|
|
1612
|
-
PS1: `\x1b[1;32muser@${DEFAULT_HOSTNAME}\x1b[0m:\x1b[1;34m\\w\x1b[0m$ `,
|
|
1613
|
-
NODE_ENV: 'development',
|
|
1614
|
-
LANG: 'en_US.UTF-8',
|
|
1615
|
-
EDITOR: 'nano',
|
|
1616
|
-
NIMBUS_VERSION: NIMBUS_VERSION,
|
|
1617
|
-
TMPDIR: '/tmp',
|
|
1618
|
-
XDG_CONFIG_HOME: '/home/user/.config',
|
|
1619
|
-
XDG_DATA_HOME: '/home/user/.local/share',
|
|
1620
|
-
npm_config_prefix: '/usr/local',
|
|
1621
|
-
// Primitive #7 contract additions.
|
|
1622
|
-
PORT: '3000',
|
|
1623
|
-
HOST: '0.0.0.0',
|
|
1624
|
-
NIMBUS_SESSION_ID: '', // patched after Shell ctor — see below.
|
|
1625
|
-
// The session AI gateway (session/ai.ts). A coding agent, a user's own
|
|
1626
|
-
// script or curl reaches the session's models from these without being
|
|
1627
|
-
// configured: by OPENAI_BASE_URL if it reads one, and otherwise by
|
|
1628
|
-
// CLOUDFLARE_API_KEY, this session's capability token, which mediates the
|
|
1629
|
-
// tool's own egress back to the gateway (_shared/ai-egress.ts). A user
|
|
1630
|
-
// who exports their own OPENAI_BASE_URL or CLOUDFLARE_API_KEY still wins,
|
|
1631
|
-
// via the persisted spread — their key is not this session's token, so
|
|
1632
|
-
// their request goes to their own account.
|
|
1633
|
-
...sessionAiEnv(),
|
|
1634
|
-
// Persisted env keys win over defaults — the user's `export FOO=bar`
|
|
1635
|
-
// survives reconnect.
|
|
1636
|
-
...(persisted.env || {}),
|
|
1637
|
-
};
|
|
1638
|
-
// ── Create shell ──
|
|
1639
|
-
const processRegistry = new ProcessRegistry();
|
|
1640
|
-
if (self.shellProcessPid !== null) {
|
|
1641
|
-
self.processes.exit(self.shellProcessPid, 0);
|
|
1642
|
-
}
|
|
1643
|
-
const shellProcess = self.processes.spawn('sh', ['sh'], persisted.cwd || '/home/user');
|
|
1644
|
-
self.shellProcessPid = shellProcess.pid;
|
|
1645
|
-
const runAsProcess = async (parent, cred, argv) => {
|
|
1646
|
-
if (argv.length === 0)
|
|
1647
|
-
return 0;
|
|
1648
|
-
const child = self.processes.spawn(argv.join(' '), argv, parent.cwd, { parentPid: parent.pid, cred });
|
|
1649
|
-
const activeShell = self.shell;
|
|
1650
|
-
if (!activeShell) {
|
|
1651
|
-
self.processes.exit(child.pid, 1);
|
|
1652
|
-
throw new Error('shell is not initialized');
|
|
1653
|
-
}
|
|
1654
|
-
const identity = commandIdentityFor(child.pid);
|
|
1655
|
-
let exitCode = 1;
|
|
1656
|
-
try {
|
|
1657
|
-
const stdin = parent.stdin && parent.stdin !== parent.terminalStdin
|
|
1658
|
-
? await parent.stdin.readAll()
|
|
1659
|
-
: undefined;
|
|
1660
|
-
const result = await activeShell.execute(argv.map(quoteShellArgument).join(' '), {
|
|
1661
|
-
cwd: parent.cwd,
|
|
1662
|
-
env: parent.env,
|
|
1663
|
-
stdin,
|
|
1664
|
-
terminalStdin: parent.terminalStdin,
|
|
1665
|
-
signal: parent.signal,
|
|
1666
|
-
isolateShellState: true,
|
|
1667
|
-
terminalFds: {
|
|
1668
|
-
stdin: parent.isFdTerminal?.(0) ?? false,
|
|
1669
|
-
stdout: parent.isFdTerminal?.(1) ?? false,
|
|
1670
|
-
stderr: parent.isFdTerminal?.(2) ?? false,
|
|
1671
|
-
},
|
|
1672
|
-
onStdout: (data) => parent.stdout.write(data),
|
|
1673
|
-
onStderr: (data) => parent.stderr.write(data),
|
|
1674
|
-
commandContext: {
|
|
1675
|
-
pid: identity.pid,
|
|
1676
|
-
cred: identity.cred,
|
|
1677
|
-
setUmask: identity.setUmask,
|
|
1678
|
-
},
|
|
1679
|
-
runAs: runAsProcess,
|
|
1680
|
-
});
|
|
1681
|
-
exitCode = result.exitCode;
|
|
1682
|
-
return exitCode;
|
|
1683
|
-
}
|
|
1684
|
-
finally {
|
|
1685
|
-
self.processes.exit(child.pid, exitCode);
|
|
1686
|
-
}
|
|
1687
|
-
};
|
|
1688
|
-
const commandIdentityFor = (pid) => ({
|
|
1689
|
-
pid,
|
|
1690
|
-
get cred() {
|
|
1691
|
-
return self.processes.cred(pid);
|
|
1692
|
-
},
|
|
1693
|
-
setUmask(mask) {
|
|
1694
|
-
self.processes.setUmask(pid, mask);
|
|
1695
|
-
},
|
|
1696
|
-
runAs: runAsProcess,
|
|
1697
|
-
});
|
|
1698
|
-
self.shell = new Shell(self.terminal, self.kernel.vfs, registry, env, processRegistry, commandIdentityFor(shellProcess.pid));
|
|
1699
1685
|
// Primitive #7: patch NIMBUS_SESSION_ID into the live shell env.
|
|
1700
1686
|
// sessionBasePath is "/s/<sid>" set by the X-Nimbus-Base header on
|
|
1701
1687
|
// the first /ws upgrade — by the time initSession runs (after the
|
|
1702
1688
|
// ws handshake), it's populated. Older /ws-pre-base callers see
|
|
1703
1689
|
// an empty string, which is the safe placeholder (no false id).
|
|
1704
1690
|
//
|
|
1705
|
-
// We patch the live env (not the
|
|
1706
|
-
// shell state on warm-rejoin still picks up the
|
|
1707
|
-
// the DO's name is stable across hibernation
|
|
1708
|
-
// `export NIMBUS_SESSION_ID=...` would have been
|
|
1709
|
-
// persisted.env and the spread
|
|
1710
|
-
// empty placeholder; we only set when the live env
|
|
1711
|
-
// (don't clobber a user-set value).
|
|
1691
|
+
// We patch the live env (not the overlay the workspace was built
|
|
1692
|
+
// from) so persisted shell state on warm-rejoin still picks up the
|
|
1693
|
+
// SAME session id — the DO's name is stable across hibernation
|
|
1694
|
+
// cycles. Any user `export NIMBUS_SESSION_ID=...` would have been
|
|
1695
|
+
// persisted to persisted.env and the overlay's spread would have
|
|
1696
|
+
// overridden the empty placeholder; we only set when the live env
|
|
1697
|
+
// is empty (don't clobber a user-set value).
|
|
1712
1698
|
const sessionIdFromBase = (self.sessionBasePath || '').replace(/^\/s\//, '');
|
|
1713
1699
|
if (sessionIdFromBase) {
|
|
1714
1700
|
// Shell.env is declared private but mutable at runtime — there's
|
|
1715
1701
|
// no public setter. We `any`-cast deliberately; the alternative
|
|
1716
|
-
// (replacing the whole Shell after
|
|
1717
|
-
// registry wiring. Anti-req note: this is NOT a defensive
|
|
1718
|
-
// it's a deliberate single-write operation to plug the
|
|
1719
|
-
// gap that env-construction couldn't fill (sessionBasePath
|
|
1720
|
-
// wasn't yet hydrated
|
|
1702
|
+
// (replacing the whole Shell after construction) would lose the
|
|
1703
|
+
// kernel + registry wiring. Anti-req note: this is NOT a defensive
|
|
1704
|
+
// cast, it's a deliberate single-write operation to plug the
|
|
1705
|
+
// contract gap that env-construction couldn't fill (sessionBasePath
|
|
1706
|
+
// wasn't yet hydrated when the workspace was composed).
|
|
1721
1707
|
const shellAny = self.shell;
|
|
1722
1708
|
if (!shellAny.env.NIMBUS_SESSION_ID) {
|
|
1723
1709
|
shellAny.env.NIMBUS_SESSION_ID = sessionIdFromBase;
|
|
@@ -2733,17 +2719,15 @@ export function initSession(self, ws) {
|
|
|
2733
2719
|
catch { /* non-critical */ }
|
|
2734
2720
|
}
|
|
2735
2721
|
// ── Start shell ──
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
catch { }
|
|
2746
|
-
})();
|
|
2722
|
+
//
|
|
2723
|
+
// Now, and not inside the workspace, because the login files are the
|
|
2724
|
+
// user's and may name any of the commands registered above. Not awaited:
|
|
2725
|
+
// `shell.start()` runs synchronously and the rc files apply as they
|
|
2726
|
+
// finish, which is what the terminal has always done. A user's broken
|
|
2727
|
+
// rc file must not take the socket down with it.
|
|
2728
|
+
workspace.start().catch((e) => {
|
|
2729
|
+
console.warn('[nimbus] shell start failed:', e?.message || e);
|
|
2730
|
+
});
|
|
2747
2731
|
ws.send(JSON.stringify({ type: 'ready' }));
|
|
2748
2732
|
}
|
|
2749
2733
|
function installShellExecutionFeatures(shell, terminal) {
|
package/dist/session/keys.d.ts
CHANGED
|
@@ -61,16 +61,18 @@ export declare const W1_NEXT_ALARM_REASONS_KEY = "w1_next_alarm_reasons";
|
|
|
61
61
|
*/
|
|
62
62
|
export declare const SESSION_DESTROYED_KEY = "session_destroyed";
|
|
63
63
|
/**
|
|
64
|
-
* Prefix for the resident-
|
|
65
|
-
* by the pid it
|
|
64
|
+
* Prefix for the resident-process journal: one row per resident this session
|
|
65
|
+
* owes the user, keyed by the pid it was built for.
|
|
66
66
|
*
|
|
67
|
-
* A
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
67
|
+
* A resident holds its state in memory — the process table entry, the facet
|
|
68
|
+
* handle, the terminal — so an instance reset destroys it silently. The row
|
|
69
|
+
* is what a LATER instance reads to know a resident ended that way rather
|
|
70
|
+
* than on purpose: a pid at or below the reader's own pid base was allocated
|
|
71
|
+
* by a previous generation (see PID_GEN_STRIDE). Written (and synced) before
|
|
72
|
+
* the launch's first byte of work, rewritten as `running` when the launch
|
|
73
|
+
* settles, and released only when the PROCESS ends — because the resets this
|
|
74
|
+
* row survives strike after the launch as often as during it (measured live,
|
|
75
|
+
* staging 2026-08-13: every observed reset landed seconds AFTER settle).
|
|
74
76
|
*/
|
|
75
77
|
export declare const RESIDENT_LAUNCH_KEY_PREFIX = "resident-launch:";
|
|
76
78
|
/** Prefix for consumed single-use attach bootstrap token ids (`jti`).
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"keys.d.ts","sourceRoot":"","sources":["../../src/session/keys.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH;+DAC+D;AAC/D,eAAO,MAAM,kBAAkB,mBAAmB,CAAC;AAEnD;2EAC2E;AAC3E,eAAO,MAAM,oBAAoB,MAAM,CAAC;AAExC;;yDAEyD;AACzD,eAAO,MAAM,mBAAmB,mBAAmB,CAAC;AAEpD;0DAC0D;AAC1D,eAAO,MAAM,qBAAqB,sBAAsB,CAAC;AAEzD;;uBAEuB;AACvB,eAAO,MAAM,eAAe,gBAAgB,CAAC;AAE7C;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,yBAAyB,0BAA0B,CAAC;AAEjE;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,sBAAsB,CAAC;AAEzD
|
|
1
|
+
{"version":3,"file":"keys.d.ts","sourceRoot":"","sources":["../../src/session/keys.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH;+DAC+D;AAC/D,eAAO,MAAM,kBAAkB,mBAAmB,CAAC;AAEnD;2EAC2E;AAC3E,eAAO,MAAM,oBAAoB,MAAM,CAAC;AAExC;;yDAEyD;AACzD,eAAO,MAAM,mBAAmB,mBAAmB,CAAC;AAEpD;0DAC0D;AAC1D,eAAO,MAAM,qBAAqB,sBAAsB,CAAC;AAEzD;;uBAEuB;AACvB,eAAO,MAAM,eAAe,gBAAgB,CAAC;AAE7C;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,yBAAyB,0BAA0B,CAAC;AAEjE;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,sBAAsB,CAAC;AAEzD;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,0BAA0B,qBAAqB,CAAC;AAE7D;;;4EAG4E;AAC5E,eAAO,MAAM,+BAA+B,0BAA0B,CAAC"}
|
package/dist/session/keys.js
CHANGED
|
@@ -61,16 +61,18 @@ export const W1_NEXT_ALARM_REASONS_KEY = 'w1_next_alarm_reasons';
|
|
|
61
61
|
*/
|
|
62
62
|
export const SESSION_DESTROYED_KEY = 'session_destroyed';
|
|
63
63
|
/**
|
|
64
|
-
* Prefix for the resident-
|
|
65
|
-
* by the pid it
|
|
64
|
+
* Prefix for the resident-process journal: one row per resident this session
|
|
65
|
+
* owes the user, keyed by the pid it was built for.
|
|
66
66
|
*
|
|
67
|
-
* A
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
67
|
+
* A resident holds its state in memory — the process table entry, the facet
|
|
68
|
+
* handle, the terminal — so an instance reset destroys it silently. The row
|
|
69
|
+
* is what a LATER instance reads to know a resident ended that way rather
|
|
70
|
+
* than on purpose: a pid at or below the reader's own pid base was allocated
|
|
71
|
+
* by a previous generation (see PID_GEN_STRIDE). Written (and synced) before
|
|
72
|
+
* the launch's first byte of work, rewritten as `running` when the launch
|
|
73
|
+
* settles, and released only when the PROCESS ends — because the resets this
|
|
74
|
+
* row survives strike after the launch as often as during it (measured live,
|
|
75
|
+
* staging 2026-08-13: every observed reset landed seconds AFTER settle).
|
|
74
76
|
*/
|
|
75
77
|
export const RESIDENT_LAUNCH_KEY_PREFIX = 'resident-launch:';
|
|
76
78
|
/** Prefix for consumed single-use attach bootstrap token ids (`jti`).
|
|
@@ -560,8 +560,18 @@ export declare class NimbusSession extends CloudflareDurableObject {
|
|
|
560
560
|
* private API across modules.
|
|
561
561
|
*/
|
|
562
562
|
_envFlagDefaultOn(name: string): boolean;
|
|
563
|
-
initSession(ws: WebSocket): void
|
|
563
|
+
initSession(ws: WebSocket): Promise<void>;
|
|
564
564
|
ensureGlobalPrefixDirs(prefix: string): void;
|
|
565
|
+
/**
|
|
566
|
+
* The starter content a fresh Nimbus session shows a user: the banner, the
|
|
567
|
+
* sample files, and the Vite starter app.
|
|
568
|
+
*
|
|
569
|
+
* The base the OS boots on — the mount directories, /etc/passwd, /etc/group,
|
|
570
|
+
* /etc/profile, ~/.nimbusrc — comes from the workspace's own seed, which
|
|
571
|
+
* this calls rather than restates. Both are idempotent, so the workspace
|
|
572
|
+
* re-running it when it composes the shell costs a handful of existence
|
|
573
|
+
* checks and changes nothing.
|
|
574
|
+
*/
|
|
565
575
|
seedFilesystem(): void;
|
|
566
576
|
webSocketMessage(ws: WebSocket, message: string | ArrayBuffer): Promise<void>;
|
|
567
577
|
webSocketClose(ws: WebSocket, code?: number, reason?: string, wasClean?: boolean): Promise<void>;
|