@nimbus-sh/core 0.3.0 → 0.5.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 (155) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +140 -0
  3. package/dist/_shared/error-text.d.ts +15 -0
  4. package/dist/_shared/error-text.d.ts.map +1 -0
  5. package/dist/_shared/error-text.js +21 -0
  6. package/dist/_shared/exports-resolver.d.ts +19 -7
  7. package/dist/_shared/exports-resolver.d.ts.map +1 -1
  8. package/dist/_shared/exports-resolver.js +1 -1
  9. package/dist/_shared/retry.d.ts.map +1 -1
  10. package/dist/_shared/retry.js +3 -1
  11. package/dist/_shared/tarball-stream.d.ts +93 -0
  12. package/dist/_shared/tarball-stream.d.ts.map +1 -0
  13. package/dist/_shared/tarball-stream.js +235 -0
  14. package/dist/_shared/tarball.d.ts +17 -0
  15. package/dist/_shared/tarball.d.ts.map +1 -0
  16. package/dist/_shared/tarball.js +39 -0
  17. package/dist/_shared/w7-frame.js +2 -1
  18. package/dist/constants.d.ts.map +1 -1
  19. package/dist/constants.js +30 -16
  20. package/dist/observability/oom-classify.d.ts +1 -1
  21. package/dist/observability/oom-classify.d.ts.map +1 -1
  22. package/dist/observability/oom-classify.js +7 -0
  23. package/dist/observability/oom-discriminator.d.ts +1 -1
  24. package/dist/runtime/clang-runner.d.ts +38 -0
  25. package/dist/runtime/clang-runner.d.ts.map +1 -0
  26. package/dist/runtime/clang-runner.js +866 -0
  27. package/dist/runtime/esbuild-service.d.ts +4 -3
  28. package/dist/runtime/esbuild-service.d.ts.map +1 -1
  29. package/dist/runtime/esbuild-service.js +9 -4
  30. package/dist/runtime/facet-host.d.ts +12 -0
  31. package/dist/runtime/facet-host.d.ts.map +1 -1
  32. package/dist/runtime/installed-runtimes.d.ts +4 -3
  33. package/dist/runtime/installed-runtimes.d.ts.map +1 -1
  34. package/dist/runtime/local-facet-host.d.ts.map +1 -1
  35. package/dist/runtime/local-facet-host.js +29 -9
  36. package/dist/runtime/long-running-handle.d.ts +3 -3
  37. package/dist/runtime/long-running-handle.d.ts.map +1 -1
  38. package/dist/runtime/os-contracts.d.ts +7 -5
  39. package/dist/runtime/os-contracts.d.ts.map +1 -1
  40. package/dist/runtime/port-registry.d.ts +28 -0
  41. package/dist/runtime/port-registry.d.ts.map +1 -1
  42. package/dist/runtime/port-registry.js +79 -9
  43. package/dist/runtime/process-logs.d.ts +9 -1
  44. package/dist/runtime/process-logs.d.ts.map +1 -1
  45. package/dist/runtime/project-detect.d.ts +6 -1
  46. package/dist/runtime/project-detect.d.ts.map +1 -1
  47. package/dist/runtime/require-resolver.d.ts.map +1 -1
  48. package/dist/runtime/require-resolver.js +3 -4
  49. package/dist/runtime/ruby-gems.d.ts +30 -0
  50. package/dist/runtime/ruby-gems.d.ts.map +1 -0
  51. package/dist/runtime/ruby-gems.js +636 -0
  52. package/dist/runtime/ruby-runner.d.ts +127 -0
  53. package/dist/runtime/ruby-runner.d.ts.map +1 -0
  54. package/dist/runtime/ruby-runner.js +1357 -0
  55. package/dist/runtime/runtime-registry.d.ts +4 -3
  56. package/dist/runtime/runtime-registry.d.ts.map +1 -1
  57. package/dist/runtime/runtime-registry.js +2 -1
  58. package/dist/runtime/session-process-supervisor.d.ts +15 -0
  59. package/dist/runtime/session-process-supervisor.d.ts.map +1 -1
  60. package/dist/runtime/session-process-supervisor.js +30 -0
  61. package/dist/runtime/sqlite-runtime-fs-bridge.js +1 -5
  62. package/dist/runtime/virtual-socket-kernel.d.ts.map +1 -1
  63. package/dist/runtime/virtual-socket-kernel.generated.d.ts +1 -1
  64. package/dist/runtime/virtual-socket-kernel.generated.d.ts.map +1 -1
  65. package/dist/runtime/virtual-socket-kernel.generated.js +2 -2
  66. package/dist/runtime/virtual-socket-kernel.js +10 -5
  67. package/dist/runtime/wasm-runner.d.ts.map +1 -1
  68. package/dist/runtime/wasm-runner.js +22 -14
  69. package/dist/shell/features.d.ts.map +1 -1
  70. package/dist/shell/features.js +5 -4
  71. package/dist/shell/unix-commands.d.ts +14 -1
  72. package/dist/shell/unix-commands.d.ts.map +1 -1
  73. package/dist/shell/unix-commands.js +115 -69
  74. package/dist/substrate/lifo/commands/net/ip.d.ts.map +1 -1
  75. package/dist/substrate/lifo/commands/system/ps.d.ts +2 -1
  76. package/dist/substrate/lifo/commands/system/ps.d.ts.map +1 -1
  77. package/dist/substrate/lifo/kernel/ServiceManager.d.ts.map +1 -1
  78. package/dist/substrate/lifo/kernel/ServiceManager.js +0 -2
  79. package/dist/substrate/lifo/kernel/network/NetworkStack.d.ts +22 -11
  80. package/dist/substrate/lifo/kernel/network/NetworkStack.d.ts.map +1 -1
  81. package/dist/substrate/lifo/kernel/network/NetworkStack.js +0 -2
  82. package/dist/substrate/lifo/kernel/network/tunnel/WebSocketTunnel.d.ts +2 -1
  83. package/dist/substrate/lifo/kernel/network/tunnel/WebSocketTunnel.d.ts.map +1 -1
  84. package/dist/substrate/lifo/kernel/network/tunnel/WebSocketTunnel.js +3 -4
  85. package/dist/substrate/lifo/kernel/vfs/VFS.d.ts +17 -0
  86. package/dist/substrate/lifo/kernel/vfs/VFS.d.ts.map +1 -1
  87. package/dist/substrate/lifo/kernel/vfs/VFS.js +33 -0
  88. package/dist/substrate/lifo/node-compat/esbuild.d.ts.map +1 -1
  89. package/dist/substrate/lifo/node-compat/esbuild.js +2 -4
  90. package/dist/substrate/lifo/node-compat/index.d.ts.map +1 -1
  91. package/dist/substrate/lifo/node-compat/index.js +3 -2
  92. package/dist/substrate/lifo/node-compat/loose-equality.d.ts +19 -0
  93. package/dist/substrate/lifo/node-compat/loose-equality.d.ts.map +1 -0
  94. package/dist/substrate/lifo/node-compat/loose-equality.js +91 -0
  95. package/dist/substrate/lifo/node-compat/process.d.ts.map +1 -1
  96. package/dist/substrate/lifo/node-compat/process.js +6 -3
  97. package/dist/substrate/lifo/shell/Shell.d.ts +19 -0
  98. package/dist/substrate/lifo/shell/Shell.d.ts.map +1 -1
  99. package/dist/substrate/lifo/shell/Shell.js +86 -75
  100. package/dist/substrate/lifo/shell/lexer.d.ts +9 -0
  101. package/dist/substrate/lifo/shell/lexer.d.ts.map +1 -1
  102. package/dist/substrate/lifo/shell/lexer.js +40 -0
  103. package/dist/vfs/seed-project.d.ts.map +1 -1
  104. package/dist/vfs/seed-project.js +8 -2
  105. package/dist/vfs/sqlite-vfs.d.ts +61 -1
  106. package/dist/vfs/sqlite-vfs.d.ts.map +1 -1
  107. package/dist/vfs/sqlite-vfs.js +199 -35
  108. package/dist/workspace/nimbus-workspace.d.ts.map +1 -1
  109. package/dist/workspace/nimbus-workspace.js +9 -2
  110. package/package.json +4 -2
  111. package/src/_shared/error-text.ts +21 -0
  112. package/src/_shared/exports-resolver.ts +24 -4
  113. package/src/_shared/retry.ts +4 -2
  114. package/src/_shared/tarball-stream.ts +263 -0
  115. package/src/_shared/tarball.ts +46 -0
  116. package/src/_shared/w7-frame.ts +2 -1
  117. package/src/constants.ts +30 -16
  118. package/src/observability/oom-classify.ts +7 -0
  119. package/src/observability/oom-discriminator.ts +1 -1
  120. package/src/runtime/clang-runner.ts +924 -0
  121. package/src/runtime/esbuild-service.ts +16 -11
  122. package/src/runtime/facet-host.ts +12 -0
  123. package/src/runtime/installed-runtimes.ts +4 -3
  124. package/src/runtime/local-facet-host.ts +28 -8
  125. package/src/runtime/long-running-handle.ts +3 -3
  126. package/src/runtime/os-contracts.ts +7 -5
  127. package/src/runtime/port-registry.ts +97 -9
  128. package/src/runtime/process-logs.ts +12 -3
  129. package/src/runtime/project-detect.ts +7 -1
  130. package/src/runtime/require-resolver.ts +7 -7
  131. package/src/runtime/ruby-gems.ts +682 -0
  132. package/src/runtime/ruby-runner.ts +1484 -0
  133. package/src/runtime/runtime-registry.ts +9 -7
  134. package/src/runtime/session-process-supervisor.ts +27 -0
  135. package/src/runtime/sqlite-runtime-fs-bridge.ts +9 -6
  136. package/src/runtime/virtual-socket-kernel.generated.ts +2 -2
  137. package/src/runtime/virtual-socket-kernel.ts +10 -5
  138. package/src/runtime/wasm-runner.ts +55 -29
  139. package/src/shell/features.ts +5 -4
  140. package/src/shell/unix-commands.ts +273 -128
  141. package/src/substrate/lifo/commands/net/ip.ts +15 -15
  142. package/src/substrate/lifo/commands/system/ps.ts +2 -1
  143. package/src/substrate/lifo/kernel/ServiceManager.ts +0 -2
  144. package/src/substrate/lifo/kernel/network/NetworkStack.ts +24 -17
  145. package/src/substrate/lifo/kernel/network/tunnel/WebSocketTunnel.ts +37 -12
  146. package/src/substrate/lifo/kernel/vfs/VFS.ts +34 -0
  147. package/src/substrate/lifo/node-compat/esbuild.ts +16 -8
  148. package/src/substrate/lifo/node-compat/index.ts +3 -2
  149. package/src/substrate/lifo/node-compat/loose-equality.ts +81 -0
  150. package/src/substrate/lifo/node-compat/process.ts +5 -2
  151. package/src/substrate/lifo/shell/Shell.ts +92 -70
  152. package/src/substrate/lifo/shell/lexer.ts +38 -0
  153. package/src/vfs/seed-project.ts +10 -3
  154. package/src/vfs/sqlite-vfs.ts +209 -36
  155. package/src/workspace/nimbus-workspace.ts +10 -1
