@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.
Files changed (64) hide show
  1. package/dist/index.d.ts +5 -0
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +2 -0
  4. package/dist/runtime/bash-runner.d.ts +63 -0
  5. package/dist/runtime/bash-runner.d.ts.map +1 -0
  6. package/dist/runtime/bash-runner.generated.d.ts +14 -0
  7. package/dist/runtime/bash-runner.generated.d.ts.map +1 -0
  8. package/dist/runtime/bash-runner.generated.js +13 -0
  9. package/dist/runtime/bash-runner.js +290 -0
  10. package/dist/runtime/cpython-runner.d.ts +86 -0
  11. package/dist/runtime/cpython-runner.d.ts.map +1 -0
  12. package/dist/runtime/cpython-runner.js +425 -0
  13. package/dist/runtime/facet-host.d.ts +159 -0
  14. package/dist/runtime/facet-host.d.ts.map +1 -0
  15. package/dist/runtime/facet-host.js +22 -0
  16. package/dist/runtime/installed-runtimes.d.ts +99 -0
  17. package/dist/runtime/installed-runtimes.d.ts.map +1 -0
  18. package/dist/runtime/installed-runtimes.js +162 -0
  19. package/dist/runtime/local-facet-host.d.ts +38 -0
  20. package/dist/runtime/local-facet-host.d.ts.map +1 -0
  21. package/dist/runtime/local-facet-host.js +171 -0
  22. package/dist/runtime/python-pip.d.ts +38 -0
  23. package/dist/runtime/python-pip.d.ts.map +1 -0
  24. package/dist/runtime/python-pip.js +1063 -0
  25. package/dist/runtime/runtime-manifest.d.ts +86 -0
  26. package/dist/runtime/runtime-manifest.d.ts.map +1 -0
  27. package/dist/runtime/runtime-manifest.js +72 -0
  28. package/dist/runtime/runtime-package.d.ts +63 -0
  29. package/dist/runtime/runtime-package.d.ts.map +1 -0
  30. package/dist/runtime/runtime-package.js +66 -0
  31. package/dist/runtime/runtime-registry.d.ts +162 -0
  32. package/dist/runtime/runtime-registry.d.ts.map +1 -0
  33. package/dist/runtime/runtime-registry.js +363 -0
  34. package/dist/runtime/vfs-snapshot.d.ts.map +1 -1
  35. package/dist/runtime/vfs-snapshot.js +15 -1
  36. package/dist/runtime/vfs-supervisor.d.ts +22 -0
  37. package/dist/runtime/vfs-supervisor.d.ts.map +1 -0
  38. package/dist/runtime/vfs-supervisor.js +65 -0
  39. package/dist/runtime/virtual-socket-kernel.generated.d.ts +14 -0
  40. package/dist/runtime/virtual-socket-kernel.generated.d.ts.map +1 -0
  41. package/dist/runtime/virtual-socket-kernel.generated.js +13 -0
  42. package/dist/runtime/wasm-runner.d.ts +80 -0
  43. package/dist/runtime/wasm-runner.d.ts.map +1 -0
  44. package/dist/runtime/wasm-runner.js +686 -0
  45. package/dist/workspace/nimbus-workspace.d.ts +116 -20
  46. package/dist/workspace/nimbus-workspace.d.ts.map +1 -1
  47. package/dist/workspace/nimbus-workspace.js +238 -45
  48. package/package.json +4 -2
  49. package/src/index.ts +16 -0
  50. package/src/runtime/bash-runner.generated.ts +14 -0
  51. package/src/runtime/bash-runner.ts +347 -0
  52. package/src/runtime/cpython-runner.ts +504 -0
  53. package/src/runtime/facet-host.ts +170 -0
  54. package/src/runtime/installed-runtimes.ts +235 -0
  55. package/src/runtime/local-facet-host.ts +205 -0
  56. package/src/runtime/python-pip.ts +1211 -0
  57. package/src/runtime/runtime-manifest.ts +155 -0
  58. package/src/runtime/runtime-package.ts +114 -0
  59. package/src/runtime/runtime-registry.ts +511 -0
  60. package/src/runtime/vfs-snapshot.ts +15 -1
  61. package/src/runtime/vfs-supervisor.ts +67 -0
  62. package/src/runtime/virtual-socket-kernel.generated.ts +14 -0
  63. package/src/runtime/wasm-runner.ts +835 -0
  64. package/src/workspace/nimbus-workspace.ts +349 -54
