@nimbus-sh/core 0.1.0 → 0.3.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.
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/runtime/bash-runner.d.ts +63 -0
- package/dist/runtime/bash-runner.d.ts.map +1 -0
- package/dist/runtime/bash-runner.generated.d.ts +14 -0
- package/dist/runtime/bash-runner.generated.d.ts.map +1 -0
- package/dist/runtime/bash-runner.generated.js +13 -0
- package/dist/runtime/bash-runner.js +290 -0
- package/dist/runtime/cpython-runner.d.ts +86 -0
- package/dist/runtime/cpython-runner.d.ts.map +1 -0
- package/dist/runtime/cpython-runner.js +425 -0
- package/dist/runtime/facet-host.d.ts +159 -0
- package/dist/runtime/facet-host.d.ts.map +1 -0
- package/dist/runtime/facet-host.js +22 -0
- package/dist/runtime/installed-runtimes.d.ts +99 -0
- package/dist/runtime/installed-runtimes.d.ts.map +1 -0
- package/dist/runtime/installed-runtimes.js +162 -0
- package/dist/runtime/local-facet-host.d.ts +38 -0
- package/dist/runtime/local-facet-host.d.ts.map +1 -0
- package/dist/runtime/local-facet-host.js +171 -0
- package/dist/runtime/python-pip.d.ts +38 -0
- package/dist/runtime/python-pip.d.ts.map +1 -0
- package/dist/runtime/python-pip.js +1063 -0
- package/dist/runtime/runtime-manifest.d.ts +86 -0
- package/dist/runtime/runtime-manifest.d.ts.map +1 -0
- package/dist/runtime/runtime-manifest.js +72 -0
- package/dist/runtime/runtime-package.d.ts +63 -0
- package/dist/runtime/runtime-package.d.ts.map +1 -0
- package/dist/runtime/runtime-package.js +66 -0
- package/dist/runtime/runtime-registry.d.ts +162 -0
- package/dist/runtime/runtime-registry.d.ts.map +1 -0
- package/dist/runtime/runtime-registry.js +363 -0
- package/dist/runtime/vfs-snapshot.d.ts.map +1 -1
- package/dist/runtime/vfs-snapshot.js +15 -1
- package/dist/runtime/vfs-supervisor.d.ts +22 -0
- package/dist/runtime/vfs-supervisor.d.ts.map +1 -0
- package/dist/runtime/vfs-supervisor.js +65 -0
- package/dist/runtime/virtual-socket-kernel.generated.d.ts +14 -0
- package/dist/runtime/virtual-socket-kernel.generated.d.ts.map +1 -0
- package/dist/runtime/virtual-socket-kernel.generated.js +13 -0
- package/dist/runtime/wasm-runner.d.ts +80 -0
- package/dist/runtime/wasm-runner.d.ts.map +1 -0
- package/dist/runtime/wasm-runner.js +686 -0
- package/dist/workspace/nimbus-workspace.d.ts +116 -20
- package/dist/workspace/nimbus-workspace.d.ts.map +1 -1
- package/dist/workspace/nimbus-workspace.js +238 -45
- package/package.json +4 -2
- package/src/index.ts +16 -0
- package/src/runtime/bash-runner.generated.ts +14 -0
- package/src/runtime/bash-runner.ts +347 -0
- package/src/runtime/cpython-runner.ts +504 -0
- package/src/runtime/facet-host.ts +170 -0
- package/src/runtime/installed-runtimes.ts +235 -0
- package/src/runtime/local-facet-host.ts +205 -0
- package/src/runtime/python-pip.ts +1211 -0
- package/src/runtime/runtime-manifest.ts +155 -0
- package/src/runtime/runtime-package.ts +114 -0
- package/src/runtime/runtime-registry.ts +511 -0
- package/src/runtime/vfs-snapshot.ts +15 -1
- package/src/runtime/vfs-supervisor.ts +67 -0
- package/src/runtime/virtual-socket-kernel.generated.ts +14 -0
- package/src/runtime/wasm-runner.ts +835 -0
- package/src/workspace/nimbus-workspace.ts +349 -54
|
@@ -3,34 +3,52 @@
|
|
|
3
3
|
*
|
|
4
4
|
* A workspace is a durable filesystem plus a shell over it. It owns no
|
|
5
5
|
* transport, no session, no socket and no Durable Object: the host supplies
|
|
6
|
-
*
|
|
7
|
-
* a Durable Object that is already
|
|
8
|
-
* makes it runnable in a plain bun
|
|
6
|
+
* the filesystem and gets back `.fs`, `.exec`, and a command registry to add
|
|
7
|
+
* to. That is what makes it embeddable in a Durable Object that is already
|
|
8
|
+
* busy powering something else, and what makes it runnable in a plain bun
|
|
9
|
+
* process over `bun:sqlite`.
|
|
9
10
|
*
|
|
10
|
-
* The composition here is not new. It is the one `session/init.ts`
|
|
11
|
-
* lifted out of the session so there is one recipe rather than one per
|
|
12
|
-
* —
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
11
|
+
* The composition here is not new. It is the one `session/init.ts` performed
|
|
12
|
+
* inline, lifted out of the session so there is one recipe rather than one per
|
|
13
|
+
* caller — the session now reads its kernel, shell and registry off a
|
|
14
|
+
* workspace, and five unit tests were already hand-rolling the same steps.
|
|
15
|
+
*
|
|
16
|
+
* Deliberately not `Sandbox.create`, which is the lifo demo sandbox's boot
|
|
17
|
+
* rather than this one: it registers `systemctl`, `tunnel` and the network
|
|
18
|
+
* command set, boots enabled service units out of `/etc/systemd`, and starts
|
|
19
|
+
* the shell before the host can register a command of its own. A session
|
|
20
|
+
* routed through it would silently acquire all of that.
|
|
16
21
|
*/
|
|
17
22
|
|
|
18
|
-
import {
|
|
23
|
+
import { Kernel } from '../substrate/lifo/kernel/index.js';
|
|
24
|
+
import { Shell } from '../substrate/lifo/shell/Shell.js';
|
|
25
|
+
import type { ShellCommandIdentity } from '../substrate/lifo/shell/Shell.js';
|
|
26
|
+
import { createDefaultRegistry } from '../substrate/lifo/commands/registry.js';
|
|
27
|
+
import type { CommandRegistry } from '../substrate/lifo/commands/registry.js';
|
|
28
|
+
import { SandboxCommandsImpl } from '../substrate/lifo/sandbox/SandboxCommands.js';
|
|
19
29
|
import { SandboxFsImpl } from '../substrate/lifo/sandbox/SandboxFs.js';
|
|
30
|
+
import { HeadlessTerminal } from '../substrate/lifo/sandbox/HeadlessTerminal.js';
|
|
20
31
|
import type { CommandResult, RunOptions, SandboxFs } from '../substrate/lifo/sandbox/types.js';
|
|
21
|
-
import type { Kernel } from '../substrate/lifo/kernel/index.js';
|
|
22
|
-
import type { Shell } from '../substrate/lifo/shell/Shell.js';
|
|
23
32
|
import type { ITerminal } from '../substrate/lifo/terminal/ITerminal.js';
|
|
24
33
|
import { SqliteVFS, SqliteVFSProvider } from '../vfs/sqlite-vfs.js';
|
|
25
|
-
import {
|
|
34
|
+
import {
|
|
35
|
+
DEFAULT_HOME, DEFAULT_HOSTNAME, DEFAULT_MOUNT_POINTS, DEFAULT_PATH,
|
|
36
|
+
DEFAULT_SHELL, DEFAULT_USER, NIMBUS_VERSION,
|
|
37
|
+
} from '../constants.js';
|
|
26
38
|
import { CRED_KERNEL, CRED_SESSION_USER } from '../runtime/os-contracts.js';
|
|
27
39
|
import type { SqlDatabase, TransactionHost } from '../runtime/os-contracts.js';
|
|
40
|
+
import { PID_GEN_STRIDE } from '../runtime/process-table.js';
|
|
41
|
+
import { SessionProcessSupervisor } from '../runtime/session-process-supervisor.js';
|
|
42
|
+
import type { FacetHost } from '../runtime/facet-host.js';
|
|
43
|
+
import {
|
|
44
|
+
rehydrateInstalledRuntimesView,
|
|
45
|
+
type RunnerFactory,
|
|
46
|
+
} from '../runtime/installed-runtimes.js';
|
|
47
|
+
import { seedRuntimePackage, type RuntimePackage } from '../runtime/runtime-package.js';
|
|
48
|
+
import type { EsbuildService } from '../runtime/esbuild-service.js';
|
|
28
49
|
import { registerUnixCommands } from '../shell/unix-commands.js';
|
|
29
50
|
import { installPathExecResolver } from '../shell/exec-dispatch.js';
|
|
30
51
|
|
|
31
|
-
/** Pids are `generation * PID_GEN_STRIDE + seq`; mirrors process-table.ts. */
|
|
32
|
-
const PID_GEN_STRIDE = 1_000_000;
|
|
33
|
-
|
|
34
52
|
export interface NimbusWorkspaceOptions {
|
|
35
53
|
/** The host's SQLite. In a Durable Object: `ctx.storage.sql`. */
|
|
36
54
|
readonly sql: SqlDatabase;
|
|
@@ -43,27 +61,83 @@ export interface NimbusWorkspaceOptions {
|
|
|
43
61
|
*/
|
|
44
62
|
readonly transactions?: TransactionHost;
|
|
45
63
|
/**
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
64
|
+
* The filesystem already open over `sql`, for a host that has one.
|
|
65
|
+
*
|
|
66
|
+
* A Durable Object does: its installer, its git commands and its RPC
|
|
67
|
+
* surfaces read those rows without a shell in sight, and they hold a
|
|
68
|
+
* SqliteVFS from the first request that needed one. It must hand over THAT
|
|
69
|
+
* one — a second SqliteVFS over the same database is a second cache, and
|
|
70
|
+
* one of the two will serve a stale read. Such a host has also already
|
|
71
|
+
* revoked the previous generation's append writers, which is why that only
|
|
72
|
+
* happens below when the workspace is the one opening the filesystem.
|
|
73
|
+
*/
|
|
74
|
+
readonly vfs?: SqliteVFS;
|
|
75
|
+
/**
|
|
76
|
+
* Process-id generation. Two things rest on it: the workspace revokes every
|
|
77
|
+
* append capability at or below `generation * 1_000_000` before serving
|
|
78
|
+
* anything, and the wasm runner allocates pids above it. A value that
|
|
79
|
+
* repeats across restarts hands a dead process live write authority, so
|
|
80
|
+
* hosts that persist must supply a counter that never repeats.
|
|
50
81
|
*/
|
|
51
82
|
readonly generation?: number;
|
|
52
83
|
/** Top-level directories backed by `sql`. Defaults to DEFAULT_MOUNT_POINTS. */
|
|
53
84
|
readonly mounts?: readonly string[];
|
|
85
|
+
/** Overlaid on the Nimbus default environment. */
|
|
54
86
|
readonly env?: Record<string, string>;
|
|
55
87
|
readonly cwd?: string;
|
|
56
88
|
/** Absent means headless: `.exec` captures output and nothing is drawn. */
|
|
57
89
|
readonly terminal?: ITerminal;
|
|
90
|
+
/**
|
|
91
|
+
* Who the shell acts as, when the host keeps a process table that can answer
|
|
92
|
+
* for it. Absent, commands run as uid 1000 with a umask of 022 and no
|
|
93
|
+
* process behind them, which is the Shell's own default.
|
|
94
|
+
*/
|
|
95
|
+
readonly identity?: ShellCommandIdentity;
|
|
96
|
+
/**
|
|
97
|
+
* Language runtimes to install before the shell is served, as npm packages
|
|
98
|
+
* the embedder imported (`@nimbus-sh/runtime-bash`,
|
|
99
|
+
* `@nimbus-sh/runtime-cpython`).
|
|
100
|
+
*
|
|
101
|
+
* A Durable Object gets these from R2 through `nimbus install`; an embedder
|
|
102
|
+
* off Cloudflare has no bucket and needs none, because npm already fetched
|
|
103
|
+
* and integrity-checked the same bytes. Both write the same tree at the same
|
|
104
|
+
* path, so what is installed here is indistinguishable from what is
|
|
105
|
+
* installed there — see runtime/runtime-package.ts.
|
|
106
|
+
*
|
|
107
|
+
* Independent of `facets`: this decides what the filesystem HOLDS, and
|
|
108
|
+
* `facets` decides whether anything can run it. A workspace given runtimes
|
|
109
|
+
* and no facet host installs them and still answers "command not found",
|
|
110
|
+
* because it still has nothing that could compile a module.
|
|
111
|
+
*/
|
|
112
|
+
readonly runtimes?: readonly RuntimePackage[];
|
|
113
|
+
/**
|
|
114
|
+
* Where WebAssembly runs.
|
|
115
|
+
*
|
|
116
|
+
* Absent, the workspace is the JavaScript half of Nimbus: the durable
|
|
117
|
+
* filesystem, the shell and the coreutils, and `bash` or `./prog.wasm` is
|
|
118
|
+
* "command not found" — not disabled, ABSENT, because nothing has been
|
|
119
|
+
* supplied that could compile a module or run one. Supplied, the wasm
|
|
120
|
+
* runtimes already installed in this filesystem become invokable commands
|
|
121
|
+
* and `wasm-runner` joins them, which is what makes a `\0asm` file on the
|
|
122
|
+
* PATH executable (see shell/exec-dispatch.ts).
|
|
123
|
+
*
|
|
124
|
+
* A plain process passes `localFacetHost()`. A Durable Object passes nothing
|
|
125
|
+
* here and registers its own runners instead, because the ones it needs
|
|
126
|
+
* carry REPLs and a resident-process substrate this cannot reach.
|
|
127
|
+
*/
|
|
128
|
+
readonly facets?: FacetHost;
|
|
58
129
|
}
|
|
59
130
|
|
|
60
131
|
/**
|
|
61
132
|
* A durable filesystem and a shell over it.
|
|
62
133
|
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
134
|
+
* The composition itself is synchronous. {@link create} awaits only the
|
|
135
|
+
* optional work — installing runtime packages, loading the wasm runner modules
|
|
136
|
+
* — so a host that asks for neither is never suspended between mounting the
|
|
137
|
+
* filesystem and registering the commands. A Durable Object needs that: it
|
|
138
|
+
* must not take delivery of an event with a half-built shell. The remaining
|
|
139
|
+
* async step, running the user's login files, is {@link start}, which the host
|
|
140
|
+
* calls once its own commands are in place.
|
|
67
141
|
*/
|
|
68
142
|
export class NimbusWorkspace {
|
|
69
143
|
/**
|
|
@@ -76,51 +150,109 @@ export class NimbusWorkspace {
|
|
|
76
150
|
readonly vfs: SqliteVFS;
|
|
77
151
|
readonly kernel: Kernel;
|
|
78
152
|
readonly shell: Shell;
|
|
153
|
+
/** What the shell resolves a command name against. A host adds its own. */
|
|
154
|
+
readonly registry: CommandRegistry;
|
|
155
|
+
/**
|
|
156
|
+
* The environment the shell was composed with. The shell's own copy drifts
|
|
157
|
+
* from this one the moment the user exports anything; this is what a host
|
|
158
|
+
* hands to a subordinate shell it starts itself.
|
|
159
|
+
*/
|
|
160
|
+
readonly env: Record<string, string>;
|
|
161
|
+
|
|
162
|
+
private readonly commands: SandboxCommandsImpl;
|
|
79
163
|
|
|
80
164
|
private constructor(
|
|
81
|
-
private readonly sandbox: Sandbox,
|
|
82
165
|
vfs: SqliteVFS,
|
|
166
|
+
kernel: Kernel,
|
|
167
|
+
shell: Shell,
|
|
168
|
+
registry: CommandRegistry,
|
|
169
|
+
env: Record<string, string>,
|
|
83
170
|
private readonly sql: SqlDatabase,
|
|
84
171
|
) {
|
|
85
172
|
this.vfs = vfs;
|
|
86
|
-
this.kernel =
|
|
87
|
-
this.shell =
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
);
|
|
173
|
+
this.kernel = kernel;
|
|
174
|
+
this.shell = shell;
|
|
175
|
+
this.registry = registry;
|
|
176
|
+
this.env = env;
|
|
177
|
+
this.commands = new SandboxCommandsImpl(shell, registry);
|
|
178
|
+
// NOT the kernel VFS as it stands, which is kernel-credentialed because
|
|
179
|
+
// the shell re-credentials per command; a host calling `.fs` has no
|
|
180
|
+
// process behind it and must not inherit that.
|
|
181
|
+
this.fs = new SandboxFsImpl(kernel.vfs.as(CRED_SESSION_USER), () => shell.getCwd());
|
|
95
182
|
}
|
|
96
183
|
|
|
97
184
|
static async create(options: NimbusWorkspaceOptions): Promise<NimbusWorkspace> {
|
|
98
|
-
const vfs =
|
|
99
|
-
vfs.revokeAppendWritersThrough((options.generation ?? 1) * PID_GEN_STRIDE);
|
|
100
|
-
|
|
185
|
+
const vfs = options.vfs ?? openFilesystem(options);
|
|
101
186
|
const mounts = options.mounts ?? DEFAULT_MOUNT_POINTS;
|
|
102
187
|
seedBaseFilesystem(vfs, mounts);
|
|
103
188
|
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
})),
|
|
112
|
-
});
|
|
189
|
+
const kernel = new Kernel();
|
|
190
|
+
// Seeds the in-memory tree. Mounting AFTER it is what keeps a durable
|
|
191
|
+
// /etc from being overwritten by the defaults on every boot.
|
|
192
|
+
kernel.initFilesystem();
|
|
193
|
+
for (const mount of mounts) {
|
|
194
|
+
kernel.vfs.mount(`/${mount}`, new SqliteVFSProvider(vfs, mount));
|
|
195
|
+
}
|
|
113
196
|
|
|
197
|
+
const registry = createDefaultRegistry();
|
|
114
198
|
// The durable coreutils replace ~25 lifo builtins. They are the ones that
|
|
115
199
|
// carry credentials and read this filesystem's uid/gid, so they must win.
|
|
116
|
-
registerUnixCommands(
|
|
117
|
-
|
|
200
|
+
registerUnixCommands(registry, vfs);
|
|
201
|
+
|
|
202
|
+
const env = { ...defaultEnv(), ...options.env };
|
|
203
|
+
const shell = new Shell(
|
|
204
|
+
options.terminal ?? new HeadlessTerminal(),
|
|
205
|
+
kernel.vfs,
|
|
206
|
+
registry,
|
|
207
|
+
env,
|
|
208
|
+
kernel.processRegistry,
|
|
209
|
+
options.identity,
|
|
210
|
+
);
|
|
211
|
+
if (options.cwd) shell.setCwd(options.cwd);
|
|
212
|
+
|
|
213
|
+
// Kernel-credentialed on purpose: this only INSPECTS a file to decide how
|
|
214
|
+
// to run it, and re-checks the caller's own execute permission at
|
|
215
|
+
// invocation time — the `authorize` wrapper in exec-dispatch.ts.
|
|
216
|
+
installPathExecResolver(registry, vfs.as(CRED_KERNEL), () => shell.getCwd());
|
|
118
217
|
|
|
119
|
-
|
|
218
|
+
const home = env.HOME ?? DEFAULT_HOME;
|
|
219
|
+
|
|
220
|
+
// Before the runners are wired, because registration reads what the
|
|
221
|
+
// filesystem holds — the same order `nimbus install` observes, and the
|
|
222
|
+
// same order a Durable Object observes when it rehydrates after eviction.
|
|
223
|
+
for (const runtimePackage of options.runtimes ?? []) {
|
|
224
|
+
await seedRuntimePackage(vfs.as(CRED_KERNEL), home, runtimePackage);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (options.facets) {
|
|
228
|
+
await registerWasmRuntimes({
|
|
229
|
+
facets: options.facets,
|
|
230
|
+
vfs,
|
|
231
|
+
registry,
|
|
232
|
+
generation: options.generation ?? 1,
|
|
233
|
+
home,
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return new NimbusWorkspace(vfs, kernel, shell, registry, env, options.sql);
|
|
120
238
|
}
|
|
121
239
|
|
|
122
240
|
exec(command: string, options?: RunOptions): Promise<CommandResult> {
|
|
123
|
-
return this.
|
|
241
|
+
return this.commands.run(command, options);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Apply the login files, and begin reading the terminal when there is one.
|
|
246
|
+
*
|
|
247
|
+
* Separate from {@link create} because a host with commands of its own must
|
|
248
|
+
* register them first: `/etc/profile` and `~/.nimbusrc` are the user's
|
|
249
|
+
* files, and either may name a command the host has yet to supply.
|
|
250
|
+
*/
|
|
251
|
+
async start(): Promise<void> {
|
|
252
|
+
// Sources /etc/profile and the first user rc file it finds, then prompts.
|
|
253
|
+
this.shell.start();
|
|
254
|
+
// Nimbus's own rc file, which the shell's list predates.
|
|
255
|
+
await this.shell.sourceFile(`${this.shell.getEnv().HOME ?? DEFAULT_HOME}/.nimbusrc`);
|
|
124
256
|
}
|
|
125
257
|
|
|
126
258
|
/**
|
|
@@ -150,6 +282,126 @@ export class NimbusWorkspace {
|
|
|
150
282
|
}
|
|
151
283
|
}
|
|
152
284
|
|
|
285
|
+
/**
|
|
286
|
+
* Open the durable filesystem for a host that has not opened one itself.
|
|
287
|
+
*
|
|
288
|
+
* The revocation is here rather than in `create` because it is the act of
|
|
289
|
+
* OPENING that carries it: pids at or below this generation's floor belong to
|
|
290
|
+
* an instance that is gone, and their append capabilities must stop being
|
|
291
|
+
* honoured before the first read. A host that opened the filesystem itself has
|
|
292
|
+
* already done this, at the same seam, for the same reason.
|
|
293
|
+
*/
|
|
294
|
+
function openFilesystem(options: NimbusWorkspaceOptions): SqliteVFS {
|
|
295
|
+
const vfs = new SqliteVFS(options.sql, options.transactions);
|
|
296
|
+
vfs.revokeAppendWritersThrough((options.generation ?? 1) * PID_GEN_STRIDE);
|
|
297
|
+
return vfs;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* The environment a Nimbus shell starts in.
|
|
302
|
+
*
|
|
303
|
+
* `PATH` and `EDITOR` restate what the seeded `/etc/profile` exports, so a
|
|
304
|
+
* workspace whose host never runs the login files is still on the real PATH.
|
|
305
|
+
* `PORT` and `HOST` are here because every scaffolded server reads them and
|
|
306
|
+
* gets `undefined` otherwise — Express's default app, every create-vite
|
|
307
|
+
* template, `${PORT:-3000}` in a package.json script.
|
|
308
|
+
*/
|
|
309
|
+
function defaultEnv(): Record<string, string> {
|
|
310
|
+
return {
|
|
311
|
+
HOME: DEFAULT_HOME,
|
|
312
|
+
USER: DEFAULT_USER,
|
|
313
|
+
SHELL: DEFAULT_SHELL,
|
|
314
|
+
HOSTNAME: DEFAULT_HOSTNAME,
|
|
315
|
+
TERM: 'xterm-256color',
|
|
316
|
+
PWD: DEFAULT_HOME,
|
|
317
|
+
PATH: DEFAULT_PATH,
|
|
318
|
+
PS1: `\x1b[1;32muser@${DEFAULT_HOSTNAME}\x1b[0m:\x1b[1;34m\\w\x1b[0m$ `,
|
|
319
|
+
NODE_ENV: 'development',
|
|
320
|
+
LANG: 'en_US.UTF-8',
|
|
321
|
+
EDITOR: 'nano',
|
|
322
|
+
NIMBUS_VERSION: NIMBUS_VERSION,
|
|
323
|
+
TMPDIR: '/tmp',
|
|
324
|
+
XDG_CONFIG_HOME: `${DEFAULT_HOME}/.config`,
|
|
325
|
+
XDG_DATA_HOME: `${DEFAULT_HOME}/.local/share`,
|
|
326
|
+
npm_config_prefix: '/usr/local',
|
|
327
|
+
PORT: '3000',
|
|
328
|
+
HOST: '0.0.0.0',
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Turn a facet host into commands: the runtimes this filesystem already holds,
|
|
334
|
+
* plus `wasm-runner` for everything else with a `\0asm` header.
|
|
335
|
+
*
|
|
336
|
+
* The runner factories are held here rather than in the process-global table
|
|
337
|
+
* `nimbus install` writes to, because each one closes over THIS workspace's
|
|
338
|
+
* filesystem and THIS workspace's facet host — a second workspace in the same
|
|
339
|
+
* process would otherwise silently retarget the first one's bash.
|
|
340
|
+
*
|
|
341
|
+
* Imported on demand: the runners carry the WASI shim and the bash scheduler as
|
|
342
|
+
* source strings, and a workspace with no facet host must not pay to parse
|
|
343
|
+
* them.
|
|
344
|
+
*/
|
|
345
|
+
async function registerWasmRuntimes(deps: {
|
|
346
|
+
facets: FacetHost;
|
|
347
|
+
vfs: SqliteVFS;
|
|
348
|
+
registry: CommandRegistry;
|
|
349
|
+
generation: number;
|
|
350
|
+
home: string;
|
|
351
|
+
}): Promise<void> {
|
|
352
|
+
const [
|
|
353
|
+
{ makeBashRunnerFactory },
|
|
354
|
+
{ makeCPythonRunnerFactory },
|
|
355
|
+
{ wasmRunnerSpec },
|
|
356
|
+
{ buildRuntimeHandler },
|
|
357
|
+
] = await Promise.all([
|
|
358
|
+
import('../runtime/bash-runner.js'),
|
|
359
|
+
import('../runtime/cpython-runner.js'),
|
|
360
|
+
import('../runtime/wasm-runner.js'),
|
|
361
|
+
import('../runtime/runtime-registry.js'),
|
|
362
|
+
]);
|
|
363
|
+
|
|
364
|
+
// wasm-runner allocates pids for what it runs, so it needs a process table
|
|
365
|
+
// whose pid space is this generation's.
|
|
366
|
+
const processes = new SessionProcessSupervisor();
|
|
367
|
+
processes.setPidBase(deps.generation * PID_GEN_STRIDE);
|
|
368
|
+
|
|
369
|
+
// Loaded on the first TypeScript or ESM script and not before. The module
|
|
370
|
+
// statically imports `esbuild-wasm/esbuild.wasm`, which only wrangler
|
|
371
|
+
// resolves — node instantiates it as a wasm module and fails on its Go
|
|
372
|
+
// imports — so a host outside Cloudflare must be able to run a shell, bash
|
|
373
|
+
// and python without that module ever entering its graph.
|
|
374
|
+
let esbuild: Promise<EsbuildService> | null = null;
|
|
375
|
+
deps.registry.register('wasm-runner', buildRuntimeHandler(
|
|
376
|
+
wasmRunnerSpec({ vfs: deps.vfs, facets: deps.facets, processes }),
|
|
377
|
+
{
|
|
378
|
+
vfs: deps.vfs,
|
|
379
|
+
getEsbuild: () => {
|
|
380
|
+
if (!esbuild) {
|
|
381
|
+
esbuild = import('../runtime/esbuild-service.js')
|
|
382
|
+
.then((module) => new module.EsbuildService(deps.vfs));
|
|
383
|
+
}
|
|
384
|
+
return esbuild;
|
|
385
|
+
},
|
|
386
|
+
registry: deps.registry,
|
|
387
|
+
},
|
|
388
|
+
));
|
|
389
|
+
|
|
390
|
+
const runners: Record<string, RunnerFactory> = {
|
|
391
|
+
'bash-runner': makeBashRunnerFactory({ facets: deps.facets, vfs: deps.vfs }),
|
|
392
|
+
// No `startResident`: a workspace owns no actor that could outlive the
|
|
393
|
+
// call, so a program that keeps serving is refused by name rather than
|
|
394
|
+
// run as a one-shot that dies with it.
|
|
395
|
+
'cpython-runner': makeCPythonRunnerFactory({ facets: deps.facets, vfs: deps.vfs }),
|
|
396
|
+
};
|
|
397
|
+
rehydrateInstalledRuntimesView(
|
|
398
|
+
deps.vfs.as(CRED_KERNEL),
|
|
399
|
+
deps.registry,
|
|
400
|
+
deps.home,
|
|
401
|
+
(key) => runners[key],
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
|
|
153
405
|
/**
|
|
154
406
|
* Every table the filesystem creates.
|
|
155
407
|
*
|
|
@@ -178,8 +430,14 @@ const WORKSPACE_TABLES = [
|
|
|
178
430
|
* a workspace reopened over a populated database keeps whatever the user did
|
|
179
431
|
* to these files. `/etc/passwd` and `/etc/group` are load-bearing rather than
|
|
180
432
|
* decorative — `id`, `chown` and `su` resolve names through them.
|
|
433
|
+
*
|
|
434
|
+
* What a PRODUCT puts in a fresh filesystem — a banner, a welcome file, a
|
|
435
|
+
* starter app — is not here. This is the base an OS needs in order to boot,
|
|
436
|
+
* and it is exported because a host may need the filesystem before it needs a
|
|
437
|
+
* shell: the Nimbus session seeds its starter project for a browser that hits
|
|
438
|
+
* `/preview` without ever opening a terminal.
|
|
181
439
|
*/
|
|
182
|
-
function seedBaseFilesystem(vfs: SqliteVFS, mounts: readonly string[]): void {
|
|
440
|
+
export function seedBaseFilesystem(vfs: SqliteVFS, mounts: readonly string[]): void {
|
|
183
441
|
const fs = vfs.as(CRED_SESSION_USER);
|
|
184
442
|
const rootFs = vfs.as(CRED_KERNEL);
|
|
185
443
|
|
|
@@ -188,11 +446,37 @@ function seedBaseFilesystem(vfs: SqliteVFS, mounts: readonly string[]): void {
|
|
|
188
446
|
for (const mount of mounts) {
|
|
189
447
|
if (mount !== 'etc' && !fs.exists(mount)) fs.mkdir(mount, { recursive: true });
|
|
190
448
|
}
|
|
191
|
-
for (const dir of [
|
|
449
|
+
for (const dir of [
|
|
450
|
+
'home/user', 'home/user/.config', 'home/user/projects',
|
|
451
|
+
'tmp', 'var/log',
|
|
452
|
+
'usr/bin', 'usr/lib', 'usr/lib/node_modules',
|
|
453
|
+
'usr/share', 'usr/share/pkg', 'usr/share/pkg/node_modules',
|
|
454
|
+
'usr/local', 'usr/local/lib', 'usr/local/lib/node_modules', 'usr/local/bin',
|
|
455
|
+
]) {
|
|
192
456
|
if (!fs.exists(dir)) fs.mkdir(dir, { recursive: true });
|
|
193
457
|
}
|
|
194
458
|
|
|
195
|
-
|
|
459
|
+
// /etc belongs to root, and is re-asserted rather than only created: a
|
|
460
|
+
// user-writable /etc is an authority bug, not an untidy directory.
|
|
461
|
+
if (!rootFs.exists('etc')) {
|
|
462
|
+
rootFs.mkdir('etc', { mode: 0o755 });
|
|
463
|
+
} else {
|
|
464
|
+
const etc = rootFs.stat('etc');
|
|
465
|
+
if (etc.uid !== 0 || etc.gid !== 0) rootFs.chown('etc', 0, 0);
|
|
466
|
+
if ((etc.mode & 0o7777) !== 0o755) rootFs.chmod('etc', 0o755);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
if (!rootFs.exists('etc/hostname')) {
|
|
470
|
+
rootFs.writeFile('etc/hostname', `${DEFAULT_HOSTNAME}\n`);
|
|
471
|
+
rootFs.chown('etc/hostname', CRED_SESSION_USER.uid, CRED_SESSION_USER.gid);
|
|
472
|
+
}
|
|
473
|
+
if (!rootFs.exists('etc/os-release')) {
|
|
474
|
+
rootFs.writeFile('etc/os-release',
|
|
475
|
+
`NAME="Nimbus"\nVERSION="${NIMBUS_VERSION}"\nID=nimbus\n`
|
|
476
|
+
+ 'PRETTY_NAME="Nimbus — Cloud Dev Environment"\n',
|
|
477
|
+
);
|
|
478
|
+
rootFs.chown('etc/os-release', CRED_SESSION_USER.uid, CRED_SESSION_USER.gid);
|
|
479
|
+
}
|
|
196
480
|
|
|
197
481
|
// Root-owned 0644, and re-asserted rather than only created: these decide
|
|
198
482
|
// what `id`, `chown` and `su` believe, so a user-writable /etc/passwd would
|
|
@@ -206,8 +490,19 @@ function seedBaseFilesystem(vfs: SqliteVFS, mounts: readonly string[]): void {
|
|
|
206
490
|
accountFile('etc/passwd', 'root:x:0:0:root:/root:/bin/sh\nuser:x:1000:1000:Nimbus User:/home/user:/bin/sh\n');
|
|
207
491
|
accountFile('etc/group', 'root:x:0:\nuser:x:1000:user\n');
|
|
208
492
|
|
|
493
|
+
const defaultProfile = `export PATH=${DEFAULT_PATH}\nexport EDITOR=nano\n`;
|
|
209
494
|
if (!rootFs.exists('etc/profile')) {
|
|
210
|
-
rootFs.writeFile('etc/profile',
|
|
495
|
+
rootFs.writeFile('etc/profile', defaultProfile);
|
|
211
496
|
rootFs.chown('etc/profile', CRED_SESSION_USER.uid, CRED_SESSION_USER.gid);
|
|
497
|
+
} else if (rootFs.readFileString('etc/profile') === 'export PATH=/usr/bin:/bin\nexport EDITOR=nano\n') {
|
|
498
|
+
// The lifo default, from before Nimbus had a PATH of its own. Nobody ever
|
|
499
|
+
// chose it, so replacing it is not overwriting a user's file.
|
|
500
|
+
rootFs.writeFile('etc/profile', defaultProfile);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
if (!fs.exists('home/user/.nimbusrc')) {
|
|
504
|
+
fs.writeFile('home/user/.nimbusrc',
|
|
505
|
+
'# Nimbus shell config\nalias ll="ls -la"\nalias la="ls -a"\nalias l="ls -1"\n',
|
|
506
|
+
);
|
|
212
507
|
}
|
|
213
508
|
}
|