@ai-ecoverse/slicc-kernel 1.85.1 → 1.87.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
@@ -107,6 +107,12 @@ async function lutimesOf(fs, path, atime, mtime) {
107
107
  function mayLink(fs, path) {
108
108
  return fs.links?.(path) ?? true;
109
109
  }
110
+ function modeOption(mode) {
111
+ if (!Number.isInteger(mode) || mode < 0 || mode > 4095) {
112
+ throw fsError("EINVAL", `not a mode: ${String(mode)}`);
113
+ }
114
+ return mode;
115
+ }
110
116
  function fsError(code, path) {
111
117
  return Object.assign(new Error(`${code}: ${path}`), { code });
112
118
  }
@@ -570,7 +576,9 @@ function id(value) {
570
576
  return value;
571
577
  }
572
578
  function changeCred(cur, change) {
573
- if (typeof change !== "object" || change === null) throw fsError("EINVAL", "not a change");
579
+ if (typeof change !== "object" || change === null || Array.isArray(change)) {
580
+ throw fsError("EINVAL", "not a change");
581
+ }
574
582
  const privileged = cur.euid === 0;
575
583
  const next2 = copyCred(cur);
576
584
  for (const keys of [UIDS, GIDS]) {
@@ -745,6 +753,10 @@ var KernelError = class extends Error {
745
753
  if (detail !== void 0) this.detail = detail;
746
754
  }
747
755
  };
756
+ function openAccess(flags2) {
757
+ const mode = flags2 & 3;
758
+ return mode === 0 ? "read" : mode === 1 ? "write" : "both";
759
+ }
748
760
  function pollFile(file) {
749
761
  return file.poll?.() ?? { readable: !!file.read, writable: !!file.write, hangup: false };
750
762
  }
@@ -3002,6 +3014,9 @@ var MemoryMeta = class {
3002
3014
  async get(paths) {
3003
3015
  return paths.map((path) => this.entries.get(path));
3004
3016
  }
3017
+ async fresh(path) {
3018
+ return this.entries.get(path);
3019
+ }
3005
3020
  async update(path, change) {
3006
3021
  this.set(path, change(this.entries.get(path)));
3007
3022
  }
@@ -3473,7 +3488,8 @@ function sidecar(meta) {
3473
3488
  const guarded3 = (op) => (...args) => meta[op].apply(meta, args).catch((err) => {
3474
3489
  throw translate(err, pathOf(args), "sidecar", false);
3475
3490
  });
3476
- const ops = Object.fromEntries(META_OPS.map((op) => [op, guarded3(op)]));
3491
+ const names = meta.fresh ? [...META_OPS, "fresh"] : META_OPS;
3492
+ const ops = Object.fromEntries(names.map((op) => [op, guarded3(op)]));
3477
3493
  if (meta.absent) ops.absent = meta.absent.bind(meta);
3478
3494
  const { removeFile } = meta;
3479
3495
  if (removeFile)
@@ -3597,6 +3613,24 @@ var OpfsFs = class {
3597
3613
  committed(target) {
3598
3614
  if (this.remote?.watched()) this.remote.wrote(target);
3599
3615
  }
3616
+ counted = [];
3617
+ async stamp(path) {
3618
+ const entry = this.meta.fresh ? await this.meta.fresh(path) : (await this.meta.get([path]))[0];
3619
+ return String(entry?.writes ?? 0);
3620
+ }
3621
+ async counting(target) {
3622
+ if (!isProtected(this.counted, target)) return;
3623
+ await this.meta.update(target, (entry) => ({
3624
+ ...entry,
3625
+ path: target,
3626
+ writes: (entry?.writes ?? 0) + 1
3627
+ }));
3628
+ }
3629
+ async recounting(handle, target) {
3630
+ if (this.counted.length === 0) return;
3631
+ const where = await this.root.resolve(handle).catch(() => null);
3632
+ await this.counting(where ? `/${where.join("/")}` : target);
3633
+ }
3600
3634
  resolvePath(base, path) {
3601
3635
  return resolvePath(base, path);
3602
3636
  }
@@ -3655,6 +3689,7 @@ var OpfsFs = class {
3655
3689
  guarded = [];
3656
3690
  protect(prefixes2) {
3657
3691
  this.guarded = prefixes2;
3692
+ this.counted = prefixes2;
3658
3693
  }
3659
3694
  unguarded() {
3660
3695
  return Object.create(this, { guarded: { value: [] } });
@@ -3991,9 +4026,10 @@ var OpfsFs = class {
3991
4026
  return this.retrying(
3992
4027
  path,
3993
4028
  async () => {
4029
+ const stamp = await this.stamp(path);
3994
4030
  const file = await handle.getFile();
3995
4031
  const bytes = new Uint8Array(await file.arrayBuffer());
3996
- return { bytes, version: `${file.lastModified}:${file.size}` };
4032
+ return { bytes, version: `${file.lastModified}:${file.size}:${stamp}` };
3997
4033
  },
3998
4034
  false
3999
4035
  );
@@ -4003,8 +4039,9 @@ var OpfsFs = class {
4003
4039
  return this.retrying(
4004
4040
  path,
4005
4041
  async () => {
4042
+ const stamp = await this.stamp(path);
4006
4043
  const file = await handle.getFile();
4007
- return `${file.lastModified}:${file.size}`;
4044
+ return `${file.lastModified}:${file.size}:${stamp}`;
4008
4045
  },
4009
4046
  false
4010
4047
  );
@@ -4019,14 +4056,24 @@ var OpfsFs = class {
4019
4056
  }
4020
4057
  async pwrite(path, offset, bytes) {
4021
4058
  const [handle, , target] = await this.fileFor(path);
4022
- await this.retrying(path, () => positioned(handle, (io) => io.write(offset, bytes)), true);
4059
+ await this.counting(target);
4060
+ try {
4061
+ await this.retrying(path, () => positioned(handle, (io) => io.write(offset, bytes)), true);
4062
+ } finally {
4063
+ await this.recounting(handle, target);
4064
+ }
4023
4065
  this.written(target);
4024
4066
  }
4025
4067
  async truncate(path, size) {
4026
4068
  const handle = await this.existing(path, true);
4027
4069
  const resolve2 = this.ranged || this.remote?.watched();
4028
- const target = resolve2 ? (await this.locate(path, true)).path : path;
4029
- await this.retrying(path, () => positioned(handle, (io) => io.truncate(size)), true);
4070
+ const target = resolve2 || this.counted.length > 0 ? (await this.locate(path, true)).path : path;
4071
+ await this.counting(target);
4072
+ try {
4073
+ await this.retrying(path, () => positioned(handle, (io) => io.truncate(size)), true);
4074
+ } finally {
4075
+ await this.recounting(handle, target);
4076
+ }
4030
4077
  this.written(target);
4031
4078
  }
4032
4079
  async writeSession(path, truncate) {
@@ -12854,17 +12901,15 @@ var WasmProcess = class {
12854
12901
  const file = tty.file();
12855
12902
  return this.fds.install(this.options.outputFilter?.(file) ?? file, 3);
12856
12903
  }
12857
- openSealed() {
12904
+ openSealed(flags2) {
12858
12905
  const broker = this.options.sealed;
12859
12906
  if (!broker) throw new KernelError("ENOENT");
12860
- return this.fds.install(broker.open(), 3);
12907
+ return this.fds.install(broker.open(openAccess(flags2)), 3);
12861
12908
  }
12862
12909
  async openClipboard(flags2) {
12863
12910
  const broker = this.options.clipboard;
12864
12911
  if (!broker) throw new KernelError("ENOENT");
12865
- const mode = flags2 & WRITE_ACCESS;
12866
- const access2 = mode === 0 ? "read" : mode === 1 ? "write" : "both";
12867
- const file = broker.open(access2, await this.clipboardStamp());
12912
+ const file = broker.open(openAccess(flags2), await this.clipboardStamp());
12868
12913
  return this.fds.install(this.options.outputFilter?.(file) ?? file, 3);
12869
12914
  }
12870
12915
  async clipboardStamp() {
@@ -13023,7 +13068,7 @@ var WasmProcess = class {
13023
13068
  case "fd-tty-names":
13024
13069
  return { ok: true, kind: "json", json: this.options.jobs?.terminalNames() ?? [] };
13025
13070
  case "fd-open-sealed":
13026
- return { ok: true, kind: "json", json: this.openSealed() };
13071
+ return { ok: true, kind: "json", json: this.openSealed(req.flags) };
13027
13072
  case "fd-open-clipboard":
13028
13073
  return { ok: true, kind: "json", json: await this.openClipboard(req.flags) };
13029
13074
  case "fd-tty-owner":
@@ -14742,14 +14787,19 @@ var SealedChannel = class {
14742
14787
  offset = 0;
14743
14788
  inflight;
14744
14789
  broker;
14745
- constructor(broker) {
14790
+ access;
14791
+ constructor(broker, access2) {
14746
14792
  this.broker = broker;
14793
+ this.access = access2;
14747
14794
  }
14748
14795
  send(bytes, caller, gone) {
14796
+ if (this.access === "read") {
14797
+ bytes.fill(0);
14798
+ throw new KernelError("EBADF");
14799
+ }
14749
14800
  const length2 = bytes.length;
14750
14801
  const fields = parseSealed(bytes);
14751
- this.reply = void 0;
14752
- this.offset = 0;
14802
+ this.drop();
14753
14803
  this.inflight?.control.abort("replaced");
14754
14804
  const control = new AbortController();
14755
14805
  const withdraw = () => control.abort("withdrawn");
@@ -14785,15 +14835,18 @@ var SealedChannel = class {
14785
14835
  if (!signal.aborted) return reply;
14786
14836
  return signal.reason === "timeout" ? errorReply("ETIMEDOUT", "the vault did not answer within 10 s") : void 0;
14787
14837
  }
14838
+ drop() {
14839
+ this.reply?.fill(0);
14840
+ this.reply = void 0;
14841
+ this.offset = 0;
14842
+ }
14788
14843
  async read(max, signal) {
14844
+ if (this.access === "write") throw new KernelError("EBADF");
14789
14845
  for (; ; ) {
14790
14846
  if (this.reply) {
14791
14847
  const out = this.reply.slice(this.offset, this.offset + max);
14792
14848
  this.offset += out.length;
14793
- if (this.offset >= this.reply.length) {
14794
- this.reply = void 0;
14795
- this.offset = 0;
14796
- }
14849
+ if (this.offset >= this.reply.length) this.drop();
14797
14850
  return out;
14798
14851
  }
14799
14852
  const flight = this.inflight;
@@ -14826,7 +14879,7 @@ var SealedChannel = class {
14826
14879
  close() {
14827
14880
  this.inflight?.control.abort("withdrawn");
14828
14881
  this.inflight = void 0;
14829
- this.reply = void 0;
14882
+ this.drop();
14830
14883
  }
14831
14884
  };
14832
14885
  function swapOf(reply) {
@@ -14883,12 +14936,14 @@ var SealedBroker = class {
14883
14936
  control.abort("done");
14884
14937
  }
14885
14938
  }
14886
- open() {
14887
- const channel = new SealedChannel(this);
14939
+ open(access2 = "both") {
14940
+ const channel = new SealedChannel(this, access2);
14888
14941
  return new OpenFile({
14889
- read: (max, signal) => channel.read(max, signal),
14890
- write: async () => {
14891
- throw new KernelError("EINVAL");
14942
+ ...access2 === "write" ? {} : { read: (max, signal) => channel.read(max, signal) },
14943
+ ...access2 === "read" ? {} : {
14944
+ write: async () => {
14945
+ throw new KernelError("EINVAL");
14946
+ }
14892
14947
  },
14893
14948
  poll: () => channel.poll(),
14894
14949
  changed: (signal) => channel.changed(signal),
@@ -16610,7 +16665,10 @@ var SECURE_PATH = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
16610
16665
  var DEFAULT_SUDOERS = "%sudo Cmnd *\n";
16611
16666
  var SUDO_TIMEOUT_MS = 3e5;
16612
16667
  var NAME3 = /^[a-z_][a-z0-9_.-]*$/;
16613
- var RULE = /^(NOPASSWD\s+)?(\S+)\s+(?:\(([^()\s]+)\)\s+)?Cmnd(?:\s+(.*))?$/;
16668
+ var WORD = String.raw`(?:'[^']*'|"(?:[^"\\]|\\[\s\S])*"|\\[\s\S]|[^\s'"\\])+`;
16669
+ var RULE = new RegExp(
16670
+ String.raw`^(NOPASSWD\s+)?(\S+)\s+(?:\(([^()\s]+)\)\s+)?(?:Cwd\s+(${WORD})\s+)?Cmnd(?:\s+(.*))?$`
16671
+ );
16614
16672
  var TERM = /^[A-Za-z0-9._+-]+$/;
16615
16673
  var PLAIN = /^[A-Za-z0-9_@%+=:,./-]+$/;
16616
16674
  var USAGE = "usage: sudo [-n] [-u user] [--] command [args...]\n sudo -l";
@@ -16645,17 +16703,25 @@ function validWho(who) {
16645
16703
  if (who === "ALL") return true;
16646
16704
  return NAME3.test(who.startsWith("%") ? who.slice(1) : who);
16647
16705
  }
16706
+ function cwdOf(word) {
16707
+ const words2 = splitWords(word);
16708
+ const path = words2?.length === 1 ? words2[0] : "";
16709
+ return path.startsWith("/") ? normalizePath(path) : "";
16710
+ }
16648
16711
  function parseRule(line) {
16649
16712
  const text = line.trim();
16650
16713
  if (text === "" || text.startsWith("#")) return null;
16651
16714
  const m = RULE.exec(text);
16652
16715
  if (!m) return void 0;
16653
- const [, nopasswd, who = "", target = "root", rest2 = ""] = m;
16716
+ const [, nopasswd, who = "", target = "root", dir, rest2 = ""] = m;
16654
16717
  if (!validWho(who) || target !== "ALL" && !NAME3.test(target)) return void 0;
16655
- if (rest2.trim() === "*") return { nopasswd: !!nopasswd, who, target, cmnd: "*" };
16718
+ const cwd = dir === void 0 ? void 0 : cwdOf(dir);
16719
+ if (cwd === "") return void 0;
16720
+ const rule = { nopasswd: !!nopasswd, who, target, ...cwd !== void 0 ? { cwd } : {} };
16721
+ if (rest2.trim() === "*") return { ...rule, cmnd: "*" };
16656
16722
  const cmnd = splitWords(rest2);
16657
16723
  if (!cmnd?.[0]?.startsWith("/")) return void 0;
16658
- return { nopasswd: !!nopasswd, who, target, cmnd };
16724
+ return { ...rule, cmnd };
16659
16725
  }
16660
16726
  function flags(parsed2, arg, next2) {
16661
16727
  for (let j = 1; j < arg.length; j++) {
@@ -16694,7 +16760,8 @@ function parseArgs(args) {
16694
16760
  }
16695
16761
  function described(rule) {
16696
16762
  const cmnd = rule.cmnd === "*" ? "ALL" : rule.cmnd.map(quoteWord).join(" ");
16697
- return ` (${rule.target}) ${rule.nopasswd ? "NOPASSWD: " : ""}${cmnd}`;
16763
+ const cwd = rule.cwd === void 0 ? "" : `CWD=${quoteWord(rule.cwd)} `;
16764
+ return ` (${rule.target}) ${cwd}${rule.nopasswd ? "NOPASSWD: " : ""}${cmnd}`;
16698
16765
  }
16699
16766
  var Sudo = class {
16700
16767
  o;
@@ -16834,7 +16901,7 @@ var Sudo = class {
16834
16901
  if (untrusted) return untrusted;
16835
16902
  const argv = [path, ...args];
16836
16903
  const matching = rules.filter(
16837
- (rule) => applies(rule) && (rule.target === "ALL" || rule.target === target.name) && (rule.cmnd === "*" || rule.cmnd.length === argv.length && rule.cmnd.every((w, i) => w === argv[i]))
16904
+ (rule) => applies(rule) && (rule.target === "ALL" || rule.target === target.name) && (rule.cwd === void 0 || rule.cwd === cwd) && (rule.cmnd === "*" || rule.cmnd.length === argv.length && rule.cmnd.every((w, i) => w === argv[i]))
16838
16905
  );
16839
16906
  const grant = () => this.grant(self, target, cred, parsed2.command, path, cwd, call.term);
16840
16907
  if (root || matching.some((rule) => rule.nopasswd)) return grant();
@@ -16856,7 +16923,7 @@ var Sudo = class {
16856
16923
  }
16857
16924
  const changed = await this.untrusted(path, cwd, word);
16858
16925
  if (changed) return changed;
16859
- if (answer === "always" && !await this.remember(self, target, argv)) {
16926
+ if (answer === "always" && !await this.remember(self, target, cwd, argv)) {
16860
16927
  return failed2("EIO", "sudo: the rule could not be recorded");
16861
16928
  }
16862
16929
  return grant();
@@ -16921,7 +16988,7 @@ ${lines2}
16921
16988
  requester.removeEventListener("abort", withdraw);
16922
16989
  }
16923
16990
  }
16924
- async remember(self, target, argv) {
16991
+ async remember(self, target, cwd, argv) {
16925
16992
  const { fs } = this.o;
16926
16993
  const path = `${SUDOERS_D}/${self.name}`;
16927
16994
  if (!NAME3.test(self.name)) return false;
@@ -16931,7 +16998,8 @@ ${lines2}
16931
16998
  const st = await fs.lstat(path).catch(() => void 0);
16932
16999
  if (st && !await this.trusted(path)) return false;
16933
17000
  const old = st ? await fs.readFile(path) : "";
16934
- const line = `NOPASSWD ${self.name} (${target.name}) Cmnd ${argv.map(quoteWord).join(" ")}
17001
+ const words2 = argv.map(quoteWord).join(" ");
17002
+ const line = `NOPASSWD ${self.name} (${target.name}) Cwd ${quoteWord(cwd)} Cmnd ${words2}
16935
17003
  `;
16936
17004
  const text = old === "" || old.endsWith("\n") ? `${old}${line}` : `${old}
16937
17005
  ${line}`;
@@ -18381,17 +18449,21 @@ var Launcher = class {
18381
18449
  const real = await this.realLayoutPath("/tmp");
18382
18450
  const st = real === void 0 ? void 0 : await this.base.lstat(real).catch(() => void 0);
18383
18451
  if (real !== void 0 && st?.isDirectory && this.base.hasOwner && !await this.base.hasOwner(real)) {
18452
+ const names = await this.base.readdir(real).catch(() => void 0);
18384
18453
  let removed = 0;
18385
- for (const name2 of await this.base.readdir(real).catch(() => [])) {
18454
+ let left = names === void 0 ? 1 : 0;
18455
+ for (const name2 of names ?? []) {
18386
18456
  const path = joinName(real, name2);
18387
- if (await this.base.hasOwner(path) || this.inUse(path)) continue;
18388
- const failed3 = await this.base.rm(path, { recursive: true, force: true }).catch(String);
18457
+ if (await this.base.hasOwner(path)) continue;
18458
+ const failed3 = this.inUse(path) ? "busy" : await this.base.rm(path, { recursive: true, force: true }).catch(String);
18389
18459
  if (failed3 === void 0) removed++;
18460
+ else left++;
18390
18461
  }
18391
18462
  if (removed > 0) {
18392
18463
  console.warn(`slicc-kernel users: removed ${removed} entries from /tmp that had no owner`);
18393
18464
  }
18394
- await this.base.chown?.(real, { uid: 0, gid: 0, mode: 1023, follow: false });
18465
+ if (left === 0)
18466
+ await this.base.chown?.(real, { uid: 0, gid: 0, mode: 1023, follow: false });
18395
18467
  }
18396
18468
  if (st?.isDirectory) await this.base.chmod(real, 1023);
18397
18469
  }
@@ -20060,11 +20132,17 @@ async function createNodeKernel(options = {}) {
20060
20132
  close: () => session.close()
20061
20133
  };
20062
20134
  },
20063
- async writeFile(path, data) {
20135
+ async writeFile(path, data, options2 = {}) {
20136
+ const mode = options2.mode === void 0 ? void 0 : modeOption(options2.mode);
20064
20137
  const parent = path.slice(0, path.lastIndexOf("/")) || "/";
20065
20138
  await launcher.fs.mkdir(parent, { recursive: true });
20139
+ if (mode !== void 0) {
20140
+ await launcher.fs.writeFile(path, "");
20141
+ await launcher.fs.chmod(path, mode);
20142
+ }
20066
20143
  await launcher.fs.writeFile(path, data);
20067
20144
  },
20145
+ chmod: async (path, mode) => launcher.fs.chmod(path, modeOption(mode)),
20068
20146
  readFile: async (path) => launcher.fs.readFileBuffer(path),
20069
20147
  mount: (spec) => guard2(() => launcher.mount(spec)),
20070
20148
  umount: (target) => guard2(async () => launcher.umount(target)),