@nimbus-sh/worker 0.8.0 → 0.10.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 (124) hide show
  1. package/dist/esbuild-wasm-bundle.generated.d.ts +24 -24
  2. package/dist/esbuild-wasm-bundle.generated.d.ts.map +1 -1
  3. package/dist/esbuild-wasm-bundle.generated.js +24 -24
  4. package/dist/facets/cirrus-real.js +3 -3
  5. package/dist/facets/compose.d.ts +3 -0
  6. package/dist/facets/compose.d.ts.map +1 -1
  7. package/dist/facets/compose.js +10 -3
  8. package/dist/facets/esbuild-bundle-pool.d.ts +1 -1
  9. package/dist/facets/esbuild-bundle-pool.js +3 -3
  10. package/dist/facets/esbuild-transform.d.ts +6 -2
  11. package/dist/facets/esbuild-transform.d.ts.map +1 -1
  12. package/dist/facets/esbuild-transform.js +8 -4
  13. package/dist/facets/manager.d.ts +54 -15
  14. package/dist/facets/manager.d.ts.map +1 -1
  15. package/dist/facets/manager.js +227 -144
  16. package/dist/facets/real-vite-hmr.d.ts +2 -0
  17. package/dist/facets/real-vite-hmr.d.ts.map +1 -1
  18. package/dist/facets/real-vite-hmr.js +3 -2
  19. package/dist/git/commands.d.ts +17 -1
  20. package/dist/git/commands.d.ts.map +1 -1
  21. package/dist/git/commands.js +73 -9
  22. package/dist/git/network-facet.d.ts +2 -0
  23. package/dist/git/network-facet.d.ts.map +1 -1
  24. package/dist/git/network-facet.js +10 -5
  25. package/dist/hosted/commands.d.ts.map +1 -1
  26. package/dist/hosted/commands.js +28 -39
  27. package/dist/hosted/runtime.d.ts +24 -2
  28. package/dist/hosted/runtime.d.ts.map +1 -1
  29. package/dist/hosted/runtime.js +70 -13
  30. package/dist/hosted/services.d.ts +14 -2
  31. package/dist/hosted/services.d.ts.map +1 -1
  32. package/dist/hosted/services.js +15 -2
  33. package/dist/loaders/generated-workers.d.ts +2 -2
  34. package/dist/loaders/generated-workers.d.ts.map +1 -1
  35. package/dist/loaders/generated-workers.js +4 -4
  36. package/dist/loaders/pre-bundle-preamble.d.ts +6 -3
  37. package/dist/loaders/pre-bundle-preamble.d.ts.map +1 -1
  38. package/dist/loaders/pre-bundle-preamble.js +13 -8
  39. package/dist/node-shims-artifact.generated.js +3 -3
  40. package/dist/npm/cache.d.ts +6 -1
  41. package/dist/npm/cache.d.ts.map +1 -1
  42. package/dist/npm/cache.js +8 -6
  43. package/dist/npm/install-batch-facet.d.ts +2 -0
  44. package/dist/npm/install-batch-facet.d.ts.map +1 -1
  45. package/dist/npm/install-batch-facet.js +29 -15
  46. package/dist/npm/installer.d.ts +8 -4
  47. package/dist/npm/installer.d.ts.map +1 -1
  48. package/dist/npm/installer.js +173 -113
  49. package/dist/npm/npx-install.d.ts +3 -1
  50. package/dist/npm/npx-install.d.ts.map +1 -1
  51. package/dist/npm/npx-install.js +4 -1
  52. package/dist/npm/placement.d.ts +16 -0
  53. package/dist/npm/placement.d.ts.map +1 -0
  54. package/dist/npm/placement.js +33 -0
  55. package/dist/npm/r2-cache.d.ts +10 -6
  56. package/dist/npm/r2-cache.d.ts.map +1 -1
  57. package/dist/npm/r2-cache.js +32 -16
  58. package/dist/npm/resolve-one-facet.d.ts +3 -0
  59. package/dist/npm/resolve-one-facet.d.ts.map +1 -1
  60. package/dist/npm/resolve-one-facet.js +1 -0
  61. package/dist/npm/resolver.d.ts +11 -6
  62. package/dist/npm/resolver.d.ts.map +1 -1
  63. package/dist/npm/resolver.js +12 -3
  64. package/dist/runtime/bash-repl.d.ts +5 -3
  65. package/dist/runtime/bash-repl.d.ts.map +1 -1
  66. package/dist/runtime/bash-repl.js +8 -3
  67. package/dist/runtime/cpython-resident.d.ts.map +1 -1
  68. package/dist/runtime/cpython-resident.js +2 -10
  69. package/dist/runtime/esbuild-wasm-bytes.d.ts +25 -18
  70. package/dist/runtime/esbuild-wasm-bytes.d.ts.map +1 -1
  71. package/dist/runtime/esbuild-wasm-bytes.js +55 -30
  72. package/dist/runtime/facet-loader-host.d.ts.map +1 -1
  73. package/dist/runtime/facet-loader-host.js +0 -9
  74. package/dist/runtime/node-shims.d.ts.map +1 -1
  75. package/dist/runtime/node-shims.js +853 -181
  76. package/dist/runtime/python-repl.d.ts +4 -3
  77. package/dist/runtime/python-repl.d.ts.map +1 -1
  78. package/dist/runtime/python-repl.js +10 -39
  79. package/dist/runtime/ruby-repl.d.ts +3 -2
  80. package/dist/runtime/ruby-repl.d.ts.map +1 -1
  81. package/dist/runtime/ruby-repl.js +7 -6
  82. package/dist/runtime/ruby-resident.d.ts.map +1 -1
  83. package/dist/runtime/ruby-resident.js +5 -16
  84. package/dist/runtime/runtime-catalog.js +1 -1
  85. package/dist/runtime/wasi/preamble.d.ts +0 -22
  86. package/dist/runtime/wasi/preamble.d.ts.map +1 -1
  87. package/dist/runtime/wasi/preamble.js +160 -1445
  88. package/dist/runtime-catalog.generated.js +1 -1
  89. package/dist/session/hibernation.d.ts +45 -2
  90. package/dist/session/hibernation.d.ts.map +1 -1
  91. package/dist/session/hibernation.js +78 -1
  92. package/dist/session/init.d.ts.map +1 -1
  93. package/dist/session/init.js +1 -0
  94. package/dist/session/nimbus-session.d.ts +16 -23
  95. package/dist/session/nimbus-session.d.ts.map +1 -1
  96. package/dist/session/nimbus-session.js +25 -36
  97. package/dist/session/npm-install-port.d.ts.map +1 -1
  98. package/dist/session/npm-install-port.js +1 -0
  99. package/dist/session/port-capability.d.ts.map +1 -1
  100. package/dist/session/port-capability.js +11 -1
  101. package/dist/session/programmatic.js +2 -2
  102. package/dist/session/routes.d.ts.map +1 -1
  103. package/dist/session/routes.js +4 -2
  104. package/dist/session/rpc.d.ts +7 -44
  105. package/dist/session/rpc.d.ts.map +1 -1
  106. package/dist/session/rpc.js +17 -67
  107. package/dist/session/supervisor-op.d.ts +8 -3
  108. package/dist/session/supervisor-op.d.ts.map +1 -1
  109. package/dist/session/supervisor-op.js +10 -45
  110. package/dist/session/supervisor-rpc.d.ts +47 -22
  111. package/dist/session/supervisor-rpc.d.ts.map +1 -1
  112. package/dist/session/supervisor-rpc.js +64 -26
  113. package/dist/vfs/facet-resident-store.d.ts.map +1 -1
  114. package/dist/vfs/facet-resident-store.js +4 -2
  115. package/dist/workspace-host.d.ts +2 -1
  116. package/dist/workspace-host.d.ts.map +1 -1
  117. package/dist/wrangler/nimbus-wrangler.d.ts.map +1 -1
  118. package/dist/wrangler/nimbus-wrangler.js +3 -0
  119. package/package.json +30 -4
  120. package/public/_assets/esbuild-0.24.2.js +2380 -0
  121. package/public/_assets/runtime/{node-shims-8f8c64edd2a1f0f5.js → node-shims-7b4dd7a76d95d21e.js} +852 -180
  122. package/scripts/bundle-esbuild-wasm.mjs +55 -39
  123. package/scripts/bundle-facet-workers.mjs +45 -7
  124. package/scripts/bundle-runtime.mjs +35 -8
@@ -1,3 +1,6 @@
1
+ import { installAuthorityFilesystem, WASI_ACCEPTED_PATH_PREFIX, WASI_LISTEN_PATH_PREFIX, WASI_TCP_PATH_PREFIX, } from '@nimbus-sh/core/runtime/wasi/filesystem.js';
2
+ import { supervisorFilesystem } from '@nimbus-sh/core/runtime/vfs-supervisor.js';
3
+ import { WASI_RESIDENT_FILE_CAP_BYTES } from '@nimbus-sh/core/constants.js';
1
4
  // errno constants
2
5
  const __WASI_ESUCCESS = 0;
3
6
  const __WASI_EAGAIN = 6;
@@ -65,21 +68,6 @@ const __WASI_EVENTTYPE_CLOCK = 0;
65
68
  const __WASI_EVENTTYPE_FD_READ = 1;
66
69
  const __WASI_EVENTTYPE_FD_WRITE = 2;
67
70
  const __WASI_SUBCLOCKFLAGS_ABSTIME = 1; // SUBSCRIPTION_CLOCK_ABSTIME
68
- // WASI socket and polling support B7: synthetic-path prefix recognised by path_open as a request
69
- // to open a TCP socket. Mirrors bash's /dev/tcp/<host>/<port> convention
70
- // (https://www.gnu.org/software/bash/manual/html_node/Redirections.html).
71
- const __WASI_TCP_PATH_PREFIX = '/dev/tcp/';
72
- // The accept half of the same synthetic-socket family: binds a connection the
73
- // virtual socket kernel has already accepted to a file descriptor. It has to go
74
- // through path_open like the dial half - guests layered over wasi-vfs
75
- // (ruby.wasm) resolve descriptors through their own fd table, so a descriptor
76
- // handed to them out of band is not one they can use.
77
- const __WASI_ACCEPTED_PATH_PREFIX = '/dev/nimbus/socket/';
78
- // A listening socket, as a descriptor. Reading it is accept(2): the read
79
- // suspends until a connection is queued and yields that connection's id, so a
80
- // server's accept loop is an ordinary blocking read and needs no cooperative
81
- // pump driving it from outside.
82
- const __WASI_LISTEN_PATH_PREFIX = '/dev/nimbus/listen/';
83
71
  // WASI socket and polling support B7: resolved at preamble module-init via dynamic import.
84
72
  // CF docs: "TCP sockets cannot be created in global scope and shared
85
73
  // across requests" — so we only IMPORT the module here (cheap symbol