@@ -51,11 +51,22 @@ import { WeightedCreditPool, } from '../_shared/weighted-credit-pool.js';
51
51
  import { LEGACY_SYMLINK_REGISTRY_PATH } from './symlink-registry.js';
52
52
  import { CRED_KERNEL, } from '../runtime/os-contracts.js';
53
53
  const CONTENT_ID_ALLOCATION_ATTEMPTS = 8;
54
+ /**
55
+ * Live view of the global object. `process` is not in the Workers lib, so its
56
+ * shape is declared here rather than assumed present.
57
+ */
58
+ const nodeHost = globalThis;
59
+ /** Single gate for the W2.5b install-pipeline diagnostics below. */
60
+ function installPipelineDiagEnabled() {
61
+ return nodeHost.process?.env?.NIMBUS_DIAG_INSTALL_PIPELINE === '1';
62
+ }
54
63
  const INODE_ROWS_PER_SQL_EXEC = 9;
55
64
  const CHUNK_ROWS_PER_SQL_EXEC = 33;
56
65
  const CONTENT_IDS_PER_SQL_EXEC = 50;
57
66
  const TRANSACTION_DURATION_SAMPLE_COUNT = 128;
58
67
  const CONTENT_SCHEMA_MIGRATION = 'content_generations_v1';
