@nimbus-sh/core 0.5.0 → 0.7.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 (145) hide show
  1. package/README.md +47 -26
  2. package/dist/_shared/byte-stream.d.ts +5 -4
  3. package/dist/_shared/byte-stream.d.ts.map +1 -1
  4. package/dist/_shared/cache-stats.d.ts +23 -1
  5. package/dist/_shared/cache-stats.d.ts.map +1 -1
  6. package/dist/_shared/cache-stats.js +13 -1
  7. package/dist/_shared/exports-resolver.d.ts +1 -1
  8. package/dist/_shared/exports-resolver.js +1 -1
  9. package/dist/_shared/real-node-imports.d.ts +7 -0
  10. package/dist/_shared/real-node-imports.d.ts.map +1 -1
  11. package/dist/_shared/real-node-imports.js +8 -0
  12. package/dist/_shared/retry.js +1 -1
  13. package/dist/_shared/tarball.d.ts +23 -5
  14. package/dist/_shared/tarball.d.ts.map +1 -1
  15. package/dist/_shared/tarball.js +38 -5
  16. package/dist/_shared/vfs-write-ledger.d.ts.map +1 -1
  17. package/dist/_shared/vfs-write-ledger.js +22 -16
  18. package/dist/constants.d.ts +5 -11
  19. package/dist/constants.d.ts.map +1 -1
  20. package/dist/constants.js +16 -56
  21. package/dist/runtime/assets-loader.js +1 -1
  22. package/dist/runtime/comment-strip.d.ts +0 -35
  23. package/dist/runtime/comment-strip.d.ts.map +1 -1
  24. package/dist/runtime/comment-strip.js +48 -20
  25. package/dist/runtime/esbuild-service.d.ts +38 -42
  26. package/dist/runtime/esbuild-service.d.ts.map +1 -1
  27. package/dist/runtime/esbuild-service.js +494 -197
  28. package/dist/runtime/javascript-ast.d.ts.map +1 -1
  29. package/dist/runtime/javascript-ast.js +45 -7
  30. package/dist/runtime/opentui-wasm-backend.d.ts.map +1 -1
  31. package/dist/runtime/opentui-wasm-backend.js +18 -16
  32. package/dist/runtime/port-registry.d.ts +4 -2
  33. package/dist/runtime/port-registry.d.ts.map +1 -1
  34. package/dist/runtime/port-registry.js +60 -8
  35. package/dist/runtime/virtual-socket-kernel.generated.d.ts +1 -1
  36. package/dist/runtime/virtual-socket-kernel.generated.js +1 -1
  37. package/dist/shell/stdin-adapter.d.ts +11 -0
  38. package/dist/shell/stdin-adapter.d.ts.map +1 -0
  39. package/dist/shell/stdin-adapter.js +59 -0
  40. package/dist/shell/unix-commands.d.ts +2 -1
  41. package/dist/shell/unix-commands.d.ts.map +1 -1
  42. package/dist/shell/unix-commands.js +1078 -50
  43. package/dist/substrate/lifo/commands/io/dd.js +1 -1
  44. package/dist/substrate/lifo/commands/net/curl.d.ts.map +1 -1
  45. package/dist/substrate/lifo/commands/net/curl.js +270 -54
  46. package/dist/substrate/lifo/commands/system/npm.d.ts +18 -1
  47. package/dist/substrate/lifo/commands/system/npm.d.ts.map +1 -1
  48. package/dist/substrate/lifo/commands/system/npm.js +26 -43
  49. package/dist/substrate/lifo/commands/text/head.d.ts.map +1 -1
  50. package/dist/substrate/lifo/commands/text/head.js +2 -1
  51. package/dist/substrate/lifo/commands/text/sed.d.ts.map +1 -1
  52. package/dist/substrate/lifo/commands/text/sed.js +499 -117
  53. package/dist/substrate/lifo/commands/types.d.ts +8 -4
  54. package/dist/substrate/lifo/commands/types.d.ts.map +1 -1
  55. package/dist/substrate/lifo/node-compat/zlib.d.ts +37 -18
  56. package/dist/substrate/lifo/node-compat/zlib.d.ts.map +1 -1
  57. package/dist/substrate/lifo/node-compat/zlib.js +122 -23
  58. package/dist/substrate/lifo/shell/interpreter.d.ts +9 -1
  59. package/dist/substrate/lifo/shell/interpreter.d.ts.map +1 -1
  60. package/dist/substrate/lifo/shell/interpreter.js +79 -29
  61. package/dist/substrate/lifo/shell/pipe.d.ts +16 -0
  62. package/dist/substrate/lifo/shell/pipe.d.ts.map +1 -1
  63. package/dist/substrate/lifo/shell/pipe.js +62 -18
  64. package/dist/substrate/lifo/shell/terminal-stdin.d.ts +20 -3
  65. package/dist/substrate/lifo/shell/terminal-stdin.d.ts.map +1 -1
  66. package/dist/substrate/lifo/shell/terminal-stdin.js +60 -24
  67. package/dist/vfs/seed-project.d.ts.map +1 -1
  68. package/dist/vfs/seed-project.js +1 -1
  69. package/dist/vfs/sqlite-vfs.d.ts +21 -30
  70. package/dist/vfs/sqlite-vfs.d.ts.map +1 -1
  71. package/dist/vfs/sqlite-vfs.js +113 -29
  72. package/dist/workspace/nimbus-workspace.d.ts +15 -0
  73. package/dist/workspace/nimbus-workspace.d.ts.map +1 -1
  74. package/dist/workspace/nimbus-workspace.js +20 -3
  75. package/dist/workspace/supervisor-op.d.ts +23 -0
  76. package/dist/workspace/supervisor-op.d.ts.map +1 -0
  77. package/dist/workspace/supervisor-op.js +92 -0
  78. package/package.json +3 -2
  79. package/src/_shared/byte-stream.ts +5 -4
  80. package/src/_shared/cache-stats.ts +27 -1
  81. package/src/_shared/exports-resolver.ts +1 -1
  82. package/src/_shared/real-node-imports.ts +8 -0
  83. package/src/_shared/retry.ts +1 -1
  84. package/src/_shared/tarball.ts +56 -5
  85. package/src/_shared/vfs-write-ledger.ts +22 -16
  86. package/src/constants.ts +30 -60
  87. package/src/runtime/assets-loader.ts +1 -1
  88. package/src/runtime/comment-strip.ts +42 -23
  89. package/src/runtime/esbuild-service.ts +566 -217
  90. package/src/runtime/javascript-ast.ts +40 -9
  91. package/src/runtime/opentui-wasm-backend.ts +19 -17
  92. package/src/runtime/port-registry.ts +66 -8
  93. package/src/runtime/virtual-socket-kernel.generated.ts +1 -1
  94. package/src/shell/stdin-adapter.ts +58 -0
  95. package/src/shell/unix-commands.ts +1054 -50
  96. package/src/substrate/lifo/commands/io/dd.ts +2 -2
  97. package/src/substrate/lifo/commands/net/curl.ts +310 -58
  98. package/src/substrate/lifo/commands/system/npm.ts +58 -38
  99. package/src/substrate/lifo/commands/text/head.ts +2 -1
  100. package/src/substrate/lifo/commands/text/sed.ts +519 -109
  101. package/src/substrate/lifo/commands/types.ts +8 -5
  102. package/src/substrate/lifo/node-compat/zlib.ts +155 -26
  103. package/src/substrate/lifo/shell/interpreter.ts +74 -27
  104. package/src/substrate/lifo/shell/pipe.ts +65 -24
  105. package/src/substrate/lifo/shell/terminal-stdin.ts +66 -30
  106. package/src/vfs/seed-project.ts +3 -3
  107. package/src/vfs/sqlite-vfs.ts +128 -64
  108. package/src/workspace/nimbus-workspace.ts +26 -2
  109. package/src/workspace/supervisor-op.ts +117 -0
  110. package/dist/_shared/install-phase.d.ts +0 -47
  111. package/dist/_shared/install-phase.d.ts.map +0 -1
  112. package/dist/_shared/install-phase.js +0 -1
  113. package/dist/_shared/rpc-dispose.d.ts +0 -4
  114. package/dist/_shared/rpc-dispose.d.ts.map +0 -1
  115. package/dist/_shared/rpc-dispose.js +0 -30
  116. package/dist/_shared/w7-frame.d.ts +0 -79
  117. package/dist/_shared/w7-frame.d.ts.map +0 -1
  118. package/dist/_shared/w7-frame.js +0 -840
  119. package/dist/_shared/weighted-credit-pool.d.ts +0 -61
  120. package/dist/_shared/weighted-credit-pool.d.ts.map +0 -1
  121. package/dist/_shared/weighted-credit-pool.js +0 -196
  122. package/dist/observability/diag-counters.d.ts +0 -236
  123. package/dist/observability/diag-counters.d.ts.map +0 -1
  124. package/dist/observability/diag-counters.js +0 -195
  125. package/dist/observability/heap-estimate.d.ts +0 -225
  126. package/dist/observability/heap-estimate.d.ts.map +0 -1
  127. package/dist/observability/heap-estimate.js +0 -185
  128. package/dist/observability/heavy-alloc-coord.d.ts +0 -93
  129. package/dist/observability/heavy-alloc-coord.d.ts.map +0 -1
  130. package/dist/observability/heavy-alloc-coord.js +0 -181
  131. package/dist/observability/oom-classify.d.ts +0 -138
  132. package/dist/observability/oom-classify.d.ts.map +0 -1
  133. package/dist/observability/oom-classify.js +0 -268
  134. package/dist/observability/oom-discriminator.d.ts +0 -176
  135. package/dist/observability/oom-discriminator.d.ts.map +0 -1
  136. package/dist/observability/oom-discriminator.js +0 -272
  137. package/src/_shared/install-phase.ts +0 -56
  138. package/src/_shared/rpc-dispose.ts +0 -31
  139. package/src/_shared/w7-frame.ts +0 -1013
  140. package/src/_shared/weighted-credit-pool.ts +0 -244
  141. package/src/observability/diag-counters.ts +0 -343
  142. package/src/observability/heap-estimate.ts +0 -331
  143. package/src/observability/heavy-alloc-coord.ts +0 -241
  144. package/src/observability/oom-classify.ts +0 -260
  145. package/src/observability/oom-discriminator.ts +0 -405