@@ -3,23 +3,32 @@
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
- * SQLite and gets back `.fs` and `.exec`. That is what makes it embeddable in
7
- * a Durable Object that is already busy powering something else, and what
8
- * makes it runnable in a plain bun process over `bun:sqlite`.
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` performs,
11
- * lifted out of the session so there is one recipe rather than one per caller
12
- * — five unit tests were already hand-rolling it, one of them reaching a
13
- * private field to do so. The boot itself still belongs to `Sandbox.create`;
14
- * this adds only what a durable, credentialed filesystem needs on top and
15
- * nothing the sandbox already knows how to do.
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
  */
22
+ import { Kernel } from '../substrate/lifo/kernel/index.js';
23
+ import { Shell } from '../substrate/lifo/shell/Shell.js';
24
+ import type { ShellCommandIdentity } from '../substrate/lifo/shell/Shell.js';
25
+ import type { CommandRegistry } from '../substrate/lifo/commands/registry.js';
17
26
  import type { CommandResult, RunOptions, SandboxFs } from '../substrate/lifo/sandbox/types.js';
18
- import type { Kernel } from '../substrate/lifo/kernel/index.js';
19
- import type { Shell } from '../substrate/lifo/shell/Shell.js';
20
27
  import type { ITerminal } from '../substrate/lifo/terminal/ITerminal.js';
21
28
  import { SqliteVFS } from '../vfs/sqlite-vfs.js';
22
29
  import type { SqlDatabase, TransactionHost } from '../runtime/os-contracts.js';
30
+ import type { FacetHost } from '../runtime/facet-host.js';
31
+ import { type RuntimePackage } from '../runtime/runtime-package.js';
23
32
  export interface NimbusWorkspaceOptions {
24
33
  /** The host's SQLite. In a Durable Object: `ctx.storage.sql`. */
25
34
  readonly sql: SqlDatabase;
@@ -32,29 +41,84 @@ export interface NimbusWorkspaceOptions {
32
41
  */
33
42
  readonly transactions?: TransactionHost;
34
43
  /**
35
- * Process-id generation. The workspace revokes every append capability at
36
- * or below `generation * 1_000_000` before serving anything, so a value
37
- * that repeats across restarts hands a dead process live write authority.
38
- * Hosts that persist must supply a counter that never repeats.
44
+ * The filesystem already open over `sql`, for a host that has one.
45
+ *
46
+ * A Durable Object does: its installer, its git commands and its RPC
47
+ * surfaces read those rows without a shell in sight, and they hold a
48
+ * SqliteVFS from the first request that needed one. It must hand over THAT
49
+ * one — a second SqliteVFS over the same database is a second cache, and
50
+ * one of the two will serve a stale read. Such a host has also already
51
+ * revoked the previous generation's append writers, which is why that only
52
+ * happens below when the workspace is the one opening the filesystem.
53
+ */
54
+ readonly vfs?: SqliteVFS;
55
+ /**
56
+ * Process-id generation. Two things rest on it: the workspace revokes every
57
+ * append capability at or below `generation * 1_000_000` before serving
58
+ * anything, and the wasm runner allocates pids above it. A value that
59
+ * repeats across restarts hands a dead process live write authority, so
60
+ * hosts that persist must supply a counter that never repeats.
39
61
  */
40
62
  readonly generation?: number;
41
63
  /** Top-level directories backed by `sql`. Defaults to DEFAULT_MOUNT_POINTS. */
42
64
  readonly mounts?: readonly string[];
65
+ /** Overlaid on the Nimbus default environment. */
43
66
  readonly env?: Record<string, string>;
44
67
  readonly cwd?: string;
45
68
  /** Absent means headless: `.exec` captures output and nothing is drawn. */
46
69
  readonly terminal?: ITerminal;
70
+ /**
71
+ * Who the shell acts as, when the host keeps a process table that can answer
72
+ * for it. Absent, commands run as uid 1000 with a umask of 022 and no
73
+ * process behind them, which is the Shell's own default.
74
+ */
75
+ readonly identity?: ShellCommandIdentity;
76
+ /**
77
+ * Language runtimes to install before the shell is served, as npm packages
78
+ * the embedder imported (`@nimbus-sh/runtime-bash`,
79
+ * `@nimbus-sh/runtime-cpython`).
80
+ *
81
+ * A Durable Object gets these from R2 through `nimbus install`; an embedder
82
+ * off Cloudflare has no bucket and needs none, because npm already fetched
83
+ * and integrity-checked the same bytes. Both write the same tree at the same
84
+ * path, so what is installed here is indistinguishable from what is
85
+ * installed there — see runtime/runtime-package.ts.
86
+ *
87
+ * Independent of `facets`: this decides what the filesystem HOLDS, and
88
+ * `facets` decides whether anything can run it. A workspace given runtimes
89
+ * and no facet host installs them and still answers "command not found",
90
+ * because it still has nothing that could compile a module.
91
+ */
92
+ readonly runtimes?: readonly RuntimePackage[];
93
+ /**
94
+ * Where WebAssembly runs.
95
+ *
96
+ * Absent, the workspace is the JavaScript half of Nimbus: the durable
97
+ * filesystem, the shell and the coreutils, and `bash` or `./prog.wasm` is
98
+ * "command not found" — not disabled, ABSENT, because nothing has been
99
+ * supplied that could compile a module or run one. Supplied, the wasm
100
+ * runtimes already installed in this filesystem become invokable commands
101
+ * and `wasm-runner` joins them, which is what makes a `\0asm` file on the
102
+ * PATH executable (see shell/exec-dispatch.ts).
103
+ *
104
+ * A plain process passes `localFacetHost()`. A Durable Object passes nothing
105
+ * here and registers its own runners instead, because the ones it needs
106
+ * carry REPLs and a resident-process substrate this cannot reach.
107
+ */
108
+ readonly facets?: FacetHost;
47
109
  }
48
110
  /**
49
111
  * A durable filesystem and a shell over it.
50
112
  *
51
- * Created with {@link NimbusWorkspace.create} rather than `new` because the
52
- * boot it delegates to sources `/etc/profile`, which is genuinely async. A
53
- * Durable Object constructor cannot await, so a host constructs the workspace
54
- * in its first request rather than in its constructor.
113
+ * The composition itself is synchronous. {@link create} awaits only the
114
+ * optional work installing runtime packages, loading the wasm runner modules
115
+ * so a host that asks for neither is never suspended between mounting the
116
+ * filesystem and registering the commands. A Durable Object needs that: it
117
+ * must not take delivery of an event with a half-built shell. The remaining
118
+ * async step, running the user's login files, is {@link start}, which the host
119
+ * calls once its own commands are in place.
55
120
  */
56
121
  export declare class NimbusWorkspace {
57
- private readonly sandbox;
58
122
  private readonly sql;
59
123
  /**
60
124
  * Credentialed and mount-aware. Acts as the session user, never as the
@@ -66,9 +130,26 @@ export declare class NimbusWorkspace {
66
130
  readonly vfs: SqliteVFS;
67
131
  readonly kernel: Kernel;
68
132
  readonly shell: Shell;
133
+ /** What the shell resolves a command name against. A host adds its own. */
134
+ readonly registry: CommandRegistry;
135
+ /**
136
+ * The environment the shell was composed with. The shell's own copy drifts
137
+ * from this one the moment the user exports anything; this is what a host
138
+ * hands to a subordinate shell it starts itself.
139
+ */
140
+ readonly env: Record<string, string>;
141
+ private readonly commands;
69
142
  private constructor();
70
143
  static create(options: NimbusWorkspaceOptions): Promise<NimbusWorkspace>;
71
144
  exec(command: string, options?: RunOptions): Promise<CommandResult>;
145
+ /**
146
+ * Apply the login files, and begin reading the terminal when there is one.
147
+ *
148
+ * Separate from {@link create} because a host with commands of its own must
149
+ * register them first: `/etc/profile` and `~/.nimbusrc` are the user's
150
+ * files, and either may name a command the host has yet to supply.
151
+ */
152
+ start(): Promise<void>;
72
153
  /**
73
154
  * Files, directories and bytes this workspace occupies.
74
155
  *
@@ -91,4 +172,19 @@ export declare class NimbusWorkspace {
91
172
  */
92
173
  destroy(): void;
93
174
  }
175
+ /**
176
+ * The directories and account files the shell cannot start without.
177
+ *
178
+ * Idempotent by construction: every write is guarded by an existence check, so
179
+ * a workspace reopened over a populated database keeps whatever the user did
180
+ * to these files. `/etc/passwd` and `/etc/group` are load-bearing rather than
181
+ * decorative — `id`, `chown` and `su` resolve names through them.
182
+ *
183
+ * What a PRODUCT puts in a fresh filesystem — a banner, a welcome file, a
184
+ * starter app — is not here. This is the base an OS needs in order to boot,
185
+ * and it is exported because a host may need the filesystem before it needs a
186
+ * shell: the Nimbus session seeds its starter project for a browser that hits
187
+ * `/preview` without ever opening a terminal.
188
+ */
189
+ export declare function seedBaseFilesystem(vfs: SqliteVFS, mounts: readonly string[]): void;
94
190
  //# sourceMappingURL=nimbus-workspace.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"nimbus-workspace.d.ts","sourceRoot":"","sources":["../../src/workspace/nimbus-workspace.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAIH,OAAO,KAAK,EAAE,aAAa,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,oCAAoC,CAAC;AAC/F,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,mCAAmC,CAAC;AAChE,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,kCAAkC,CAAC;AAC9D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACzE,OAAO,EAAE,SAAS,EAAqB,MAAM,sBAAsB,CAAC;AAGpE,OAAO,KAAK,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAO/E,MAAM,WAAW,sBAAsB;IACrC,iEAAiE;IACjE,QAAQ,CAAC,GAAG,EAAE,WAAW,CAAC;IAC1B;;;;;;OAMG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,eAAe,CAAC;IACxC;;;;;OAKG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,+EAA+E;IAC/E,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACtC,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,2EAA2E;IAC3E,QAAQ,CAAC,QAAQ,CAAC,EAAE,SAAS,CAAC;CAC/B;AAED;;;;;;;GAOG;AACH,qBAAa,eAAe;IAaxB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAExB,OAAO,CAAC,QAAQ,CAAC,GAAG;IAdtB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,EAAE,SAAS,CAAC;IACvB,4EAA4E;IAC5E,QAAQ,CAAC,GAAG,EAAE,SAAS,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC;IAEtB,OAAO;WAiBM,MAAM,CAAC,OAAO,EAAE,sBAAsB,GAAG,OAAO,CAAC,eAAe,CAAC;IAyB9E,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,UAAU,GAAG,OAAO,CAAC,aAAa,CAAC;IAInE;;;;;;OAMG;IACH,KAAK,IAAI;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE;IAK3D;;;;;;;OAOG;IACH,OAAO,IAAI,IAAI;CAKhB"}
1
+ {"version":3,"file":"nimbus-workspace.d.ts","sourceRoot":"","sources":["../../src/workspace/nimbus-workspace.ts"],"names":[],"mappings":"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"}
@@ -3,36 +3,49 @@
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
- * SQLite and gets back `.fs` and `.exec`. That is what makes it embeddable in
7
- * a Durable Object that is already busy powering something else, and what
8
- * makes it runnable in a plain bun process over `bun:sqlite`.
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` performs,
11
- * lifted out of the session so there is one recipe rather than one per caller
12
- * — five unit tests were already hand-rolling it, one of them reaching a
13
- * private field to do so. The boot itself still belongs to `Sandbox.create`;
14
- * this adds only what a durable, credentialed filesystem needs on top and
15
- * nothing the sandbox already knows how to do.
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
- import { Sandbox } from '../substrate/lifo/sandbox/Sandbox.js';
22
+ import { Kernel } from '../substrate/lifo/kernel/index.js';
23
+ import { Shell } from '../substrate/lifo/shell/Shell.js';
24
+ import { createDefaultRegistry } from '../substrate/lifo/commands/registry.js';
25
+ import { SandboxCommandsImpl } from '../substrate/lifo/sandbox/SandboxCommands.js';
18
26
  import { SandboxFsImpl } from '../substrate/lifo/sandbox/SandboxFs.js';
