@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.
@@ -141,11 +141,18 @@ var IndexedDbMeta = class {
141
141
  cache = new PathMap();
142
142
  channel;
143
143
  generation = 0;
144
+ listeners = /* @__PURE__ */ new Set();
145
+ listen(listener) {
146
+ this.listeners.add(listener);
147
+ }
144
148
  constructor(db) {
145
149
  this.db = db;
146
150
  db.onversionchange = () => db.close();
147
151
  this.channel = new BroadcastChannel(`slicc-kernel-meta:${db.name}`);
148
- this.channel.onmessage = ({ data }) => this.drop(data);
152
+ this.channel.onmessage = ({ data }) => {
153
+ this.drop(data);
154
+ for (const listener of this.listeners) listener(data);
155
+ };
149
156
  this.channel.unref?.();
150
157
  }
151
158
  drop({ paths = [], trees = [] }) {
@@ -354,7 +361,17 @@ async function chownAs(fs, cred, path, change) {
354
361
  }
355
362
 
356
363
  // src/fs/protect.ts
357
- var PROTECT_WRITER = "slicc-kernel: protect.writer needs the process tag (#405), which this kernel does not have yet";
364
+ function protectInit(protection) {
365
+ const writers = [...writerTags(protection)];
366
+ return { paths: [...protection.paths], ...writers.length > 0 ? { writers } : {} };
367
+ }
368
+ function writerTags(protection) {
369
+ const writers = protection?.writers ?? [];
370
+ if (!Array.isArray(writers) || writers.some((tag) => typeof tag !== "string" || tag === "")) {
371
+ throw new TypeError("protect.writers must be a list of process tags");
372
+ }
373
+ return new Set(writers);
374
+ }
358
375
  function protectedPrefixes(paths) {
359
376
  return [...new Set(paths.map((p) => normalizePath(p)))];
360
377
  }
@@ -530,6 +547,121 @@ var ProtectedFs = class {
530
547
  }
531
548
  };
532
549
 
550
+ // src/fs/remote.ts
551
+ var ANNOUNCE_WAIT_MS = 500;
552
+ var PRUNE_EVERY_MS = 3e4;
553
+ function lockManager() {
554
+ return globalThis.navigator?.locks;
555
+ }
556
+ var RemoteWatch = class {
557
+ channel;
558
+ name;
559
+ tab = crypto.randomUUID();
560
+ peers = /* @__PURE__ */ new Set();
561
+ others = /* @__PURE__ */ new Map();
562
+ union = /* @__PURE__ */ new Set();
563
+ listeners = /* @__PURE__ */ new Set();
564
+ waiting = /* @__PURE__ */ new Map();
565
+ locks;
566
+ mine = [];
567
+ seq = 0;
568
+ pruned = 0;
569
+ constructor(name2, options = {}) {
570
+ this.name = name2;
571
+ this.locks = options.locks === void 0 ? lockManager() : options.locks ?? void 0;
572
+ this.channel = new BroadcastChannel(name2);
573
+ this.channel.onmessage = ({ data }) => this.receive(data);
574
+ this.channel.unref?.();
575
+ void this.locks?.request(this.lockOf(this.tab), () => new Promise(() => {
576
+ }));
577
+ this.channel.postMessage({ hello: this.tab });
578
+ }
579
+ lockOf(tab) {
580
+ return `${this.name}:${tab}`;
581
+ }
582
+ receive(data) {
583
+ if (data.hello !== void 0) this.greeted(data.hello);
584
+ if (data.here !== void 0 && data.to === this.tab) this.peers.add(data.here);
585
+ if (data.tab !== void 0 && data.paths !== void 0) this.announced(data);
586
+ if (data.ack !== void 0 && data.to === this.tab && data.from !== void 0) {
587
+ this.acked(data.ack, data.from);
588
+ }
589
+ if (data.changed !== void 0) this.emit({ paths: data.changed, trees: [] });
590
+ }
591
+ greeted(peer) {
592
+ this.peers.add(peer);
593
+ this.channel.postMessage({ here: this.tab, to: peer });
594
+ if (this.mine.length > 0) void this.announce(this.mine);
595
+ }
596
+ announced({ tab, paths, seq: seq2 }) {
597
+ const from = tab;
598
+ this.peers.add(from);
599
+ if (paths.length > 0) this.others.set(from, paths);
600
+ else this.others.delete(from);
601
+ this.rebuild();
602
+ this.channel.postMessage({ ack: seq2, from: this.tab, to: from });
603
+ }
604
+ acked(seq2, from) {
605
+ const wait2 = this.waiting.get(seq2);
606
+ if (!wait2) return;
607
+ wait2.left.delete(from);
608
+ if (wait2.left.size === 0) wait2.done();
609
+ }
610
+ rebuild() {
611
+ this.union.clear();
612
+ for (const paths of this.others.values()) for (const path of paths) this.union.add(path);
613
+ }
614
+ async prune() {
615
+ if (!this.locks) return;
616
+ const held = new Set(((await this.locks.query()).held ?? []).map((lock) => lock.name));
617
+ for (const tab of /* @__PURE__ */ new Set([...this.others.keys(), ...this.peers])) {
618
+ if (!held.has(this.lockOf(tab))) {
619
+ this.others.delete(tab);
620
+ this.peers.delete(tab);
621
+ }
622
+ }
623
+ this.rebuild();
624
+ }
625
+ watched() {
626
+ return this.union.size > 0;
627
+ }
628
+ wrote(path) {
629
+ if (!this.union.has(path)) return;
630
+ this.channel.postMessage({ changed: [path] });
631
+ const now = Date.now();
632
+ if (now - this.pruned < PRUNE_EVERY_MS) return;
633
+ this.pruned = now;
634
+ void this.prune();
635
+ }
636
+ announce(paths) {
637
+ this.mine = [...paths];
638
+ const seq2 = ++this.seq;
639
+ this.channel.postMessage({ tab: this.tab, paths: this.mine, seq: seq2 });
640
+ if (this.peers.size === 0) return Promise.resolve();
641
+ return new Promise((resolve) => {
642
+ const done4 = () => {
643
+ clearTimeout(timer);
644
+ this.waiting.delete(seq2);
645
+ resolve();
646
+ };
647
+ const timer = setTimeout(() => {
648
+ done4();
649
+ void this.prune();
650
+ }, ANNOUNCE_WAIT_MS);
651
+ this.waiting.set(seq2, { left: new Set(this.peers), done: done4 });
652
+ });
653
+ }
654
+ emit(change) {
655
+ for (const listener of this.listeners) listener(change);
656
+ }
657
+ listen(listener) {
658
+ this.listeners.add(listener);
659
+ return () => {
660
+ this.listeners.delete(listener);
661
+ };
662
+ }
663
+ };
664
+
533
665
  // src/fs/opfs.ts
534
666
  var S_IFDIR = 16384;
535
667
  var S_IFREG = 32768;
@@ -657,6 +789,7 @@ var OpfsFs = class {
657
789
  dirs = new PathMap();
658
790
  checked = /* @__PURE__ */ new WeakMap();
659
791
  channel;
792
+ remote;
660
793
  ranged;
661
794
  owners = false;
662
795
  constructor(root, meta = new MemoryMeta(), channel, options = {}) {
@@ -664,9 +797,15 @@ var OpfsFs = class {
664
797
  this.meta = sidecar(meta);
665
798
  this.ranged = options.ranged === true;
666
799
  this.channel = channel === void 0 ? void 0 : new BroadcastChannel(channel);
800
+ this.remote = channel === void 0 ? void 0 : new RemoteWatch(`${channel}:watch`);
667
801
  if (this.channel) {
802
+ const remote = this.remote;
803
+ meta.listen?.(
804
+ (c) => remote?.emit({ paths: c.paths ?? [], trees: c.trees ?? [] })
805
+ );
668
806
  this.channel.onmessage = ({ data }) => {
669
807
  for (const path of data) this.forget(path);
808
+ remote?.emit({ paths: data, trees: [] });
670
809
  };
671
810
  this.channel.unref?.();
672
811
  }
@@ -675,6 +814,16 @@ var OpfsFs = class {
675
814
  for (const path of paths) this.forget(path);
676
815
  this.channel?.postMessage(paths);
677
816
  }
817
+ remoteChanges(listener) {
818
+ return this.remote?.listen(listener) ?? (() => {
819
+ });
820
+ }
821
+ announceWatch(paths) {
822
+ return this.remote?.announce(paths) ?? Promise.resolve();
823
+ }
824
+ committed(target) {
825
+ if (this.remote?.watched()) this.remote.wrote(target);
826
+ }
678
827
  resolvePath(base, path) {
679
828
  return resolvePath(base, path);
680
829
  }
@@ -734,6 +883,9 @@ var OpfsFs = class {
734
883
  protect(prefixes2) {
735
884
  this.guarded = prefixes2;
736
885
  }
886
+ unguarded() {
887
+ return Object.create(this, { guarded: { value: [] } });
888
+ }
737
889
  refuse(path, holding = false) {
738
890
  if (isProtected(this.guarded, path) || holding && holdsProtected(this.guarded, path)) {
739
891
  throw fsError("EROFS", path);
@@ -903,15 +1055,15 @@ var OpfsFs = class {
903
1055
  }
904
1056
  dirTimes = /* @__PURE__ */ new Map();
905
1057
  dirDirty = /* @__PURE__ */ new Set();
906
- dirFlush;
1058
+ flushing = {};
907
1059
  touch(...dirs) {
908
1060
  const now = Date.now();
909
1061
  for (const dir of dirs) {
910
1062
  this.dirTimes.set(dir, now);
911
1063
  this.dirDirty.add(dir);
912
1064
  }
913
- this.dirFlush ??= Promise.resolve().then(() => this.flushDirTimes()).catch(() => void 0).finally(() => {
914
- this.dirFlush = void 0;
1065
+ this.flushing.dir ??= Promise.resolve().then(() => this.flushDirTimes()).catch(() => void 0).finally(() => {
1066
+ this.flushing.dir = void 0;
915
1067
  });
916
1068
  }
917
1069
  async flushDirTimes() {
@@ -1026,15 +1178,18 @@ var OpfsFs = class {
1026
1178
  );
1027
1179
  }
1028
1180
  async pwrite(path, offset, bytes) {
1029
- const handle = (await this.fileFor(path))[0];
1181
+ const [handle, , target] = await this.fileFor(path);
1030
1182
  await this.retrying(path, () => positioned(handle, (io) => io.write(offset, bytes)), true);
1183
+ this.committed(target);
1031
1184
  }
1032
1185
  async truncate(path, size) {
1033
1186
  const handle = await this.existing(path, true);
1187
+ const target = this.remote?.watched() ? (await this.locate(path, true)).path : path;
1034
1188
  await this.retrying(path, () => positioned(handle, (io) => io.truncate(size)), true);
1189
+ this.committed(target);
1035
1190
  }
1036
1191
  async writeSession(path, truncate) {
1037
- const handle = (await this.fileFor(path))[0];
1192
+ const [handle, , target] = await this.fileFor(path);
1038
1193
  const writable2 = await this.retrying(
1039
1194
  path,
1040
1195
  () => handle.createWritable({ keepExistingData: !truncate }),
@@ -1047,9 +1202,12 @@ var OpfsFs = class {
1047
1202
  truncate: (size) => writable2.truncate(size).catch((err) => {
1048
1203
  throw translate(err, path, "write", false);
1049
1204
  }),
1050
- close: () => writable2.close().catch((err) => {
1051
- throw translate(err, path, "commit", false);
1052
- })
1205
+ close: async () => {
1206
+ await writable2.close().catch((err) => {
1207
+ throw translate(err, path, "commit", false);
1208
+ });
1209
+ this.committed(target);
1210
+ }
1053
1211
  };
1054
1212
  }
1055
1213
  async readFile(path) {
@@ -1098,6 +1256,7 @@ var OpfsFs = class {
1098
1256
  await writable2?.abort().catch(() => void 0);
1099
1257
  throw translate(err, path, "commit", false);
1100
1258
  }
1259
+ this.committed(target);
1101
1260
  }
1102
1261
  async listing(path) {
1103
1262
  const found = await this.locate(path, true);
@@ -1119,12 +1278,12 @@ var OpfsFs = class {
1119
1278
  const base = dir === "/" ? "" : dir;
1120
1279
  const metas = await this.meta.get(children2.map(([name2]) => `${base}/${name2}`));
1121
1280
  const handed2 = this.owners ? await this.handedTo(dir) : void 0;
1122
- const owned3 = (stat, entry) => handed2 ? { ...stat, ...ownOf(entry) ?? handed2 } : stat;
1281
+ const owned4 = (stat, entry) => handed2 ? { ...stat, ...ownOf(entry) ?? handed2 } : stat;
1123
1282
  return Promise.all(
1124
1283
  children2.map(async ([name2, child], i) => {
1125
- if (!isHandle(child)) return [name2, owned3(this.linkStat(child), child)];
1284
+ if (!isHandle(child)) return [name2, owned4(this.linkStat(child), child)];
1126
1285
  const stat = await this.statOf(`${base}/${name2}`, child, metas[i]).catch(() => null);
1127
- return [name2, stat && owned3(stat, metas[i])];
1286
+ return [name2, stat && owned4(stat, metas[i])];
1128
1287
  })
1129
1288
  );
1130
1289
  }
@@ -1733,6 +1892,7 @@ var Dac = class {
1733
1892
  log;
1734
1893
  seen = /* @__PURE__ */ new Set();
1735
1894
  suppressed = false;
1895
+ tagOf = () => void 0;
1736
1896
  constructor(mode, hook, log = (line) => console.warn(line)) {
1737
1897
  this.mode = mode;
1738
1898
  this.hook = hook;
@@ -1741,7 +1901,9 @@ var Dac = class {
1741
1901
  get enforcing() {
1742
1902
  return this.mode === "enforce";
1743
1903
  }
1744
- denied(denial) {
1904
+ denied(given) {
1905
+ const tag = given.tag ?? (given.pid ? this.tagOf(given.pid) : void 0);
1906
+ const denial = tag === void 0 ? given : { ...given, tag };
1745
1907
  try {
1746
1908
  this.hook?.({ ...denial });
1747
1909
  } catch {
@@ -1759,7 +1921,7 @@ var Dac = class {
1759
1921
  }
1760
1922
  this.seen.add(key);
1761
1923
  this.log(
1762
- `slicc-kernel users report: ${d.errno} ${d.op} ${d.path} (uid ${d.uid}, pid ${d.pid})`
1924
+ `slicc-kernel users report: ${d.errno} ${d.op} ${d.path} (uid ${d.uid}, pid ${d.pid}${d.tag === void 0 ? "" : `, tag ${d.tag}`})`
1763
1925
  );
1764
1926
  }
1765
1927
  };
@@ -1775,7 +1937,8 @@ var Created = class {
1775
1937
  }
1776
1938
  };
1777
1939
  function deny(g, op, path, errno = "EACCES") {
1778
- g.dac.denied({ pid: g.pid, uid: g.cred.euid, op, path, errno });
1940
+ const tag = g.tag === void 0 ? {} : { tag: g.tag };
1941
+ g.dac.denied({ pid: g.pid, uid: g.cred.euid, op, path, errno, ...tag });
1779
1942
  }
1780
1943
  var TTY_MODE = 400;
1781
1944
  function ttyOpenable(g, tty) {
@@ -3671,6 +3834,12 @@ var CLIENT_CHECKS = {
3671
3834
  readlink: (g, at) => searchable(g, "readlink", at(0), false),
3672
3835
  exists: (g, at) => searchable(g, "exists", at(0), true)
3673
3836
  };
3837
+ function clientFs(l, tag, self, req) {
3838
+ if (!FS_METHODS.includes(req.method)) {
3839
+ throw new Error(`unknown file system call ${req.method}`);
3840
+ }
3841
+ return fsCall(l.fsFor(tag), req.method, req.args, ownerFor(l, self), guardFor(l, self, tag));
3842
+ }
3674
3843
  async function fsCall(fs, method, args, owner, g) {
3675
3844
  const path = (i) => fs.resolvePath("/", String(args[i]));
3676
3845
  if (g) {
@@ -3745,9 +3914,9 @@ function ownerFor(l, self) {
3745
3914
  if (!l.owners || !self) return void 0;
3746
3915
  return { uid: self.uid, gid: self.gid, umask: self.umask ?? 18 };
3747
3916
  }
3748
- function guardFor(l, self) {
3917
+ function guardFor(l, self, tag) {
3749
3918
  if (!l.dac || !self) return void 0;
3750
- return { fs: l.fs, cred: credOf(self), pid: 0, dac: l.dac };
3919
+ return { fs: l.fs, cred: credOf(self), pid: 0, dac: l.dac, tag };
3751
3920
  }
3752
3921
  async function accountOf(l, user) {
3753
3922
  const found = await l.users.lookup(user);
@@ -3851,6 +4020,7 @@ async function watchOp(l, req, watches, send, g) {
3851
4020
  function serveClient(port, host) {
3852
4021
  let state = "new";
3853
4022
  let self;
4023
+ const guard2 = (l) => guardFor(l, self, host.tag);
3854
4024
  const terminals = /* @__PURE__ */ new Map();
3855
4025
  const groups = /* @__PURE__ */ new Set();
3856
4026
  const sessions = /* @__PURE__ */ new Set();
@@ -3957,13 +4127,10 @@ function serveClient(port, host) {
3957
4127
  case "ps":
3958
4128
  return { result: processes(l, self) };
3959
4129
  case "fs":
3960
- if (!FS_METHODS.includes(req.method)) {
3961
- throw new Error(`unknown file system call ${req.method}`);
3962
- }
3963
- return fsCall(l.processFs, req.method, req.args, ownerFor(l, self), guardFor(l, self));
4130
+ return clientFs(l, host.tag, self, req);
3964
4131
  case "watch":
3965
4132
  case "unwatch":
3966
- return watchOp(l, req, watches, send, guardFor(l, self));
4133
+ return watchOp(l, req, watches, send, guard2(l));
3967
4134
  case "mount":
3968
4135
  return { result: await l.mountAs(clientCred(self), req.spec) };
3969
4136
  case "umount":
@@ -6624,6 +6791,136 @@ function binfmtOf(binfmts, path) {
6624
6791
  return best === void 0 ? void 0 : binfmts.get(best);
6625
6792
  }
6626
6793
 
6794
+ // src/fs/attest.ts
6795
+ var ATTEST_MAX = 1024 * 1024;
6796
+ var UNOWNED = 1e3;
6797
+ function refuse(code, path, why) {
6798
+ throw Object.assign(new Error(`attest ${path}: ${why}`), { code });
6799
+ }
6800
+ function chainOf(path) {
6801
+ const parts2 = path.split("/").filter(Boolean);
6802
+ return ["/", ...parts2.map((_, i) => `/${parts2.slice(0, i + 1).join("/")}`)];
6803
+ }
6804
+ function under2(path, target) {
6805
+ return path === target || path.startsWith(`${target}/`);
6806
+ }
6807
+ function owned(path, st) {
6808
+ return { path, uid: st.uid ?? UNOWNED, gid: st.gid ?? UNOWNED, mode: st.mode };
6809
+ }
6810
+ function unchanged(a, b, bytes) {
6811
+ return a.ino === b.ino && a.mtime.getTime() === b.mtime.getTime() && a.size === b.size && bytes.length === b.size;
6812
+ }
6813
+ async function sha256(bytes) {
6814
+ const digest = new Uint8Array(
6815
+ await crypto.subtle.digest("SHA-256", bytes)
6816
+ );
6817
+ return Array.from(digest, (b) => b.toString(16).padStart(2, "0")).join("");
6818
+ }
6819
+ function attested(path, st, bytes, digest, o, parents) {
6820
+ const { uid, gid, mode } = owned(path, st);
6821
+ return {
6822
+ path,
6823
+ bytes,
6824
+ uid,
6825
+ gid,
6826
+ mode,
6827
+ ino: st.ino,
6828
+ mtimeMs: st.mtime.getTime(),
6829
+ sha256: digest,
6830
+ users: o.users,
6831
+ onRootFs: true,
6832
+ parents
6833
+ };
6834
+ }
6835
+ async function trustedParents(fs, path, o) {
6836
+ const parents = [];
6837
+ for (const dir of chainOf(path).slice(0, -1)) {
6838
+ const st = await fs.lstat(dir);
6839
+ if (st.isSymbolicLink) refuse("EACCES", path, `${dir} is a symlink`);
6840
+ if (!st.isDirectory) refuse("ENOTDIR", path, `${dir} is not a directory`);
6841
+ const perm = owned(dir, st);
6842
+ if (o.users !== "off" && (perm.uid !== 0 || (perm.mode & 18) !== 0)) {
6843
+ refuse("EACCES", path, `${dir} is not root-owned or is writable by group or others`);
6844
+ }
6845
+ parents.push(perm);
6846
+ }
6847
+ return parents;
6848
+ }
6849
+ async function attest(fs, asked, o) {
6850
+ if (typeof asked !== "string" || !asked.startsWith("/")) {
6851
+ refuse("EINVAL", String(asked), "not an absolute path");
6852
+ }
6853
+ const path = normalizePath(asked);
6854
+ const mount = o.mounts.find((target) => {
6855
+ const at = normalizePath(target);
6856
+ return at === "/" || under2(path, at);
6857
+ });
6858
+ if (mount !== void 0) refuse("EACCES", path, `under the mount ${mount}`);
6859
+ const parents = await trustedParents(fs, path, o);
6860
+ for (let tries = 0; ; tries++) {
6861
+ const before = await fs.lstat(path);
6862
+ if (before.isSymbolicLink) refuse("EACCES", path, "is a symlink");
6863
+ if (!before.isFile)
6864
+ refuse(before.isDirectory ? "EISDIR" : "EINVAL", path, "is not a regular file");
6865
+ if (before.size > ATTEST_MAX) refuse("EFBIG", path, `is over ${ATTEST_MAX} bytes`);
6866
+ const bytes = (await fs.readFileBuffer(path)).slice();
6867
+ const after = await fs.lstat(path);
6868
+ if (unchanged(before, after, bytes)) {
6869
+ return attested(path, after, bytes, await sha256(bytes), o, parents);
6870
+ }
6871
+ if (tries >= 1) refuse("EAGAIN", path, "changed while it was read");
6872
+ }
6873
+ }
6874
+ var WATCH_DEBOUNCE_MS = 50;
6875
+ var FileWatches = class {
6876
+ live = /* @__PURE__ */ new Set();
6877
+ local;
6878
+ remote;
6879
+ constructor(local, remote) {
6880
+ this.local = local;
6881
+ this.remote = remote;
6882
+ }
6883
+ announce() {
6884
+ return this.remote.announceWatch?.([...new Set([...this.live].flat())]) ?? Promise.resolve();
6885
+ }
6886
+ async watch(paths, notify) {
6887
+ const files = paths.map(normalizePath);
6888
+ const timers = /* @__PURE__ */ new Map();
6889
+ let active = true;
6890
+ const fire = (changed, trees) => {
6891
+ for (const file of files) {
6892
+ const hit = [...changed, ...trees].some((p) => under2(file, normalizePath(p)));
6893
+ if (!hit) continue;
6894
+ clearTimeout(timers.get(file));
6895
+ timers.set(
6896
+ file,
6897
+ setTimeout(() => {
6898
+ timers.delete(file);
6899
+ if (active) notify(file);
6900
+ }, WATCH_DEBOUNCE_MS)
6901
+ );
6902
+ }
6903
+ };
6904
+ const unwatch = this.local.watch(
6905
+ files,
6906
+ { recursive: false },
6907
+ (change) => fire("overflow" in change ? files : change.paths, [])
6908
+ );
6909
+ const unlisten = this.remote.remoteChanges?.((change) => fire(change.paths, change.trees));
6910
+ this.live.add(files);
6911
+ await this.announce();
6912
+ return () => {
6913
+ if (!active) return;
6914
+ active = false;
6915
+ for (const timer of timers.values()) clearTimeout(timer);
6916
+ unwatch();
6917
+ unlisten?.();
6918
+ this.live.delete(files);
6919
+ void this.announce();
6920
+ };
6921
+ }
6922
+ };
6923
+
6627
6924
  // src/fs/commands.ts
6628
6925
  var S_IFDIR2 = 16384;
6629
6926
  var S_IFREG2 = 32768;
@@ -6662,7 +6959,7 @@ function withCommandDirs(fs, names, userDir = async () => void 0, owners = false
6662
6959
  if (st || path === "/") return st ?? {};
6663
6960
  return nearest(path.slice(0, path.lastIndexOf("/")) || "/");
6664
6961
  }
6665
- async function owned3(stat2, dir) {
6962
+ async function owned4(stat2, dir) {
6666
6963
  if (!owners) return stat2;
6667
6964
  const { uid = 0, gid = 0 } = dir === void 0 ? {} : await nearest(dir);
6668
6965
  return { ...stat2, uid, gid };
@@ -6676,17 +6973,17 @@ function withCommandDirs(fs, names, userDir = async () => void 0, owners = false
6676
6973
  }
6677
6974
  async function virtual(path) {
6678
6975
  if (path === "/usr" || path === "/bin" || path === "/usr/bin") {
6679
- return owned3(synthetic(true, path));
6976
+ return owned4(synthetic(true, path));
6680
6977
  }
6681
6978
  const name2 = COMMAND.exec(path)?.[1];
6682
6979
  if (name2 !== void 0) {
6683
- return (await names()).has(name2) ? owned3(synthetic(false, path)) : void 0;
6980
+ return (await names()).has(name2) ? owned4(synthetic(false, path)) : void 0;
6684
6981
  }
6685
- if ((await userDir(path))?.size) return owned3(synthetic(true, path), path);
6982
+ if ((await userDir(path))?.size) return owned4(synthetic(true, path), path);
6686
6983
  const slash = path.lastIndexOf("/");
6687
6984
  const dir = path.slice(0, slash);
6688
6985
  const own = await userDir(dir);
6689
- return own?.has(path.slice(slash + 1)) ? owned3(synthetic(false, path), dir) : void 0;
6986
+ return own?.has(path.slice(slash + 1)) ? owned4(synthetic(false, path), dir) : void 0;
6690
6987
  }
6691
6988
  async function linked(path) {
6692
6989
  return await virtual(path) ?? virtual(await followLinks(fs, path));
@@ -7035,11 +7332,11 @@ var UnlinkedKeeper = class {
7035
7332
 
7036
7333
  // src/fs/watch.ts
7037
7334
  var WATCH_BATCH_LIMIT = 1e3;
7038
- var under2 = (path, dir) => dir === "/" || path.startsWith(`${dir}/`);
7335
+ var under3 = (path, dir) => dir === "/" || path.startsWith(`${dir}/`);
7039
7336
  var parentOf6 = (path) => path.slice(0, path.lastIndexOf("/")) || "/";
7040
7337
  function touches(watcher, changed) {
7041
7338
  return watcher.paths.some(
7042
- (watched) => changed === watched || under2(watched, changed) || (watcher.recursive ? under2(changed, watched) : parentOf6(changed) === watched)
7339
+ (watched) => changed === watched || under3(watched, changed) || (watcher.recursive ? under3(changed, watched) : parentOf6(changed) === watched)
7043
7340
  );
7044
7341
  }
7045
7342
  var FsWatchers = class {
@@ -8758,7 +9055,7 @@ async function keptStat(entry, req) {
8758
9055
  return { ok: false, errno: "ENOENT", message: "the handle keeps no file" };
8759
9056
  return json2(statJson(await entry.fs.stat(hidden)));
8760
9057
  }
8761
- async function owned(entry, own, req, path, target) {
9058
+ async function owned2(entry, own, req, path, target) {
8762
9059
  const source = target === path ? void 0 : path;
8763
9060
  if (UNLINKING_OPS.has(req.op) && await linkedElsewhere(entry.fs, target, source)) {
8764
9061
  return run(entry, path, req);
@@ -8813,7 +9110,7 @@ async function dispatchOne(entry, req) {
8813
9110
  }
8814
9111
  if (!entry.own || !OWN_OPS.has(req.op)) return await run(entry, path, req);
8815
9112
  const target = TWO_PATHS.has(req.op) ? fs.resolvePath(cwd, req.arg2 ?? "") : path;
8816
- return await owned(entry, entry.own, req, path, target);
9113
+ return await owned2(entry, entry.own, req, path, target);
8817
9114
  } catch (err) {
8818
9115
  return toErrno(err);
8819
9116
  }
@@ -11668,6 +11965,7 @@ var SYSCALL_OPS = /* @__PURE__ */ new Set([
11668
11965
  "proc-sudo",
11669
11966
  "proc-run",
11670
11967
  "proc-exit",
11968
+ "proc-kill-self",
11671
11969
  "proc-fork",
11672
11970
  "proc-kill",
11673
11971
  "thread-kill",
@@ -11795,6 +12093,20 @@ var WasmProcess = class {
11795
12093
  else this.adoptExec(pid);
11796
12094
  return pid;
11797
12095
  }
12096
+ async killSelf(sig, target, probe) {
12097
+ if (!this.options.exitSignal) throw new KernelError("ENOSYS");
12098
+ if (!isSignal(sig) || defaultAction(sig) !== "terminate") throw new KernelError("EINVAL");
12099
+ const { caught, ignored } = this.signalMasks();
12100
+ const to = target === 0 ? -this.pgid() : target ?? this.pid;
12101
+ if (sig !== SIG.KILL && (caught | ignored) & sigbit(sig)) {
12102
+ if (!await this.options.kill?.(to, sig)) throw new KernelError("ESRCH");
12103
+ return { ok: true, kind: "void" };
12104
+ }
12105
+ if (probe) return { ok: true, kind: "json", json: "terminate" };
12106
+ if (to !== this.pid) await this.options.kill?.(to, sig);
12107
+ this.options.exitSignal(sig);
12108
+ return DYING;
12109
+ }
11798
12110
  run(req) {
11799
12111
  return runChild(req, {
11800
12112
  children: this.children,
@@ -12392,6 +12704,8 @@ var WasmProcess = class {
12392
12704
  if (!this.options.exitNow) throw new KernelError("ENOSYS");
12393
12705
  this.options.exitNow(req.status & 255);
12394
12706
  return DYING;
12707
+ case "proc-kill-self":
12708
+ return this.killSelf(req.sig, req.target, req.probe);
12395
12709
  case "proc-identity":
12396
12710
  return { ok: true, kind: "json", json: await this.options.identity?.() ?? null };
12397
12711
  case "proc-exec": {
@@ -12826,6 +13140,7 @@ function processOptions(opts, header3, hooks, created3) {
12826
13140
  raise: hooks.raise,
12827
13141
  threadKill: hooks.threadKill,
12828
13142
  ...hooks.exitNow ? { exitNow: hooks.exitNow } : {},
13143
+ ...hooks.exitSignal ? { exitSignal: hooks.exitSignal } : {},
12829
13144
  ...opts.program.abi === "wasi" || opts.program.abi === "js" ? { pendingBits: () => Atomics.load(header3, SAB_I_SIGNALS) } : {}
12830
13145
  };
12831
13146
  }
@@ -12859,7 +13174,10 @@ function spawnWasmProcess(opts) {
12859
13174
  {
12860
13175
  raise: (sig) => signal(sig),
12861
13176
  threadKill: (tid, sig) => killThread(threadSignals.get(tid), process.signalMasks().caught, sig),
12862
- ...opts.program.abi === "js" ? { exitNow: (code) => exitNow(code) } : {}
13177
+ ...opts.program.abi === "js" ? {
13178
+ exitNow: (code) => exitNow(code),
13179
+ exitSignal: (sig) => queueMicrotask(() => finish(128 + sig, sig))
13180
+ } : {}
12863
13181
  },
12864
13182
  created3
12865
13183
  )
@@ -14664,8 +14982,8 @@ var ChunkedBytes = class {
14664
14982
  }
14665
14983
  return out;
14666
14984
  }
14667
- write(offset, bytes, owned3 = false) {
14668
- const whole = owned3 && bytes.byteOffset === 0 && bytes.byteLength === bytes.buffer.byteLength;
14985
+ write(offset, bytes, owned4 = false) {
14986
+ const whole = owned4 && bytes.byteOffset === 0 && bytes.byteLength === bytes.buffer.byteLength;
14669
14987
  for (let done4 = 0; done4 < bytes.length; ) {
14670
14988
  const at = offset + done4;
14671
14989
  const index = Math.floor(at / CHUNK3);
@@ -14715,7 +15033,7 @@ function created2(ino, kind, mode, from, owner) {
14715
15033
  gid: owner?.gid ?? from?.gid ?? 0
14716
15034
  };
14717
15035
  }
14718
- function owned2(node, change) {
15036
+ function owned3(node, change) {
14719
15037
  if (change.uid !== void 0) node.uid = change.uid;
14720
15038
  if (change.gid !== void 0) node.gid = change.gid;
14721
15039
  }
@@ -14886,7 +15204,7 @@ function tmpfs() {
14886
15204
  node.mtime = Date.now();
14887
15205
  }
14888
15206
  if (change.mtime !== void 0) node.mtime = change.mtime;
14889
- owned2(node, change);
15207
+ owned3(node, change);
14890
15208
  },
14891
15209
  async statfs() {
14892
15210
  return { bsize: 4096, blocks: 262144, bfree: 262144 };
@@ -15578,6 +15896,10 @@ var Launcher = class {
15578
15896
  fs;
15579
15897
  processFs;
15580
15898
  prefixes;
15899
+ writerTags;
15900
+ unguardedRoot;
15901
+ writers;
15902
+ unlinkOwnerOption;
15581
15903
  base;
15582
15904
  createWorker;
15583
15905
  modulesDir;
@@ -15635,12 +15957,16 @@ var Launcher = class {
15635
15957
  usersMode;
15636
15958
  dac;
15637
15959
  sealed;
15960
+ rootFs;
15961
+ fileWatches;
15638
15962
  nextPid = 1e3;
15639
15963
  boot = Date.now();
15640
15964
  terminals = 0;
15641
15965
  constructor(options) {
15642
15966
  this.usersMode = usersMode(options.users);
15967
+ this.rootFs = options.fs;
15643
15968
  this.dac = this.usersMode === "off" ? void 0 : new Dac(this.usersMode, options.onDenial);
15969
+ if (this.dac) this.dac.tagOf = (pid) => this.tagOf(pid);
15644
15970
  this.sealed = new SealedBroker(
15645
15971
  options.sealed,
15646
15972
  this.usersMode,
@@ -15691,7 +16017,7 @@ var Launcher = class {
15691
16017
  this.nodes = new VfsNodes(fs, (path) => this.released(path));
15692
16018
  this.nodes.keepsOnDisk = true;
15693
16019
  this.openFiles.add(this.nodes);
15694
- const holders = [this.held, [this.nodes]];
16020
+ const holders = [this.held, this.openFiles];
15695
16021
  this.unlinked = new UnlinkedKeeper(
15696
16022
  fs,
15697
16023
  { [Symbol.iterator]: () => holders.flatMap((set2) => [...set2]).values() },
@@ -15704,7 +16030,12 @@ var Launcher = class {
15704
16030
  }
15705
16031
  );
15706
16032
  this.fs = this.unlinked.wrap(keepingOpen(fs, this.nodes));
15707
- if (options.protect?.writer) throw new Error(PROTECT_WRITER);
16033
+ this.writerTags = writerTags(options.protect);
16034
+ this.unlinkOwnerOption = options.unlinkOwner;
16035
+ if (this.writerTags.size > 0 && options.fs.protect && !options.fs.unguarded) {
16036
+ throw new TypeError("protect.writers needs a root file system with an unguarded view");
16037
+ }
16038
+ this.unguardedRoot = options.fs.unguarded?.() ?? options.fs;
15708
16039
  this.prefixes = protectedPrefixes(options.protect?.paths ?? []);
15709
16040
  this.processFs = this.prefixes.length > 0 ? new ProtectedFs(this.prefixes).view(this.fs) : this.fs;
15710
16041
  options.fs.protect?.(this.prefixes);
@@ -15890,7 +16221,7 @@ var Launcher = class {
15890
16221
  async mayExec(cred, req, planned) {
15891
16222
  const { dac } = this;
15892
16223
  if (!dac) return;
15893
- const g = { fs: this.fs, cred, pid: req.pid, dac };
16224
+ const g = { fs: this.fs, cred, pid: req.pid, dac, tag: req.tag };
15894
16225
  const byPath = (file) => file.includes("/") && !COMMAND2.test(file);
15895
16226
  if (byPath(req.file)) {
15896
16227
  const path = this.fs.resolvePath(req.cwd, req.file);
@@ -16028,6 +16359,7 @@ var Launcher = class {
16028
16359
  throw fsError("ESRCH", `no process group ${req.pgid}`);
16029
16360
  }
16030
16361
  const pid = this.nextPid++;
16362
+ const stack = this.stackFor(req.tag);
16031
16363
  const terminal = req.fds.stdioTerminal();
16032
16364
  const decided = req.decided ?? Promise.resolve(true);
16033
16365
  const count = (byParent) => {
@@ -16058,8 +16390,8 @@ var Launcher = class {
16058
16390
  env: req.env,
16059
16391
  cwd: req.cwd,
16060
16392
  fds: req.fds,
16061
- fs: this.processFs,
16062
- ...this.prefixes.length > 0 ? { readonlyAt: (path) => isProtected(this.prefixes, path) } : {},
16393
+ fs: stack.fs,
16394
+ ...this.prefixes.length > 0 && !stack.writer ? { readonlyAt: (path) => isProtected(this.prefixes, path) } : {},
16063
16395
  createWorker: this.createWorker,
16064
16396
  onError: req.report,
16065
16397
  spawner: this.spawner(pid, req.report),
@@ -16074,9 +16406,9 @@ var Launcher = class {
16074
16406
  }),
16075
16407
  sudo: (call, signal) => this.sudo(pid, call, signal),
16076
16408
  openFiles: this.openFiles,
16077
- nodes: this.nodes,
16409
+ nodes: stack.nodes,
16078
16410
  held: this.held,
16079
- unlinkGate: this.unlinked.gate,
16411
+ unlinkGate: stack.unlinked.gate,
16080
16412
  statfs: (path) => this.mounts.statfs(path),
16081
16413
  mounts: () => this.mounts.list(),
16082
16414
  mount: (call, signal) => this.processMount(pid, call, signal),
@@ -16377,6 +16709,7 @@ var Launcher = class {
16377
16709
  released(path) {
16378
16710
  const at = normalizePath(path);
16379
16711
  this.unlinked.closed(at);
16712
+ this.writers?.unlinked.closed(at);
16380
16713
  const open = [...this.openFiles].some((nodes) => nodes.writes(at)) || heldUnder(this.held, at);
16381
16714
  if (!open) void this.mounts.commit(at).catch(() => void 0);
16382
16715
  }
@@ -16393,6 +16726,13 @@ var Launcher = class {
16393
16726
  kill(pid, sig, sender, from) {
16394
16727
  const session = from === void 0 ? void 0 : this.jobs.sidOf(from);
16395
16728
  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;
16729
+ if (pid === -1) {
16730
+ const sender2 = from === void 0 ? void 0 : this.imageOf(from);
16731
+ return this.killAll(
16732
+ sig,
16733
+ (member) => this.imageOf(member) !== sender2 && may(this.imageOf(member))
16734
+ );
16735
+ }
16396
16736
  if (pid < 0) return this.jobs.killGroup(-pid, sig, (member) => may(this.imageOf(member)));
16397
16737
  const handle = this.processes.get(pid);
16398
16738
  if (!handle) return false;
@@ -16400,6 +16740,15 @@ var Launcher = class {
16400
16740
  if (sig !== 0) handle.signal(sig);
16401
16741
  return true;
16402
16742
  }
16743
+ killAll(sig, may) {
16744
+ let any = false;
16745
+ for (const [pid, handle] of [...this.processes]) {
16746
+ if (pid === 1 || !may(pid)) continue;
16747
+ any = true;
16748
+ if (sig !== 0) handle.signal(sig);
16749
+ }
16750
+ return any;
16751
+ }
16403
16752
  async driver(type, spec) {
16404
16753
  if (type === "fsa") return this.fsaDriver(spec);
16405
16754
  if (type === "hostfs") {
@@ -16489,6 +16838,16 @@ var Launcher = class {
16489
16838
  }
16490
16839
  };
16491
16840
  }
16841
+ attest(path) {
16842
+ return attest(this.rootFs, path, {
16843
+ users: this.usersMode,
16844
+ mounts: this.mounts.list().map((entry) => entry.target).filter((target) => this.mounts.mounted(target) !== void 0)
16845
+ });
16846
+ }
16847
+ watchFiles(paths, notify) {
16848
+ this.fileWatches ??= new FileWatches(this.watchers, this.rootFs);
16849
+ return this.fileWatches.watch(paths, notify);
16850
+ }
16492
16851
  async mount(spec) {
16493
16852
  this.guardMount(spec.target);
16494
16853
  const fsa = spec.type === "fsa" && !spec.source.startsWith("fsa:");
@@ -16669,21 +17028,57 @@ var Launcher = class {
16669
17028
  ...account.umask !== void 0 ? { umask: account.umask } : {}
16670
17029
  };
16671
17030
  }
16672
- async starting(argv, dir, account) {
17031
+ fsFor(tag) {
17032
+ return this.stackFor(tag).fs;
17033
+ }
17034
+ isWriter(tag) {
17035
+ return tag !== void 0 && this.prefixes.length > 0 && this.writerTags.has(tag);
17036
+ }
17037
+ stackFor(tag) {
17038
+ if (!this.isWriter(tag)) {
17039
+ return { fs: this.processFs, nodes: this.nodes, unlinked: this.unlinked, writer: false };
17040
+ }
17041
+ this.writers ??= this.writerStack();
17042
+ return this.writers;
17043
+ }
17044
+ writerStack() {
17045
+ const fs = withCommandDirs(
17046
+ this.watchers.wrap(this.mounts.wrap(this.unguardedRoot)),
17047
+ async () => new Set((await this.commands()).keys()),
17048
+ (dir) => this.userCommandDir(dir),
17049
+ this.owners
17050
+ );
17051
+ const nodes = new VfsNodes(fs, (path) => this.released(path));
17052
+ nodes.keepsOnDisk = true;
17053
+ this.openFiles.add(nodes);
17054
+ const unlinked = new UnlinkedKeeper(
17055
+ fs,
17056
+ { [Symbol.iterator]: () => [this.held, this.openFiles].flatMap((set2) => [...set2]).values() },
17057
+ (path) => this.mountRoot(path),
17058
+ {
17059
+ locks: webLocks(),
17060
+ busy: (path) => [...this.openFiles].some((open) => open.holds(path)),
17061
+ owner: this.unlinkOwnerOption,
17062
+ ...this.owners ? { dirOwner: { uid: 0, gid: 0, mode: 448 } } : {}
17063
+ }
17064
+ );
17065
+ return { fs: unlinked.wrap(keepingOpen(fs, nodes)), nodes, unlinked, writer: true };
17066
+ }
17067
+ async starting(argv, dir, account, tag) {
16673
17068
  this.resetCatalogs();
16674
17069
  const scope = account.uid === 0 ? "" : pnpmHomeOf(account, this.pnpmHome);
16675
17070
  const start2 = dir ?? "/";
16676
17071
  const cwd = await physicalPathAsync(this.base, start2.startsWith("/") ? start2 : `/${start2}`);
16677
17072
  const [file = ""] = argv;
16678
- if (!await this.base.exists(cwd)) await this.processFs.mkdir(cwd, { recursive: true });
17073
+ if (!await this.base.exists(cwd)) await this.stackFor(tag).fs.mkdir(cwd, { recursive: true });
16679
17074
  const planned = await this.plan(file, argv, cwd, scope);
16680
- await this.mayExec(credOf(account), { file, cwd, pid: 0 }, planned);
17075
+ await this.mayExec(credOf(account), { file, cwd, pid: 0, tag }, planned);
16681
17076
  return { cwd, file, planned };
16682
17077
  }
16683
17078
  async openTerminal(argv, options) {
16684
17079
  tagOption(options.tag);
16685
17080
  const { account, tty: ttyGroup } = await this.terminalAccount(options.user);
16686
- const { cwd, file, planned } = await this.starting(argv, options.cwd, account);
17081
+ const { cwd, file, planned } = await this.starting(argv, options.cwd, account, options.tag);
16687
17082
  if (!planned) throw new Error(`${file}: command not found`);
16688
17083
  let leader = 0;
16689
17084
  const tty = new KernelTty(
@@ -16734,7 +17129,7 @@ var Launcher = class {
16734
17129
  async run(argv, options = {}) {
16735
17130
  tagOption(options.tag);
16736
17131
  const account = await this.account(options.user);
16737
- const { cwd, file, planned } = await this.starting(argv, options.cwd, account);
17132
+ const { cwd, file, planned } = await this.starting(argv, options.cwd, account, options.tag);
16738
17133
  const env = this.environment(cwd, options.env, account);
16739
17134
  const out = [];
16740
17135
  const err = [];
@@ -16893,9 +17288,31 @@ var USERS_OPS = /* @__PURE__ */ new Set([
16893
17288
  "groups-add-member",
16894
17289
  "groups-remove-member"
16895
17290
  ]);
17291
+ var ATTEST_OPS = /* @__PURE__ */ new Set(["attest", "watch-files", "unwatch-files"]);
17292
+ function isAttestRequest(req) {
17293
+ return ATTEST_OPS.has(req.op);
17294
+ }
16896
17295
  function isUsersRequest(req) {
16897
17296
  return USERS_OPS.has(req.op);
16898
17297
  }
17298
+ function transferOf2(req, result) {
17299
+ if (result instanceof MessagePort) return [result];
17300
+ if (req.op === "attest") return [result.bytes.buffer];
17301
+ return void 0;
17302
+ }
17303
+ function attestOps(port) {
17304
+ const watches = /* @__PURE__ */ new Map();
17305
+ return async (l, req) => {
17306
+ if (req.op === "attest") return l.attest(req.path);
17307
+ if (req.op === "unwatch-files") {
17308
+ watches.get(req.watch)?.();
17309
+ return watches.delete(req.watch);
17310
+ }
17311
+ const notify = (path) => port.postMessage({ fileChanged: { watch: req.id, path } });
17312
+ watches.set(req.id, await l.watchFiles(req.paths, notify));
17313
+ return req.id;
17314
+ };
17315
+ }
16899
17316
  function failure(err) {
16900
17317
  const code = err?.code;
16901
17318
  return {
@@ -16916,7 +17333,7 @@ function hostOptions(req, port, sealed) {
16916
17333
  ...req.hostname !== void 0 ? { hostname: req.hostname } : {},
16917
17334
  ...req.users !== void 0 ? { users: req.users } : {},
16918
17335
  ...req.denials ? { onDenial: (denial) => port.postMessage({ denial }) } : {},
16919
- ...req.protect ? { protect: { paths: [...req.protect.paths] } } : {},
17336
+ ...req.protect ? { protect: protectInit(req.protect) } : {},
16920
17337
  ...req.sealed ? { sealed } : {}
16921
17338
  };
16922
17339
  }
@@ -17034,8 +17451,10 @@ function serveKernel(port, deps) {
17034
17451
  if (req.op === "insert") return l.insert(req.target, req.handle, req.source);
17035
17452
  return l.mounts.list();
17036
17453
  }
17454
+ const attestOp = attestOps(port);
17037
17455
  async function handle(req) {
17038
17456
  if (req.op === "init") return init(req);
17457
+ if (isAttestRequest(req)) return attestOp(await ready2(), req);
17039
17458
  if (req.op === "open-terminal") return terminal(req);
17040
17459
  if (req.op === "connect") return connect(req.user, req.tag);
17041
17460
  if (isUsersRequest(req)) return usersOp(await ready2(), req);
@@ -17084,7 +17503,7 @@ function serveKernel(port, deps) {
17084
17503
  if (answered(event.data, remote, uplink, approvals)) return;
17085
17504
  const req = event.data;
17086
17505
  handle(req).then(
17087
- (result) => reply(req.id, { result }, result instanceof MessagePort ? [result] : void 0),
17506
+ (result) => reply(req.id, { result }, transferOf2(req, result)),
17088
17507
  (err) => reply(req.id, failure(err))
17089
17508
  );
17090
17509
  });