@@ -1,14 +1,18 @@
1
+ import { encode } from '../utils/encoding.js';
1
2
  import type { TerminalInputStream } from '../commands/types.js';
2
3
 
3
4
  /**
4
- * A bridge between terminal keyboard input and command stdin.
5
- * The Shell feeds lines in via feed(), commands consume via read()/readAll().
5
+ * A bridge between terminal keyboard input and command stdin. The Shell
6
+ * feeds lines in via feed(); commands consume bytes or text. Bytes are the
7
+ * storage format, so a byte read always makes progress even when maxLength
8
+ * splits a multi-byte code point — `dd bs=1` over `é` yields c3, then a9.
6
9
  */
7
10
  export class TerminalStdin implements TerminalInputStream {
8
- private buffer: string[] = [];
11
+ private buffer: Uint8Array[] = [];
9
12
  private closed = false;
10
- private resolvers: Array<(value: string | null) => void> = [];
13
+ private resolvers: Array<(value: Uint8Array | null) => void> = [];
11
14
  private _rawMode = false;
15
+ private decoder = new TextDecoder('utf-8');
12
16
 
13
17
  /** True when a command has called read() and is waiting for input. */
14
18
  get isWaiting(): boolean {
@@ -26,16 +30,13 @@ export class TerminalStdin implements TerminalInputStream {
26
30
 
27
31
  /** Shell calls this on Enter (with line + '\n'). */
28
32
  feed(text: string): void {
29
- if (this.closed) return;
30
-
31
- this.deliver(text, 'back');
33
+ if (this.closed || text === '') return;
34
+ this.deliver(encode(text), 'back');
32
35
  }
33
36
 
34
37
  /** Shell calls this on Ctrl+D to signal EOF. */
35
38
  close(): void {
36
- if (this.closed) return;
37
39
  this.closed = true;
38
-
39
40
  while (this.resolvers.length > 0) {
40
41
  const resolve = this.resolvers.shift()!;
41
42
  resolve(null);
@@ -44,15 +45,15 @@ export class TerminalStdin implements TerminalInputStream {
44
45
 
45
46
  /** Commands consume input. Returns null on EOF. */
46
47
  async read(): Promise<string | null> {
47
- if (this.buffer.length > 0) {
48
- return this.buffer.shift()!;
49
- }
50
- if (this.closed) {
51
- return null;
48
+ while (true) {
49
+ const bytes = await this.pull();
50
+ if (bytes === null) {
51
+ const tail = this.decoder.decode();
52
+ return tail.length > 0 ? tail : null;
53
+ }
54
+ const text = this.decoder.decode(bytes, { stream: true });
55
+ if (text.length > 0) return text;
52
56
  }
53
- return new Promise<string | null>((resolve) => {
54
- this.resolvers.push(resolve);
55
- });
56
57
  }
57
58
 
58
59
  async readLine(): Promise<string | null> {
@@ -64,7 +65,7 @@ export class TerminalStdin implements TerminalInputStream {
64
65
  const newline = chunk.indexOf('\n');
65
66
  if (newline >= 0) {
66
67
  const rest = chunk.slice(newline + 1);
67
- if (rest.length > 0) this.deliver(rest, 'front');
68
+ if (rest.length > 0) this.deliverBackText(rest);
68
69
  return line + chunk.slice(0, newline);
69
70
  }
70
71
 
@@ -72,15 +73,21 @@ export class TerminalStdin implements TerminalInputStream {
72
73
  }
73
74
  }
74
75
 
75
- async readBytes(maxLength: number): Promise<string | null> {
76
- if (maxLength <= 0) return '';
77
-
78
- const chunk = await this.read();
76
+ /**
77
+ * Bounded byte read: returns whatever the user has already typed, capped
78
+ * at maxLength. maxLength bounds the result, it is never a fill target —
79
+ * a command reading bytes must not stall until maxLength arrive. A larger
80
+ * queued chunk keeps only its first maxLength bytes; the remainder stays
81
+ * queued, so `dd bs=1` over `é` still yields c3, then a9.
82
+ */
83
+ async readBytes(maxLength: number): Promise<Uint8Array | null> {
84
+ if (maxLength <= 0) return new Uint8Array(0);
85
+
86
+ const chunk = await this.pull();
79
87
  if (chunk === null) return null;
80
88
  if (chunk.length <= maxLength) return chunk;
81
-
82
- this.deliver(chunk.slice(maxLength), 'front');
83
- return chunk.slice(0, maxLength);
89
+ this.buffer.unshift(chunk.subarray(maxLength));
90
+ return chunk.subarray(0, maxLength);
84
91
  }
85
92
 
86
93
  /** Read all remaining input until EOF, joined together. */
@@ -94,16 +101,45 @@ export class TerminalStdin implements TerminalInputStream {
94
101
  return parts.join('');
95
102
  }
96
103
 
97
- private deliver(text: string, position: 'front' | 'back'): void {
98
- if (text.length === 0) return;
104
+ /**
105
+ * Text snapshot of everything queued but not yet consumed. The shell's
106
+ * wrap layer uses this for commands that want drained terminal input.
107
+ */
108
+ drainBuffered(): string {
109
+ let text = '';
110
+ while (this.buffer.length > 0) {
111
+ text += this.decoder.decode(this.buffer.shift()!, { stream: true });
112
+ }
113
+ text += this.decoder.decode();
114
+ return text;
115
+ }
116
+
117
+ private pull(): Promise<Uint8Array | null> {
118
+ if (this.buffer.length > 0) {
119
+ return Promise.resolve(this.buffer.shift() ?? null);
120
+ }
121
+ if (this.closed) {
122
+ return Promise.resolve(null);
123
+ }
124
+ return new Promise<Uint8Array | null>((resolve) => {
125
+ this.resolvers.push(resolve);
126
+ });
127
+ }
128
+
129
+ private deliver(bytes: Uint8Array, position: 'front' | 'back'): void {
130
+ if (bytes.length === 0) return;
99
131
 
100
132
  if (this.resolvers.length > 0) {
101
133
  const resolve = this.resolvers.shift()!;
102
- resolve(text);
134
+ resolve(bytes);
103
135
  return;
104
136
  }
105
137
 
106
- if (position === 'front') this.buffer.unshift(text);
107
- else this.buffer.push(text);
138
+ if (position === 'front') this.buffer.unshift(bytes);
139
+ else this.buffer.push(bytes);
140
+ }
141
+
142
+ private deliverBackText(text: string): void {
143
+ this.deliver(encode(text), 'front');
108
144
  }
109
145
  }
@@ -18,12 +18,12 @@
18
18
  * - Basename injection (commit 2) handles the /preview/ basepath wiring.
19
19
  */
20
20
 
21
+ import type { SqliteVFS } from './sqlite-vfs.js';
21
22
  import type {
22
- SqliteVFS,
23
23
  BatchInodeEntry,
24
24
  BatchChunkEntry,
25
- } from './sqlite-vfs.js';
26
- import { CHUNK_SIZE } from '../constants.js';
25
+ } from '@nimbus-sh/platform/w7-frame.js';
26
+ import { CHUNK_SIZE } from '@nimbus-sh/platform/limits.js';
27
27
  import { enc } from '../_shared/bytes.js';
28
28
  import { errorText } from '../_shared/error-text.js';
29
29
  import { CRED_KERNEL } from '../runtime/os-contracts.js';
@@ -43,27 +43,33 @@
43
43
  import { VfsEventEmitter, type VfsEventType } from './events.js';
44
44
  import { normalizeVfsPath } from './path.js';
45
45
  import {
46
- CHUNK_SIZE,
47
46
  LRU_MAX_ENTRIES,
48
47
  BATCH_SIZE,
48
+ FS_LIST_PAGE_LIMIT,
49
+ } from '../constants.js';
50
+ import {
51
+ CHUNK_SIZE,
49
52
  MAX_TX_BLOB_BYTES,
50
53
  MAX_TX_LOGICAL_ROWS,
51
54
  MAX_TX_SQL_EXECS,
52
55
  MAX_GLOBAL_WRITE_STREAM_CREDIT_BYTES,
53
- FS_LIST_PAGE_LIMIT,
54
- } from '../constants.js';
55
- import { recordFailure } from '../observability/oom-discriminator.js';
56
- import { classifyError } from '../observability/oom-classify.js';
57
- import { acquireSupervisorAllocation } from '../observability/heavy-alloc-coord.js';
56
+ } from '@nimbus-sh/platform/limits.js';
57
+ import { recordFailure } from '@nimbus-sh/platform/oom-discriminator.js';
58
+ import { classifyError } from '@nimbus-sh/platform/oom-classify.js';
59
+ import { acquireSupervisorAllocation } from '@nimbus-sh/platform/heavy-alloc-coord.js';
58
60
  import { enc, dec } from '../_shared/bytes.js';
59
61
  import {
60
62
  decodeWriteBatchStream,
63
+ type BatchChunkEntry,
64
+ type BatchInodeEntry,
65
+ type BatchWritePayload,
66
+ type VfsInodeKind,
61
67
  type W7DecodedRecord,
62
- } from '../_shared/w7-frame.js';
68
+ } from '@nimbus-sh/platform/w7-frame.js';
63
69
  import {
64
70
  WeightedCreditPool,
65
71
  type CreditLease,
66
- } from '../_shared/weighted-credit-pool.js';
72
+ } from '@nimbus-sh/platform/weighted-credit-pool.js';
67
73
  import { LEGACY_SYMLINK_REGISTRY_PATH } from './symlink-registry.js';
68
74
  import {
69
75
  CRED_KERNEL,
@@ -104,8 +110,16 @@ function installPipelineDiagEnabled(): boolean {
104
110
  }
105
111
 
106
112
  // ── Types ───────────────────────────────────────────────────────────────────
107
-
108
- export type VfsInodeKind = 'file' | 'directory' | 'symlink';
113
+ // VfsInodeKind and the writeBatch payload types (BatchInodeEntry,
114
+ // BatchChunkEntry, BatchWritePayload) live with the wire format that encodes
115
+ // them: @nimbus-sh/platform/w7-frame.js. Forwarded here because core@0.5.0
116
+ // published them from this module; platform stays the definition.
117
+ export type {
118
+ BatchChunkEntry,
119
+ BatchInodeEntry,
120
+ BatchWritePayload,
121
+ VfsInodeKind,
122
+ } from '@nimbus-sh/platform/w7-frame.js';
109
123
 
110
124
  export interface ExclusiveMutationLease {
111
125
  readonly root: string;
@@ -133,22 +147,6 @@ interface INode {
133
147
  contentId: string | null;
134
148
  }
135
149
 
136
- /** Entry for bulk inode creation via writeBatch(). */
137
- export interface BatchInodeEntry {
138
- path: string;
139
- parentPath: string;
140
- /** Defaults to isDir ? directory : file for legacy/non-symlink producers. */
141
- kind?: VfsInodeKind;
142
- isDir: boolean;
143
- size: number;
144
- atime?: number;
145
- mtime: number;
146
- mode: number;
147
- uid?: number;
148
- gid?: number;
149
- chunkCount: number;
150
- }
151
-
152
150
  export interface VfsStat {
153
151
  type: VfsInodeKind;
154
152
  size: number;
@@ -233,21 +231,6 @@ export interface CredentialedVfs {
233
231
  readonly epoch: string;
234
232
  }
235
233
 
236
- /** Entry for bulk chunk creation via writeBatch(). */
237
- export interface BatchChunkEntry {
238
- path: string;
239
- chunkId: number;
240
- data: Uint8Array;
241
- }
242
-
243
- /** Payload for writeBatch() — all inodes + chunks written in ONE transactionSync(). */
244
- export interface BatchWritePayload {
245
- inodes: BatchInodeEntry[];
246
- chunks: BatchChunkEntry[];
247
- /** Paths to delete before writing (for clean reinstall). */
248
- deletePaths?: string[];
249
- }
250
-
251
234
  export interface WriteBatchStreamProgress {
252
235
  /** 1-based sequence of the last durable publish group; zero means none. */
253
236
  committedGroupSequence: number;
@@ -631,10 +614,9 @@ export class SqliteVFS {
631
614
 
632
615
  // ── Running counters for O(1) getStats() (B3 / AUDIT M10 / M-S8) ──
633
616
  // Replaces the triple scan of this.inodes on every /api/stats poll.
634
- // Bootstrapped in loadInodes(); maintained at every mutator entry:
635
- // mkdir/rmdir/writeFile/unlink/writeBatch (rename is a no-op
636
- // same inode, new path). Invariant: these match a fresh O(N)
637
- // walk of this.inodes. Unit-tested in the A5/B3 runtime tests.
617
+ // Bootstrapped in loadInodes(); maintained at committed publication by
618
+ // mkdir, batch writes/deletes and rename. These match a fresh O(N)
619
+ // walk of this.inodes; rollback reloads them from the durable rows.
638
620
  private _totalFiles = 0;
639
621
  private _totalDirs = 0;
640
622
  private _usedBytes = 0;
@@ -649,6 +631,10 @@ export class SqliteVFS {
649
631
  // so unrelated writes no longer invalidate them. In-memory only — the
650
632
  // clock resets with the DO lifetime, exactly like the caches keyed on it.
651
633
  private _pathRevisions = new Map<string, number>();
634
+ private transactionPublication: {
635
+ paths: Set<string>;
636
+ events: { type: VfsEventType; path: string; oldPath?: string }[];
637
+ } | null = null;
652
638
 
653
639
  // ── Invalidation log (facet cache coherence) ──────────────────────────
654
640
  // A facet's resident set is a cache of this VFS, and it learns what to
@@ -1690,6 +1676,10 @@ export class SqliteVFS {
1690
1676
  * additional coverage, since no facet view keys on a grandparent.
1691
1677
  */
1692
1678
  private bumpRevision(paths: readonly string[]): void {
1679
+ if (this.transactionPublication) {
1680
+ for (const path of paths) this.transactionPublication.paths.add(path);
1681
+ return;
1682
+ }
1693
1683
  const rev = ++this._revision;
1694
1684
  for (const path of paths) {
1695
1685
  let p = normalizeVfsPath(path);
@@ -1901,7 +1891,7 @@ export class SqliteVFS {
1901
1891
  this._addToChildrenIndex(pp, path);
1902
1892
  this._totalDirs++; // B3
1903
1893
  this.bumpRevision([path]);
1904
- this.events.emit('addDir', path);
1894
+ this.emitMutation('addDir', path);
1905
1895
  }
1906
1896
 
1907
1897
  private writeFile(
@@ -2922,11 +2912,11 @@ export class SqliteVFS {
2922
2912
  }
2923
2913
  const atime = atimeMs !== null && Number.isFinite(atimeMs) ? Math.trunc(atimeMs) : this.now();
2924
2914
  const mtime = mtimeMs !== null && Number.isFinite(mtimeMs) ? Math.trunc(mtimeMs) : this.now();
2915
+ this.sql.exec("UPDATE inodes SET atime = ?, mtime = ? WHERE path = ?", atime, mtime, inode.path);
2925
2916
  inode.atime = atime;
2926
2917
  inode.mtime = mtime;
2927
- this.sql.exec("UPDATE inodes SET atime = ?, mtime = ? WHERE path = ?", atime, mtime, inode.path);
2928
2918
  this.bumpRevision([inode.path]);
2929
- this.events.emit('change', inode.path);
2919
+ this.emitMutation('change', inode.path);
2930
2920
  }
2931
2921
 
2932
2922
  /**
@@ -2966,10 +2956,10 @@ export class SqliteVFS {
2966
2956
  }
2967
2957
  this.assertMutationsAllowed([inode.path]);
2968
2958
  const full = inodeTypeBits(inode.kind) | (mode & 0o7777);
2969
- inode.mode = full;
2970
2959
  this.sql.exec("UPDATE inodes SET mode = ? WHERE path = ?", full, inode.path);
2960
+ inode.mode = full;
2971
2961
  this.bumpRevision([inode.path]);
2972
- this.events.emit('change', inode.path);
2962
+ this.emitMutation('change', inode.path);
2973
2963
  }
2974
2964
 
2975
2965
  private chown(
@@ -2992,18 +2982,21 @@ export class SqliteVFS {
2992
2982
  }
2993
2983
  if (gid !== null && gid === inode.gid && cred.uid !== 0 && !owner) throw vfsError('EPERM', resolved.path);
2994
2984
  this.assertMutationsAllowed([inode.path]);
2995
- inode.uid = uid ?? inode.uid;
2996
- inode.gid = gid ?? inode.gid;
2997
- if (cred.uid !== 0) inode.mode &= ~0o6000;
2985
+ const nextUid = uid ?? inode.uid;
2986
+ const nextGid = gid ?? inode.gid;
2987
+ const nextMode = cred.uid === 0 ? inode.mode : inode.mode & ~0o6000;
2998
2988
  this.sql.exec(
2999
2989
  'UPDATE inodes SET uid = ?, gid = ?, mode = ? WHERE path = ?',
3000
- inode.uid,
3001
- inode.gid,
3002
- inode.mode,
2990
+ nextUid,
2991
+ nextGid,
2992
+ nextMode,
3003
2993
  inode.path,
3004
2994
  );
2995
+ inode.uid = nextUid;
2996
+ inode.gid = nextGid;
2997
+ inode.mode = nextMode;
3005
2998
  this.bumpRevision([inode.path]);
3006
- this.events.emit('change', inode.path);
2999
+ this.emitMutation('change', inode.path);
3007
3000
  }
3008
3001
 
3009
3002
  /**
@@ -3216,15 +3209,19 @@ export class SqliteVFS {
3216
3209
 
3217
3210
  private rename(oldPath: string, newPath: string, cred: VfsCred): void {
3218
3211
  this.assertMutationsAllowed([oldPath, newPath]);
3219
- this.checkAccess(oldPath, 0, cred, { followLeaf: false });
3212
+ // Resolve private /tmp names to storage keys before reading or mutating.
3213
+ const source = this.checkAccess(oldPath, 0, cred, { followLeaf: false });
3214
+ const inode = source.inode;
3215
+ if (!inode) throw vfsError('ENOENT', oldPath);
3216
+ const target = this.checkAccess(newPath, 0, cred, { followLeaf: false, allowMissingLeaf: true });
3217
+ oldPath = source.path;
3218
+ newPath = target.path;
3220
3219
  this.checkParentAccess(oldPath, cred);
3221
3220
  this.checkParentAccess(newPath, cred);
3222
3221
  if (oldPath === newPath) return;
3223
3222
  if (newPath.startsWith(`${oldPath}/`)) {
3224
3223
  throw new Error(`EINVAL: cannot move ${oldPath} inside itself`);
3225
3224
  }
3226
- const inode = this.inodes.get(oldPath);
3227
- if (!inode) throw new Error("ENOENT: " + oldPath);
3228
3225
  this.checkStickyParentMutation(oldPath, inode, cred);
3229
3226
 
3230
3227
  // W-3 (WASI filesystem WASI): if newPath already exists, unlink it first so the
@@ -3234,7 +3231,7 @@ export class SqliteVFS {
3234
3231
  // a 2nd `make` after a prior successful build throws on UPDATE
3235
3232
  // failure. Pre-unlink covers both file-over-file and file-over-dir
3236
3233
  // (the latter is rare but POSIX permits it for empty dirs).
3237
- const destInode = this.inodes.get(newPath);
3234
+ const destInode = target.inode;
3238
3235
  if (destInode) {
3239
3236
  this.checkStickyParentMutation(newPath, destInode, cred);
3240
3237
  if (destInode.isDir) {
@@ -3398,7 +3395,7 @@ export class SqliteVFS {
3398
3395
  }
3399
3396
 
3400
3397
  this.bumpRevision([...touchedPaths]);
3401
- this.events.emit('rename', newPath, oldPath);
3398
+ this.emitMutation('rename', newPath, oldPath);
3402
3399
  this.runContentMaintenanceSafely(1);
3403
3400
  }
3404
3401
 
@@ -4097,6 +4094,72 @@ export class SqliteVFS {
4097
4094
  return this.errorMessage(error).toUpperCase().includes('SQLITE_NOMEM');
4098
4095
  }
4099
4096
 
4097
+ /**
4098
+ * Participate in an embedder's synchronous transaction: VFS writes and SQL
4099
+ * issued by callback commit together; on rollback the in-memory mirror
4100
+ * returns to the committed state before the error is rethrown with cause.
4101
+ *
4102
+ * This method MUST own the outermost transaction on this VFS's SQL host;
4103
+ * use it instead of wrapping VFS calls in storage.transactionSync. Do not
4104
+ * nest it, return a Promise/thenable, or start asynchronous work inside it.
4105
+ * The callback may read its writes. Revisions and events publish only on
4106
+ * commit, once for the combined mutation. Existing credential checks apply.
4107
+ *
4108
+ * Inodes are an always-resident cache (absence means ENOENT), so rollback
4109
+ * discards cached chunks and rebuilds metadata/children/counters from SQL.
4110
+ * No inode snapshot or undo log is retained. Recovery failure is surfaced
4111
+ * with both errors; the embedder must discard this VFS in that case.
4112
+ */
4113
+ withTransaction<T>(callback: () => T): T {
4114
+ if (this.transactionPublication !== null) {
4115
+ throw new Error('[sqlite-vfs] nested embedder transactions are not supported');
4116
+ }
4117
+ const storage = this.ctx.storage;
4118
+ if (!storage) throw new Error('[sqlite-vfs] atomic storage operation requires transactionSync');
4119
+ const publication = {
4120
+ paths: new Set<string>(),
4121
+ events: new Array<{ type: VfsEventType; path: string; oldPath?: string }>(),
4122
+ };
4123
+ const maintenancePending = this.maintenancePending;
4124
+ this.transactionPublication = publication;
4125
+ let result: T;
4126
+ try {
4127
+ result = storage.transactionSync(() => {
4128
+ const value = callback();
4129
+ if (value !== null && (typeof value === 'object' || typeof value === 'function')
4130
+ && 'then' in value && typeof value.then === 'function') {
4131
+ throw new TypeError('[sqlite-vfs] transaction callback must be synchronous');
4132
+ }
4133
+ return value;
4134
+ });
4135
+ } catch (error) {
4136
+ this.evictAll();
4137
+ this.maintenancePending = maintenancePending;
4138
+ try {
4139
+ this.loadInodes();
4140
+ } catch (reloadError) {
4141
+ throw new AggregateError([error, reloadError], '[sqlite-vfs] transaction rollback reload failed', { cause: error });
4142
+ }
4143
+ throw new Error('[sqlite-vfs] embedder transaction rolled back', { cause: error });
4144
+ } finally {
4145
+ this.transactionPublication = null;
4146
+ }
4147
+ if (publication.paths.size > 0) this.bumpRevision([...publication.paths]);
4148
+ for (const event of publication.events) {
4149
+ this.events.emit(event.type, event.path, event.oldPath);
4150
+ }
4151
+ this.runContentMaintenanceSafely(1);
4152
+ return result;
4153
+ }
4154
+
4155
+ private emitMutation(type: VfsEventType, path: string, oldPath?: string): void {
4156
+ if (this.transactionPublication) {
4157
+ this.transactionPublication.events.push({ type, path, oldPath });
4158
+ } else {
4159
+ this.events.emit(type, path, oldPath);
4160
+ }
4161
+ }
4162
+
4100
4163
  private transactionSync(callback: () => void): void {
4101
4164
  if (!this.ctx?.storage?.transactionSync) {
4102
4165
  throw new Error('[sqlite-vfs] atomic storage operation requires transactionSync');
@@ -4465,6 +4528,7 @@ export class SqliteVFS {
4465
4528
  }
4466
4529
 
4467
4530
  private runContentMaintenanceSafely(maxTransactions: number, force = false): void {
4531
+ if (this.transactionPublication) return;
4468
4532
  if (!force && !this.maintenancePending) return;
4469
4533
  const startedAt = performance.now();
4470
4534
  try {
@@ -4790,10 +4854,10 @@ export class SqliteVFS {
4790
4854
 
4791
4855
  // 5. Events observe the already-published metadata and revision.
4792
4856
  for (const inode of deletedInodes) {
4793
- this.events.emit(inode.isDir ? 'unlinkDir' : 'unlink', inode.path);
4857
+ this.emitMutation(inode.isDir ? 'unlinkDir' : 'unlink', inode.path);
4794
4858
  }
4795
4859
  for (const entry of plan.inodes) {
4796
- this.events.emit(entry.isDir ? 'addDir' : replacedPaths.has(entry.path) ? 'change' : 'add', entry.path);
4860
+ this.emitMutation(entry.isDir ? 'addDir' : replacedPaths.has(entry.path) ? 'change' : 'add', entry.path);
4797
4861
  }
4798
4862
 
4799
4863
  this.recordDuration(this._postCommitDuration, performance.now() - postCommitStartedAt);
@@ -48,6 +48,8 @@ import { seedRuntimePackage, type RuntimePackage } from '../runtime/runtime-pack
48
48
  import type { EsbuildService } from '../runtime/esbuild-service.js';
49
49
  import { registerUnixCommands } from '../shell/unix-commands.js';
50
50
  import { installPathExecResolver } from '../shell/exec-dispatch.js';
51
+ import { adoptCtxExports, composeFabric, type CtxExports, type FabricComposition } from '@nimbus-sh/platform/composition.js';
52
+ import { createSupervisorOpHandler, type SupervisorOpEnvelope, type SupervisorOpHandler } from './supervisor-op.js';
51
53
 
52
54
  export interface NimbusWorkspaceOptions {
53
55
  /** The host's SQLite. In a Durable Object: `ctx.storage.sql`. */
@@ -126,6 +128,15 @@ export interface NimbusWorkspaceOptions {
126
128
  * carry REPLs and a resident-process substrate this cannot reach.
127
129
  */
128
130
  readonly facets?: FacetHost;
131
+ /** Isolate-wide fabric composition, including the hosting namespace. */
132
+ readonly fabric?: FabricComposition;
133
+ /** Explicit exports override the bag on the transaction host. */
134
+ readonly ctxExports?: CtxExports;
135
+ /** The process table that allocated supervisor-binding pids. */
136
+ readonly processes?: SessionProcessSupervisor;
137
+ readonly processOutput?: (stream: 'stdout' | 'stderr', pid: number, data: string) => void;
138
+ /** Host operations, including overrides for host-specific accounting. */
139
+ readonly supervisorOps?: Readonly<Record<string, SupervisorOpHandler>>;
129
140
  }
130
141
 
131
142
  /**
@@ -168,6 +179,7 @@ export class NimbusWorkspace {
168
179
  registry: CommandRegistry,
169
180
  env: Record<string, string>,
170
181
  private readonly sql: SqlDatabase,
182
+ private readonly supervisorOps: (envelope: SupervisorOpEnvelope) => Promise<unknown>,
171
183
  ) {
172
184
  this.vfs = vfs;
173
185
  this.kernel = kernel;
@@ -182,6 +194,10 @@ export class NimbusWorkspace {
182
194
  }
183
195
 
184
196
  static async create(options: NimbusWorkspaceOptions): Promise<NimbusWorkspace> {
197
+ if (options.fabric) composeFabric(options.fabric);
198
+ const exports = options.ctxExports
199
+ ?? (options.transactions as (TransactionHost & { exports?: CtxExports }) | undefined)?.exports;
200
+ if (exports) adoptCtxExports(exports);
185
201
  const vfs = options.vfs ?? openFilesystem(options);
186
202
  const mounts = options.mounts ?? DEFAULT_MOUNT_POINTS;
187
203
  seedBaseFilesystem(vfs, mounts);
@@ -234,13 +250,21 @@ export class NimbusWorkspace {
234
250
  });
235
251
  }
236
252
 
237
- return new NimbusWorkspace(vfs, kernel, shell, registry, env, options.sql);
253
+ const supervisorOps = createSupervisorOpHandler({
254
+ vfs, processes: options.processes, output: options.processOutput, extend: options.supervisorOps,
255
+ });
256
+ return new NimbusWorkspace(vfs, kernel, shell, registry, env, options.sql, supervisorOps);
238
257
  }
239
258
 
240
259
  exec(command: string, options?: RunOptions): Promise<CommandResult> {
241
260
  return this.commands.run(command, options);
242
261
  }
243
262
 
263
+ /** The hosting object forwards its supervisorOp RPC to this method. */
264
+ supervisorOp(envelope: SupervisorOpEnvelope): Promise<unknown> {
265
+ return this.supervisorOps(envelope);
266
+ }
267
+
244
268
  /**
245
269
  * Apply the login files, and begin reading the terminal when there is one.
246
270
  *
@@ -383,7 +407,7 @@ async function registerWasmRuntimes(deps: {
383
407
  getEsbuild: () => {
384
408
  if (!esbuild) {
385
409
  esbuild = import('../runtime/esbuild-service.js')
386
- .then((module) => new module.EsbuildService(deps.vfs));
410
+ .then((module) => new module.EsbuildService(deps.vfs.as(CRED_KERNEL)));
387
411
  }
388
412
  return esbuild;
389
413
  },