27
+ import { HeadlessTerminal } from '../substrate/lifo/sandbox/HeadlessTerminal.js';
19
28
  import { SqliteVFS, SqliteVFSProvider } from '../vfs/sqlite-vfs.js';
20
- import { DEFAULT_MOUNT_POINTS, DEFAULT_PATH } from '../constants.js';
29
+ import { DEFAULT_HOME, DEFAULT_HOSTNAME, DEFAULT_MOUNT_POINTS, DEFAULT_PATH, DEFAULT_SHELL, DEFAULT_USER, NIMBUS_VERSION, } from '../constants.js';
21
30
  import { CRED_KERNEL, CRED_SESSION_USER } from '../runtime/os-contracts.js';
31
+ import { PID_GEN_STRIDE } from '../runtime/process-table.js';
32
+ import { SessionProcessSupervisor } from '../runtime/session-process-supervisor.js';
33
+ import { rehydrateInstalledRuntimesView, } from '../runtime/installed-runtimes.js';
34
+ import { seedRuntimePackage } from '../runtime/runtime-package.js';
22
35
  import { registerUnixCommands } from '../shell/unix-commands.js';
23
36
  import { installPathExecResolver } from '../shell/exec-dispatch.js';
24
- /** Pids are `generation * PID_GEN_STRIDE + seq`; mirrors process-table.ts. */
25
- const PID_GEN_STRIDE = 1_000_000;
26
37
  /**
27
38
  * A durable filesystem and a shell over it.
28
39
  *
29
- * Created with {@link NimbusWorkspace.create} rather than `new` because the
30
- * boot it delegates to sources `/etc/profile`, which is genuinely async. A
31
- * Durable Object constructor cannot await, so a host constructs the workspace
32
- * in its first request rather than in its constructor.
40
+ * The composition itself is synchronous. {@link create} awaits only the
41
+ * optional work installing runtime packages, loading the wasm runner modules
42
+ * so a host that asks for neither is never suspended between mounting the
43
+ * filesystem and registering the commands. A Durable Object needs that: it
44
+ * must not take delivery of an event with a half-built shell. The remaining
45
+ * async step, running the user's login files, is {@link start}, which the host
46
+ * calls once its own commands are in place.
33
47
  */