68
+ /** Storage key of the shared scratch tree. `normalizeVfsPath` drops the slash. */
69
+ const TMP_ROOT = 'tmp';
59
70
  export const VFS_APPEND_RECEIPT_LIMIT = 2048;
60
71
  const INODE_KIND_FILE = 0;
61
72
  const INODE_KIND_DIRECTORY = 1;
@@ -357,6 +368,9 @@ export class SqliteVFS {
357
368
  activeStagingContentIds = new Set();
358
369
  /** True only while durable GC work or a known abandoned staging row exists. */
359
370
  maintenancePending = false;
371
+ // Keyset cursor for the orphan scan: each maintenance call reads one bounded
372
+ // page of distinct content ids past this bound, wrapping to '' at the end.
373
+ orphanScanCursor = '';
360
374
  // Stage 2 transaction/phase telemetry. Scalar writes stay cheap; the
361
375
  // percentile is computed from the fixed ring only when diagnostics read it.
362
376
  _activeTransaction = null;
@@ -939,6 +953,85 @@ export class SqliteVFS {
939
953
  return null;
940
954
  return this.blobToUint8Array(rows[0].data);
941
955
  }
956
+ /**
957
+ * Principals whose `/tmp` is private, keyed by uid, valued by the storage
958
+ * root their `/tmp` resolves to.
959
+ *
960
+ * `/tmp` keeps its path in every view and only the bytes behind it differ,
961
+ * so nothing has to be told which principal it is. The credential is the
962
+ * only per-process state visible where paths are RESOLVED — there is no pid
963
+ * and no cwd down here — so it is what the private view is keyed on.
964
+ *
965
+ * Resolution rather than a mount, because a mount diverges the two planes:
966
+ * the shell writing `/tmp/a` and the file API writing `/tmp/b` landed in
967
+ * different trees under the same name. `resolvePath` already takes `cred`,
968
+ * and every plane goes through it.
969
+ *
970
+ * Registration is also what makes a principal CONFINED for `chmod`. The two
971
+ * properties travel together because they answer one question: is this
972
+ * principal a guest in this filesystem. Nothing here applies to an
973
+ * unregistered credential, so the ordinary session user is untouched.
974
+ */
975
+ confinedTmpRoots = new Map();
976
+ /**
977
+ * Confine a principal. `tmpRoot` is a storage key, not a logical path — the
978
+ * caller owns creating and chowning it, because a per-principal `chown` is
979
+ * uid-0 only and a guest cannot provision its own.
980
+ */
981
+ confinePrincipal(uid, tmpRoot) {
982
+ const root = normalizeVfsPath(tmpRoot);
983
+ if (root === '')
984
+ throw vfsError('EINVAL', 'a private /tmp root cannot be the filesystem root');
985
+ this.confinedTmpRoots.set(uid, root);
986
+ }
987
+ /** Drop a confinement. A principal's `/tmp` dies with it; its home does not. */
988
+ releasePrincipal(uid) {
989
+ this.confinedTmpRoots.delete(uid);
990
+ }
991
+ /**
992
+ * Logical path -> storage key, for one credential.
993
+ *
994
+ * Everything under `/tmp` belongs to the caller's own private root, which is
995
+ * what makes the same path mean different bytes per principal. Idempotent: a
996
+ * key already inside that root is returned untouched, so the several methods
997
+ * that derive a key before handing it on cannot stack the rewrite.
998
+ */
999
+ storageKey(path, cred) {
1000
+ const key = normalizeVfsPath(path);
1001
+ const root = this.confinedTmpRoots.get(cred.uid);
1002
+ if (root === undefined)
1003
+ return key;
1004
+ if (key === root || key.startsWith(`${root}/`))
1005
+ return key;
1006
+ if (key === TMP_ROOT)
1007
+ return root;
1008
+ if (!key.startsWith(`${TMP_ROOT}/`))
1009
+ return key;
1010
+ return `${root}/${key.slice(TMP_ROOT.length + 1)}`;
1011
+ }
1012
+ /**
1013
+ * Storage key -> the name this credential knows it by, or `null` when it has
1014
+ * none. The inverse of {@link storageKey}, for the one surface that reports
1015
+ * paths it was not asked about: {@link list}.
1016
+ *
1017
+ * A confined caller has no name for the shared scratch tree — `/tmp` is its
1018
+ * own root — nor for another principal's, so both answer `null` and are
1019
+ * omitted. An unconfined caller sees storage as it is, which is what the
1020
+ * kernel and the session user need.
1021
+ */
1022
+ logicalPath(key, cred) {
1023
+ const root = this.confinedTmpRoots.get(cred.uid);
1024
+ if (root === undefined)
1025
+ return key;
1026
+ if (key === root)
1027
+ return TMP_ROOT;
1028
+ if (key.startsWith(`${root}/`))
1029
+ return `${TMP_ROOT}/${key.slice(root.length + 1)}`;
1030
+ // The SHARED scratch root has no name here — this caller's `/tmp` is its
1031
+ // own root, which already supplied that entry. Returning it too would
1032
+ // enumerate one name twice, for two different directories.
1033
+ return key === TMP_ROOT || key.startsWith(`${TMP_ROOT}/`) ? null : key;
1034
+ }
942
1035
  // ── Filesystem operations ─────────────────────────────────────────────
943
1036
  as(cred) {
944
1037
  const bound = Object.freeze({
@@ -983,7 +1076,7 @@ export class SqliteVFS {
983
1076
  writeBatch: (payload) => this.writeBatch(payload, bound),
984
1077
  writeStream: (stream, options) => this.writeStream(stream, options, bound),
985
1078
  mkdirBatch: (paths) => this.mkdirBatch(paths, bound),
986
- revision: (path) => this.revision(path),
1079
+ revision: (path) => this.revision(path, bound),
987
1080
  epoch: this._epoch,
988
1081
  };
989
1082
  }
@@ -1007,7 +1100,7 @@ export class SqliteVFS {
1007
1100
  return (granted & requested) === requested;
1008
1101
  }
1009
1102
  resolvePath(path, cred, followLeaf, allowMissing) {
1010
- let current = normalizeVfsPath(path);
1103
+ let current = this.storageKey(path, cred);
1011
1104
  const seen = new Set();
1012
1105
  for (let hops = 0; hops <= 40; hops++) {
1013
1106
  const parts = current.split('/').filter(Boolean);
@@ -1028,8 +1121,13 @@ export class SqliteVFS {
1028
1121
  seen.add(prefix);
1029
1122
  const target = dec.decode(this.readInodeBytes(prefix, inode));
1030
1123
  const suffix = parts.slice(index + 1).join('/');
1124
+ // An ABSOLUTE target is a logical path the same way the caller's
1125
+ // was, so it goes through the same rewrite: otherwise a symlink to
1126
+ // /tmp/y stored inside a private tree would read the shared one,
1127
+ // which is the one way out of the confinement. A RELATIVE target
1128
+ // resolves against a key that is already private, so it stays there.
1031
1129
  const resolvedTarget = target.startsWith('/')
1032
- ? normalizeVfsPath(target)
1130
+ ? this.storageKey(target, cred)
1033
1131
  : normalizeVfsPath(`${this.parentPath(prefix)}/${target}`);
1034
1132
  current = suffix ? normalizeVfsPath(`${resolvedTarget}/${suffix}`) : resolvedTarget;
1035
1133
  restarted = true;
@@ -1068,6 +1166,29 @@ export class SqliteVFS {
1068
1166
  if (resolved.inode?.kind !== 'directory')
1069
1167
  throw vfsError('ENOTDIR', parent);
1070
1168
  }
1169
+ /**
1170
+ * POSIX sticky-bit restriction on a shared directory.
1171
+ *
1172
+ * Write permission on a directory is normally enough to remove or rename
1173
+ * anything inside it, which is why `/tmp` is `1777` and not `0777`: the
1174
+ * sticky bit narrows that to the entry's owner, the directory's owner, and
1175
+ * root. Nothing enforced it here, so a world-writable shared directory gave
1176
+ * every principal the ability to delete every other principal's files —
1177
+ * the mode said one thing and the filesystem did another.
1178
+ */
1179
+ checkStickyParentMutation(path, inode, cred) {
1180
+ if (cred.uid === 0)
1181
+ return;
1182
+ const parent = this.parentPath(normalizeVfsPath(path));
1183
+ if (parent === '')
1184
+ return;
1185
+ const parentInode = this.inodes.get(parent);
1186
+ if (!parentInode || (parentInode.mode & 0o1000) === 0)
1187
+ return;
1188
+ if (cred.uid !== parentInode.uid && cred.uid !== inode.uid) {
1189
+ throw vfsError('EPERM', normalizeVfsPath(path));
1190
+ }
1191
+ }
1071
1192
  /**
1072
1193
  * Shared resolver for the boolean probes (exists/isDirectory/isFile/
1073
1194
  * isSymlink). Resolution-structure failures — a missing or non-directory
@@ -1105,10 +1226,12 @@ export class SqliteVFS {
1105
1226
  * under it changed in this DO lifetime). `revision('')` equals the
1106
1227
  * global clock by construction (every mutation stamps all ancestors).
1107
1228
  */
1108
- revision(path) {
1229
+ revision(path, cred) {
1109
1230
  if (path === undefined)
1110
1231
  return this._revision;
1111
- const p = normalizeVfsPath(path);
1232
+ // A credentialed caller asks about its own view; a confined one asking
1233
+ // after /tmp/x means its own file, so the counter must be that file's.
1234
+ const p = cred === undefined ? normalizeVfsPath(path) : this.storageKey(path, cred);
1112
1235
  if (p === '')
1113
1236
  return this._revision;
1114
1237
  return this._pathRevisions.get(p) ?? 0;
@@ -1288,7 +1411,7 @@ export class SqliteVFS {
1288
1411
  }
1289
1412
  }
1290
1413
  mkdir(path, options, cred) {
1291
- const normalized = normalizeVfsPath(path);
1414
+ const normalized = this.storageKey(path, cred);
1292
1415
  this.assertMutationsAllowed([normalized]);
1293
1416
  if (this.exists(normalized, cred))
1294
1417
  return;
@@ -1389,7 +1512,7 @@ export class SqliteVFS {
1389
1512
  }
1390
1513
  symlink(target, path, cred) {
1391
1514
  this.assertMutationsAllowed([path]);
1392
- const normalized = normalizeVfsPath(path);
1515
+ const normalized = this.storageKey(path, cred);
1393
1516
  const prior = this.checkAccess(normalized, 0, cred, { followLeaf: false, allowMissingLeaf: true });
1394
1517
  if (prior.inode)
1395
1518
  throw vfsError('EEXIST', normalized);
@@ -2124,6 +2247,22 @@ export class SqliteVFS {
2124
2247
  throw vfsError('ENOENT', path);
2125
2248
  if (cred.uid !== 0 && cred.uid !== inode.uid)
2126
2249
  throw vfsError('EPERM', resolved.path);
2250
+ // A confined principal owns its own triad and nothing else. Refusing chmod
2251
+ // outright would be simpler and wrong: execution is gated on the x bit, so
2252
+ // a guest that writes build.sh and cannot chmod it cannot run it. What
2253
+ // must not happen is WIDENING past its own principal, so the group, other
2254
+ // and setuid/setgid/sticky bits keep the value they were provisioned with
2255
+ // while the owner triad moves freely. `u+x` works; `+x` and `777` do not.
2256
+ //
2257
+ // Refused, never clamped. Quietly narrowing a mutation to the part that
2258
+ // was allowed reports success for something other than what was asked, so
2259
+ // the refusal names the spelling that works instead.
2260
+ if (cred.uid !== 0 && this.confinedTmpRoots.has(cred.uid)) {
2261
+ const beyondOwner = (mode & 0o7777) & ~0o700;
2262
+ if (beyondOwner !== ((inode.mode & 0o7777) & ~0o700)) {
2263
+ throw vfsError('EPERM', `${resolved.path}: mode change would grant permission outside your own principal; use u+x`);
2264
+ }
2265
+ }
2127
2266
  this.assertMutationsAllowed([inode.path]);
2128
2267
  const full = inodeTypeBits(inode.kind) | (mode & 0o7777);
2129
2268
  inode.mode = full;
@@ -2183,7 +2322,7 @@ export class SqliteVFS {
2183
2322
  // Before the walk, deliberately — see VfsListPage.
2184
2323
  const epoch = this._epoch;
2185
2324
  const rev = this._revision;
2186
- const from = after === null || after === undefined ? '' : normalizeVfsPath(after);
2325
+ const from = after === null || after === undefined ? '' : this.storageKey(after, cred);
2187
2326
  // this.inodes is insertion-ordered, not path-ordered, so the sort is what
2188
2327
  // makes `after` a resume key at all. Paid once per page against a map the
2189
2328
  // DO already holds whole.
@@ -2210,8 +2349,16 @@ export class SqliteVFS {
2210
2349
  catch {
2211
2350
  continue;
2212
2351
  }
2352
+ // Reported in the caller's OWN path space. Enumerating raw storage keys
2353
+ // would name a private root the caller cannot address and does not know
2354
+ // it has, and a caller feeding such a path back would be asking about
2355
+ // someone else's tree. `null` means the path has no name for this
2356
+ // caller, which is the same OMIT the access check above performs.
2357
+ const logical = this.logicalPath(path, cred);
2358
+ if (logical === null)
2359
+ continue;
2213
2360
  entries.push({
2214
- path,
2361
+ path: logical,
2215
2362
  kind: inode.kind,
2216
2363
  size: inode.size,
2217
2364
  rev: this._pathRevisions.get(path) ?? 0,
@@ -2220,7 +2367,7 @@ export class SqliteVFS {
2220
2367
  return { epoch, rev, entries, next };
2221
2368
  }
2222
2369
  readdir(path, cred) {
2223
- const np = path.replace(/^\/+/, '').replace(/\/+$/, '');
2370
+ const np = this.storageKey(path, cred);
2224
2371
  const resolved = np ? this.checkAccess(np, 0o4, cred) : { path: '', inode: undefined };
2225
2372
  const inode = resolved.inode;
2226
2373
  if (inode && inode.kind !== 'directory')
@@ -2229,7 +2376,7 @@ export class SqliteVFS {
2229
2376
  if (!kids) {
2230
2377
  // W2.5b diagnostic: empty children-set for a directory we expected
2231
2378
  // to be populated.
2232
- if (globalThis.process?.env?.NIMBUS_DIAG_INSTALL_PIPELINE === '1') {
2379
+ if (installPipelineDiagEnabled()) {
2233
2380
  // eslint-disable-next-line no-console
2234
2381
  console.warn('[sqlite-vfs/W2.5b] readdir miss path=' + np +
2235
2382
  ' kidsUndefined=true inodeExists=' + this.inodes.has(np));
@@ -2248,7 +2395,7 @@ export class SqliteVFS {
2248
2395
  // fewer (some entries' inodes are missing from this.inodes), log it.
2249
2396
  // This distinguishes (a) "children index broken" from (b) "inodes
2250
2397
  // map lost entries".
2251
- if (globalThis.process?.env?.NIMBUS_DIAG_INSTALL_PIPELINE === '1' &&
2398
+ if (installPipelineDiagEnabled() &&
2252
2399
  kids.size !== results.length) {
2253
2400
  // eslint-disable-next-line no-console
2254
2401
  console.warn('[sqlite-vfs/W2.5b] readdir size mismatch path=' + np +
@@ -2273,23 +2420,25 @@ export class SqliteVFS {
2273
2420
  if (!inode)
2274
2421
  throw vfsError('ENOENT', path);
2275
2422
  this.checkParentAccess(resolved.path, cred);
2423
+ this.checkStickyParentMutation(resolved.path, inode, cred);
2276
2424
  if (inode.isDir)
2277
2425
  throw vfsError('EISDIR', resolved.path);
2278
2426
  this.writeBatch({ inodes: [], chunks: [], deletePaths: [resolved.path] }, cred);
2279
2427
  }
2280
2428
  rmdir(path, cred) {
2281
2429
  this.assertMutationsAllowed([path]);
2282
- const np = path.replace(/^\/+/, '').replace(/\/+$/, '');
2430
+ const np = this.storageKey(path, cred);
2283
2431
  const resolved = this.checkAccess(np, 0, cred, { followLeaf: false });
2284
2432
  this.checkParentAccess(resolved.path, cred);
2433
+ const inode = this.inodes.get(np);
2434
+ if (!inode)
2435
+ throw vfsError('ENOENT', path);
2436
+ this.checkStickyParentMutation(resolved.path, inode, cred);
2285
2437
  // Check if empty using children index (O(1) instead of O(N))
2286
2438
  const kids = this.children.get(np);
2287
2439
  if (kids && kids.size > 0) {
2288
2440
  throw vfsError('ENOTEMPTY', path);
2289
2441
  }
2290
- const inode = this.inodes.get(np);
2291
- if (!inode)
2292
- throw vfsError('ENOENT', path);
2293
2442
  if (!inode.isDir)
2294
2443
  throw vfsError('ENOTDIR', path);
2295
2444
  this.writeBatch({ inodes: [], chunks: [], deletePaths: [np] }, cred);
@@ -2316,6 +2465,7 @@ export class SqliteVFS {
2316
2465
  if (!resolved.inode)
2317
2466
  throw vfsError('ENOENT', normalizeVfsPath(path));
2318
2467
  this.checkParentAccess(resolved.path, cred);
2468
+ this.checkStickyParentMutation(resolved.path, resolved.inode, cred);
2319
2469
  const removable = this.collectSubtreeInodes([resolved.path]);
2320
2470
  // The directories the recursive walk used to enumerate are exactly the
2321
2471
  // directory inodes of the subtree, and enumerating one needed read
@@ -2367,6 +2517,7 @@ export class SqliteVFS {
2367
2517
  const inode = this.inodes.get(oldPath);
2368
2518
  if (!inode)
2369
2519
  throw new Error("ENOENT: " + oldPath);
2520
+ this.checkStickyParentMutation(oldPath, inode, cred);
2370
2521
  // W-3 (WASI filesystem WASI): if newPath already exists, unlink it first so the
2371
2522
  // SQL UPDATE doesn't conflict on inodes.path uniqueness. POSIX rename(2)
2372
2523
  // overwrites the destination atomically; clang's atomic-write pattern
@@ -2376,6 +2527,7 @@ export class SqliteVFS {
2376
2527
  // (the latter is rare but POSIX permits it for empty dirs).
2377
2528
  const destInode = this.inodes.get(newPath);
2378
2529
  if (destInode) {
2530
+ this.checkStickyParentMutation(newPath, destInode, cred);
2379
2531
  if (destInode.isDir) {
2380
2532
  // POSIX semantics: rename onto a non-empty dir is an error
2381
2533
  // (ENOTEMPTY); rename onto an empty dir is allowed. We surface
@@ -2590,14 +2742,14 @@ export class SqliteVFS {
2590
2742
  }
2591
2743
  // ── Batch write (npm install fast path) ───────────────────────────────
2592
2744
  normalizeBatchInode(entry, cred) {
2593
- const path = normalizeVfsPath(entry.path);
2745
+ const path = this.storageKey(entry.path, cred);
2594
2746
  const prior = this.inodes.get(path);
2595
2747
  const newUid = cred.uid === 0 ? (entry.uid ?? 1000) : cred.uid;
2596
2748
  const newGid = cred.uid === 0 ? (entry.gid ?? 1000) : cred.gid;
2597
2749
  return {
2598
2750
  ...entry,
2599
2751
  path,
2600
- parentPath: normalizeVfsPath(entry.parentPath),
2752
+ parentPath: this.storageKey(entry.parentPath, cred),
2601
2753
  mode: prior
2602
2754
  ? prior.mode
2603
2755
  : inodeKind(entry) === 'symlink'
@@ -2632,7 +2784,7 @@ export class SqliteVFS {
2632
2784
  }
2633
2785
  };
2634
2786
  for (const path of payload.deletePaths ?? []) {
2635
- const normalized = normalizeVfsPath(path);
2787
+ const normalized = this.storageKey(path, cred);
2636
2788
  const existing = this.checkAccess(normalized, 0, cred, {
2637
2789
  followLeaf: false,
2638
2790
  allowMissingLeaf: true,
@@ -2648,14 +2800,14 @@ export class SqliteVFS {
2648
2800
  checkParent(entry.path);
2649
2801
  }
2650
2802
  for (const chunk of payload.chunks) {
2651
- const path = normalizeVfsPath(chunk.path);
2803
+ const path = this.storageKey(chunk.path, cred);
2652
2804
  if (!pending.has(path))
2653
2805
  this.checkAccess(path, 0o2, cred, { followLeaf: false });
2654
2806
  }
2655
2807
  return {
2656
2808
  ...payload,
2657
2809
  inodes,
2658
- deletePaths: payload.deletePaths?.map(normalizeVfsPath),
2810
+ deletePaths: payload.deletePaths?.map((path) => this.storageKey(path, cred)),
2659
2811
  };
2660
2812
  }
2661
2813
  /**
@@ -3307,18 +3459,30 @@ export class SqliteVFS {
3307
3459
  let orphanScanComplete = true;
3308
3460
  let stagingScanComplete = true;
3309
3461
  if (transactions < maximum) {
3310
- const orphanRows = [...this.sql.exec(`SELECT chunks.content_id
3311
- FROM file_chunks AS chunks
3312
- WHERE NOT EXISTS (
3313
- SELECT 1 FROM content_lifecycle AS lifecycle
3314
- WHERE lifecycle.content_id = chunks.content_id
3315
- )
3316
- AND ${sqlNoInodeContentReference('chunks.content_id')}
3317
- GROUP BY chunks.content_id
3318
- ORDER BY chunks.content_id
3319
- LIMIT ?`, CONTENT_IDS_PER_SQL_EXEC)];
3320
- orphanScanComplete = orphanRows.length < CONTENT_IDS_PER_SQL_EXEC;
3321
- const contentIds = orphanRows.map((row) => String(row.content_id));
3462
+ // The page is selected before the orphan predicates so a scan that finds
3463
+ // nothing still reads only one page, not the whole covering index.
3464
+ const pageRows = [...this.sql.exec(`SELECT page.content_id AS content_id,
3465
+ (NOT EXISTS (
3466
+ SELECT 1 FROM content_lifecycle AS lifecycle
3467
+ WHERE lifecycle.content_id = page.content_id
3468
+ )
3469
+ AND ${sqlNoInodeContentReference('page.content_id')}) AS orphaned
3470
+ FROM (
3471
+ SELECT chunks.content_id
3472
+ FROM file_chunks AS chunks
3473
+ WHERE chunks.content_id > ?
3474
+ GROUP BY chunks.content_id
3475
+ ORDER BY chunks.content_id
3476
+ LIMIT ?
3477
+ ) AS page
3478
+ ORDER BY page.content_id`, this.orphanScanCursor, CONTENT_IDS_PER_SQL_EXEC)];
3479
+ orphanScanComplete = pageRows.length < CONTENT_IDS_PER_SQL_EXEC;
3480
+ this.orphanScanCursor = orphanScanComplete
3481
+ ? ''
3482
+ : String(pageRows[pageRows.length - 1].content_id);
3483
+ const contentIds = pageRows
3484
+ .filter((row) => Number(row.orphaned) === 1)
3485
+ .map((row) => String(row.content_id));
3322
3486
  if (contentIds.length > 0) {
3323
3487
  const candidates = contentIds
3324
3488
  .map((_, index) => index === 0 ? 'SELECT ? AS content_id' : 'SELECT ?')
@@ -3480,7 +3644,7 @@ export class SqliteVFS {
3480
3644
  /** Best-effort process.memoryUsage().heapUsed; 0 in DO contexts. */
3481
3645
  _safeHeapUsed() {
3482
3646
  try {
3483
- const mu = globalThis.process?.memoryUsage?.();
3647
+ const mu = nodeHost.process?.memoryUsage?.();
3484
3648
  return Number(mu?.heapUsed) || 0;
3485
3649
  }
3486
3650
  catch {
@@ -3658,7 +3822,7 @@ export class SqliteVFS {
3658
3822
  // `_addToChildrenIndex` uses Set.add so repeated calls are idempotent;
3659
3823
  // gating it on `prior === undefined` was the bug. Counters remain
3660
3824
  // gated correctly so they don't double-count.
3661
- const __diag = (globalThis.process?.env?.NIMBUS_DIAG_INSTALL_PIPELINE === '1');
3825
+ const __diag = installPipelineDiagEnabled();
3662
3826
  const replacedPaths = new Set();
3663
3827
  for (const entry of plan.inodes) {
3664
3828
  const prior = this.inodes.get(entry.path);
@@ -1 +1 @@
1
- {"version":3,"file":"nimbus-workspace.d.ts","sourceRoot":"","sources":["../../src/workspace/nimbus-workspace.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"nimbus-workspace.d.ts","sourceRoot":"","sources":["../../src/workspace/nimbus-workspace.ts"],"names":[],"mappings":"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"}
@@ -220,9 +220,11 @@ function defaultEnv() {
220
220
  * them.
221
221
  */
222
222
  async function registerWasmRuntimes(deps) {
223
- const [{ makeBashRunnerFactory }, { makeCPythonRunnerFactory }, { wasmRunnerSpec }, { buildRuntimeHandler },] = await Promise.all([
223
+ const [{ makeBashRunnerFactory }, { makeCPythonRunnerFactory }, { makeRubyRunnerFactory }, { makeClangRunnerFactory }, { wasmRunnerSpec }, { buildRuntimeHandler },] = await Promise.all([
224
224
  import('../runtime/bash-runner.js'),
225
225
  import('../runtime/cpython-runner.js'),
226
+ import('../runtime/ruby-runner.js'),
227
+ import('../runtime/clang-runner.js'),
226
228
  import('../runtime/wasm-runner.js'),
227
229
  import('../runtime/runtime-registry.js'),
228
230
  ]);
@@ -251,8 +253,13 @@ async function registerWasmRuntimes(deps) {
251
253
  'bash-runner': makeBashRunnerFactory({ facets: deps.facets, vfs: deps.vfs }),
252
254
  // No `startResident`: a workspace owns no actor that could outlive the
253
255
  // call, so a program that keeps serving is refused by name rather than
254
- // run as a one-shot that dies with it.
256
+ // run as a one-shot that dies with it. Same for ruby, where a script is
257
+ // the shape that may bind a port.
255
258
  'cpython-runner': makeCPythonRunnerFactory({ facets: deps.facets, vfs: deps.vfs }),
259
+ 'ruby-runner': makeRubyRunnerFactory({
260
+ facets: deps.facets, vfs: deps.vfs, registry: deps.registry,
261
+ }),
262
+ 'clang-runner': makeClangRunnerFactory({ facets: deps.facets, vfs: deps.vfs }),
256
263
  };
257
264
  rehydrateInstalledRuntimesView(deps.vfs.as(CRED_KERNEL), deps.registry, deps.home, (key) => runners[key]);
258
265
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nimbus-sh/core",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
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",
@@ -45,7 +45,9 @@
45
45
  },
46
46
  "files": [
47
47
  "dist",
48
- "src"
48
+ "src",
49
+ "README.md",
50
+ "LICENSE"
49
51
  ],
50
52
  "scripts": {
51
53
  "build": "tsc -p tsconfig.json --noCheck --noEmit false --declaration true --declarationMap true --outDir dist --rootDir src",
@@ -0,0 +1,21 @@
1
+ /**
2
+ * error-text.ts — the text a caught value carries.
3
+ *
4
+ * A `catch` binding is `unknown`: the throw site may have thrown an Error, a
5
+ * string, or a plain object with a `message`, and the shell and runtime paths
6
+ * report all three the same way. This is that report, in one place, so a
7
+ * command's diagnostics do not depend on which of those a facet threw.
8
+ */
9
+
10
+ /**
11
+ * The message a thrown value names, or the value itself when it names none.
12
+ * Empty and other falsy messages fall through to the value, so an
13
+ * `new Error('')` still reports as `Error` rather than as nothing.
14
+ */
15
+ export function errorText(error: unknown): string {
16
+ if (typeof error === 'object' && error !== null && 'message' in error) {
17
+ const { message } = error;
18
+ if (message) return String(message);
19
+ }
20
+ return String(error);
21
+ }
@@ -7,7 +7,7 @@
7
7
  * 1. Install-time supervisor (TS) — `src/npm-resolver.ts` re-exports the
8
8
  * typed functions for tree resolution.
9
9
  *
10
- * 2. NimbusLoaderPool isolates (JS string) — `src/loaders/pre-bundle-preamble.ts`
10
+ * 2. LoaderPool isolates (JS string) — `src/loaders/pre-bundle-preamble.ts`
11
11
  * embeds `getExportsResolverJS()` as part of the pool preamble so the
12
12
  * pre-bundle facet uses identical resolution semantics to the supervisor.
13
13
  *
@@ -37,6 +37,26 @@ export const DEFAULT_ESM_CONDITIONS = ['import', 'module', 'browser', 'default']
37
37
  /** Default conditions for CJS runtime resolution (user-shell node). */
38
38
  export const DEFAULT_CJS_CONDITIONS = ['require', 'node', 'default'];
39
39
 
40
+ /**
41
+ * A `package.json#exports` / `#imports` value: a target path, an ordered list
42
+ * of fallbacks to try in turn, or a map keyed by subpath (`"./client"`) or by
43
+ * condition (`"import"`) whose values are the same shape again. `null` is the
44
+ * spec's "this subpath is not exported" marker, and blocks fallback.
45
+ */
46
+ export type ExportsField =
47
+ | string
48
+ | null
49
+ | ExportsField[]
50
+ | { [key: string]: ExportsField };
51
+
52
+ /** The package.json fields entry-point resolution reads. */
53
+ export interface ResolvablePackageJson {
54
+ exports?: ExportsField;
55
+ imports?: ExportsField;
56
+ main?: string;
57
+ module?: string;
58
+ }
59
+
40
60
  /**
41
61
  * Resolve `package.json#exports` (or `#imports`) per Node spec.
42
62
  *
@@ -46,7 +66,7 @@ export const DEFAULT_CJS_CONDITIONS = ['require', 'node', 'default'];
46
66
  * @returns Relative path target string, or null if not found / forbidden
47
67
  */
48
68
  export function resolveExports(
49
- exportsField: any,
69
+ exportsField: ExportsField | undefined,
50
70
  subpath: string = '.',
51
71
  conditions: string[] = DEFAULT_ESM_CONDITIONS,
52
72
  ): string | null {
@@ -123,7 +143,7 @@ export function resolveExports(
123
143
  * array (Node spec).
124
144
  */
125
145
  function resolveConditionValue(
126
- target: any,
146
+ target: ExportsField | undefined,
127
147
  conditions: string[],
128
148
  ): string | null {
129
149
  if (target === null || target === undefined) return null;
@@ -162,7 +182,7 @@ function resolveConditionValue(
162
182
  * itself (caller probes filesystem with extension-list).
163
183
  */
164
184
  export function resolvePackageEntry(
165
- pkg: { exports?: any; module?: string; main?: string },
185
+ pkg: ResolvablePackageJson,
166
186
  subpath: string = '.',
167
187
  conditions: string[] = DEFAULT_ESM_CONDITIONS,
168
188
  ): string | null {
@@ -24,6 +24,7 @@
24
24
  */
25
25
 
26
26
  import { disposeRpcResource } from './rpc-dispose.js';
27
+ import { errorText } from './error-text.js';
27
28
 
28
29
  /** Default retry count AFTER the first attempt (3 = up to 4 total attempts). */
29
30
  export const DEFAULT_RETRIES = 3;
@@ -179,7 +180,7 @@ export async function retryableFetch(
179
180
  const delayMs = jittered(BACKOFF_MS[Math.min(attempt, BACKOFF_MS.length - 1)]);
180
181
  opts?.onRetry?.(attempt + 1, totalRetries, delayMs, `HTTP ${resp.status}`);
181
182
  await new Promise<void>((r) => setTimeout(r, delayMs));
182
- } catch (e: any) {
183
+ } catch (e) {
183
184
  if (timer) clearTimeout(timer);
184
185
  // Network-level failure (fetch rejection, timeout via AbortError,
185
186
  // DNS, connection reset). Retry the same way as a 5xx.
@@ -189,7 +190,8 @@ export async function retryableFetch(
189
190
  throw e;
190
191
  }
191
192
  const delayMs = jittered(BACKOFF_MS[Math.min(attempt, BACKOFF_MS.length - 1)]);
192
- const reason = e?.name === 'AbortError' ? 'timeout' : (e?.message || String(e));
193
+ const isAbort = typeof e === 'object' && e !== null && 'name' in e && e.name === 'AbortError';
194
+ const reason = isAbort ? 'timeout' : errorText(e);
193
195
  opts?.onRetry?.(attempt + 1, totalRetries, delayMs, reason);
194
196
  await new Promise<void>((r) => setTimeout(r, delayMs));
195
197
  }