@ai-ecoverse/slicc-kernel 1.71.1 → 1.73.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/node.js CHANGED
@@ -371,6 +371,7 @@ var Dac = class {
371
371
  log;
372
372
  seen = /* @__PURE__ */ new Set();
373
373
  suppressed = false;
374
+ tagOf = () => void 0;
374
375
  constructor(mode, hook, log = (line) => console.warn(line)) {
375
376
  this.mode = mode;
376
377
  this.hook = hook;
@@ -379,7 +380,9 @@ var Dac = class {
379
380
  get enforcing() {
380
381
  return this.mode === "enforce";
381
382
  }
382
- denied(denial) {
383
+ denied(given) {
384
+ const tag = given.tag ?? (given.pid ? this.tagOf(given.pid) : void 0);
385
+ const denial = tag === void 0 ? given : { ...given, tag };
383
386
  try {
384
387
  this.hook?.({ ...denial });
385
388
  } catch {
@@ -397,7 +400,7 @@ var Dac = class {
397
400
  }
398
401
  this.seen.add(key);
399
402
  this.log(
400
- `slicc-kernel users report: ${d.errno} ${d.op} ${d.path} (uid ${d.uid}, pid ${d.pid})`
403
+ `slicc-kernel users report: ${d.errno} ${d.op} ${d.path} (uid ${d.uid}, pid ${d.pid}${d.tag === void 0 ? "" : `, tag ${d.tag}`})`
401
404
  );
402
405
  }
403
406
  };
@@ -413,7 +416,8 @@ var Created = class {
413
416
  }
414
417
  };
415
418
  function deny(g, op, path, errno = "EACCES") {
416
- g.dac.denied({ pid: g.pid, uid: g.cred.euid, op, path, errno });
419
+ const tag = g.tag === void 0 ? {} : { tag: g.tag };
420
+ g.dac.denied({ pid: g.pid, uid: g.cred.euid, op, path, errno, ...tag });
417
421
  }
418
422
  var TTY_MODE = 400;
419
423
  function ttyOpenable(g, tty) {
@@ -2521,6 +2525,12 @@ var CLIENT_CHECKS = {
2521
2525
  readlink: (g, at) => searchable(g, "readlink", at(0), false),
2522
2526
  exists: (g, at) => searchable(g, "exists", at(0), true)
2523
2527
  };
2528
+ function clientFs(l, tag, self, req) {
2529
+ if (!FS_METHODS.includes(req.method)) {
2530
+ throw new Error(`unknown file system call ${req.method}`);
2531
+ }
2532
+ return fsCall(l.fsFor(tag), req.method, req.args, ownerFor(l, self), guardFor(l, self, tag));
2533
+ }
2524
2534
  async function fsCall(fs, method, args, owner, g) {
2525
2535
  const path = (i) => fs.resolvePath("/", String(args[i]));
2526
2536
  if (g) {
@@ -2595,9 +2605,9 @@ function ownerFor(l, self) {
2595
2605
  if (!l.owners || !self) return void 0;
2596
2606
  return { uid: self.uid, gid: self.gid, umask: self.umask ?? 18 };
2597
2607
  }
2598
- function guardFor(l, self) {
2608
+ function guardFor(l, self, tag) {
2599
2609
  if (!l.dac || !self) return void 0;
2600
- return { fs: l.fs, cred: credOf(self), pid: 0, dac: l.dac };
2610
+ return { fs: l.fs, cred: credOf(self), pid: 0, dac: l.dac, tag };
2601
2611
  }
2602
2612
  async function accountOf(l, user) {
2603
2613
  const found = await l.users.lookup(user);
@@ -2701,6 +2711,7 @@ async function watchOp(l, req, watches, send, g) {
2701
2711
  function serveClient(port, host) {
2702
2712
  let state = "new";
2703
2713
  let self;
2714
+ const guard2 = (l) => guardFor(l, self, host.tag);
2704
2715
  const terminals = /* @__PURE__ */ new Map();
2705
2716
  const groups = /* @__PURE__ */ new Set();
2706
2717
  const sessions = /* @__PURE__ */ new Set();
@@ -2807,13 +2818,10 @@ function serveClient(port, host) {
2807
2818
  case "ps":
2808
2819
  return { result: processes(l, self) };
2809
2820
  case "fs":
2810
- if (!FS_METHODS.includes(req.method)) {
2811
- throw new Error(`unknown file system call ${req.method}`);
2812
- }
2813
- return fsCall(l.processFs, req.method, req.args, ownerFor(l, self), guardFor(l, self));
2821
+ return clientFs(l, host.tag, self, req);
2814
2822
  case "watch":
2815
2823
  case "unwatch":
2816
- return watchOp(l, req, watches, send, guardFor(l, self));
2824
+ return watchOp(l, req, watches, send, guard2(l));
2817
2825
  case "mount":
2818
2826
  return { result: await l.mountAs(clientCred(self), req.spec) };
2819
2827
  case "umount":
@@ -2998,7 +3006,13 @@ var MemoryMeta = class {
2998
3006
  };
2999
3007
 
3000
3008
  // src/fs/protect.ts
3001
- var PROTECT_WRITER = "slicc-kernel: protect.writer needs the process tag (#405), which this kernel does not have yet";
3009
+ function writerTags(protection) {
3010
+ const writers = protection?.writers ?? [];
3011
+ if (!Array.isArray(writers) || writers.some((tag) => typeof tag !== "string" || tag === "")) {
3012
+ throw new TypeError("protect.writers must be a list of process tags");
3013
+ }
3014
+ return new Set(writers);
3015
+ }
3002
3016
  function protectedPrefixes(paths) {
3003
3017
  return [...new Set(paths.map((p) => normalizePath(p)))];
3004
3018
  }
@@ -3174,6 +3188,121 @@ var ProtectedFs = class {
3174
3188
  }
3175
3189
  };
3176
3190
 
3191
+ // src/fs/remote.ts
3192
+ var ANNOUNCE_WAIT_MS = 500;
3193
+ var PRUNE_EVERY_MS = 3e4;
3194
+ function lockManager() {
3195
+ return globalThis.navigator?.locks;
3196
+ }
3197
+ var RemoteWatch = class {
3198
+ channel;
3199
+ name;
3200
+ tab = crypto.randomUUID();
3201
+ peers = /* @__PURE__ */ new Set();
3202
+ others = /* @__PURE__ */ new Map();
3203
+ union = /* @__PURE__ */ new Set();
3204
+ listeners = /* @__PURE__ */ new Set();
3205
+ waiting = /* @__PURE__ */ new Map();
3206
+ locks;
3207
+ mine = [];
3208
+ seq = 0;
3209
+ pruned = 0;
3210
+ constructor(name2, options = {}) {
3211
+ this.name = name2;
3212
+ this.locks = options.locks === void 0 ? lockManager() : options.locks ?? void 0;
3213
+ this.channel = new BroadcastChannel(name2);
3214
+ this.channel.onmessage = ({ data }) => this.receive(data);
3215
+ this.channel.unref?.();
3216
+ void this.locks?.request(this.lockOf(this.tab), () => new Promise(() => {
3217
+ }));
3218
+ this.channel.postMessage({ hello: this.tab });
3219
+ }
3220
+ lockOf(tab) {
3221
+ return `${this.name}:${tab}`;
3222
+ }
3223
+ receive(data) {
3224
+ if (data.hello !== void 0) this.greeted(data.hello);
3225
+ if (data.here !== void 0 && data.to === this.tab) this.peers.add(data.here);
3226
+ if (data.tab !== void 0 && data.paths !== void 0) this.announced(data);
3227
+ if (data.ack !== void 0 && data.to === this.tab && data.from !== void 0) {
3228
+ this.acked(data.ack, data.from);
3229
+ }
3230
+ if (data.changed !== void 0) this.emit({ paths: data.changed, trees: [] });
3231
+ }
3232
+ greeted(peer) {
3233
+ this.peers.add(peer);
3234
+ this.channel.postMessage({ here: this.tab, to: peer });
3235
+ if (this.mine.length > 0) void this.announce(this.mine);
3236
+ }
3237
+ announced({ tab, paths, seq: seq2 }) {
3238
+ const from = tab;
3239
+ this.peers.add(from);
3240
+ if (paths.length > 0) this.others.set(from, paths);
3241
+ else this.others.delete(from);
3242
+ this.rebuild();
3243
+ this.channel.postMessage({ ack: seq2, from: this.tab, to: from });
3244
+ }
3245
+ acked(seq2, from) {
3246
+ const wait2 = this.waiting.get(seq2);
3247
+ if (!wait2) return;
3248
+ wait2.left.delete(from);
3249
+ if (wait2.left.size === 0) wait2.done();
3250
+ }
3251
+ rebuild() {
3252
+ this.union.clear();
3253
+ for (const paths of this.others.values()) for (const path of paths) this.union.add(path);
3254
+ }
3255
+ async prune() {
3256
+ if (!this.locks) return;
3257
+ const held = new Set(((await this.locks.query()).held ?? []).map((lock) => lock.name));
3258
+ for (const tab of /* @__PURE__ */ new Set([...this.others.keys(), ...this.peers])) {
3259
+ if (!held.has(this.lockOf(tab))) {
3260
+ this.others.delete(tab);
3261
+ this.peers.delete(tab);
3262
+ }
3263
+ }
3264
+ this.rebuild();
3265
+ }
3266
+ watched() {
3267
+ return this.union.size > 0;
3268
+ }
3269
+ wrote(path) {
3270
+ if (!this.union.has(path)) return;
3271
+ this.channel.postMessage({ changed: [path] });
3272
+ const now = Date.now();
3273
+ if (now - this.pruned < PRUNE_EVERY_MS) return;
3274
+ this.pruned = now;
3275
+ void this.prune();
3276
+ }
3277
+ announce(paths) {
3278
+ this.mine = [...paths];
3279
+ const seq2 = ++this.seq;
3280
+ this.channel.postMessage({ tab: this.tab, paths: this.mine, seq: seq2 });
3281
+ if (this.peers.size === 0) return Promise.resolve();
3282
+ return new Promise((resolve) => {
3283
+ const done4 = () => {
3284
+ clearTimeout(timer);
3285
+ this.waiting.delete(seq2);
3286
+ resolve();
3287
+ };
3288
+ const timer = setTimeout(() => {
3289
+ done4();
3290
+ void this.prune();
3291
+ }, ANNOUNCE_WAIT_MS);
3292
+ this.waiting.set(seq2, { left: new Set(this.peers), done: done4 });
3293
+ });
3294
+ }
3295
+ emit(change) {
3296
+ for (const listener of this.listeners) listener(change);
3297
+ }
3298
+ listen(listener) {
3299
+ this.listeners.add(listener);
3300
+ return () => {
3301
+ this.listeners.delete(listener);
3302
+ };
3303
+ }
3304
+ };
3305
+
3177
3306
  // src/fs/opfs.ts
3178
3307
  var S_IFDIR = 16384;
3179
3308
  var S_IFREG = 32768;
@@ -3297,6 +3426,7 @@ var OpfsFs = class {
3297
3426
  dirs = new PathMap();
3298
3427
  checked = /* @__PURE__ */ new WeakMap();
3299
3428
  channel;
3429
+ remote;
3300
3430
  ranged;
3301
3431
  owners = false;
3302
3432
  constructor(root, meta = new MemoryMeta(), channel, options = {}) {
@@ -3304,9 +3434,15 @@ var OpfsFs = class {
3304
3434
  this.meta = sidecar(meta);
3305
3435
  this.ranged = options.ranged === true;
3306
3436
  this.channel = channel === void 0 ? void 0 : new BroadcastChannel(channel);
3437
+ this.remote = channel === void 0 ? void 0 : new RemoteWatch(`${channel}:watch`);
3307
3438
  if (this.channel) {
3439
+ const remote = this.remote;
3440
+ meta.listen?.(
3441
+ (c) => remote?.emit({ paths: c.paths ?? [], trees: c.trees ?? [] })
3442
+ );
3308
3443
  this.channel.onmessage = ({ data }) => {
3309
3444
  for (const path of data) this.forget(path);
3445
+ remote?.emit({ paths: data, trees: [] });
3310
3446
  };
3311
3447
  this.channel.unref?.();
3312
3448
  }
@@ -3315,6 +3451,16 @@ var OpfsFs = class {
3315
3451
  for (const path of paths) this.forget(path);
3316
3452
  this.channel?.postMessage(paths);
3317
3453
  }
3454
+ remoteChanges(listener) {
3455
+ return this.remote?.listen(listener) ?? (() => {
3456
+ });
3457
+ }
3458
+ announceWatch(paths) {
3459
+ return this.remote?.announce(paths) ?? Promise.resolve();
3460
+ }
3461
+ committed(target) {
3462
+ if (this.remote?.watched()) this.remote.wrote(target);
3463
+ }
3318
3464
  resolvePath(base, path) {
3319
3465
  return resolvePath(base, path);
3320
3466
  }
@@ -3374,6 +3520,9 @@ var OpfsFs = class {
3374
3520
  protect(prefixes2) {
3375
3521
  this.guarded = prefixes2;
3376
3522
  }
3523
+ unguarded() {
3524
+ return Object.create(this, { guarded: { value: [] } });
3525
+ }
3377
3526
  refuse(path, holding = false) {
3378
3527
  if (isProtected(this.guarded, path) || holding && holdsProtected(this.guarded, path)) {
3379
3528
  throw fsError("EROFS", path);
@@ -3543,15 +3692,15 @@ var OpfsFs = class {
3543
3692
  }
3544
3693
  dirTimes = /* @__PURE__ */ new Map();
3545
3694
  dirDirty = /* @__PURE__ */ new Set();
3546
- dirFlush;
3695
+ flushing = {};
3547
3696
  touch(...dirs) {
3548
3697
  const now = Date.now();
3549
3698
  for (const dir of dirs) {
3550
3699
  this.dirTimes.set(dir, now);
3551
3700
  this.dirDirty.add(dir);
3552
3701
  }
3553
- this.dirFlush ??= Promise.resolve().then(() => this.flushDirTimes()).catch(() => void 0).finally(() => {
3554
- this.dirFlush = void 0;
3702
+ this.flushing.dir ??= Promise.resolve().then(() => this.flushDirTimes()).catch(() => void 0).finally(() => {
3703
+ this.flushing.dir = void 0;
3555
3704
  });
3556
3705
  }
3557
3706
  async flushDirTimes() {
@@ -3666,15 +3815,18 @@ var OpfsFs = class {
3666
3815
  );
3667
3816
  }
3668
3817
  async pwrite(path, offset, bytes) {
3669
- const handle = (await this.fileFor(path))[0];
3818
+ const [handle, , target] = await this.fileFor(path);
3670
3819
  await this.retrying(path, () => positioned(handle, (io) => io.write(offset, bytes)), true);
3820
+ this.committed(target);
3671
3821
  }
3672
3822
  async truncate(path, size) {
3673
3823
  const handle = await this.existing(path, true);
3824
+ const target = this.remote?.watched() ? (await this.locate(path, true)).path : path;
3674
3825
  await this.retrying(path, () => positioned(handle, (io) => io.truncate(size)), true);
3826
+ this.committed(target);
3675
3827
  }
3676
3828
  async writeSession(path, truncate) {
3677
- const handle = (await this.fileFor(path))[0];
3829
+ const [handle, , target] = await this.fileFor(path);
3678
3830
  const writable2 = await this.retrying(
3679
3831
  path,
3680
3832
  () => handle.createWritable({ keepExistingData: !truncate }),
@@ -3687,9 +3839,12 @@ var OpfsFs = class {
3687
3839
  truncate: (size) => writable2.truncate(size).catch((err) => {
3688
3840
  throw translate(err, path, "write", false);
3689
3841
  }),
3690
- close: () => writable2.close().catch((err) => {
3691
- throw translate(err, path, "commit", false);
3692
- })
3842
+ close: async () => {
3843
+ await writable2.close().catch((err) => {
3844
+ throw translate(err, path, "commit", false);
3845
+ });
3846
+ this.committed(target);
3847
+ }
3693
3848
  };
3694
3849
  }
3695
3850
  async readFile(path) {
@@ -3738,6 +3893,7 @@ var OpfsFs = class {
3738
3893
  await writable2?.abort().catch(() => void 0);
3739
3894
  throw translate(err, path, "commit", false);
3740
3895
  }
3896
+ this.committed(target);
3741
3897
  }
3742
3898
  async listing(path) {
3743
3899
  const found = await this.locate(path, true);
@@ -3759,12 +3915,12 @@ var OpfsFs = class {
3759
3915
  const base = dir === "/" ? "" : dir;
3760
3916
  const metas = await this.meta.get(children2.map(([name2]) => `${base}/${name2}`));
3761
3917
  const handed2 = this.owners ? await this.handedTo(dir) : void 0;
3762
- const owned3 = (stat, entry) => handed2 ? { ...stat, ...ownOf(entry) ?? handed2 } : stat;
3918
+ const owned4 = (stat, entry) => handed2 ? { ...stat, ...ownOf(entry) ?? handed2 } : stat;
3763
3919
  return Promise.all(
3764
3920
  children2.map(async ([name2, child], i) => {
3765
- if (!isHandle(child)) return [name2, owned3(this.linkStat(child), child)];
3921
+ if (!isHandle(child)) return [name2, owned4(this.linkStat(child), child)];
3766
3922
  const stat = await this.statOf(`${base}/${name2}`, child, metas[i]).catch(() => null);
3767
- return [name2, stat && owned3(stat, metas[i])];
3923
+ return [name2, stat && owned4(stat, metas[i])];
3768
3924
  })
3769
3925
  );
3770
3926
  }
@@ -5273,6 +5429,136 @@ function binfmtOf(binfmts, path) {
5273
5429
  return best === void 0 ? void 0 : binfmts.get(best);
5274
5430
  }
5275
5431
 
5432
+ // src/fs/attest.ts
5433
+ var ATTEST_MAX = 1024 * 1024;
5434
+ var UNOWNED = 1e3;
5435
+ function refuse(code, path, why) {
5436
+ throw Object.assign(new Error(`attest ${path}: ${why}`), { code });
5437
+ }
5438
+ function chainOf(path) {
5439
+ const parts2 = path.split("/").filter(Boolean);
5440
+ return ["/", ...parts2.map((_, i) => `/${parts2.slice(0, i + 1).join("/")}`)];
5441
+ }
5442
+ function under2(path, target) {
5443
+ return path === target || path.startsWith(`${target}/`);
5444
+ }
5445
+ function owned(path, st) {
5446
+ return { path, uid: st.uid ?? UNOWNED, gid: st.gid ?? UNOWNED, mode: st.mode };
5447
+ }
5448
+ function unchanged(a, b, bytes) {
5449
+ return a.ino === b.ino && a.mtime.getTime() === b.mtime.getTime() && a.size === b.size && bytes.length === b.size;
5450
+ }
5451
+ async function sha256(bytes) {
5452
+ const digest = new Uint8Array(
5453
+ await crypto.subtle.digest("SHA-256", bytes)
5454
+ );
5455
+ return Array.from(digest, (b) => b.toString(16).padStart(2, "0")).join("");
5456
+ }
5457
+ function attested(path, st, bytes, digest, o, parents) {
5458
+ const { uid, gid, mode } = owned(path, st);
5459
+ return {
5460
+ path,
5461
+ bytes,
5462
+ uid,
5463
+ gid,
5464
+ mode,
5465
+ ino: st.ino,
5466
+ mtimeMs: st.mtime.getTime(),
5467
+ sha256: digest,
5468
+ users: o.users,
5469
+ onRootFs: true,
5470
+ parents
5471
+ };
5472
+ }
5473
+ async function trustedParents(fs, path, o) {
5474
+ const parents = [];
5475
+ for (const dir of chainOf(path).slice(0, -1)) {
5476
+ const st = await fs.lstat(dir);
5477
+ if (st.isSymbolicLink) refuse("EACCES", path, `${dir} is a symlink`);
5478
+ if (!st.isDirectory) refuse("ENOTDIR", path, `${dir} is not a directory`);
5479
+ const perm = owned(dir, st);
5480
+ if (o.users !== "off" && (perm.uid !== 0 || (perm.mode & 18) !== 0)) {
5481
+ refuse("EACCES", path, `${dir} is not root-owned or is writable by group or others`);
5482
+ }
5483
+ parents.push(perm);
5484
+ }
5485
+ return parents;
5486
+ }
5487
+ async function attest(fs, asked, o) {
5488
+ if (typeof asked !== "string" || !asked.startsWith("/")) {
5489
+ refuse("EINVAL", String(asked), "not an absolute path");
5490
+ }
5491
+ const path = normalizePath(asked);
5492
+ const mount = o.mounts.find((target) => {
5493
+ const at = normalizePath(target);
5494
+ return at === "/" || under2(path, at);
5495
+ });
5496
+ if (mount !== void 0) refuse("EACCES", path, `under the mount ${mount}`);
5497
+ const parents = await trustedParents(fs, path, o);
5498
+ for (let tries = 0; ; tries++) {
5499
+ const before = await fs.lstat(path);
5500
+ if (before.isSymbolicLink) refuse("EACCES", path, "is a symlink");
5501
+ if (!before.isFile)
5502
+ refuse(before.isDirectory ? "EISDIR" : "EINVAL", path, "is not a regular file");
5503
+ if (before.size > ATTEST_MAX) refuse("EFBIG", path, `is over ${ATTEST_MAX} bytes`);
5504
+ const bytes = (await fs.readFileBuffer(path)).slice();
5505
+ const after = await fs.lstat(path);
5506
+ if (unchanged(before, after, bytes)) {
5507
+ return attested(path, after, bytes, await sha256(bytes), o, parents);
5508
+ }
5509
+ if (tries >= 1) refuse("EAGAIN", path, "changed while it was read");
5510
+ }
5511
+ }
5512
+ var WATCH_DEBOUNCE_MS = 50;
5513
+ var FileWatches = class {
5514
+ live = /* @__PURE__ */ new Set();
5515
+ local;
5516
+ remote;
5517
+ constructor(local, remote) {
5518
+ this.local = local;
5519
+ this.remote = remote;
5520
+ }
5521
+ announce() {
5522
+ return this.remote.announceWatch?.([...new Set([...this.live].flat())]) ?? Promise.resolve();
5523
+ }
5524
+ async watch(paths, notify) {
5525
+ const files = paths.map(normalizePath);
5526
+ const timers = /* @__PURE__ */ new Map();
5527
+ let active = true;
5528
+ const fire = (changed, trees) => {
5529
+ for (const file of files) {
5530
+ const hit = [...changed, ...trees].some((p) => under2(file, normalizePath(p)));
5531
+ if (!hit) continue;
5532
+ clearTimeout(timers.get(file));
5533
+ timers.set(
5534
+ file,
5535
+ setTimeout(() => {
5536
+ timers.delete(file);
5537
+ if (active) notify(file);
5538
+ }, WATCH_DEBOUNCE_MS)
5539
+ );
5540
+ }
5541
+ };
5542
+ const unwatch = this.local.watch(
5543
+ files,
5544
+ { recursive: false },
5545
+ (change) => fire("overflow" in change ? files : change.paths, [])
5546
+ );
5547
+ const unlisten = this.remote.remoteChanges?.((change) => fire(change.paths, change.trees));
5548
+ this.live.add(files);
5549
+ await this.announce();
5550
+ return () => {
5551
+ if (!active) return;
5552
+ active = false;
5553
+ for (const timer of timers.values()) clearTimeout(timer);
5554
+ unwatch();
5555
+ unlisten?.();
5556
+ this.live.delete(files);
5557
+ void this.announce();
5558
+ };
5559
+ }
5560
+ };
5561
+
5276
5562
  // src/fs/commands.ts
5277
5563
  var S_IFDIR2 = 16384;
5278
5564
  var S_IFREG2 = 32768;
@@ -5311,7 +5597,7 @@ function withCommandDirs(fs, names, userDir = async () => void 0, owners = false
5311
5597
  if (st || path === "/") return st ?? {};
5312
5598
  return nearest(path.slice(0, path.lastIndexOf("/")) || "/");
5313
5599
  }
5314
- async function owned3(stat2, dir) {
5600
+ async function owned4(stat2, dir) {
5315
5601
  if (!owners) return stat2;
5316
5602
  const { uid = 0, gid = 0 } = dir === void 0 ? {} : await nearest(dir);
5317
5603
  return { ...stat2, uid, gid };
@@ -5325,17 +5611,17 @@ function withCommandDirs(fs, names, userDir = async () => void 0, owners = false
5325
5611
  }
5326
5612
  async function virtual(path) {
5327
5613
  if (path === "/usr" || path === "/bin" || path === "/usr/bin") {
5328
- return owned3(synthetic(true, path));
5614
+ return owned4(synthetic(true, path));
5329
5615
  }
5330
5616
  const name2 = COMMAND.exec(path)?.[1];
5331
5617
  if (name2 !== void 0) {
5332
- return (await names()).has(name2) ? owned3(synthetic(false, path)) : void 0;
5618
+ return (await names()).has(name2) ? owned4(synthetic(false, path)) : void 0;
5333
5619
  }
5334
- if ((await userDir(path))?.size) return owned3(synthetic(true, path), path);
5620
+ if ((await userDir(path))?.size) return owned4(synthetic(true, path), path);
5335
5621
  const slash = path.lastIndexOf("/");
5336
5622
  const dir = path.slice(0, slash);
5337
5623
  const own = await userDir(dir);
5338
- return own?.has(path.slice(slash + 1)) ? owned3(synthetic(false, path), dir) : void 0;
5624
+ return own?.has(path.slice(slash + 1)) ? owned4(synthetic(false, path), dir) : void 0;
5339
5625
  }
5340
5626
  async function linked(path) {
5341
5627
  return await virtual(path) ?? virtual(await followLinks(fs, path));
@@ -5462,11 +5748,11 @@ async function physicalPathAsync(fs, path) {
5462
5748
 
5463
5749
  // src/fs/watch.ts
5464
5750
  var WATCH_BATCH_LIMIT = 1e3;
5465
- var under2 = (path, dir) => dir === "/" || path.startsWith(`${dir}/`);
5751
+ var under3 = (path, dir) => dir === "/" || path.startsWith(`${dir}/`);
5466
5752
  var parentOf6 = (path) => path.slice(0, path.lastIndexOf("/")) || "/";
5467
5753
  function touches(watcher, changed) {
5468
5754
  return watcher.paths.some(
5469
- (watched) => changed === watched || under2(watched, changed) || (watcher.recursive ? under2(changed, watched) : parentOf6(changed) === watched)
5755
+ (watched) => changed === watched || under3(watched, changed) || (watcher.recursive ? under3(changed, watched) : parentOf6(changed) === watched)
5470
5756
  );
5471
5757
  }
5472
5758
  var FsWatchers = class {
@@ -7185,7 +7471,7 @@ async function keptStat(entry, req) {
7185
7471
  return { ok: false, errno: "ENOENT", message: "the handle keeps no file" };
7186
7472
  return json2(statJson(await entry.fs.stat(hidden)));
7187
7473
  }
7188
- async function owned(entry, own, req, path, target) {
7474
+ async function owned2(entry, own, req, path, target) {
7189
7475
  const source = target === path ? void 0 : path;
7190
7476
  if (UNLINKING_OPS.has(req.op) && await linkedElsewhere(entry.fs, target, source)) {
7191
7477
  return run(entry, path, req);
@@ -7240,7 +7526,7 @@ async function dispatchOne(entry, req) {
7240
7526
  }
7241
7527
  if (!entry.own || !OWN_OPS.has(req.op)) return await run(entry, path, req);
7242
7528
  const target = TWO_PATHS.has(req.op) ? fs.resolvePath(cwd, req.arg2 ?? "") : path;
7243
- return await owned(entry, entry.own, req, path, target);
7529
+ return await owned2(entry, entry.own, req, path, target);
7244
7530
  } catch (err) {
7245
7531
  return toErrno(err);
7246
7532
  }
@@ -11443,6 +11729,7 @@ var SYSCALL_OPS = /* @__PURE__ */ new Set([
11443
11729
  "proc-sudo",
11444
11730
  "proc-run",
11445
11731
  "proc-exit",
11732
+ "proc-kill-self",
11446
11733
  "proc-fork",
11447
11734
  "proc-kill",
11448
11735
  "thread-kill",
@@ -11570,6 +11857,20 @@ var WasmProcess = class {
11570
11857
  else this.adoptExec(pid);
11571
11858
  return pid;
11572
11859
  }
11860
+ async killSelf(sig, target, probe2) {
11861
+ if (!this.options.exitSignal) throw new KernelError("ENOSYS");
11862
+ if (!isSignal(sig) || defaultAction(sig) !== "terminate") throw new KernelError("EINVAL");
11863
+ const { caught, ignored } = this.signalMasks();
11864
+ const to = target === 0 ? -this.pgid() : target ?? this.pid;
11865
+ if (sig !== SIG.KILL && (caught | ignored) & sigbit(sig)) {
11866
+ if (!await this.options.kill?.(to, sig)) throw new KernelError("ESRCH");
11867
+ return { ok: true, kind: "void" };
11868
+ }
11869
+ if (probe2) return { ok: true, kind: "json", json: "terminate" };
11870
+ if (to !== this.pid) await this.options.kill?.(to, sig);
11871
+ this.options.exitSignal(sig);
11872
+ return DYING;
11873
+ }
11573
11874
  run(req) {
11574
11875
  return runChild(req, {
11575
11876
  children: this.children,
@@ -12167,6 +12468,8 @@ var WasmProcess = class {
12167
12468
  if (!this.options.exitNow) throw new KernelError("ENOSYS");
12168
12469
  this.options.exitNow(req.status & 255);
12169
12470
  return DYING;
12471
+ case "proc-kill-self":
12472
+ return this.killSelf(req.sig, req.target, req.probe);
12170
12473
  case "proc-identity":
12171
12474
  return { ok: true, kind: "json", json: await this.options.identity?.() ?? null };
12172
12475
  case "proc-exec": {
@@ -12601,6 +12904,7 @@ function processOptions(opts, header4, hooks, created3) {
12601
12904
  raise: hooks.raise,
12602
12905
  threadKill: hooks.threadKill,
12603
12906
  ...hooks.exitNow ? { exitNow: hooks.exitNow } : {},
12907
+ ...hooks.exitSignal ? { exitSignal: hooks.exitSignal } : {},
12604
12908
  ...opts.program.abi === "wasi" || opts.program.abi === "js" ? { pendingBits: () => Atomics.load(header4, SAB_I_SIGNALS) } : {}
12605
12909
  };
12606
12910
  }
@@ -12634,7 +12938,10 @@ function spawnWasmProcess(opts) {
12634
12938
  {
12635
12939
  raise: (sig) => signal(sig),
12636
12940
  threadKill: (tid, sig) => killThread(threadSignals.get(tid), process2.signalMasks().caught, sig),
12637
- ...opts.program.abi === "js" ? { exitNow: (code) => exitNow(code) } : {}
12941
+ ...opts.program.abi === "js" ? {
12942
+ exitNow: (code) => exitNow(code),
12943
+ exitSignal: (sig) => queueMicrotask(() => finish(128 + sig, sig))
12944
+ } : {}
12638
12945
  },
12639
12946
  created3
12640
12947
  )
@@ -14542,8 +14849,8 @@ var ChunkedBytes = class {
14542
14849
  }
14543
14850
  return out;
14544
14851
  }
14545
- write(offset, bytes, owned3 = false) {
14546
- const whole = owned3 && bytes.byteOffset === 0 && bytes.byteLength === bytes.buffer.byteLength;
14852
+ write(offset, bytes, owned4 = false) {
14853
+ const whole = owned4 && bytes.byteOffset === 0 && bytes.byteLength === bytes.buffer.byteLength;
14547
14854
  for (let done4 = 0; done4 < bytes.length; ) {
14548
14855
  const at = offset + done4;
14549
14856
  const index = Math.floor(at / CHUNK3);
@@ -14593,7 +14900,7 @@ function created2(ino, kind, mode, from, owner) {
14593
14900
  gid: owner?.gid ?? from?.gid ?? 0
14594
14901
  };
14595
14902
  }
14596
- function owned2(node, change) {
14903
+ function owned3(node, change) {
14597
14904
  if (change.uid !== void 0) node.uid = change.uid;
14598
14905
  if (change.gid !== void 0) node.gid = change.gid;
14599
14906
  }
@@ -14764,7 +15071,7 @@ function tmpfs() {
14764
15071
  node.mtime = Date.now();
14765
15072
  }
14766
15073
  if (change.mtime !== void 0) node.mtime = change.mtime;
14767
- owned2(node, change);
15074
+ owned3(node, change);
14768
15075
  },
14769
15076
  async statfs() {
14770
15077
  return { bsize: 4096, blocks: 262144, bfree: 262144 };
@@ -15456,6 +15763,10 @@ var Launcher = class {
15456
15763
  fs;
15457
15764
  processFs;
15458
15765
  prefixes;
15766
+ writerTags;
15767
+ unguardedRoot;
15768
+ writers;
15769
+ unlinkOwnerOption;
15459
15770
  base;
15460
15771
  createWorker;
15461
15772
  modulesDir;
@@ -15513,12 +15824,16 @@ var Launcher = class {
15513
15824
  usersMode;
15514
15825
  dac;
15515
15826
  sealed;
15827
+ rootFs;
15828
+ fileWatches;
15516
15829
  nextPid = 1e3;
15517
15830
  boot = Date.now();
15518
15831
  terminals = 0;
15519
15832
  constructor(options) {
15520
15833
  this.usersMode = usersMode(options.users);
15834
+ this.rootFs = options.fs;
15521
15835
  this.dac = this.usersMode === "off" ? void 0 : new Dac(this.usersMode, options.onDenial);
15836
+ if (this.dac) this.dac.tagOf = (pid) => this.tagOf(pid);
15522
15837
  this.sealed = new SealedBroker(
15523
15838
  options.sealed,
15524
15839
  this.usersMode,
@@ -15569,7 +15884,7 @@ var Launcher = class {
15569
15884
  this.nodes = new VfsNodes(fs, (path) => this.released(path));
15570
15885
  this.nodes.keepsOnDisk = true;
15571
15886
  this.openFiles.add(this.nodes);
15572
- const holders = [this.held, [this.nodes]];
15887
+ const holders = [this.held, this.openFiles];
15573
15888
  this.unlinked = new UnlinkedKeeper(
15574
15889
  fs,
15575
15890
  { [Symbol.iterator]: () => holders.flatMap((set2) => [...set2]).values() },
@@ -15582,7 +15897,12 @@ var Launcher = class {
15582
15897
  }
15583
15898
  );
15584
15899
  this.fs = this.unlinked.wrap(keepingOpen(fs, this.nodes));
15585
- if (options.protect?.writer) throw new Error(PROTECT_WRITER);
15900
+ this.writerTags = writerTags(options.protect);
15901
+ this.unlinkOwnerOption = options.unlinkOwner;
15902
+ if (this.writerTags.size > 0 && options.fs.protect && !options.fs.unguarded) {
15903
+ throw new TypeError("protect.writers needs a root file system with an unguarded view");
15904
+ }
15905
+ this.unguardedRoot = options.fs.unguarded?.() ?? options.fs;
15586
15906
  this.prefixes = protectedPrefixes(options.protect?.paths ?? []);
15587
15907
  this.processFs = this.prefixes.length > 0 ? new ProtectedFs(this.prefixes).view(this.fs) : this.fs;
15588
15908
  options.fs.protect?.(this.prefixes);
@@ -15768,7 +16088,7 @@ var Launcher = class {
15768
16088
  async mayExec(cred, req, planned) {
15769
16089
  const { dac } = this;
15770
16090
  if (!dac) return;
15771
- const g = { fs: this.fs, cred, pid: req.pid, dac };
16091
+ const g = { fs: this.fs, cred, pid: req.pid, dac, tag: req.tag };
15772
16092
  const byPath = (file) => file.includes("/") && !COMMAND2.test(file);
15773
16093
  if (byPath(req.file)) {
15774
16094
  const path = this.fs.resolvePath(req.cwd, req.file);
@@ -15906,6 +16226,7 @@ var Launcher = class {
15906
16226
  throw fsError("ESRCH", `no process group ${req.pgid}`);
15907
16227
  }
15908
16228
  const pid = this.nextPid++;
16229
+ const stack = this.stackFor(req.tag);
15909
16230
  const terminal = req.fds.stdioTerminal();
15910
16231
  const decided = req.decided ?? Promise.resolve(true);
15911
16232
  const count = (byParent) => {
@@ -15936,8 +16257,8 @@ var Launcher = class {
15936
16257
  env: req.env,
15937
16258
  cwd: req.cwd,
15938
16259
  fds: req.fds,
15939
- fs: this.processFs,
15940
- ...this.prefixes.length > 0 ? { readonlyAt: (path) => isProtected(this.prefixes, path) } : {},
16260
+ fs: stack.fs,
16261
+ ...this.prefixes.length > 0 && !stack.writer ? { readonlyAt: (path) => isProtected(this.prefixes, path) } : {},
15941
16262
  createWorker: this.createWorker,
15942
16263
  onError: req.report,
15943
16264
  spawner: this.spawner(pid, req.report),
@@ -15952,9 +16273,9 @@ var Launcher = class {
15952
16273
  }),
15953
16274
  sudo: (call, signal) => this.sudo(pid, call, signal),
15954
16275
  openFiles: this.openFiles,
15955
- nodes: this.nodes,
16276
+ nodes: stack.nodes,
15956
16277
  held: this.held,
15957
- unlinkGate: this.unlinked.gate,
16278
+ unlinkGate: stack.unlinked.gate,
15958
16279
  statfs: (path) => this.mounts.statfs(path),
15959
16280
  mounts: () => this.mounts.list(),
15960
16281
  mount: (call, signal) => this.processMount(pid, call, signal),
@@ -16255,6 +16576,7 @@ var Launcher = class {
16255
16576
  released(path) {
16256
16577
  const at = normalizePath(path);
16257
16578
  this.unlinked.closed(at);
16579
+ this.writers?.unlinked.closed(at);
16258
16580
  const open = [...this.openFiles].some((nodes) => nodes.writes(at)) || heldUnder(this.held, at);
16259
16581
  if (!open) void this.mounts.commit(at).catch(() => void 0);
16260
16582
  }
@@ -16271,6 +16593,13 @@ var Launcher = class {
16271
16593
  kill(pid, sig, sender, from) {
16272
16594
  const session = from === void 0 ? void 0 : this.jobs.sidOf(from);
16273
16595
  const may = (target) => !sender || maySignal(sender, this.credOf(target)) || sig === SIG.CONT && session !== void 0 && this.jobs.sidOf(target) === session || from !== void 0 && this.imageOf(from) === this.imageOf(target) || from !== void 0 && this.granted.get(this.jobs.shown(target)) === from;
16596
+ if (pid === -1) {
16597
+ const sender2 = from === void 0 ? void 0 : this.imageOf(from);
16598
+ return this.killAll(
16599
+ sig,
16600
+ (member) => this.imageOf(member) !== sender2 && may(this.imageOf(member))
16601
+ );
16602
+ }
16274
16603
  if (pid < 0) return this.jobs.killGroup(-pid, sig, (member) => may(this.imageOf(member)));
16275
16604
  const handle = this.processes.get(pid);
16276
16605
  if (!handle) return false;
@@ -16278,6 +16607,15 @@ var Launcher = class {
16278
16607
  if (sig !== 0) handle.signal(sig);
16279
16608
  return true;
16280
16609
  }
16610
+ killAll(sig, may) {
16611
+ let any = false;
16612
+ for (const [pid, handle] of [...this.processes]) {
16613
+ if (pid === 1 || !may(pid)) continue;
16614
+ any = true;
16615
+ if (sig !== 0) handle.signal(sig);
16616
+ }
16617
+ return any;
16618
+ }
16281
16619
  async driver(type, spec) {
16282
16620
  if (type === "fsa") return this.fsaDriver(spec);
16283
16621
  if (type === "hostfs") {
@@ -16367,6 +16705,16 @@ var Launcher = class {
16367
16705
  }
16368
16706
  };
16369
16707
  }
16708
+ attest(path) {
16709
+ return attest(this.rootFs, path, {
16710
+ users: this.usersMode,
16711
+ mounts: this.mounts.list().map((entry) => entry.target).filter((target) => this.mounts.mounted(target) !== void 0)
16712
+ });
16713
+ }
16714
+ watchFiles(paths, notify) {
16715
+ this.fileWatches ??= new FileWatches(this.watchers, this.rootFs);
16716
+ return this.fileWatches.watch(paths, notify);
16717
+ }
16370
16718
  async mount(spec) {
16371
16719
  this.guardMount(spec.target);
16372
16720
  const fsa = spec.type === "fsa" && !spec.source.startsWith("fsa:");
@@ -16547,21 +16895,57 @@ var Launcher = class {
16547
16895
  ...account.umask !== void 0 ? { umask: account.umask } : {}
16548
16896
  };
16549
16897
  }
16550
- async starting(argv, dir, account) {
16898
+ fsFor(tag) {
16899
+ return this.stackFor(tag).fs;
16900
+ }
16901
+ isWriter(tag) {
16902
+ return tag !== void 0 && this.prefixes.length > 0 && this.writerTags.has(tag);
16903
+ }
16904
+ stackFor(tag) {
16905
+ if (!this.isWriter(tag)) {
16906
+ return { fs: this.processFs, nodes: this.nodes, unlinked: this.unlinked, writer: false };
16907
+ }
16908
+ this.writers ??= this.writerStack();
16909
+ return this.writers;
16910
+ }
16911
+ writerStack() {
16912
+ const fs = withCommandDirs(
16913
+ this.watchers.wrap(this.mounts.wrap(this.unguardedRoot)),
16914
+ async () => new Set((await this.commands()).keys()),
16915
+ (dir) => this.userCommandDir(dir),
16916
+ this.owners
16917
+ );
16918
+ const nodes = new VfsNodes(fs, (path) => this.released(path));
16919
+ nodes.keepsOnDisk = true;
16920
+ this.openFiles.add(nodes);
16921
+ const unlinked = new UnlinkedKeeper(
16922
+ fs,
16923
+ { [Symbol.iterator]: () => [this.held, this.openFiles].flatMap((set2) => [...set2]).values() },
16924
+ (path) => this.mountRoot(path),
16925
+ {
16926
+ locks: webLocks(),
16927
+ busy: (path) => [...this.openFiles].some((open) => open.holds(path)),
16928
+ owner: this.unlinkOwnerOption,
16929
+ ...this.owners ? { dirOwner: { uid: 0, gid: 0, mode: 448 } } : {}
16930
+ }
16931
+ );
16932
+ return { fs: unlinked.wrap(keepingOpen(fs, nodes)), nodes, unlinked, writer: true };
16933
+ }
16934
+ async starting(argv, dir, account, tag) {
16551
16935
  this.resetCatalogs();
16552
16936
  const scope = account.uid === 0 ? "" : pnpmHomeOf(account, this.pnpmHome);
16553
16937
  const start2 = dir ?? "/";
16554
16938
  const cwd = await physicalPathAsync(this.base, start2.startsWith("/") ? start2 : `/${start2}`);
16555
16939
  const [file = ""] = argv;
16556
- if (!await this.base.exists(cwd)) await this.processFs.mkdir(cwd, { recursive: true });
16940
+ if (!await this.base.exists(cwd)) await this.stackFor(tag).fs.mkdir(cwd, { recursive: true });
16557
16941
  const planned = await this.plan(file, argv, cwd, scope);
16558
- await this.mayExec(credOf(account), { file, cwd, pid: 0 }, planned);
16942
+ await this.mayExec(credOf(account), { file, cwd, pid: 0, tag }, planned);
16559
16943
  return { cwd, file, planned };
16560
16944
  }
16561
16945
  async openTerminal(argv, options) {
16562
16946
  tagOption(options.tag);
16563
16947
  const { account, tty: ttyGroup } = await this.terminalAccount(options.user);
16564
- const { cwd, file, planned } = await this.starting(argv, options.cwd, account);
16948
+ const { cwd, file, planned } = await this.starting(argv, options.cwd, account, options.tag);
16565
16949
  if (!planned) throw new Error(`${file}: command not found`);
16566
16950
  let leader = 0;
16567
16951
  const tty = new KernelTty(
@@ -16612,7 +16996,7 @@ var Launcher = class {
16612
16996
  async run(argv, options = {}) {
16613
16997
  tagOption(options.tag);
16614
16998
  const account = await this.account(options.user);
16615
- const { cwd, file, planned } = await this.starting(argv, options.cwd, account);
16999
+ const { cwd, file, planned } = await this.starting(argv, options.cwd, account, options.tag);
16616
17000
  const env = this.environment(cwd, options.env, account);
16617
17001
  const out = [];
16618
17002
  const err = [];