34
48
  export class NimbusWorkspace {
35
- sandbox;
36
49
  sql;
37
50
  /**
38
51
  * Credentialed and mount-aware. Acts as the session user, never as the
@@ -44,39 +57,84 @@ export class NimbusWorkspace {
44
57
  vfs;
45
58
  kernel;
46
59
  shell;
47
- constructor(sandbox, vfs, sql) {
48
- this.sandbox = sandbox;
60
+ /** What the shell resolves a command name against. A host adds its own. */
61
+ registry;
62
+ /**
63
+ * The environment the shell was composed with. The shell's own copy drifts
64
+ * from this one the moment the user exports anything; this is what a host
65
+ * hands to a subordinate shell it starts itself.
66
+ */
67
+ env;
68
+ commands;
69
+ constructor(vfs, kernel, shell, registry, env, sql) {
49
70
  this.sql = sql;
50
71
  this.vfs = vfs;
51
- this.kernel = sandbox.kernel;
52
- this.shell = sandbox.shell;
53
- // NOT `sandbox.fs`, which wraps the raw kernel VFS. The mount is
54
- // kernel-credentialed because the shell re-credentials per command; a host
55
- // calling `.fs` has no process behind it and must not inherit that.
56
- this.fs = new SandboxFsImpl(sandbox.kernel.vfs.as(CRED_SESSION_USER), () => sandbox.shell.getCwd());
72
+ this.kernel = kernel;
73
+ this.shell = shell;
74
+ this.registry = registry;
75
+ this.env = env;
76
+ this.commands = new SandboxCommandsImpl(shell, registry);
77
+ // NOT the kernel VFS as it stands, which is kernel-credentialed because
78
+ // the shell re-credentials per command; a host calling `.fs` has no
79
+ // process behind it and must not inherit that.
80
+ this.fs = new SandboxFsImpl(kernel.vfs.as(CRED_SESSION_USER), () => shell.getCwd());
57
81
  }
58
82
  static async create(options) {
59
- const vfs = new SqliteVFS(options.sql, options.transactions);
60
- vfs.revokeAppendWritersThrough((options.generation ?? 1) * PID_GEN_STRIDE);
83
+ const vfs = options.vfs ?? openFilesystem(options);
61
84
  const mounts = options.mounts ?? DEFAULT_MOUNT_POINTS;
62
85
  seedBaseFilesystem(vfs, mounts);
63
- const sandbox = await Sandbox.create({
64
- env: options.env,
65
- cwd: options.cwd,
66
- terminal: options.terminal,
67
- providerMounts: mounts.map((mount) => ({
68
- virtualPath: '/' + mount,
69
- provider: new SqliteVFSProvider(vfs, mount),
70
- })),
71
- });
86
+ const kernel = new Kernel();
87
+ // Seeds the in-memory tree. Mounting AFTER it is what keeps a durable
88
+ // /etc from being overwritten by the defaults on every boot.
89
+ kernel.initFilesystem();
90
+ for (const mount of mounts) {
91
+ kernel.vfs.mount(`/${mount}`, new SqliteVFSProvider(vfs, mount));
92
+ }
93
+ const registry = createDefaultRegistry();
72
94
  // The durable coreutils replace ~25 lifo builtins. They are the ones that
73
95
  // carry credentials and read this filesystem's uid/gid, so they must win.
74
- registerUnixCommands(sandbox.commands.registry, vfs);
75
- installPathExecResolver(sandbox.commands.registry, vfs.as(CRED_SESSION_USER), () => sandbox.shell.getCwd());
76
- return new NimbusWorkspace(sandbox, vfs, options.sql);
96
+ registerUnixCommands(registry, vfs);
97
+ const env = { ...defaultEnv(), ...options.env };
98
+ const shell = new Shell(options.terminal ?? new HeadlessTerminal(), kernel.vfs, registry, env, kernel.processRegistry, options.identity);
99
+ if (options.cwd)
100
+ shell.setCwd(options.cwd);
101
+ // Kernel-credentialed on purpose: this only INSPECTS a file to decide how
102
+ // to run it, and re-checks the caller's own execute permission at
103
+ // invocation time — the `authorize` wrapper in exec-dispatch.ts.
104
+ installPathExecResolver(registry, vfs.as(CRED_KERNEL), () => shell.getCwd());
105
+ const home = env.HOME ?? DEFAULT_HOME;
106
+ // Before the runners are wired, because registration reads what the
107
+ // filesystem holds — the same order `nimbus install` observes, and the
108
+ // same order a Durable Object observes when it rehydrates after eviction.
109
+ for (const runtimePackage of options.runtimes ?? []) {
110
+ await seedRuntimePackage(vfs.as(CRED_KERNEL), home, runtimePackage);
111
+ }
112
+ if (options.facets) {
113
+ await registerWasmRuntimes({
114
+ facets: options.facets,
115
+ vfs,
116
+ registry,
117
+ generation: options.generation ?? 1,
118
+ home,
119
+ });
120
+ }
121
+ return new NimbusWorkspace(vfs, kernel, shell, registry, env, options.sql);
77
122
  }
78
123
  exec(command, options) {
79
- return this.sandbox.commands.run(command, options);
124
+ return this.commands.run(command, options);
125
+ }
126
+ /**
127
+ * Apply the login files, and begin reading the terminal when there is one.
128
+ *
129
+ * Separate from {@link create} because a host with commands of its own must
130
+ * register them first: `/etc/profile` and `~/.nimbusrc` are the user's
131
+ * files, and either may name a command the host has yet to supply.
132
+ */
133
+ async start() {
134
+ // Sources /etc/profile and the first user rc file it finds, then prompts.
135
+ this.shell.start();
136
+ // Nimbus's own rc file, which the shell's list predates.
137
+ await this.shell.sourceFile(`${this.shell.getEnv().HOME ?? DEFAULT_HOME}/.nimbusrc`);
80
138
  }
81
139
  /**
82
140
  * Files, directories and bytes this workspace occupies.
@@ -103,6 +161,101 @@ export class NimbusWorkspace {
103
161
  }
104
162
  }
105
163
  }
164
+ /**
165
+ * Open the durable filesystem for a host that has not opened one itself.
166
+ *
167
+ * The revocation is here rather than in `create` because it is the act of
168
+ * OPENING that carries it: pids at or below this generation's floor belong to
169
+ * an instance that is gone, and their append capabilities must stop being
170
+ * honoured before the first read. A host that opened the filesystem itself has
171
+ * already done this, at the same seam, for the same reason.
172
+ */
173
+ function openFilesystem(options) {
174
+ const vfs = new SqliteVFS(options.sql, options.transactions);
175
+ vfs.revokeAppendWritersThrough((options.generation ?? 1) * PID_GEN_STRIDE);
176
+ return vfs;
177
+ }
178
+ /**
179
+ * The environment a Nimbus shell starts in.
180
+ *
181
+ * `PATH` and `EDITOR` restate what the seeded `/etc/profile` exports, so a
182
+ * workspace whose host never runs the login files is still on the real PATH.
183
+ * `PORT` and `HOST` are here because every scaffolded server reads them and
184
+ * gets `undefined` otherwise — Express's default app, every create-vite
185
+ * template, `${PORT:-3000}` in a package.json script.
186
+ */
187
+ function defaultEnv() {
188
+ return {
189
+ HOME: DEFAULT_HOME,
190
+ USER: DEFAULT_USER,
191
+ SHELL: DEFAULT_SHELL,
192
+ HOSTNAME: DEFAULT_HOSTNAME,
193
+ TERM: 'xterm-256color',
194
+ PWD: DEFAULT_HOME,
195
+ PATH: DEFAULT_PATH,
196
+ PS1: `\x1b[1;32muser@${DEFAULT_HOSTNAME}\x1b[0m:\x1b[1;34m\\w\x1b[0m$ `,
197
+ NODE_ENV: 'development',
198
+ LANG: 'en_US.UTF-8',
199
+ EDITOR: 'nano',
200
+ NIMBUS_VERSION: NIMBUS_VERSION,
201
+ TMPDIR: '/tmp',
202
+ XDG_CONFIG_HOME: `${DEFAULT_HOME}/.config`,
203
+ XDG_DATA_HOME: `${DEFAULT_HOME}/.local/share`,
204
+ npm_config_prefix: '/usr/local',
205
+ PORT: '3000',
206
+ HOST: '0.0.0.0',
207
+ };
208
+ }
209
+ /**
210
+ * Turn a facet host into commands: the runtimes this filesystem already holds,
211
+ * plus `wasm-runner` for everything else with a `\0asm` header.
212
+ *
213
+ * The runner factories are held here rather than in the process-global table
214
+ * `nimbus install` writes to, because each one closes over THIS workspace's
215
+ * filesystem and THIS workspace's facet host — a second workspace in the same
216
+ * process would otherwise silently retarget the first one's bash.
217
+ *
218
+ * Imported on demand: the runners carry the WASI shim and the bash scheduler as
219
+ * source strings, and a workspace with no facet host must not pay to parse
220
+ * them.
221
+ */
222
+ async function registerWasmRuntimes(deps) {
223
+ const [{ makeBashRunnerFactory }, { makeCPythonRunnerFactory }, { wasmRunnerSpec }, { buildRuntimeHandler },] = await Promise.all([
224
+ import('../runtime/bash-runner.js'),
225
+ import('../runtime/cpython-runner.js'),
226
+ import('../runtime/wasm-runner.js'),
227
+ import('../runtime/runtime-registry.js'),
228
+ ]);
229
+ // wasm-runner allocates pids for what it runs, so it needs a process table
230
+ // whose pid space is this generation's.
231
+ const processes = new SessionProcessSupervisor();
232
+ processes.setPidBase(deps.generation * PID_GEN_STRIDE);
233
+ // Loaded on the first TypeScript or ESM script and not before. The module
234
+ // statically imports `esbuild-wasm/esbuild.wasm`, which only wrangler
235
+ // resolves — node instantiates it as a wasm module and fails on its Go
236
+ // imports — so a host outside Cloudflare must be able to run a shell, bash
237
+ // and python without that module ever entering its graph.
238
+ let esbuild = null;
239
+ deps.registry.register('wasm-runner', buildRuntimeHandler(wasmRunnerSpec({ vfs: deps.vfs, facets: deps.facets, processes }), {
240
+ vfs: deps.vfs,
241
+ getEsbuild: () => {
242
+ if (!esbuild) {
243
+ esbuild = import('../runtime/esbuild-service.js')
244
+ .then((module) => new module.EsbuildService(deps.vfs));
245
+ }
246
+ return esbuild;
247
+ },
248
+ registry: deps.registry,
249
+ }));
250
+ const runners = {
251
+ 'bash-runner': makeBashRunnerFactory({ facets: deps.facets, vfs: deps.vfs }),
252
+ // No `startResident`: a workspace owns no actor that could outlive the
253
+ // call, so a program that keeps serving is refused by name rather than
254
+ // run as a one-shot that dies with it.
255
+ 'cpython-runner': makeCPythonRunnerFactory({ facets: deps.facets, vfs: deps.vfs }),
256
+ };
257
+ rehydrateInstalledRuntimesView(deps.vfs.as(CRED_KERNEL), deps.registry, deps.home, (key) => runners[key]);
258
+ }
106
259
  /**
107
260
  * Every table the filesystem creates.
108
261
  *
@@ -130,8 +283,14 @@ const WORKSPACE_TABLES = [
130
283
  * a workspace reopened over a populated database keeps whatever the user did
131
284
  * to these files. `/etc/passwd` and `/etc/group` are load-bearing rather than
132
285
  * decorative — `id`, `chown` and `su` resolve names through them.
286
+ *
287
+ * What a PRODUCT puts in a fresh filesystem — a banner, a welcome file, a
288
+ * starter app — is not here. This is the base an OS needs in order to boot,
289
+ * and it is exported because a host may need the filesystem before it needs a
290
+ * shell: the Nimbus session seeds its starter project for a browser that hits
291
+ * `/preview` without ever opening a terminal.
133
292
  */
134
- function seedBaseFilesystem(vfs, mounts) {
293
+ export function seedBaseFilesystem(vfs, mounts) {
135
294
  const fs = vfs.as(CRED_SESSION_USER);
136
295
  const rootFs = vfs.as(CRED_KERNEL);
137
296
  // Created AS the session user, so the user owns their own tree. Seeding
@@ -140,12 +299,37 @@ function seedBaseFilesystem(vfs, mounts) {
140
299
  if (mount !== 'etc' && !fs.exists(mount))
141
300
  fs.mkdir(mount, { recursive: true });
142
301
  }
143
- for (const dir of ['home/user', 'usr/bin', 'usr/local/bin', 'var/log', 'tmp']) {
302
+ for (const dir of [
303
+ 'home/user', 'home/user/.config', 'home/user/projects',
304
+ 'tmp', 'var/log',
305
+ 'usr/bin', 'usr/lib', 'usr/lib/node_modules',
306
+ 'usr/share', 'usr/share/pkg', 'usr/share/pkg/node_modules',
307
+ 'usr/local', 'usr/local/lib', 'usr/local/lib/node_modules', 'usr/local/bin',
308
+ ]) {
144
309
  if (!fs.exists(dir))
145
310
  fs.mkdir(dir, { recursive: true });
146
311
  }
147
- if (!rootFs.exists('etc'))
312
+ // /etc belongs to root, and is re-asserted rather than only created: a
313
+ // user-writable /etc is an authority bug, not an untidy directory.
314
+ if (!rootFs.exists('etc')) {
148
315
  rootFs.mkdir('etc', { mode: 0o755 });
316
+ }
317
+ else {
318
+ const etc = rootFs.stat('etc');
319
+ if (etc.uid !== 0 || etc.gid !== 0)
320
+ rootFs.chown('etc', 0, 0);
321
+ if ((etc.mode & 0o7777) !== 0o755)
322
+ rootFs.chmod('etc', 0o755);
323
+ }
324
+ if (!rootFs.exists('etc/hostname')) {
325
+ rootFs.writeFile('etc/hostname', `${DEFAULT_HOSTNAME}\n`);
326
+ rootFs.chown('etc/hostname', CRED_SESSION_USER.uid, CRED_SESSION_USER.gid);
327
+ }
328
+ if (!rootFs.exists('etc/os-release')) {
329
+ rootFs.writeFile('etc/os-release', `NAME="Nimbus"\nVERSION="${NIMBUS_VERSION}"\nID=nimbus\n`
330
+ + 'PRETTY_NAME="Nimbus — Cloud Dev Environment"\n');
331
+ rootFs.chown('etc/os-release', CRED_SESSION_USER.uid, CRED_SESSION_USER.gid);
332
+ }
149
333
  // Root-owned 0644, and re-asserted rather than only created: these decide
150
334
  // what `id`, `chown` and `su` believe, so a user-writable /etc/passwd would
151
335
  // be an authority bug rather than an untidy file.
@@ -160,8 +344,17 @@ function seedBaseFilesystem(vfs, mounts) {
160
344
  };
161
345
  accountFile('etc/passwd', 'root:x:0:0:root:/root:/bin/sh\nuser:x:1000:1000:Nimbus User:/home/user:/bin/sh\n');
162
346
  accountFile('etc/group', 'root:x:0:\nuser:x:1000:user\n');
347
+ const defaultProfile = `export PATH=${DEFAULT_PATH}\nexport EDITOR=nano\n`;
163
348
  if (!rootFs.exists('etc/profile')) {
164
- rootFs.writeFile('etc/profile', `export PATH=${DEFAULT_PATH}\nexport EDITOR=nano\n`);
349
+ rootFs.writeFile('etc/profile', defaultProfile);
165
350
  rootFs.chown('etc/profile', CRED_SESSION_USER.uid, CRED_SESSION_USER.gid);
166
351
  }
352
+ else if (rootFs.readFileString('etc/profile') === 'export PATH=/usr/bin:/bin\nexport EDITOR=nano\n') {
353
+ // The lifo default, from before Nimbus had a PATH of its own. Nobody ever
354
+ // chose it, so replacing it is not overwriting a user's file.
355
+ rootFs.writeFile('etc/profile', defaultProfile);
356
+ }
357
+ if (!fs.exists('home/user/.nimbusrc')) {
358
+ fs.writeFile('home/user/.nimbusrc', '# Nimbus shell config\nalias ll="ls -la"\nalias la="ls -a"\nalias l="ls -1"\n');
359
+ }
167
360
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@nimbus-sh/core",
3
- "version": "0.1.0",
4
- "description": "Backend-agnostic half of Nimbus the durable filesystem, the shell, and the OS/process contracts, over a host-supplied SQL port.",
3
+ "version": "0.3.0",
4
+ "description": "Backend-agnostic half of Nimbus \u2014 the durable filesystem, the shell, and the OS/process contracts, over a host-supplied SQL port.",
5
5
  "keywords": [
6
6
  "filesystem",
7
7
  "shell",
@@ -53,10 +53,12 @@
53
53
  "typecheck": "tsc --noEmit"
54
54
  },
55
55
  "dependencies": {
56
+ "@renovatebot/pep440": "^5.0.0",
56
57
  "acorn": "^8.15.0",
57
58
  "acorn-walk": "^8.3.5",
58
59
  "esbuild-wasm": "0.24.2",
59
60
  "magic-string": "^0.30.21",
61
+ "pip-requirements-js": "^1.0.3",
60
62
  "zod": "^4.4.3"
61
63
  },
62
64
  "devDependencies": {
package/src/index.ts CHANGED
@@ -20,3 +20,19 @@ export type {
20
20
  SqlValue,
21
21
  TransactionHost,
22
22
  } from './runtime/os-contracts.js';
23
+ export { seedRuntimePackage } from './runtime/runtime-package.js';
24
+ export type { RuntimePackage, SeededRuntime } from './runtime/runtime-package.js';
25
+ export type {
26
+ ManifestEntrypoint,
27
+ ManifestFile,
28
+ RuntimeManifest,
29
+ } from './runtime/runtime-manifest.js';
30
+ export { localFacetHost } from './runtime/local-facet-host.js';
31
+ export type {
32
+ Facet,
33
+ FacetBindings,
34
+ FacetFn,
35
+ FacetHost,
36
+ FacetSpec,
37
+ FacetSubmitOptions,
38
+ } from './runtime/facet-host.js';