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