@@ -100,49 +88,28 @@ catch (__e) {
100
88
  class __WasiExit {
101
89
  constructor(code) { this.code = code | 0; }
102
90
  }
103
- // ─── Virtual filesystem state ───────────────────────────────────────────
91
+ // The device a pathless descriptor (stdio, a socket) reports in fd_filestat_get.
92
+ // Above any device the authority can hand out, so a fd-numbered inode on it
93
+ // never collides with a file's (dev, ino).
94
+ const __WASI_STREAM_DEV = 1n << 32n;
95
+ // ─── Filesystem state ───────────────────────────────────────────────────
104
96
  //
105
- // __wasiInitFS({ root, preopens, files, dirs, modes, times?, symlinks? }) —
106
- // install a per-call FS:
107
- // root string — canonical session root, e.g. 'home/user/wasi-files'.
97
+ // __wasiInitFS({ root, preopens, residentFileCap? }) installs a per-call
98
+ // filesystem view:
99
+ // root string — canonical session root, e.g. 'home/user'.
108
100
  // preopens Array<{ wasiPath, vfsPath }> — fd>=3 preopens (in order).
109
- // files Record<vfsPath, base64 string> — initial file contents.
110
- // dirs Array<vfsPath> — initial directory list.
111
- // modes Record<vfsPath, effective rwx bits> — access already resolved
112
- // for the invoking credential by the supervisor.
113
- // times Record<vfsPath, {mtime, atime, ctime}> | undefined
114
- // — per-path nanosecond timestamps (WASI socket and polling support B1). Optional;
115
- // paths without entries default to "now" at init time.
116
- // Values are decimal strings to survive JSON.serialize
117
- // (BigInt is not JSON-safe); converted to BigInt internally.
118
- // symlinks Record<vfsPath, targetPath> | undefined
119
- // — WASI socket and polling support B3 in-memory symlink table. Optional; defaults
120
- // to empty. Target paths are canonicalized at insertion.
121
101
  //
122
- // Mutations go back to the session VFS as they happen, through the supervisor
123
- // stub __wasiAdoptSupervisor installs. A sealed instance (no stub) keeps them
124
- // in memory and its caller names what it wants back via __wasiReadFilesB64.
102
+ // There is no in-facet copy of the filesystem. Every file and directory
103
+ // syscall is answered by the authority codec (wasi/filesystem.ts) through the
104
+ // supervisor stub __wasiAdoptSupervisor installs, reading and writing the same
105
+ // inodes the shell does. What this module keeps is the descriptor table's
106
+ // non-file kinds: stdio, sockets, listeners and the preopen roots.
125
107
  //
126
- // An empty filesystem from the first instruction, replaced wholesale by
127
- // __wasiInitFS. It used to start null, which 113 syscall bodies then
128
- // dereferenced without checking — a fact only visible once a type checker read
129
- // them. Eight other places DID check, and answered null/0/false, so the same
130
- // pre-init read was a TypeError down one path and a plausible wrong answer down
131
- // another. wasm-runner worked around it by calling __wasiInitFS with an empty
132
- // seed purely so "__wasiFS isn't null when WASI fns are called".
133
- //
134
- // Starting empty makes the invariant structural instead of a caller's duty: a
135
- // syscall before init now answers ENOENT against an empty filesystem, which is
136
- // true, rather than trapping the guest or being individually guarded 113 times.
108
+ // Starting empty rather than null keeps the invariant structural: a syscall
109
+ // before init answers EBADF against an empty descriptor table, which is true,
110
+ // rather than trapping the guest.
137
111
  function __wasiEmptyFS() {
138
- return {
139
- root: '',
140
- files: new Map(), dirs: new Set(), times: new Map(), symlinks: new Map(),
141
- modes: new Map(), sizes: new Map(), origFiles: new Map(),
142
- residentFileCap: 8 * 1024 * 1024,
143
- enumeratedRoots: [],
144
- revision: null,
145
- };
112
+ return { root: '', residentFileCap: WASI_RESIDENT_FILE_CAP_BYTES };
146
113
  }
147
114
  let __wasiFS = __wasiEmptyFS();
148
115
  let __wasiPreopens = [];
@@ -157,186 +124,21 @@ let __wasiPreopens = [];
157
124
  let __wasiThreads = null;
158
125
  // ── Live backing store ───────────────────────────────────────────────────
159
126
  //
160
- // The supervisor stub, when present, makes __wasiFS a CACHE of the session
161
- // VFS rather than a closed world. Absent (unit tests, pools that legitimately
162
- // run sealed) the seed is authoritative and every path below degrades to
163
- // pure in-memory behavior.
127
+ // The supervisor stub. Every filesystem call the guest makes is answered by
128
+ // the authority codec (wasi/filesystem.ts) through it; absent (unit tests,
129
+ // compute-only instances) the guest has stdio, sockets and clocks but no
130
+ // files, and a file syscall answers EBADF.
164
131
  let __wasiSup = null;
165
- const __wasiPersistQ = { pending: [], tail: Promise.resolve(), failures: [] };
166
132
  // Adopting is idempotent and never downgrades. A resident process re-enters
167
133
  // through routed fetch/handleHttpRequest hops that resolve the entrypoint
168
134
  // WITHOUT a supervisor in env; clearing the live stub on those hops would
169
- // strand the process holding a cache it can no longer write back.
135
+ // strand the process.
170
136
  export function __wasiAdoptSupervisor(sup) {
171
137
  if (sup)
172
138
  __wasiSup = sup;
173
139
  }
174
- /**
175
- * True when the seed deliberately shipped no content and is relying on a
176
- * supervisor to back it. Only manifestVfs stamps a revision, so this is
177
- * exactly "the producer expected this filesystem to be live" — as opposed to
178
- * a sealed instance, whose seed IS the whole filesystem by design.
179
- */
180
- function __wasiExpectsLiveBacking() {
181
- return __wasiFS.revision !== null && __wasiFS.revision !== undefined;
182
- }
183
- /** Mirror one mutation to the session VFS. Cache is already updated. */
184
- function __wasiEnqueue(op, run) {
185
- if (!__wasiSup) {
186
- // A sealed instance keeps its mutations in memory on purpose. One that was
187
- // seeded as a CACHE and has no supervisor cannot: the write is already
188
- // gone. Recording it makes the next drain throw, which is the whole reason
189
- // the exit-time diff could be deleted — silence here would put the data
190
- // loss back, just without the second implementation.
191
- if (__wasiExpectsLiveBacking()) {
192
- __wasiPersistQ.failures.push(op + ': no supervisor adopted for a live-backed filesystem');
193
- }
194
- return;
195
- }
196
- // Captured HERE rather than read when the continuation runs. __wasiInitFS
197
- // nulls __wasiSup and resets the queue tail, but resetting the tail does not
198
- // cancel continuations already chained to the old one — so a pooled isolate
199
- // that re-inits with work in flight used to hand the previous program's op a
200
- // null stub, lose that mutation, and then throw the resulting TypeError out
201
- // of the NEXT program's drain. An op belongs to the process that queued it.
202
- const sup = __wasiSup;
203
- const entry = { op };
204
- __wasiPersistQ.pending.push(entry);
205
- __wasiPersistQ.tail = __wasiPersistQ.tail.then(async () => {
206
- try {
207
- await run(sup);
208
- }
209
- catch (e) {
210
- // A failed write-back is data loss and must be visible, not swallowed.
211
- __wasiPersistQ.failures.push(op + ': ' + ((e && e.message) || String(e)));
212
- }
213
- finally {
214
- const i = __wasiPersistQ.pending.indexOf(entry);
215
- if (i >= 0)
216
- __wasiPersistQ.pending.splice(i, 1);
217
- }
218
- });
219
- }
220
- // Paths with a write-back already queued. The queued op reads the cache when
221
- // it runs, so a burst of fd_writes to one file coalesces into one round trip
222
- // instead of one per write.
223
- const __wasiDirty = new Set();
224
- // Paths a live stat has already reported absent. Cleared whenever anything is
225
- // created or the cache is revalidated, so a negative never outlives the fact.
226
- const __wasiNegative = new Set();
227
- function __wasiPersistFile(vfsPath) {
228
- // No supervisor check here: __wasiEnqueue is the one place that decides what
229
- // a missing one means. Marking the path dirty first also means a burst of
230
- // writes to it reports at most once.
231
- if (__wasiDirty.has(vfsPath))
232
- return;
233
- __wasiDirty.add(vfsPath);
234
- // The bytes are read when the op RUNS, which is what coalesces a burst of
235
- // fd_writes into one round trip. The filesystem they are read from is pinned
236
- // here, though: reading the global instead meant a re-init between the queue
237
- // and the drain looked up the path in the NEXT program's empty cache, found
238
- // nothing, and returned successfully having written nothing at all.
239
- const fs = __wasiFS;
240
- __wasiEnqueue('writeFile ' + vfsPath, async (sup) => {
241
- __wasiDirty.delete(vfsPath);
242
- const bytes = fs && fs.files.get(vfsPath);
243
- if (!bytes)
244
- return;
245
- await sup.writeFile(vfsPath, bytes);
246
- });
247
- }
248
- /**
249
- * Mirror an explicit utimensat. Deliberately NOT reached from the implicit
250
- * mtime bump every write does — writeFile already sets it, and a round trip
251
- * per fd_write would be one syscall's cost paid by all of them.
252
- */
253
- function __wasiPersistTimes(vfsPath) {
254
- const t = __wasiFS.times.get(vfsPath);
255
- if (!t)
256
- return;
257
- const atimeMs = Number(t.atime / 1000000n);
258
- const mtimeMs = Number(t.mtime / 1000000n);
259
- __wasiEnqueue('utimes ' + vfsPath, (sup) => sup.utimes(vfsPath, atimeMs, mtimeMs));
260
- }
261
- function __wasiPersistSymlink(target, vfsPath) {
262
- __wasiEnqueue('symlink ' + vfsPath, (sup) => sup.symlink(target, vfsPath));
263
- }
264
- /**
265
- * Drop cached content that matches what the supervisor already has, turning
266
- * those inodes back into manifest entries so the next read refetches. Content
267
- * with a write still queued is never dropped. Returns the paths evicted.
268
- *
269
- * This is what makes the cache a cache: memory pressure and staleness are both
270
- * answered by forgetting, never by serving something known to be old.
271
- */
272
- function __wasiEvictCleanContent() {
273
- if (!__wasiFS || !__wasiSup)
274
- return [];
275
- const evicted = [];
276
- for (const [path, bytes] of [...__wasiFS.files]) {
277
- if (__wasiDirty.has(path))
278
- continue;
279
- const orig = __wasiFS.origFiles.get(path);
280
- if (!orig || orig.length !== bytes.length)
281
- continue;
282
- let same = true;
283
- for (let i = 0; i < orig.length; i++) {
284
- if (orig[i] !== bytes[i]) {
285
- same = false;
286
- break;
287
- }
288
- }
289
- if (!same)
290
- continue;
291
- __wasiFS.files.delete(path);
292
- __wasiFS.sizes.set(path, bytes.length);
293
- evicted.push(path);
294
- }
295
- return evicted;
296
- }
297
- /**
298
- * Re-sync the cache with the session VFS. A resident process (a server) must
299
- * see files that other processes created or changed after it spawned, so the
300
- * cached content is dropped and metadata re-read on next access.
301
- */
302
- export async function __wasiRevalidateFS() {
303
- if (!__wasiSup || !__wasiFS)
304
- return [];
305
- await __wasiDrainPersist();
306
- // One round trip decides it. An unchanged subtree means the cache is still
307
- // exactly right, so a resident server parking between requests pays a single
308
- // revision check rather than re-reading everything it had already loaded.
309
- if (typeof __wasiSup.fsRevision === 'function') {
310
- let revision;
311
- try {
312
- revision = await __wasiSup.fsRevision(__wasiFS.root);
313
- }
314
- catch {
315
- revision = null;
316
- }
317
- if (revision !== null && revision === __wasiFS.revision)
318
- return [];
319
- __wasiFS.revision = revision;
320
- }
321
- __wasiNegative.clear();
322
- return __wasiEvictCleanContent();
323
- }
324
- /**
325
- * Await every queued mutation. Callers drain before returning a result to the
326
- * supervisor and before any live read, so the supervisor's view always
327
- * includes this process's own writes.
328
- */
329
- export async function __wasiDrainPersist() {
330
- await __wasiPersistQ.tail;
331
- if (__wasiPersistQ.failures.length > 0) {
332
- const failures = __wasiPersistQ.failures.slice();
333
- __wasiPersistQ.failures.length = 0;
334
- throw new Error('wasi persist failed: ' + failures.join('; '));
335
- }
336
- }
337
- // All paths are stored in canonical form: no leading '/', no '..', no
338
- // double slashes. The wasm program sees '/foo/bar.txt' (with leading
339
- // slash) but the VFS keys are 'home/user/.../foo/bar.txt'.
140
+ // A guest-visible path in canonical form: no leading '/', no '..', no double
141
+ // slashes. The socket path prefixes are matched against it.
340
142
  function __wasiCanonicalize(p) {
341
143
  if (!p)
342
144
  return '';
@@ -354,122 +156,20 @@ function __wasiCanonicalize(p) {
354
156
  }
355
157
  return out.join('/');
356
158
  }
357
- // Wall-clock at init, used as the default mtime/atime/ctime for any path
358
- // that arrives without an explicit times entry. Captured once per call so
359
- // all "default-init" timestamps within a call share a value (mtime ==
360
- // atime == ctime), matching what a real cold-load would produce.
361
- // One device for this filesystem, and a stable distinct inode per path.
362
- //
363
- // A guest is entitled to treat (st_dev, st_ino) as an identity: LLVM's
364
- // FileManager keys its directory cache on exactly that pair, GNU make and
365
- // find -samefile compare it, and rsync uses it to detect hardlinks. Emitting
366
- // a constant zero collapses every inode into one — clang then searched a
367
- // single include directory and reported every system header missing. FNV-1a
368
- // over the canonical path is stable across calls within a run and across
369
- // processes seeded from the same tree, which is what those callers need.
370
- const __WASI_DEV = 1n;
371
- const __WASI_INO_MASK = 0xffffffffffffffffn;
372
- function __wasiInode(path) {
373
- let h = 14695981039346656037n;
374
- const p = String(path === undefined || path === null ? '' : path);
375
- for (let i = 0; i < p.length; i++) {
376
- h = (h ^ BigInt(p.charCodeAt(i) & 0xff)) * 1099511628211n & __WASI_INO_MASK;
377
- }
378
- // 0 is the "no inode" value a caller may test for; never hand it out.
379
- return h === 0n ? 1n : h;
380
- }
381
- function __wasiNowNs() {
382
- // Date.now() is ms since epoch; multiply by 1e6 → ns.
383
- return BigInt(Date.now()) * 1000000n;
384
- }
385
159
  export function __wasiInitFS(opts) {
386
- // A fresh init is a fresh process: no supervisor adopted yet, and none of
387
- // the previous tenant's queued writes, dirty paths or negative lookups may
388
- // survive into it. Pools reuse an isolate across calls, so leaking any of
389
- // this would let one program's state answer another program's syscalls.
160
+ // A fresh init is a fresh process: no supervisor adopted yet. Pools reuse an
161
+ // isolate across calls, so the previous tenant's stub must not answer the
162
+ // next program's syscalls.
390
163
  __wasiSup = null;
391
- __wasiDirty.clear();
392
- __wasiNegative.clear();
393
- __wasiPersistQ.pending.length = 0;
394
- __wasiPersistQ.failures.length = 0;
395
- __wasiPersistQ.tail = Promise.resolve();
396
- const files = new Map(); // canonicalVfsPath → Uint8Array
397
- const dirs = new Set(); // canonicalVfsPath
398
- // WASI socket and polling support B1: parallel timestamp + symlink maps.
399
- const times = new Map(); // canonicalVfsPath → {mtime: BigInt, atime: BigInt, ctime: BigInt}
400
- const symlinks = new Map(); // canonicalVfsPath → targetPath (canonical)
401
- const modes = new Map(); // canonicalVfsPath → effective rwx bits
402
- // Mirror of the content the seed carried (and of anything demand-loaded),
403
- // so the evictor can tell a cached-but-unchanged file from a dirty one.
404
- const origFiles = new Map();
405
- const nowNs = __wasiNowNs();
406
- for (const [path, b64] of Object.entries(opts.files || {})) {
407
- const canon = __wasiCanonicalize(path);
408
- const bin = atob(b64);
409
- const u8 = new Uint8Array(bin.length);
410
- for (let i = 0; i < bin.length; i++)
411
- u8[i] = bin.charCodeAt(i);
412
- files.set(canon, u8);
413
- origFiles.set(canon, u8.slice()); // copy so subsequent mutations detect change
414
- // Default timestamps (overwritten below if opts.times has this path).
415
- times.set(canon, { mtime: nowNs, atime: nowNs, ctime: nowNs });
416
- }
417
- for (const path of opts.dirs || []) {
418
- const canon = __wasiCanonicalize(path);
419
- dirs.add(canon);
420
- if (!times.has(canon))
421
- times.set(canon, { mtime: nowNs, atime: nowNs, ctime: nowNs });
422
- }
423
- for (const [path, mode] of Object.entries(opts.modes)) {
424
- modes.set(__wasiCanonicalize(path), Number(mode) & 7);
425
- }
426
- // WASI socket and polling support B1: load explicit per-path timestamps if supervisor provided them.
427
- // Values arrive as decimal strings (BigInt → JSON.stringify safe) or numbers.
428
- for (const [path, t] of Object.entries(opts.times || {})) {
429
- const canon = __wasiCanonicalize(path);
430
- const cur = times.get(canon);
431
- if (!cur)
432
- continue; // path not yet in files/dirs; skip
433
- const mtime = (t && t.mtime !== undefined) ? BigInt(t.mtime) : cur.mtime;
434
- const atime = (t && t.atime !== undefined) ? BigInt(t.atime) : cur.atime;
435
- const ctime = (t && t.ctime !== undefined) ? BigInt(t.ctime) : cur.ctime;
436
- times.set(canon, { mtime, atime, ctime });
437
- }
438
- // WASI socket and polling support B3: load explicit symlinks if supervisor provided them.
439
- for (const [path, target] of Object.entries(opts.symlinks || {})) {
440
- const canon = __wasiCanonicalize(path);
441
- symlinks.set(canon, String(target));
442
- }
443
- // Metadata-only entries: the manifest knows the file and its size, the seed
444
- // did not carry its bytes. Content arrives on first read via the supervisor.
445
- // A path here but absent from the files map is a cache miss, never ENOENT.
446
- const sizes = new Map();
447
- for (const [path, size] of Object.entries(opts.sizes || {})) {
448
- const canon = __wasiCanonicalize(path);
449
- sizes.set(canon, Number(size));
450
- if (!times.has(canon))
451
- times.set(canon, { mtime: nowNs, atime: nowNs, ctime: nowNs });
452
- }
453
164
  __wasiFS = {
454
165
  root: __wasiCanonicalize(opts.root || ''),
455
- files, dirs, times, symlinks, modes, sizes,
456
- origFiles,
457
- // Files at or above this size are never held whole; reads window through
458
- // the supervisor instead. Keeps a 200 MiB blob from ending the isolate.
459
- residentFileCap: Number(opts.residentFileCap ?? (8 * 1024 * 1024)),
460
- // Roots the seed claims to have listed COMPLETELY. Only a producer that
461
- // walked a subtree without exclusions may claim one. Inside such a root a
462
- // path the manifest lacks is genuinely absent, so the miss is answered
463
- // here instead of costing a round trip — which is most of the traffic a
464
- // language runtime generates ($LOAD_PATH / sys.path probing walks
465
- // candidate names that mostly do not exist).
466
- enumeratedRoots: (opts.enumeratedRoots || []).map(__wasiCanonicalize),
467
- // Supervisor revision the seed was built against; a park re-checks it and
468
- // only then drops cached content. Without this the cache could serve
469
- // content another process has since changed.
470
- revision: opts.revision ?? null,
166
+ // Largest regular file the codec answers from a resident copy.
167
+ residentFileCap: Number(opts.residentFileCap ?? WASI_RESIDENT_FILE_CAP_BYTES),
471
168
  };
472
169
  // Reset fd table baseline; install preopens as fd 3, 4, 5, ...
170
+ // Emptied rather than replaced: the authority codec and the socket helpers
171
+ // are handed this Map, and a swap would leave half of them writing into a
172
+ // table nothing reads.
473
173
  __wasiPreopens = [];
474
174
  fdTable.clear();
475
175
  fdTable.set(0, { kind: 'stdin' });
@@ -477,14 +177,10 @@ export function __wasiInitFS(opts) {
477
177
  fdTable.set(2, { kind: 'stderr' });
478
178
  nextFd = 3;
479
179
  for (const po of (opts.preopens || [])) {
480
- const fd = nextFd++;
180
+ const fd = __wasiAllocateFd();
481
181
  const vfsPath = __wasiCanonicalize(po.vfsPath);
482
182
  fdTable.set(fd, { kind: 'preopen', wasiPath: po.wasiPath, vfsPath });
483
183
  __wasiPreopens.push({ fd, wasiPath: po.wasiPath, vfsPath });
484
- if (!dirs.has(vfsPath))
485
- dirs.add(vfsPath);
486
- if (!times.has(vfsPath))
487
- times.set(vfsPath, { mtime: nowNs, atime: nowNs, ctime: nowNs });
488
184
  }
489
185
  }
490
186
  // The guest-visible absolute path a path_* call names. wasi-libc resolves an
@@ -565,7 +261,7 @@ function __wasiConnectRemote(host, port) {
565
261
  // Returns a WASI errno and writes the new fd to fdOutPtr.
566
262
  function __wasiOpenTcpSocket(pathArg, fdflags, fdOutPtr, writeU32LE) {
567
263
  // pathArg shape: "/dev/tcp/<host>/<port>".
568
- const tail = pathArg.substring(__WASI_TCP_PATH_PREFIX.length);
264
+ const tail = pathArg.substring(WASI_TCP_PATH_PREFIX.length);
569
265
  const slashIdx = tail.lastIndexOf('/');
570
266
  if (slashIdx <= 0 || slashIdx === tail.length - 1)
571
267
  return __WASI_EINVAL;
@@ -593,7 +289,7 @@ function __wasiKernelOrNull(what) {
593
289
  // Open a listening socket as a descriptor. The port must already be bound; the
594
290
  // descriptor is the accept queue, not the bind.
595
291
  function __wasiOpenListener(pathArg, fdflags, fdOutPtr, writeU32LE) {
596
- const port = parseInt(pathArg.substring(__WASI_LISTEN_PATH_PREFIX.length), 10);
292
+ const port = parseInt(pathArg.substring(WASI_LISTEN_PATH_PREFIX.length), 10);
597
293
  if (!Number.isInteger(port) || port <= 0 || port >= 65536)
598
294
  return __WASI_EINVAL;
599
295
  const kernel = __wasiKernelOrNull('ports cannot be listened on');
@@ -620,7 +316,7 @@ function __wasiOpenListener(pathArg, fdflags, fdOutPtr, writeU32LE) {
620
316
  if (typeof announce === 'function')
621
317
  announce(port);
622
318
  }
623
- const fd = nextFd++;
319
+ const fd = __wasiAllocateFd();
624
320
  fdTable.set(fd, { kind: 'listener', port, fdflags: fdflags | 0 });
625
321
  writeU32LE(fdOutPtr, fd);
626
322
  return __WASI_ESUCCESS;
@@ -670,7 +366,7 @@ async function __wasiAcceptRead(entry, iovsPtr, iovsLen, nreadPtr, writeU32LE, v
670
366
  // Bind an already-accepted kernel connection to a file descriptor, so a
671
367
  // server's accepted socket is the same kind of fd as a client's dialed one.
672
368
  function __wasiOpenAcceptedSocket(pathArg, fdflags, fdOutPtr, writeU32LE) {
673
- const id = parseInt(pathArg.substring(__WASI_ACCEPTED_PATH_PREFIX.length), 10);
369
+ const id = parseInt(pathArg.substring(WASI_ACCEPTED_PATH_PREFIX.length), 10);
674
370
  if (!Number.isInteger(id) || id <= 0)
675
371
  return __WASI_EINVAL;
676
372
  const kernel = globalThis.__nimbusVirtualSockets;
@@ -695,7 +391,7 @@ function __wasiOpenAcceptedSocket(pathArg, fdflags, fdOutPtr, writeU32LE) {
695
391
  // connection, or an accepted one. Every socket fd in the table comes from here,
696
392
  // which is why nothing downstream has to tell them apart.
697
393
  function __wasiAdoptSocket(socket, fdflags) {
698
- const fd = nextFd++;
394
+ const fd = __wasiAllocateFd();
699
395
  fdTable.set(fd, {
700
396
  kind: 'socket',
701
397
  socket,
@@ -710,203 +406,26 @@ function __wasiAdoptSocket(socket, fdflags) {
710
406
  });
711
407
  return fd;
712
408
  }
713
- // WASI socket and polling support B1+B2 helpers: update tracked timestamps for a path. Idempotent.
714
- // Caller passes nanosecond BigInt(s); pass null for fields to keep unchanged.
715
- function __wasiTouchTimes(canonPath, mtimeNs, atimeNs, ctimeNs) {
716
- const cur = __wasiFS.times.get(canonPath);
717
- if (cur) {
718
- if (mtimeNs !== null)
719
- cur.mtime = mtimeNs;
720
- if (atimeNs !== null)
721
- cur.atime = atimeNs;
722
- if (ctimeNs !== null)
723
- cur.ctime = ctimeNs;
724
- }
725
- else {
726
- const now = __wasiNowNs();
727
- __wasiFS.times.set(canonPath, {
728
- mtime: mtimeNs !== null ? mtimeNs : now,
729
- atime: atimeNs !== null ? atimeNs : now,
730
- ctime: ctimeNs !== null ? ctimeNs : now,
731
- });
732
- }
733
- }
734
- // Convenience: bump mtime + ctime to "now" on a write/create.
735
- function __wasiBumpMtime(canonPath) {
736
- const now = __wasiNowNs();
737
- __wasiTouchTimes(canonPath, now, null, now);
738
- }
739
- /**
740
- * Read named files back out as base64.
741
- *
742
- * A process with a supervisor has already written everything through as it
743
- * happened and never calls this. A SEALED one — no supervisor, by design,
744
- * because it must not be able to reach the session VFS at all — has no other
745
- * channel, so its caller names the paths it wants and gets exactly those.
746
- * A path with no file is simply absent from the result.
747
- */
748
- export function __wasiReadFilesB64(paths) {
749
- const out = {};
750
- for (const path of paths) {
751
- const canon = __wasiCanonicalize(path);
752
- const bytes = __wasiFS.files.get(canon);
753
- if (!bytes)
754
- continue;
755
- const CHUNK = 0x8000;
756
- let s = '';
757
- for (let i = 0; i < bytes.length; i += CHUNK) {
758
- s += String.fromCharCode.apply(null, bytes.subarray(i, Math.min(i + CHUNK, bytes.length)));
759
- }
760
- out[path] = btoa(s);
761
- }
762
- return out;
763
- }
764
409
  // ─── fd table ──────────────────────────────────────────────────────────
765
410
  //
766
- // Entry shapes:
411
+ // Entry shapes this module owns:
767
412
  // { kind: 'stdin' | 'stdout' | 'stderr' }
768
413
  // { kind: 'preopen', wasiPath, vfsPath, rights? }
769
- // { kind: 'file', vfsPath, offset, oflags, fdflags, rights? }
770
- // { kind: 'dir', vfsPath, readdirEntries: null | Array, cookie, rights? }
771
- // { kind: 'socket', socket, reader, writer, readBuf, ... }
414
+ // { kind: 'socket' | 'listener', socket, reader, writer, readBuf, ... }
415
+ // and the authority codec's (wasi/filesystem.ts), which shares the table:
416
+ // { kind: 'authority', handle, type, rights, ... } a live descriptor
417
+ // { kind: 'resident', stat, bytes, position, ... } a read-only copy
772
418
  //
773
419
  // WASI socket and polling support B6: 'rights' is an optional BigInt mask. When set, fd_fdstat_get
774
420
  // returns it (instead of the wide-open default). fd_fdstat_set_rights
775
- // writes to it. The mask is advisory in this shim (we don't enforce per-fn
776
- // rights checks in v1 — single-tenant facet, no untrusted callers).
421
+ // writes to it. The mask is advisory for this module's own kinds; the codec
422
+ // enforces rights on its descriptors.
777
423
  export const fdTable = new Map();
778
424
  let nextFd = 3;
779
- // Resolve a WASI path against a preopen fd; returns the canonical VFS path
780
- // WITHOUT following symlinks. WASI socket and polling support kept this name compatible with all
781
- // existing call sites (~10 of them inside the imports object).
782
- //
783
- // wasi-path-fix: a single preopen { wasiPath: '/', vfsPath: 'home/user' }
784
- // exposes the user's home as the wasm-side root. wasi-libc strips '/'
785
- // from absolute paths and hands the rest to path_open. For "/tmp/x" the
786
- // stripped path "tmp/x" + vfsPath "home/user" yields "home/user/tmp/x"
787
- // (correct). For "/home/user/x" the stripped path "home/user/x" +
788
- // vfsPath "home/user" yields "home/user/home/user/x" — the file lands
789
- // at the wrong location. The user mental model is that "/home/user/x"
790
- // IS the user's file at home/user/x in the VFS.
791
- //
792
- // Fix: in __wasiResolvePath, when baseFd is a preopen AND the supplied
793
- // pathStr already starts with the preopen's vfsPath (followed by '/' or
794
- // end-of-string), strip the vfsPath prefix BEFORE prepending it. This
795
- // makes "/home/user/x" → wasi-libc strip → "home/user/x" → resolver
796
- // strip vfsPath → "x" → final canonical "home/user/x" ✓. Paths that
797
- // don't share the vfsPath prefix (like "tmp/x") follow the legacy path.
798
- //
799
- // Skip the strip when vfsPath is empty (no chroot mismatch possible).
800
- // Be careful with segment boundaries: vfsPath "home/user" must NOT
801
- // strip from path "home/userfoo/bar" (it's a different directory).
802
- function __wasiResolvePath(baseFd, pathStr) {
803
- const entry = fdTable.get(baseFd);
804
- if (!entry)
805
- return null;
806
- let baseVfs;
807
- if (entry.kind === 'preopen' || entry.kind === 'dir')
808
- baseVfs = entry.vfsPath;
809
- else
810
- return null;
811
- // Strip leading './' or '/' segments (POSIX legacy from wasi-libc).
812
- let trimmed = pathStr.replace(/^\.?\/+/, '').replace(/^\/+/, '');
813
- // wasi-path-fix: chroot-collision strip. Only applies to preopens (not
814
- // 'dir' fds — those are opened via path_open with already-resolved
815
- // paths). Only when baseVfs is non-empty AND trimmed starts with
816
- // baseVfs at a segment boundary.
817
- if (entry.kind === 'preopen' && baseVfs.length > 0) {
818
- if (trimmed === baseVfs) {
819
- trimmed = '';
820
- }
821
- else if (trimmed.length > baseVfs.length &&
822
- trimmed.charCodeAt(baseVfs.length) === 47 /* '/' */ &&
823
- trimmed.substring(0, baseVfs.length) === baseVfs) {
824
- trimmed = trimmed.substring(baseVfs.length + 1);
825
- }
826
- }
827
- return __wasiCanonicalize(baseVfs + '/' + trimmed);
828
- }
829
- // WASI socket and polling support B3: resolve a path WITH optional symlink-follow + POSIX loop
830
- // detection. Returns {path: canonical, isSymlink: bool, err?: errno}.
831
- // - If followFlag is true (default — matches LOOKUPFLAGS_SYMLINK_FOLLOW),
832
- // walks the symlink chain up to __WASI_SYMLOOP_MAX depth. ELOOP if exceeded.
833
- // - If followFlag is false, returns the bare resolved path; if the path
834
- // itself is in __wasiFS.symlinks, isSymlink=true so callers know to
835
- // open it as a symlink fd rather than a regular file.
836
- // Errors return {path: '', isSymlink: false, err: __WASI_E...}.
837
- function __wasiResolvePathFull(baseFd, pathStr, followFlag) {
838
- let p = __wasiResolvePath(baseFd, pathStr);
839
- if (p === null)
840
- return { path: '', isSymlink: false, err: __WASI_EBADF };
841
- if (!followFlag) {
842
- return { path: p, isSymlink: __wasiFS.symlinks.has(p), err: __WASI_ESUCCESS };
843
- }
844
- // Follow symlinks with bounded loop counter.
845
- let depth = 0;
846
- while (__wasiFS.symlinks.has(p)) {
847
- if (depth++ >= __WASI_SYMLOOP_MAX) {
848
- return { path: p, isSymlink: true, err: __WASI_ELOOP };
849
- }
850
- const target = __wasiFS.symlinks.get(p);
851
- // Target may be relative or absolute. Absolute targets (leading '/')
852
- // resolve against the same preopen root as the original path; relative
853
- // targets resolve against the directory containing the symlink.
854
- let next;
855
- if (target.startsWith('/')) {
856
- // Resolve against the preopen of baseFd.
857
- const e = fdTable.get(baseFd);
858
- const baseVfs = (e && (e.kind === 'preopen' || e.kind === 'dir')) ? e.vfsPath : '';
859
- next = __wasiCanonicalize(baseVfs + '/' + target);
860
- }
861
- else {
862
- const lastSlash = p.lastIndexOf('/');
863
- const parent = lastSlash >= 0 ? p.substring(0, lastSlash) : '';
864
- next = __wasiCanonicalize(parent + '/' + target);
865
- }
866
- p = next;
867
- }
868
- return { path: p, isSymlink: false, err: __WASI_ESUCCESS };
869
- }
870
- function __wasiEffectiveMode(path) {
871
- const mode = __wasiFS.modes.get(path);
872
- if (mode !== undefined)
873
- return mode;
874
- return __wasiInodeExists(path) ? 0 : 7;
875
- }
876
- function __wasiInodeExists(path) {
877
- return (__wasiFS.files.has(path) ||
878
- __wasiFS.dirs.has(path) ||
879
- __wasiFS.symlinks.has(path) ||
880
- __wasiFS.modes.has(path));
881
- }
882
- function __wasiCheckTraversal(baseFd, resolved) {
883
- const entry = fdTable.get(baseFd);
884
- if (!entry || (entry.kind !== 'preopen' && entry.kind !== 'dir'))
885
- return __WASI_EBADF;
886
- const base = entry.vfsPath;
887
- if (resolved === base)
888
- return __WASI_ESUCCESS;
889
- const prefix = base ? base + '/' : '';
890
- const relative = resolved.startsWith(prefix) ? resolved.substring(prefix.length) : resolved;
891
- const parts = relative.split('/').filter(Boolean);
892
- let ancestor = base;
893
- for (let i = 0; i < parts.length; i++) {
894
- if (!__wasiFS.dirs.has(ancestor)) {
895
- return __wasiInodeExists(ancestor) ? __WASI_ENOTDIR : __WASI_ENOENT;
896
- }
897
- if ((__wasiEffectiveMode(ancestor) & 1) === 0)
898
- return __WASI_EACCES;
899
- if (i === parts.length - 1)
900
- break;
901
- ancestor = ancestor ? ancestor + '/' + parts[i] : parts[i];
902
- }
903
- return __WASI_ESUCCESS;
904
- }
905
- function __wasiCheckMode(path, requested) {
906
- return (__wasiEffectiveMode(path) & requested) === requested
907
- ? __WASI_ESUCCESS
908
- : __WASI_EACCES;
909
- }
425
+ // The only source of descriptor numbers. A second counter — one for files, one
426
+ // for sockets — hands the same number out twice and the later open silently
427
+ // destroys the earlier fd's entry.
428
+ function __wasiAllocateFd() { return nextFd++; }
910
429
  // ─── makeImports ────────────────────────────────────────────────────────
911
430
  // ── Park watchdog ────────────────────────────────────────────────────────
912
431
  //
@@ -1009,35 +528,6 @@ export function __wasiMakeImports(opts) {
1009
528
  const bytes = u8().subarray(ptr, ptr + len);
1010
529
  return utf8dec.decode(bytes);
1011
530
  }
1012
- // The single filestat emitter for both ABIs. preview1 is 64 bytes with an
1013
- // eight-byte nlink at +24; preview0 is 56 with a four-byte nlink at +20, so
1014
- // every field from nlink onward shifts. A caller decoding the wrong layout
1015
- // reads st_size out of the nlink slot and sees 1 — silently, since the
1016
- // function signature is identical either way.
1017
- function writeFilestat(statPtr, path, ftype, size, t) {
1018
- const dv = view();
1019
- writeU64LE(statPtr, __WASI_DEV);
1020
- writeU64LE(statPtr + 8, __wasiInode(path));
1021
- dv.setUint8(statPtr + 16, ftype);
1022
- if (preview0) {
1023
- for (let i = 17; i < 20; i++)
1024
- dv.setUint8(statPtr + i, 0);
1025
- dv.setUint32(statPtr + 20, 1, true); // nlink u32
1026
- writeU64LE(statPtr + 24, size);
1027
- writeU64LE(statPtr + 32, t.atime);
1028
- writeU64LE(statPtr + 40, t.mtime);
1029
- writeU64LE(statPtr + 48, t.ctime);
1030
- return __WASI_ESUCCESS;
1031
- }
1032
- for (let i = 17; i < 24; i++)
1033
- dv.setUint8(statPtr + i, 0);
1034
- writeU64LE(statPtr + 24, 1n); // nlink u64
1035
- writeU64LE(statPtr + 32, size);
1036
- writeU64LE(statPtr + 40, t.atime);
1037
- writeU64LE(statPtr + 48, t.mtime);
1038
- writeU64LE(statPtr + 56, t.ctime);
1039
- return __WASI_ESUCCESS;
1040
- }
1041
531
  let stdoutBuf = '';
1042
532
  let stderrBuf = '';
1043
533
  // A sink and the readable buffer are mutually exclusive: with a sink the
@@ -1059,65 +549,6 @@ export function __wasiMakeImports(opts) {
1059
549
  stderrBuf += s;
1060
550
  }
1061
551
  }
1062
- // ── Helpers operating on the VFS state ───────────────────────────────
1063
- function getFile(vfsPath) {
1064
- return __wasiFS.files.get(vfsPath) || null;
1065
- }
1066
- /** Known to exist — resident content or a manifest entry. */
1067
- function fileKnown(vfsPath) {
1068
- return __wasiFS.files.has(vfsPath) || __wasiFS.sizes.has(vfsPath);
1069
- }
1070
- /** Size without forcing a load. */
1071
- function fileSize(vfsPath) {
1072
- const resident = __wasiFS.files.get(vfsPath);
1073
- if (resident)
1074
- return resident.length;
1075
- return __wasiFS.sizes.get(vfsPath) ?? 0;
1076
- }
1077
- /**
1078
- * Bytes for [offset, offset+length) of a file. Returns synchronously when
1079
- * the content is resident — JSPI passes a plain value through with no
1080
- * suspender, so sync-only guests are unaffected by the live path existing.
1081
- * Returns a Promise only on a genuine cache miss.
1082
- */
1083
- function readRange(vfsPath, offset, length) {
1084
- const resident = __wasiFS.files.get(vfsPath);
1085
- if (resident) {
1086
- const start = Math.min(offset, resident.length);
1087
- return resident.subarray(start, Math.min(resident.length, start + length));
1088
- }
1089
- const size = __wasiFS.sizes.get(vfsPath);
1090
- if (size === undefined)
1091
- return new Uint8Array(0);
1092
- // A manifest entry says "this file exists and has N bytes"; the bytes live
1093
- // in the session VFS. With no supervisor there is no way to get them, and
1094
- // answering with zero bytes is not a degraded read — it is a DIFFERENT
1095
- // FILE, reported as a success. Ruby's require took that empty content,
1096
- // defined nothing, and failed later as an undefined constant with nothing
1097
- // pointing back here. Callers turn null into EIO.
1098
- if (!__wasiSup)
1099
- return null;
1100
- // Queued writes must be visible to our own live read.
1101
- return (async () => {
1102
- await __wasiDrainPersist();
1103
- if (size <= __wasiFS.residentFileCap) {
1104
- const whole = await __wasiSup.fsReadRange(vfsPath, 0, size);
1105
- const bytes = whole instanceof Uint8Array ? whole : new Uint8Array(whole);
1106
- __wasiFS.files.set(vfsPath, bytes);
1107
- // The diff-back mirror must agree, or a demand-loaded file would be
1108
- // reported as newly written by every runner still using the diff.
1109
- __wasiFS.origFiles.set(vfsPath, bytes.slice());
1110
- const start = Math.min(offset, bytes.length);
1111
- return bytes.subarray(start, Math.min(bytes.length, start + length));
1112
- }
1113
- // Too large to hold: window straight through, nothing cached.
1114
- const want = Math.max(0, Math.min(length, size - offset));
1115
- if (want === 0)
1116
- return new Uint8Array(0);
1117
- const win = await __wasiSup.fsReadRange(vfsPath, offset, want);
1118
- return win instanceof Uint8Array ? win : new Uint8Array(win);
1119
- })();
1120
- }
1121
552
  /** Scatter the given bytes across the iovec list; returns bytes consumed. */
1122
553
  function scatterIovs(bytes, iovsPtr, iovsLen, dv, memU8) {
1123
554
  let done = 0;
@@ -1135,49 +566,6 @@ export function __wasiMakeImports(opts) {
1135
566
  }
1136
567
  return done;
1137
568
  }
1138
- /**
1139
- * Resolve a path the manifest does not know. Files created after this
1140
- * process spawned are invisible to a seed, so a miss asks the supervisor
1141
- * once and records the answer — including the negative, so a program that
1142
- * probes the same absent path in a loop pays a single round trip.
1143
- */
1144
- /** True when the seed listed this path's subtree exhaustively. */
1145
- function underEnumeratedRoot(vfsPath) {
1146
- for (const root of __wasiFS.enumeratedRoots) {
1147
- if (root === '' || vfsPath === root || vfsPath.startsWith(root + '/'))
1148
- return true;
1149
- }
1150
- return false;
1151
- }
1152
- function statLive(vfsPath) {
1153
- if (!__wasiSup || __wasiNegative.has(vfsPath))
1154
- return false;
1155
- // The manifest is authoritative inside a root it fully enumerated, so an
1156
- // absent path there is absent — no round trip. Revalidation at a park is
1157
- // what lets files created after spawn become visible.
1158
- if (underEnumeratedRoot(vfsPath))
1159
- return false;
1160
- return (async () => {
1161
- await __wasiDrainPersist();
1162
- const st = await __wasiSup.stat(vfsPath);
1163
- if (!st) {
1164
- __wasiNegative.add(vfsPath);
1165
- return false;
1166
- }
1167
- ensureParentDirs(vfsPath);
1168
- if (st.type === 'directory') {
1169
- __wasiFS.dirs.add(vfsPath);
1170
- __wasiFS.modes.set(vfsPath, 7);
1171
- }
1172
- else {
1173
- __wasiFS.sizes.set(vfsPath, Number(st.size ?? 0));
1174
- __wasiFS.modes.set(vfsPath, 6);
1175
- }
1176
- const ns = BigInt(st.mtime ?? Date.now()) * 1000000n;
1177
- __wasiFS.times.set(vfsPath, { mtime: ns, atime: ns, ctime: ns });
1178
- return true;
1179
- })();
1180
- }
1181
569
  /** Total capacity of an iovec list. */
1182
570
  function iovsCapacity(iovsPtr, iovsLen, dv) {
1183
571
  let total = 0;
@@ -1185,89 +573,6 @@ export function __wasiMakeImports(opts) {
1185
573
  total += dv.getUint32(iovsPtr + i * 8 + 4, true);
1186
574
  return total;
1187
575
  }
1188
- function hasDir(vfsPath) {
1189
- if (__wasiFS.dirs.has(vfsPath))
1190
- return true;
1191
- // Root preopens implicitly exist as dirs.
1192
- return false;
1193
- }
1194
- function setFile(vfsPath, bytes) {
1195
- __wasiFS.files.set(vfsPath, bytes);
1196
- __wasiFS.sizes.delete(vfsPath);
1197
- __wasiNegative.delete(vfsPath);
1198
- if (!__wasiFS.modes.has(vfsPath))
1199
- __wasiFS.modes.set(vfsPath, 6);
1200
- // WASI socket and polling support B1: bump mtime+ctime on every write. atime stays as-is
1201
- // (read paths bump atime explicitly via touchAccess()).
1202
- __wasiBumpMtime(vfsPath);
1203
- __wasiPersistFile(vfsPath);
1204
- }
1205
- function unsetFile(vfsPath) {
1206
- __wasiFS.files.delete(vfsPath);
1207
- __wasiFS.sizes.delete(vfsPath);
1208
- __wasiFS.times.delete(vfsPath);
1209
- __wasiFS.modes.delete(vfsPath);
1210
- __wasiEnqueue('unlink ' + vfsPath, (sup) => sup.unlink(vfsPath));
1211
- }
1212
- function touchAccess(vfsPath) {
1213
- // WASI socket and polling support B1: bump atime on a read. mtime/ctime unchanged.
1214
- const cur = __wasiFS.times.get(vfsPath);
1215
- if (cur)
1216
- cur.atime = __wasiNowNs();
1217
- }
1218
- function ensureParentDirs(vfsPath) {
1219
- // Ensure all ancestor dirs of vfsPath exist (they should, but mkdir -p
1220
- // semantics are handled in path_create_directory by callers).
1221
- const parts = vfsPath.split('/');
1222
- if (parts.length <= 1)
1223
- return;
1224
- let p = '';
1225
- for (let i = 0; i < parts.length - 1; i++) {
1226
- p = p ? (p + '/' + parts[i]) : parts[i];
1227
- __wasiFS.dirs.add(p);
1228
- if (!__wasiFS.modes.has(p))
1229
- __wasiFS.modes.set(p, 7);
1230
- // WASI socket and polling support B1: ensure ancestor dirs have a times entry.
1231
- if (!__wasiFS.times.has(p)) {
1232
- const now = __wasiNowNs();
1233
- __wasiFS.times.set(p, { mtime: now, atime: now, ctime: now });
1234
- }
1235
- }
1236
- }
1237
- function readdirChildren(vfsPath) {
1238
- const out = [];
1239
- const seen = new Set();
1240
- const prefix = vfsPath === '' ? '' : vfsPath + '/';
1241
- // Files
1242
- for (const path of __wasiFS.files.keys()) {
1243
- if (path.startsWith(prefix)) {
1244
- const rest = path.substring(prefix.length);
1245
- if (rest && rest.indexOf('/') === -1) {
1246
- out.push({ name: rest, type: __WASI_FT_REGULAR_FILE });
1247
- seen.add(rest);
1248
- }
1249
- }
1250
- }
1251
- // Dirs
1252
- for (const path of __wasiFS.dirs) {
1253
- if (path !== vfsPath && path.startsWith(prefix)) {
1254
- const rest = path.substring(prefix.length);
1255
- if (rest && rest.indexOf('/') === -1) {
1256
- out.push({ name: rest, type: __WASI_FT_DIRECTORY });
1257
- seen.add(rest);
1258
- }
1259
- }
1260
- }
1261
- for (const path of __wasiFS.modes.keys()) {
1262
- if (path.startsWith(prefix)) {
1263
- const rest = path.substring(prefix.length);
1264
- if (rest && rest.indexOf('/') === -1 && !seen.has(rest)) {
1265
- out.push({ name: rest, type: __WASI_FT_REGULAR_FILE });
1266
- }
1267
- }
1268
- }
1269
- return out;
1270
- }
1271
576
  const imports = {
1272
577
  // ── args / env ──
1273
578
  args_get(argvPtr, argvBufPtr) {
@@ -1353,32 +658,9 @@ export function __wasiMakeImports(opts) {
1353
658
  if (entry.kind === 'listener') {
1354
659
  return __wasiAcceptRead(entry, iovsPtr, iovsLen, nreadPtr, writeU32LE, view, u8);
1355
660
  }
1356
- if (entry.kind === 'dir' || entry.kind === 'preopen')
661
+ if (entry.kind === 'preopen')
1357
662
  return __WASI_EISDIR;
1358
- if (entry.kind !== 'file')
1359
- return __WASI_EBADF;
1360
- if (!fileKnown(entry.vfsPath))
1361
- return __WASI_ENOENT;
1362
- const want = iovsCapacity(iovsPtr, iovsLen, view());
1363
- const take = Math.min(want, Math.max(0, fileSize(entry.vfsPath) - entry.offset));
1364
- if (take === 0) {
1365
- writeU32LE(nreadPtr, 0);
1366
- return __WASI_ESUCCESS;
1367
- }
1368
- const chunk = readRange(entry.vfsPath, entry.offset, take);
1369
- // Re-read memory views after a possible suspension: the guest may have
1370
- // grown its memory while we were parked, detaching the old buffer.
1371
- const deliver = (bytes) => {
1372
- // Null is "the content exists but is unreachable" — never zero bytes,
1373
- // which the guest cannot tell from an empty file.
1374
- if (bytes === null)
1375
- return __WASI_EIO;
1376
- const n = scatterIovs(bytes, iovsPtr, iovsLen, view(), u8());
1377
- entry.offset += n;
1378
- writeU32LE(nreadPtr, n);
1379
- return __WASI_ESUCCESS;
1380
- };
1381
- return (chunk && typeof chunk.then === 'function') ? chunk.then(deliver) : deliver(chunk);
663
+ return __WASI_EBADF;
1382
664
  },
1383
665
  fd_write(fd, iovsPtr, iovsLen, nwrittenPtr) {
1384
666
  // wasi-libc maps write(2) to fd_write for every fd kind; a socket fd
@@ -1387,6 +669,8 @@ export function __wasiMakeImports(opts) {
1387
669
  if (sockEntry && sockEntry.kind === 'socket') {
1388
670
  return __rawSockSend(fd, iovsPtr, iovsLen, 0, nwrittenPtr);
1389
671
  }
672
+ if (fd !== 1 && fd !== 2)
673
+ return __WASI_EBADF;
1390
674
  const dv = view();
1391
675
  const memU8 = u8();
1392
676
  // Gather all iov bytes
@@ -1413,79 +697,27 @@ export function __wasiMakeImports(opts) {
1413
697
  off += p.length;
1414
698
  }
1415
699
  }
1416
- if (fd === 1 || fd === 2) {
1417
- appendStream(fd === 1 ? 'stdout' : 'stderr', combined);
1418
- writeU32LE(nwrittenPtr, total);
1419
- return __WASI_ESUCCESS;
1420
- }
1421
- const entry = fdTable.get(fd);
1422
- if (!entry || entry.kind !== 'file')
1423
- return __WASI_EBADF;
1424
- const file = getFile(entry.vfsPath) || new Uint8Array(0);
1425
- // O_APPEND: each write atomically seeks to EOF, ignoring the current
1426
- // offset, then advances the offset past the written bytes (POSIX).
1427
- const appendMode = (entry.fdflags & __WASI_FDFLAGS_APPEND) !== 0;
1428
- const writeAt = appendMode ? file.length : entry.offset;
1429
- const newLen = Math.max(file.length, writeAt + total);
1430
- const next = new Uint8Array(newLen);
1431
- next.set(file, 0);
1432
- next.set(combined, writeAt);
1433
- setFile(entry.vfsPath, next);
1434
- entry.offset = writeAt + total;
700
+ appendStream(fd === 1 ? 'stdout' : 'stderr', combined);
1435
701
  writeU32LE(nwrittenPtr, total);
1436
702
  return __WASI_ESUCCESS;
1437
703
  },
1438
- fd_seek(fd, offsetArg, whence, newOffsetPtr) {
1439
- // WASI/V8 passes i64 args as BigInt. core WASI mistakenly assumed
1440
- // (lo, hi) i32 pairs; that worked only because hello-world never
1441
- // exercised seek. filesystem WASI: accept BigInt directly.
1442
- if (fd === 0 || fd === 1 || fd === 2) {
1443
- // Seeking a pipe/tty is ESPIPE — lets guests detect non-seekable
1444
- // stdio (POSIX lseek on a pipe).
704
+ fd_seek(fd, _offsetArg, _whence, _newOffsetPtr) {
705
+ // Seeking a pipe/tty is ESPIPE — lets guests detect non-seekable
706
+ // stdio (POSIX lseek on a pipe). A socket is a non-seekable stream too.
707
+ if (fd === 0 || fd === 1 || fd === 2)
1445
708
  return __WASI_ESPIPE;
1446
- }
1447
709
  const entry = fdTable.get(fd);
1448
- // A socket is a non-seekable stream, same as stdio: ESPIPE, not EBADF.
1449
710
  if (entry && (entry.kind === 'socket' || entry.kind === 'listener'))
1450
711
  return __WASI_ESPIPE;
1451
- if (!entry || entry.kind !== 'file')
1452
- return __WASI_EBADF;
1453
- const delta = typeof offsetArg === 'bigint' ? offsetArg : BigInt(offsetArg | 0);
1454
- const cur = BigInt(entry.offset);
1455
- // fileSize, not the resident buffer: a manifest entry is a file whose
1456
- // bytes have not been demand-loaded yet, and measuring it by what
1457
- // happens to be in memory calls it empty. SEEK_END then lands at 0, and
1458
- // every guest that sizes a file by seeking to its end — zipimport does
1459
- // exactly that before looking for the end-of-central-directory record —
1460
- // reads the head of the file and concludes it is not an archive.
1461
- const fileLen = BigInt(fileSize(entry.vfsPath));
1462
- let next;
1463
- if (whence === WHENCE_SET)
1464
- next = delta;
1465
- else if (whence === WHENCE_CUR)
1466
- next = cur + delta;
1467
- else if (whence === WHENCE_END)
1468
- next = fileLen + delta;
1469
- else
1470
- return __WASI_EINVAL;
1471
- if (next < 0n)
1472
- return __WASI_EINVAL;
1473
- entry.offset = Number(next);
1474
- writeU64LE(newOffsetPtr, next);
1475
- return __WASI_ESUCCESS;
712
+ return __WASI_EBADF;
1476
713
  },
1477
- fd_tell(fd, offsetPtr) {
1478
- if (fd === 0 || fd === 1 || fd === 2) {
1479
- // ftell on a pipe/tty is ESPIPE, same non-seekable rule as fd_seek.
714
+ fd_tell(fd, _offsetPtr) {
715
+ if (fd === 0 || fd === 1 || fd === 2)
1480
716
  return __WASI_ESPIPE;
1481
- }
1482
717
  const entry = fdTable.get(fd);
1483
718
  if (entry && (entry.kind === 'socket' || entry.kind === 'listener'))
1484
719
  return __WASI_ESPIPE;
1485
- if (!entry || entry.kind !== 'file')
1486
- return __WASI_EBADF;
1487
- writeU64LE(offsetPtr, BigInt(entry.offset));
1488
- return __WASI_ESUCCESS;
720
+ return __WASI_EBADF;
1489
721
  },
1490
722
  fd_fdstat_get(fd, statPtr) {
1491
723
  const entry = fdTable.get(fd);
@@ -1496,12 +728,9 @@ export function __wasiMakeImports(opts) {
1496
728
  if (entry.kind === 'stdin' || entry.kind === 'stdout' || entry.kind === 'stderr') {
1497
729
  ftype = __WASI_FT_CHARACTER_DEVICE;
1498
730
  }
1499
- else if (entry.kind === 'preopen' || entry.kind === 'dir') {
731
+ else if (entry.kind === 'preopen') {
1500
732
  ftype = __WASI_FT_DIRECTORY;
1501
733
  }
1502
- else if (entry.kind === 'file') {
1503
- ftype = __WASI_FT_REGULAR_FILE;
1504
- }
1505
734
  else if (entry.kind === 'socket' || entry.kind === 'listener') {
1506
735
  ftype = __WASI_FT_SOCKET_STREAM; // WASI socket and polling support B7
1507
736
  }
@@ -1525,7 +754,7 @@ export function __wasiMakeImports(opts) {
1525
754
  return __WASI_EBADF;
1526
755
  // Sockets track the mask too: guests set O_NONBLOCK on a socket and
1527
756
  // then read it back through fd_fdstat_get.
1528
- if (entry.kind === 'file' || entry.kind === 'socket' || entry.kind === 'listener')
757
+ if (entry.kind === 'socket' || entry.kind === 'listener')
1529
758
  entry.fdflags = flags;
1530
759
  return __WASI_ESUCCESS;
1531
760
  },
@@ -1579,399 +808,42 @@ export function __wasiMakeImports(opts) {
1579
808
  // WASI socket and polling support B7: synthetic /dev/tcp/<host>/<port> path — open a TCP
1580
809
  // socket (remote via cloudflare:sockets, loopback via the virtual
1581
810
  // socket kernel). Bash-like convention.
1582
- // Intercept BEFORE resolution because /dev/tcp/* doesn't exist in
1583
- // the in-memory FS — __wasiResolvePath would canonicalize it but
1584
- // not find any entry.
1585
811
  const guestPath = __wasiGuestPath(baseFd, pathArg);
1586
- if (guestPath.startsWith(__WASI_TCP_PATH_PREFIX)) {
812
+ if (guestPath.startsWith(WASI_TCP_PATH_PREFIX)) {
1587
813
  return __wasiOpenTcpSocket(guestPath, fdflags, fdOutPtr, writeU32LE);
1588
814
  }
1589
- if (guestPath.startsWith(__WASI_ACCEPTED_PATH_PREFIX)) {
815
+ if (guestPath.startsWith(WASI_ACCEPTED_PATH_PREFIX)) {
1590
816
  return __wasiOpenAcceptedSocket(guestPath, fdflags, fdOutPtr, writeU32LE);
1591
817
  }
1592
- if (guestPath.startsWith(__WASI_LISTEN_PATH_PREFIX)) {
818
+ if (guestPath.startsWith(WASI_LISTEN_PATH_PREFIX)) {
1593
819
  return __wasiOpenListener(guestPath, fdflags, fdOutPtr, writeU32LE);
1594
820
  }
1595
- // Honor LOOKUPFLAGS_SYMLINK_FOLLOW strictly: bit 0 set → follow.
1596
- // wasi-libc clears this bit for O_NOFOLLOW, so dirflags === 0 IS
1597
- // O_NOFOLLOW and must NOT be treated as follow.
1598
- const follow = (dirflags & __WASI_LOOKUPFLAGS_SYMLINK_FOLLOW) !== 0;
1599
- const rp = __wasiResolvePathFull(baseFd, pathArg, follow);
1600
- if (rp.err === __WASI_EBADF)
1601
- return __WASI_EBADF;
1602
- if (rp.err === __WASI_ELOOP)
1603
- return __WASI_ELOOP;
1604
- const resolved = rp.path;
1605
- const traversal = __wasiCheckTraversal(baseFd, resolved);
1606
- if (traversal !== __WASI_ESUCCESS)
1607
- return traversal;
1608
- const isCreate = (oflags & __WASI_O_CREAT) !== 0;
1609
- const isDirectory = (oflags & __WASI_O_DIRECTORY) !== 0;
1610
- const isExcl = (oflags & __WASI_O_EXCL) !== 0;
1611
- const isTrunc = (oflags & __WASI_O_TRUNC) !== 0;
1612
- // O_NOFOLLOW (symlink_follow cleared) on a trailing symlink: POSIX
1613
- // open() fails with ELOOP rather than opening the link itself.
1614
- if (!follow && rp.isSymlink) {
1615
- return __WASI_ELOOP;
1616
- }
1617
- const fileExists = __wasiFS.files.has(resolved) || __wasiFS.sizes.has(resolved) || (__wasiFS.modes.has(resolved) &&
1618
- !__wasiFS.dirs.has(resolved) &&
1619
- !__wasiFS.symlinks.has(resolved));
1620
- const dirExists = __wasiFS.dirs.has(resolved);
1621
- if (isDirectory) {
1622
- if (!dirExists)
1623
- return __WASI_ENOENT;
1624
- const access = __wasiCheckMode(resolved, 1);
1625
- if (access !== __WASI_ESUCCESS)
1626
- return access;
1627
- const fd = nextFd++;
1628
- fdTable.set(fd, { kind: 'dir', vfsPath: resolved, readdirEntries: null, cookie: 0n, oflags, fdflags });
1629
- writeU32LE(fdOutPtr, fd);
1630
- return __WASI_ESUCCESS;
1631
- }
1632
- if (dirExists) {
1633
- // Opening a directory without O_DIRECTORY: WASI returns EISDIR.
1634
- return __WASI_EISDIR;
1635
- }
1636
- if (!fileExists) {
1637
- if (!isCreate)
1638
- return __WASI_ENOENT;
1639
- const parentSlash = resolved.lastIndexOf('/');
1640
- const parent = parentSlash < 0 ? '' : resolved.substring(0, parentSlash);
1641
- const access = __wasiCheckMode(parent, 3);
1642
- if (access !== __WASI_ESUCCESS)
1643
- return access;
1644
- // create empty file
1645
- ensureParentDirs(resolved);
1646
- setFile(resolved, new Uint8Array(0));
1647
- }
1648
- else if (isExcl) {
1649
- return __WASI_EEXIST;
1650
- }
1651
- else if (isTrunc) {
1652
- const access = __wasiCheckMode(resolved, 2);
1653
- if (access !== __WASI_ESUCCESS)
1654
- return access;
1655
- setFile(resolved, new Uint8Array(0));
1656
- }
1657
- const requested = typeof rightsBase === 'bigint' ? rightsBase : BigInt(rightsBase >>> 0);
1658
- let requiredMode = 0;
1659
- if ((requested & __WASI_RIGHT_FD_READ) !== 0n)
1660
- requiredMode |= 4;
1661
- if ((requested & __WASI_RIGHT_FD_WRITE) !== 0n)
1662
- requiredMode |= 2;
1663
- if (requiredMode !== 0) {
1664
- const access = __wasiCheckMode(resolved, requiredMode);
1665
- if (access !== __WASI_ESUCCESS)
1666
- return access;
1667
- }
1668
- const fd = nextFd++;
1669
- fdTable.set(fd, { kind: 'file', vfsPath: resolved, offset: 0, oflags, fdflags });
1670
- writeU32LE(fdOutPtr, fd);
1671
- return __WASI_ESUCCESS;
821
+ // Every other path is the authority codec's; without a supervisor there
822
+ // is no filesystem to open it on.
823
+ return __WASI_EBADF;
1672
824
  },
1673
825
  // ── path_create_directory ──
1674
- path_create_directory(baseFd, pathPtr, pathLen) {
1675
- const path = readPath(pathPtr, pathLen);
1676
- const resolved = __wasiResolvePath(baseFd, path);
1677
- if (resolved === null)
1678
- return __WASI_EBADF;
1679
- if (__wasiFS.dirs.has(resolved))
1680
- return __WASI_EEXIST;
1681
- if (__wasiFS.files.has(resolved))
1682
- return __WASI_EEXIST;
1683
- ensureParentDirs(resolved);
1684
- __wasiFS.dirs.add(resolved);
1685
- __wasiFS.modes.set(resolved, 7);
1686
- __wasiNegative.delete(resolved);
1687
- // WASI socket and polling support B1: seed times for the new dir.
1688
- const now = __wasiNowNs();
1689
- __wasiFS.times.set(resolved, { mtime: now, atime: now, ctime: now });
1690
- __wasiEnqueue('mkdir ' + resolved, (sup) => sup.mkdir(resolved));
1691
- return __WASI_ESUCCESS;
1692
- },
1693
- path_remove_directory(baseFd, pathPtr, pathLen) {
1694
- const path = readPath(pathPtr, pathLen);
1695
- const resolved = __wasiResolvePath(baseFd, path);
1696
- if (resolved === null)
1697
- return __WASI_EBADF;
1698
- if (!__wasiFS.dirs.has(resolved))
1699
- return __WASI_ENOENT;
1700
- // Must be empty
1701
- const children = readdirChildren(resolved);
1702
- if (children.length > 0)
1703
- return __WASI_ENOTEMPTY;
1704
- __wasiFS.dirs.delete(resolved);
1705
- __wasiFS.modes.delete(resolved);
1706
- __wasiEnqueue('rmdir ' + resolved, (sup) => sup.rmdir(resolved));
1707
- return __WASI_ESUCCESS;
1708
- },
1709
- path_unlink_file(baseFd, pathPtr, pathLen) {
1710
- const path = readPath(pathPtr, pathLen);
1711
- const resolved = __wasiResolvePath(baseFd, path);
1712
- if (resolved === null)
1713
- return __WASI_EBADF;
1714
- if (__wasiFS.dirs.has(resolved))
1715
- return __WASI_EISDIR;
1716
- // WASI socket and polling support B3: unlink also removes a symlink at this path. We
1717
- // operate on the unfollowed path (POSIX unlink semantics — removes
1718
- // the directory entry itself, not the symlink target).
1719
- if (__wasiFS.symlinks.has(resolved)) {
1720
- __wasiFS.symlinks.delete(resolved);
1721
- __wasiFS.times.delete(resolved);
1722
- __wasiFS.modes.delete(resolved);
1723
- return __WASI_ESUCCESS;
1724
- }
1725
- if (!fileKnown(resolved))
1726
- return __WASI_ENOENT;
1727
- unsetFile(resolved);
1728
- return __WASI_ESUCCESS;
1729
- },
826
+ path_create_directory(_baseFd, _pathPtr, _pathLen) { return __WASI_EBADF; },
827
+ path_remove_directory(_baseFd, _pathPtr, _pathLen) { return __WASI_EBADF; },
828
+ path_unlink_file(_baseFd, _pathPtr, _pathLen) { return __WASI_EBADF; },
1730
829
  // ── path_rename (atomic; overwrites destination) ──
1731
- path_rename(srcFd, srcPathPtr, srcPathLen, dstFd, dstPathPtr, dstPathLen) {
1732
- const srcPath = readPath(srcPathPtr, srcPathLen);
1733
- const dstPath = readPath(dstPathPtr, dstPathLen);
1734
- const src = __wasiResolvePath(srcFd, srcPath);
1735
- const dst = __wasiResolvePath(dstFd, dstPath);
1736
- if (src === null || dst === null)
1737
- return __WASI_EBADF;
1738
- // rename operates on the entry itself (no symlink-follow on the final
1739
- // component, per POSIX). src may be a file, a directory, or a symlink.
1740
- const srcFile = __wasiFS.files.get(src);
1741
- const srcIsDir = __wasiFS.dirs.has(src);
1742
- const srcIsSymlink = __wasiFS.symlinks.has(src);
1743
- // A manifest-only file is a file: its bytes just are not resident yet.
1744
- const srcIsLazy = srcFile === undefined && __wasiFS.sizes.has(src);
1745
- if (srcFile === undefined && !srcIsLazy && !srcIsDir && !srcIsSymlink)
1746
- return __WASI_ENOENT;
1747
- if (src === dst)
1748
- return __WASI_ESUCCESS; // rename to itself is a no-op
1749
- // Move the path's timestamps with it, bumping ctime (metadata change).
1750
- const moveTimes = (from, to) => {
1751
- const t = __wasiFS.times.get(from);
1752
- __wasiFS.times.delete(from);
1753
- const now = __wasiNowNs();
1754
- __wasiFS.times.set(to, t
1755
- ? { mtime: t.mtime, atime: t.atime, ctime: now }
1756
- : { mtime: now, atime: now, ctime: now });
1757
- };
1758
- const moveMode = (from, to) => {
1759
- const mode = __wasiFS.modes.get(from);
1760
- __wasiFS.modes.delete(from);
1761
- if (mode !== undefined)
1762
- __wasiFS.modes.set(to, mode);
1763
- };
1764
- // Pre-remove the destination of any kind (atomic overwrite).
1765
- __wasiFS.files.delete(dst);
1766
- __wasiFS.sizes.delete(dst);
1767
- __wasiFS.dirs.delete(dst);
1768
- __wasiFS.symlinks.delete(dst);
1769
- __wasiFS.times.delete(dst);
1770
- __wasiFS.modes.delete(dst);
1771
- if (srcIsLazy) {
1772
- __wasiFS.sizes.set(dst, __wasiFS.sizes.get(src));
1773
- __wasiFS.sizes.delete(src);
1774
- moveTimes(src, dst);
1775
- moveMode(src, dst);
1776
- }
1777
- else if (srcIsSymlink) {
1778
- __wasiFS.symlinks.set(dst, __wasiFS.symlinks.get(src));
1779
- __wasiFS.symlinks.delete(src);
1780
- moveTimes(src, dst);
1781
- moveMode(src, dst);
1782
- }
1783
- else if (srcFile !== undefined) {
1784
- __wasiFS.files.set(dst, srcFile);
1785
- __wasiFS.files.delete(src);
1786
- moveTimes(src, dst);
1787
- moveMode(src, dst);
1788
- }
1789
- else {
1790
- __wasiFS.dirs.delete(src);
1791
- __wasiFS.dirs.add(dst);
1792
- moveTimes(src, dst);
1793
- moveMode(src, dst);
1794
- // Rebase every descendant key (files, dirs, symlinks) + its times.
1795
- const srcPrefix = src + '/';
1796
- const rebase = (key) => dst + '/' + key.substring(srcPrefix.length);
1797
- for (const key of [...__wasiFS.files.keys()]) {
1798
- if (key.startsWith(srcPrefix)) {
1799
- const nk = rebase(key);
1800
- __wasiFS.files.set(nk, __wasiFS.files.get(key));
1801
- __wasiFS.files.delete(key);
1802
- moveTimes(key, nk);
1803
- moveMode(key, nk);
1804
- }
1805
- }
1806
- for (const key of [...__wasiFS.dirs]) {
1807
- if (key.startsWith(srcPrefix)) {
1808
- const nk = rebase(key);
1809
- __wasiFS.dirs.add(nk);
1810
- __wasiFS.dirs.delete(key);
1811
- moveTimes(key, nk);
1812
- moveMode(key, nk);
1813
- }
1814
- }
1815
- for (const key of [...__wasiFS.symlinks.keys()]) {
1816
- if (key.startsWith(srcPrefix)) {
1817
- const nk = rebase(key);
1818
- __wasiFS.symlinks.set(nk, __wasiFS.symlinks.get(key));
1819
- __wasiFS.symlinks.delete(key);
1820
- moveTimes(key, nk);
1821
- moveMode(key, nk);
1822
- }
1823
- }
1824
- for (const key of [...__wasiFS.sizes.keys()]) {
1825
- if (key.startsWith(srcPrefix)) {
1826
- __wasiFS.sizes.set(rebase(key), __wasiFS.sizes.get(key));
1827
- __wasiFS.sizes.delete(key);
1828
- }
1829
- }
1830
- }
1831
- // One op: the supervisor renames the whole subtree atomically, which a
1832
- // per-descendant replay could not.
1833
- __wasiEnqueue('rename ' + src + ' -> ' + dst, (sup) => sup.rename(src, dst));
1834
- return __WASI_ESUCCESS;
1835
- },
830
+ path_rename(_srcFd, _srcPathPtr, _srcPathLen, _dstFd, _dstPathPtr, _dstPathLen) { return __WASI_EBADF; },
1836
831
  // ── path_filestat_get ──
1837
- path_filestat_get(baseFd, lookupflags, pathPtr, pathLen, statPtr) {
1838
- const path = readPath(pathPtr, pathLen);
1839
- // Follow symlinks only when SYMLINK_FOLLOW is set (stat); flags=0 is lstat.
1840
- const follow = (lookupflags & __WASI_LOOKUPFLAGS_SYMLINK_FOLLOW) !== 0;
1841
- const rp = __wasiResolvePathFull(baseFd, path, follow);
1842
- if (rp.err === __WASI_EBADF)
1843
- return __WASI_EBADF;
1844
- if (rp.err === __WASI_ELOOP)
1845
- return __WASI_ELOOP;
1846
- const resolved = rp.path;
1847
- const traversal = __wasiCheckTraversal(baseFd, resolved);
1848
- if (traversal !== __WASI_ESUCCESS)
1849
- return traversal;
1850
- const classify = () => {
1851
- if (!follow && rp.isSymlink) {
1852
- return [__WASI_FT_SYMBOLIC_LINK,
1853
- BigInt(new TextEncoder().encode(__wasiFS.symlinks.get(resolved)).length)];
1854
- }
1855
- if (__wasiFS.files.has(resolved) || __wasiFS.sizes.has(resolved)) {
1856
- return [__WASI_FT_REGULAR_FILE, BigInt(fileSize(resolved))];
1857
- }
1858
- if (__wasiFS.dirs.has(resolved))
1859
- return [__WASI_FT_DIRECTORY, 0n];
1860
- if (__wasiFS.modes.has(resolved))
1861
- return [__WASI_FT_REGULAR_FILE, 0n];
1862
- return null;
1863
- };
1864
- let kind = classify();
1865
- if (!kind) {
1866
- const live = statLive(resolved);
1867
- if (live && typeof live.then === 'function') {
1868
- return live.then((found) => {
1869
- // classify() is re-run because the live stat is what populated the
1870
- // maps. It can still answer null: the stat and this callback are
1871
- // separated by a suspension, and anything that unlinks the path in
1872
- // between — another thread, another request on a resident server —
1873
- // leaves nothing to classify. Spreading that null threw "is not
1874
- // iterable" from inside a Suspending import, which reaches the
1875
- // guest as a trap carrying no errno. The path is gone, so ENOENT is
1876
- // the answer the guest can act on.
1877
- const settled = found ? classify() : null;
1878
- return settled ? emitStat(...settled) : __WASI_ENOENT;
1879
- });
1880
- }
1881
- return __WASI_ENOENT;
1882
- }
1883
- return emitStat(...kind);
1884
- // Hoisted: reached from both the resident and the post-live-stat path.
1885
- function emitStat(ftype, size) {
1886
- // WASI socket and polling support B1: emit real timestamps.
1887
- const t = __wasiFS.times.get(resolved) || { mtime: 0n, atime: 0n, ctime: 0n };
1888
- return writeFilestat(statPtr, resolved, ftype, size, t);
1889
- }
1890
- },
832
+ path_filestat_get(_baseFd, _lookupflags, _pathPtr, _pathLen, _statPtr) { return __WASI_EBADF; },
1891
833
  // WASI socket and polling support B2: real path_filestat_set_times. Honors ATIM/ATIM_NOW/
1892
834
  // MTIM/MTIM_NOW flags. Spec: atim_ns and mtim_ns are absolute
1893
835
  // nanosecond timestamps; flags select which fields to update + whether
1894
836
  // to clamp to "now". ENOENT if path doesn't exist.
1895
- path_filestat_set_times(baseFd, lookupflags, pathPtr, pathLen, atimArg, mtimArg, fstflags) {
1896
- const path = readPath(pathPtr, pathLen);
1897
- const follow = (lookupflags & __WASI_LOOKUPFLAGS_SYMLINK_FOLLOW) !== 0;
1898
- const rp = __wasiResolvePathFull(baseFd, path, follow);
1899
- if (rp.err === __WASI_EBADF)
1900
- return __WASI_EBADF;
1901
- if (rp.err === __WASI_ELOOP)
1902
- return __WASI_ELOOP;
1903
- const resolved = rp.path;
1904
- const isSymlinkOnly = !follow && rp.isSymlink;
1905
- if (!isSymlinkOnly && !__wasiFS.files.has(resolved) && !__wasiFS.dirs.has(resolved)) {
1906
- return __WASI_ENOENT;
1907
- }
1908
- // Decode flags. Mutually-exclusive pairs ATIM vs ATIM_NOW (similarly
1909
- // for MTIM) — caller error if both set; we accept and prefer _NOW.
1910
- const setAtim = (fstflags & __WASI_FSTFLAGS_ATIM) !== 0;
1911
- const setAtimNow = (fstflags & __WASI_FSTFLAGS_ATIM_NOW) !== 0;
1912
- const setMtim = (fstflags & __WASI_FSTFLAGS_MTIM) !== 0;
1913
- const setMtimNow = (fstflags & __WASI_FSTFLAGS_MTIM_NOW) !== 0;
1914
- if ((setAtim && setAtimNow) || (setMtim && setMtimNow))
1915
- return __WASI_EINVAL;
1916
- const now = __wasiNowNs();
1917
- const atimNs = setAtimNow ? now : (setAtim ? (typeof atimArg === 'bigint' ? atimArg : BigInt(atimArg)) : null);
1918
- const mtimNs = setMtimNow ? now : (setMtim ? (typeof mtimArg === 'bigint' ? mtimArg : BigInt(mtimArg)) : null);
1919
- __wasiTouchTimes(resolved, mtimNs, atimNs, now);
1920
- __wasiPersistTimes(resolved);
1921
- return __WASI_ESUCCESS;
1922
- },
837
+ path_filestat_set_times(_baseFd, _lookupflags, _pathPtr, _pathLen, _atimArg, _mtimArg, _fstflags) { return __WASI_EBADF; },
1923
838
  // WASI socket and polling support B3: read the target string of a symlink. Spec:
1924
839
  // path_readlink(fd, path, path_len, buf, buf_len, *bufused)
1925
840
  // Truncates to buf_len; writes actual bytes to *bufused.
1926
- path_readlink(baseFd, pathPtr, pathLen, bufPtr, bufLen, bufUsedPtr) {
1927
- const path = readPath(pathPtr, pathLen);
1928
- // readlink NEVER follows symlinks on the last component (POSIX).
1929
- const rp = __wasiResolvePathFull(baseFd, path, false);
1930
- if (rp.err === __WASI_EBADF)
1931
- return __WASI_EBADF;
1932
- if (rp.err === __WASI_ELOOP)
1933
- return __WASI_ELOOP;
1934
- const resolved = rp.path;
1935
- if (!__wasiFS.symlinks.has(resolved))
1936
- return __WASI_EINVAL; // not a symlink
1937
- const target = __wasiFS.symlinks.get(resolved);
1938
- const enc = new TextEncoder();
1939
- const targetBytes = enc.encode(target);
1940
- const n = Math.min(targetBytes.length, bufLen);
1941
- u8().set(targetBytes.subarray(0, n), bufPtr);
1942
- writeU32LE(bufUsedPtr, n);
1943
- // POSIX: readlink doesn't update atime per most filesystems; but
1944
- // some (ext4 with strictatime) do. We follow Linux default: no
1945
- // atime bump on readlink.
1946
- return __WASI_ESUCCESS;
1947
- },
841
+ path_readlink(_baseFd, _pathPtr, _pathLen, _bufPtr, _bufLen, _bufUsedPtr) { return __WASI_EBADF; },
1948
842
  // WASI socket and polling support B3: create a symlink. Spec:
1949
843
  // path_symlink(old_path, fd, new_path)
1950
844
  // old_path is the symlink's TARGET (stored verbatim); fd+new_path
1951
845
  // is the location where the symlink itself is created.
1952
- path_symlink(oldPathPtr, oldPathLen, newFd, newPathPtr, newPathLen) {
1953
- const oldPath = readPath(oldPathPtr, oldPathLen); // target string
1954
- const newPath = readPath(newPathPtr, newPathLen);
1955
- const resolved = __wasiResolvePath(newFd, newPath);
1956
- if (resolved === null)
1957
- return __WASI_EBADF;
1958
- if (__wasiFS.files.has(resolved))
1959
- return __WASI_EEXIST;
1960
- if (__wasiFS.dirs.has(resolved))
1961
- return __WASI_EEXIST;
1962
- if (__wasiFS.symlinks.has(resolved))
1963
- return __WASI_EEXIST;
1964
- // Store the target verbatim — symlinks per POSIX are dumb strings;
1965
- // resolution happens at lookup time.
1966
- __wasiFS.symlinks.set(resolved, oldPath);
1967
- __wasiFS.modes.set(resolved, 7);
1968
- ensureParentDirs(resolved);
1969
- const now = __wasiNowNs();
1970
- __wasiFS.times.set(resolved, { mtime: now, atime: now, ctime: now });
1971
- __wasiNegative.delete(resolved);
1972
- __wasiPersistSymlink(oldPath, resolved);
1973
- return __WASI_ESUCCESS;
1974
- },
846
+ path_symlink(_oldPathPtr, _oldPathLen, _newFd, _newPathPtr, _newPathLen) { return __WASI_EBADF; },
1975
847
  // WASI socket and polling support B3: hardlink. Spec:
1976
848
  // path_link(old_fd, old_flags, old_path, new_fd, new_path)
1977
849
  // Implements as a shared-buffer alias. POSIX semantics: both names
@@ -1982,54 +854,20 @@ export function __wasiMakeImports(opts) {
1982
854
  // don't propagate. Matches link(2) at the WASI-layer (link itself
1983
855
  // succeeds; concurrent-mutation semantics are filesystem-dependent
1984
856
  // and our shim is a single-call sandbox).
1985
- path_link(oldFd, oldFlags, oldPathPtr, oldPathLen, newFd, newPathPtr, newPathLen) {
1986
- const oldPath = readPath(oldPathPtr, oldPathLen);
1987
- const newPath = readPath(newPathPtr, newPathLen);
1988
- const follow = (oldFlags & __WASI_LOOKUPFLAGS_SYMLINK_FOLLOW) !== 0;
1989
- const rpOld = __wasiResolvePathFull(oldFd, oldPath, follow);
1990
- if (rpOld.err === __WASI_EBADF)
1991
- return __WASI_EBADF;
1992
- if (rpOld.err === __WASI_ELOOP)
1993
- return __WASI_ELOOP;
1994
- const src = rpOld.path;
1995
- const dst = __wasiResolvePath(newFd, newPath);
1996
- if (dst === null)
1997
- return __WASI_EBADF;
1998
- if (!__wasiFS.files.has(src))
1999
- return __WASI_ENOENT;
2000
- if (__wasiFS.files.has(dst) || __wasiFS.dirs.has(dst) || __wasiFS.symlinks.has(dst)) {
2001
- return __WASI_EEXIST;
2002
- }
2003
- __wasiFS.files.set(dst, __wasiFS.files.get(src)); // shared reference
2004
- __wasiFS.modes.set(dst, __wasiEffectiveMode(src));
2005
- ensureParentDirs(dst);
2006
- const now = __wasiNowNs();
2007
- __wasiFS.times.set(dst, { mtime: now, atime: now, ctime: now });
2008
- __wasiNegative.delete(dst);
2009
- // The session VFS has no hardlinks; the new name is persisted as its own
2010
- // file with the same bytes, which is what link(2) already degrades to
2011
- // here (fd_write REPLACES the buffer rather than mutating it in place).
2012
- __wasiPersistFile(dst);
2013
- return __WASI_ESUCCESS;
2014
- },
857
+ path_link(_oldFd, _oldFlags, _oldPathPtr, _oldPathLen, _newFd, _newPathPtr, _newPathLen) { return __WASI_EBADF; },
2015
858
  // ── fd_filestat_get / fd_filestat_set_size ──
2016
859
  fd_filestat_get(fd, statPtr) {
2017
860
  const entry = fdTable.get(fd);
2018
861
  if (!entry)
2019
862
  return __WASI_EBADF;
863
+ // Files and directories are the authority codec's. A descriptor with no
864
+ // path (stdio, a socket) is still its own object and must not share an
865
+ // inode with every other one, so it is named by its fd — on a device of
866
+ // its own, because (dev, ino) is the identity and the authority's
867
+ // devices already use the small numbers (the kernel namespace is 0,
868
+ // SQLite devices count from 1, and their inodes from 1 too).
2020
869
  let ftype = __WASI_FT_UNKNOWN;
2021
- let size = 0n;
2022
- let timesPath = null;
2023
- if (entry.kind === 'file') {
2024
- ftype = __WASI_FT_REGULAR_FILE;
2025
- size = BigInt(fileSize(entry.vfsPath));
2026
- timesPath = entry.vfsPath;
2027
- }
2028
- else if (entry.kind === 'dir' || entry.kind === 'preopen') {
2029
- ftype = __WASI_FT_DIRECTORY;
2030
- timesPath = entry.vfsPath;
2031
- }
2032
- else if (entry.kind === 'stdin' || entry.kind === 'stdout' || entry.kind === 'stderr') {
870
+ if (entry.kind === 'stdin' || entry.kind === 'stdout' || entry.kind === 'stderr') {
2033
871
  ftype = __WASI_FT_CHARACTER_DEVICE;
2034
872
  }
2035
873
  else if (entry.kind === 'socket' || entry.kind === 'listener') {
@@ -2037,111 +875,33 @@ export function __wasiMakeImports(opts) {
2037
875
  // IO layer keys buffering and seekability off exactly this).
2038
876
  ftype = __WASI_FT_SOCKET_STREAM;
2039
877
  }
2040
- const t = (timesPath && __wasiFS.times.get(timesPath)) || { mtime: 0n, atime: 0n, ctime: 0n };
2041
- // A path names the inode when there is one. An fd with no path — stdio,
2042
- // a socket — is still its own object and must not share an inode with
2043
- // every other one, so it is named by the fd instead.
2044
- return writeFilestat(statPtr, timesPath === null ? '\0fd/' + fd : timesPath, ftype, size, t);
2045
- },
2046
- fd_filestat_set_size(fd, size) {
2047
- const entry = fdTable.get(fd);
2048
- if (!entry || entry.kind !== 'file')
2049
- return __WASI_EBADF;
2050
- const cur = getFile(entry.vfsPath) || new Uint8Array(0);
2051
- const newSize = Number(size);
2052
- const next = new Uint8Array(newSize);
2053
- next.set(cur.subarray(0, Math.min(cur.length, newSize)), 0);
2054
- setFile(entry.vfsPath, next);
2055
- return __WASI_ESUCCESS;
2056
- },
2057
- // WASI socket and polling support B2: real fd_filestat_set_times.
2058
- fd_filestat_set_times(fd, atimArg, mtimArg, fstflags) {
2059
- const entry = fdTable.get(fd);
2060
- if (!entry)
878
+ else
2061
879
  return __WASI_EBADF;
2062
- if (entry.kind !== 'file' && entry.kind !== 'dir' && entry.kind !== 'preopen') {
2063
- // stdio / socket fds: no-op success (POSIX touches /dev/stdin etc. silently).
880
+ const dv = view();
881
+ dv.setUint8(statPtr + 16, ftype);
882
+ writeU64LE(statPtr, __WASI_STREAM_DEV);
883
+ writeU64LE(statPtr + 8, BigInt(fd) + 1n);
884
+ if (preview0) {
885
+ for (let i = 17; i < 20; i++)
886
+ dv.setUint8(statPtr + i, 0);
887
+ dv.setUint32(statPtr + 20, 1, true); // nlink u32
888
+ for (let off = 24; off < 56; off += 8)
889
+ writeU64LE(statPtr + off, 0n);
2064
890
  return __WASI_ESUCCESS;
2065
891
  }
2066
- const setAtim = (fstflags & __WASI_FSTFLAGS_ATIM) !== 0;
2067
- const setAtimNow = (fstflags & __WASI_FSTFLAGS_ATIM_NOW) !== 0;
2068
- const setMtim = (fstflags & __WASI_FSTFLAGS_MTIM) !== 0;
2069
- const setMtimNow = (fstflags & __WASI_FSTFLAGS_MTIM_NOW) !== 0;
2070
- if ((setAtim && setAtimNow) || (setMtim && setMtimNow))
2071
- return __WASI_EINVAL;
2072
- const now = __wasiNowNs();
2073
- const atimNs = setAtimNow ? now : (setAtim ? (typeof atimArg === 'bigint' ? atimArg : BigInt(atimArg)) : null);
2074
- const mtimNs = setMtimNow ? now : (setMtim ? (typeof mtimArg === 'bigint' ? mtimArg : BigInt(mtimArg)) : null);
2075
- __wasiTouchTimes(entry.vfsPath, mtimNs, atimNs, now);
2076
- __wasiPersistTimes(entry.vfsPath);
892
+ for (let i = 17; i < 24; i++)
893
+ dv.setUint8(statPtr + i, 0);
894
+ writeU64LE(statPtr + 24, 1n); // nlink u64
895
+ for (let off = 32; off < 64; off += 8)
896
+ writeU64LE(statPtr + off, 0n);
2077
897
  return __WASI_ESUCCESS;
2078
898
  },
899
+ fd_filestat_set_size(_fd, _size) { return __WASI_EBADF; },
900
+ // WASI socket and polling support B2: real fd_filestat_set_times.
901
+ fd_filestat_set_times(_fd, _atimArg, _mtimArg, _fstflags) { return __WASI_EBADF; },
2079
902
  // ── fd_pread / fd_pwrite (offset-explicit) ──
2080
- fd_pread(fd, iovsPtr, iovsLen, offsetArg, nreadPtr) {
2081
- const entry = fdTable.get(fd);
2082
- if (!entry || entry.kind !== 'file')
2083
- return __WASI_EBADF;
2084
- if (!fileKnown(entry.vfsPath))
2085
- return __WASI_ENOENT;
2086
- const offset = typeof offsetArg === 'bigint'
2087
- ? Number(offsetArg)
2088
- : (offsetArg >>> 0);
2089
- const want = iovsCapacity(iovsPtr, iovsLen, view());
2090
- const take = Math.min(want, Math.max(0, fileSize(entry.vfsPath) - offset));
2091
- if (take === 0) {
2092
- writeU32LE(nreadPtr, 0);
2093
- return __WASI_ESUCCESS;
2094
- }
2095
- const chunk = readRange(entry.vfsPath, offset, take);
2096
- const deliver = (bytes) => {
2097
- if (bytes === null)
2098
- return __WASI_EIO;
2099
- writeU32LE(nreadPtr, scatterIovs(bytes, iovsPtr, iovsLen, view(), u8()));
2100
- return __WASI_ESUCCESS;
2101
- };
2102
- return (chunk && typeof chunk.then === 'function') ? chunk.then(deliver) : deliver(chunk);
2103
- },
2104
- fd_pwrite(fd, iovsPtr, iovsLen, offsetArg, nwrittenPtr) {
2105
- const entry = fdTable.get(fd);
2106
- if (!entry || entry.kind !== 'file')
2107
- return __WASI_EBADF;
2108
- let offset = typeof offsetArg === 'bigint'
2109
- ? Number(offsetArg)
2110
- : (offsetArg >>> 0);
2111
- const dv = view();
2112
- const memU8 = u8();
2113
- let total = 0;
2114
- const parts = [];
2115
- for (let i = 0; i < iovsLen; i++) {
2116
- const iov = iovsPtr + i * 8;
2117
- const bufPtr = dv.getUint32(iov, true);
2118
- const bufLen = dv.getUint32(iov + 4, true);
2119
- if (bufLen > 0)
2120
- parts.push(memU8.slice(bufPtr, bufPtr + bufLen));
2121
- total += bufLen;
2122
- }
2123
- let combined;
2124
- if (parts.length === 0)
2125
- combined = new Uint8Array(0);
2126
- else if (parts.length === 1)
2127
- combined = parts[0];
2128
- else {
2129
- combined = new Uint8Array(total);
2130
- let off = 0;
2131
- for (const p of parts) {
2132
- combined.set(p, off);
2133
- off += p.length;
2134
- }
2135
- }
2136
- const cur = getFile(entry.vfsPath) || new Uint8Array(0);
2137
- const newLen = Math.max(cur.length, offset + total);
2138
- const next = new Uint8Array(newLen);
2139
- next.set(cur, 0);
2140
- next.set(combined, offset);
2141
- setFile(entry.vfsPath, next);
2142
- writeU32LE(nwrittenPtr, total);
2143
- return __WASI_ESUCCESS;
2144
- },
903
+ fd_pread(_fd, _iovsPtr, _iovsLen, _offsetArg, _nreadPtr) { return __WASI_EBADF; },
904
+ fd_pwrite(_fd, _iovsPtr, _iovsLen, _offsetArg, _nwrittenPtr) { return __WASI_EBADF; },
2145
905
  // ── fd_readdir ──
2146
906
  //
2147
907
  // WASI dirent layout (24 bytes per entry):
@@ -2151,72 +911,14 @@ export function __wasiMakeImports(opts) {
2151
911
  // d_type u8 @ 20
2152
912
  // pad 21..23
2153
913
  // followed by name bytes (variable).
2154
- fd_readdir(fd, bufPtr, bufLen, cookieArg, bufusedPtr) {
2155
- const entry = fdTable.get(fd);
2156
- if (!entry || (entry.kind !== 'dir' && entry.kind !== 'preopen'))
2157
- return __WASI_EBADF;
2158
- const access = __wasiCheckMode(entry.vfsPath, 4);
2159
- if (access !== __WASI_ESUCCESS)
2160
- return access;
2161
- if (!entry.readdirEntries) {
2162
- const kids = readdirChildren(entry.vfsPath);
2163
- entry.readdirEntries = [
2164
- { name: '.', type: __WASI_FT_DIRECTORY },
2165
- { name: '..', type: __WASI_FT_DIRECTORY },
2166
- ...kids,
2167
- ];
2168
- }
2169
- let startCookie = typeof cookieArg === 'bigint'
2170
- ? Number(cookieArg)
2171
- : (cookieArg >>> 0);
2172
- let written = 0;
2173
- const dv = view();
2174
- const memU8 = u8();
2175
- for (let i = startCookie; i < entry.readdirEntries.length; i++) {
2176
- const e = entry.readdirEntries[i];
2177
- const nameBytes = utf8enc.encode(e.name);
2178
- const recordSize = 24 + nameBytes.length;
2179
- if (written + 24 > bufLen)
2180
- break;
2181
- // d_next = i+1 (next cookie)
2182
- writeU64LE(bufPtr + written, BigInt(i + 1));
2183
- writeU64LE(bufPtr + written + 8, 0n); // d_ino
2184
- dv.setUint32(bufPtr + written + 16, nameBytes.length, true);
2185
- dv.setUint8(bufPtr + written + 20, e.type);
2186
- for (let j = 21; j < 24; j++)
2187
- dv.setUint8(bufPtr + written + j, 0);
2188
- written += 24;
2189
- // Write as much of the name as fits.
2190
- const nameRoom = Math.min(nameBytes.length, bufLen - written);
2191
- memU8.set(nameBytes.subarray(0, nameRoom), bufPtr + written);
2192
- written += nameRoom;
2193
- if (nameRoom < nameBytes.length)
2194
- break;
2195
- }
2196
- writeU32LE(bufusedPtr, written);
2197
- return __WASI_ESUCCESS;
2198
- },
2199
- fd_advise() { return __WASI_ESUCCESS; },
914
+ fd_readdir(_fd, _bufPtr, _bufLen, _cookieArg, _bufusedPtr) { return __WASI_EBADF; },
915
+ fd_advise(_fd, _offset, _len, _advice) { return __WASI_ESUCCESS; },
2200
916
  // WASI socket and polling support B4: real fd_allocate. Extends the file's byte buffer with
2201
917
  // zeros so [offset, offset+len) is allocated. POSIX posix_fallocate
2202
918
  // semantics. ENOSPC is not reachable in our in-memory FS (the
2203
919
  // 32 MiB snapshot cap is enforced at supervisor level; in-call we
2204
920
  // just allocate the JS Uint8Array).
2205
- fd_allocate(fd, offsetArg, lenArg) {
2206
- const entry = fdTable.get(fd);
2207
- if (!entry || entry.kind !== 'file')
2208
- return __WASI_EBADF;
2209
- const offset = typeof offsetArg === 'bigint' ? Number(offsetArg) : (offsetArg >>> 0);
2210
- const len = typeof lenArg === 'bigint' ? Number(lenArg) : (lenArg >>> 0);
2211
- const cur = getFile(entry.vfsPath) || new Uint8Array(0);
2212
- const needed = offset + len;
2213
- if (needed <= cur.length)
2214
- return __WASI_ESUCCESS; // already big enough
2215
- const next = new Uint8Array(needed);
2216
- next.set(cur, 0); // [cur.length, needed) is implicitly zero-initialised
2217
- setFile(entry.vfsPath, next);
2218
- return __WASI_ESUCCESS;
2219
- },
921
+ fd_allocate(_fd, _offsetArg, _lenArg) { return __WASI_EBADF; },
2220
922
  fd_datasync() { return __WASI_ESUCCESS; },
2221
923
  fd_sync() { return __WASI_ESUCCESS; },
2222
924
  // dup2(2), and the only way to reach it: preview1 has no dup. Renumbering
@@ -2428,17 +1130,15 @@ export function __wasiMakeImports(opts) {
2428
1130
  });
2429
1131
  }
2430
1132
  // Regular files, dirs, stdio: always ready (POSIX: regular files
2431
- // never block — read returns immediately even if at EOF).
2432
- if (entry.kind === 'file' || entry.kind === 'dir' ||
2433
- entry.kind === 'preopen' ||
1133
+ // never block — read returns immediately even if at EOF). Files and
1134
+ // directories are the authority's descriptors; leaving them out made
1135
+ // polling one EBADF, which a guest reads as "this fd is gone".
1136
+ if (entry.kind === 'preopen' || entry.kind === 'authority' || entry.kind === 'resident' ||
2434
1137
  entry.kind === 'stdin' || entry.kind === 'stdout' || entry.kind === 'stderr') {
2435
1138
  let nbytes = 0n;
2436
- if (entry.kind === 'file' && s.tag === __WASI_EVENTTYPE_FD_READ) {
2437
- const f = getFile(entry.vfsPath);
2438
- if (f) {
2439
- const remain = f.length - (entry.offset || 0);
2440
- nbytes = remain > 0 ? BigInt(remain) : 0n;
2441
- }
1139
+ if (entry.kind === 'resident' && s.tag === __WASI_EVENTTYPE_FD_READ) {
1140
+ const remain = entry.bytes.length - entry.position;
1141
+ nbytes = remain > 0 ? BigInt(remain) : 0n;
2442
1142
  }
2443
1143
  else if (s.tag === __WASI_EVENTTYPE_FD_WRITE) {
2444
1144
  // Writable: report large available capacity. Most user code
@@ -2843,6 +1543,15 @@ export function __wasiMakeImports(opts) {
2843
1543
  }
2844
1544
  },
2845
1545
  };
1546
+ installAuthorityFilesystem(imports, {
1547
+ fs: () => __wasiSup ? supervisorFilesystem(__wasiSup, opts.parking === 'none' ? __wasiSup.synchronous : undefined) : null,
1548
+ memory: opts.getMemory,
1549
+ fds: fdTable,
1550
+ allocateFd: __wasiAllocateFd,
1551
+ abi: opts.abi,
1552
+ synchronous: opts.parking === 'none',
1553
+ residentBytes: __wasiFS.residentFileCap,
1554
+ });
2846
1555
  // Raw async socket bodies, captured BEFORE JSPI-wrapping so fd_read /
2847
1556
  // fd_write can route socket fds through them (wasi-libc maps read(2)/
2848
1557
  // write(2) to fd_read/fd_write for every fd kind, sockets included).
@@ -2858,7 +1567,13 @@ export function __wasiMakeImports(opts) {
2858
1567
  // Every import that can park.
2859
1568
  const parkable = [
2860
1569
  'sock_send', 'sock_recv', 'sock_shutdown', 'sock_accept', 'poll_oneoff',
2861
- 'fd_read', 'fd_write', 'fd_pread', 'path_filestat_get',
1570
+ 'fd_read', 'fd_write', 'fd_pread', 'fd_pwrite', 'path_filestat_get',
1571
+ 'path_open', 'fd_close', 'fd_renumber', 'fd_seek', 'fd_tell', 'fd_filestat_get',
1572
+ 'fd_fdstat_set_flags', 'fd_fdstat_set_rights', 'fd_filestat_set_size',
1573
+ 'fd_sync', 'fd_datasync', 'fd_allocate', 'fd_advise', 'fd_readdir',
1574
+ 'path_create_directory', 'path_remove_directory', 'path_unlink_file',
1575
+ 'path_rename', 'path_symlink', 'path_readlink', 'path_link',
1576
+ 'fd_filestat_set_times', 'path_filestat_set_times',
2862
1577
  ];
2863
1578
  // Applied before Suspending wraps them.
2864
1579
  for (const name of parkable) {
@@ -2908,33 +1623,35 @@ export function __wasiMakeImports(opts) {
2908
1623
  imports.sock_recv = new WebAssembly.Suspending(imports.sock_recv);
2909
1624
  imports.sock_shutdown = new WebAssembly.Suspending(imports.sock_shutdown);
2910
1625
  imports.sock_accept = new WebAssembly.Suspending(imports.sock_accept);
2911
- // WASI socket and polling support B8: poll_oneoff also needs JSPI to support CLOCK
2912
- // subscriptions (await setTimeout) and socket-fd readiness
2913
- // (await reader.read()). Same Suspending shape as sock_*.
2914
1626
  imports.poll_oneoff = new WebAssembly.Suspending(imports.poll_oneoff);
2915
- // fd_read/fd_write become suspending too: a socket fd routes to the
2916
- // async sock bodies (Promise → JSPI suspends). File/stdio ops return a
2917
- // plain errno number, which JSPI passes through UNDER AN ACTIVE
2918
- // SUSPENDER. There is no passthrough without one: V8 traps ANY call
2919
- // into a Suspending import off a non-promising stack with SuspendError
2920
- // "trying to suspend without WebAssembly.promising", even when the
2921
- // import returns a plain number (measured on workerd, compat
2922
- // 2026-04-01). Every export that can reach these imports must be
2923
- // entered via WebAssembly.promising — assuming sync callers were
2924
- // unaffected is exactly how the Ruby VM boot went dark.
2925
1627
  imports.fd_read = new WebAssembly.Suspending(imports.fd_read);
2926
1628
  imports.fd_write = new WebAssembly.Suspending(imports.fd_write);
2927
- // fd_pread reaches the same file bodies as fd_read and so can equally
2928
- // land on a cache miss that must go to the supervisor. Leaving it
2929
- // unwrapped would trap the guest the first time a demand load happened
2930
- // to arrive through pread(2) rather than read(2).
2931
1629
  imports.fd_pread = new WebAssembly.Suspending(imports.fd_pread);
2932
- // path_filestat_get resolves a path the seed manifest never listed by
2933
- // asking the supervisor, so it too can return a Promise.
1630
+ imports.fd_pwrite = new WebAssembly.Suspending(imports.fd_pwrite);
2934
1631
  imports.path_filestat_get = new WebAssembly.Suspending(imports.path_filestat_get);
2935
- // sched_yield parks only in a threaded process, so it is wrapped only for
2936
- // one: every other guest keeps the plain i32 return it has today, and no
2937
- // existing runtime changes shape.
1632
+ imports.path_open = new WebAssembly.Suspending(imports.path_open);
1633
+ imports.fd_close = new WebAssembly.Suspending(imports.fd_close);
1634
+ imports.fd_renumber = new WebAssembly.Suspending(imports.fd_renumber);
1635
+ imports.fd_seek = new WebAssembly.Suspending(imports.fd_seek);
1636
+ imports.fd_tell = new WebAssembly.Suspending(imports.fd_tell);
1637
+ imports.fd_filestat_get = new WebAssembly.Suspending(imports.fd_filestat_get);
1638
+ imports.fd_fdstat_set_flags = new WebAssembly.Suspending(imports.fd_fdstat_set_flags);
1639
+ imports.fd_fdstat_set_rights = new WebAssembly.Suspending(imports.fd_fdstat_set_rights);
1640
+ imports.fd_filestat_set_size = new WebAssembly.Suspending(imports.fd_filestat_set_size);
1641
+ imports.fd_sync = new WebAssembly.Suspending(imports.fd_sync);
1642
+ imports.fd_datasync = new WebAssembly.Suspending(imports.fd_datasync);
1643
+ imports.fd_allocate = new WebAssembly.Suspending(imports.fd_allocate);
1644
+ imports.fd_advise = new WebAssembly.Suspending(imports.fd_advise);
1645
+ imports.fd_readdir = new WebAssembly.Suspending(imports.fd_readdir);
1646
+ imports.path_create_directory = new WebAssembly.Suspending(imports.path_create_directory);
1647
+ imports.path_remove_directory = new WebAssembly.Suspending(imports.path_remove_directory);
1648
+ imports.path_unlink_file = new WebAssembly.Suspending(imports.path_unlink_file);
1649
+ imports.path_rename = new WebAssembly.Suspending(imports.path_rename);
1650
+ imports.path_symlink = new WebAssembly.Suspending(imports.path_symlink);
1651
+ imports.path_readlink = new WebAssembly.Suspending(imports.path_readlink);
1652
+ imports.path_link = new WebAssembly.Suspending(imports.path_link);
1653
+ imports.fd_filestat_set_times = new WebAssembly.Suspending(imports.fd_filestat_set_times);
1654
+ imports.path_filestat_set_times = new WebAssembly.Suspending(imports.path_filestat_set_times);
2938
1655
  if (opts.threads)
2939
1656
  imports.sched_yield = new WebAssembly.Suspending(imports.sched_yield);
2940
1657
  }
@@ -3006,6 +1723,4 @@ export async function __wasiRunStartAsync(instance, ctx) {
3006
1723
  // already follow) because a direct reference to a preamble-only symbol will
3007
1724
  // not typecheck in the supervisor bundle the body is authored in.
3008
1725
  globalThis.__wasiAdoptSupervisor = __wasiAdoptSupervisor;
3009
- globalThis.__wasiDrainPersist = __wasiDrainPersist;
3010
- globalThis.__wasiRevalidateFS = __wasiRevalidateFS;
3011
1726
  // ── END: wasi-instance preamble ─────────────────────────────────────────