@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.
@@ -97,6 +97,9 @@ var MemoryMeta = class {
97
97
  async get(paths) {
98
98
  return paths.map((path) => this.entries.get(path));
99
99
  }
100
+ async fresh(path) {
101
+ return this.entries.get(path);
102
+ }
100
103
  async update(path, change) {
101
104
  this.set(path, change(this.entries.get(path)));
102
105
  }
@@ -193,6 +196,16 @@ var IndexedDbMeta = class {
193
196
  }
194
197
  return paths.map((path) => this.cache.get(path) ?? void 0);
195
198
  }
199
+ async fresh(path) {
200
+ const [transaction, store] = this.transaction("readonly");
201
+ const request2 = store.get(path);
202
+ let entry;
203
+ request2.onsuccess = () => {
204
+ entry = request2.result ?? void 0;
205
+ };
206
+ await settle(transaction, () => void 0);
207
+ return entry;
208
+ }
196
209
  async update(path, change) {
197
210
  this.settleRemoves();
198
211
  const [transaction, store] = this.transaction("readwrite");
@@ -207,6 +220,19 @@ var IndexedDbMeta = class {
207
220
  this.changed({ paths: [path] });
208
221
  this.cache.set(path, next2 ?? null);
209
222
  }
223
+ async updateMany(changes) {
224
+ const [transaction, store] = this.transaction("readwrite");
225
+ for (const { path, change } of changes) {
226
+ const request2 = store.get(path);
227
+ request2.onsuccess = () => store.put(change(request2.result));
228
+ }
229
+ await settle(transaction, () => void 0);
230
+ this.changed({ paths: changes.map(({ path }) => path) });
231
+ }
232
+ close() {
233
+ this.channel.close();
234
+ this.db.close();
235
+ }
210
236
  async move(from, to, root) {
211
237
  this.settleRemoves();
212
238
  const [transaction, store] = this.transaction("readwrite");
@@ -326,6 +352,12 @@ async function lutimesOf(fs, path, atime, mtime) {
326
352
  function mayLink(fs, path) {
327
353
  return fs.links?.(path) ?? true;
328
354
  }
355
+ function modeOption(mode) {
356
+ if (!Number.isInteger(mode) || mode < 0 || mode > 4095) {
357
+ throw fsError("EINVAL", `not a mode: ${String(mode)}`);
358
+ }
359
+ return mode;
360
+ }
329
361
  function fsError(code, path) {
330
362
  return Object.assign(new Error(`${code}: ${path}`), { code });
331
363
  }
@@ -857,7 +889,8 @@ function sidecar(meta) {
857
889
  const guarded2 = (op) => (...args) => meta[op].apply(meta, args).catch((err) => {
858
890
  throw translate(err, pathOf(args), "sidecar", false);
859
891
  });
860
- const ops = Object.fromEntries(META_OPS.map((op) => [op, guarded2(op)]));
892
+ const names = meta.fresh ? [...META_OPS, "fresh"] : META_OPS;
893
+ const ops = Object.fromEntries(names.map((op) => [op, guarded2(op)]));
861
894
  if (meta.absent) ops.absent = meta.absent.bind(meta);
862
895
  const { removeFile } = meta;
863
896
  if (removeFile)
@@ -985,6 +1018,24 @@ var OpfsFs = class {
985
1018
  committed(target) {
986
1019
  if (this.remote?.watched()) this.remote.wrote(target);
987
1020
  }
1021
+ counted = [];
1022
+ async stamp(path) {
1023
+ const entry = this.meta.fresh ? await this.meta.fresh(path) : (await this.meta.get([path]))[0];
1024
+ return String(entry?.writes ?? 0);
1025
+ }
1026
+ async counting(target) {
1027
+ if (!isProtected(this.counted, target)) return;
1028
+ await this.meta.update(target, (entry) => ({
1029
+ ...entry,
1030
+ path: target,
1031
+ writes: (entry?.writes ?? 0) + 1
1032
+ }));
1033
+ }
1034
+ async recounting(handle, target) {
1035
+ if (this.counted.length === 0) return;
1036
+ const where = await this.root.resolve(handle).catch(() => null);
1037
+ await this.counting(where ? `/${where.join("/")}` : target);
1038
+ }
988
1039
  resolvePath(base, path) {
989
1040
  return resolvePath(base, path);
990
1041
  }
@@ -1043,6 +1094,7 @@ var OpfsFs = class {
1043
1094
  guarded = [];
1044
1095
  protect(prefixes2) {
1045
1096
  this.guarded = prefixes2;
1097
+ this.counted = prefixes2;
1046
1098
  }
1047
1099
  unguarded() {
1048
1100
  return Object.create(this, { guarded: { value: [] } });
@@ -1379,9 +1431,10 @@ var OpfsFs = class {
1379
1431
  return this.retrying(
1380
1432
  path,
1381
1433
  async () => {
1434
+ const stamp = await this.stamp(path);
1382
1435
  const file = await handle.getFile();
1383
1436
  const bytes = new Uint8Array(await file.arrayBuffer());
1384
- return { bytes, version: `${file.lastModified}:${file.size}` };
1437
+ return { bytes, version: `${file.lastModified}:${file.size}:${stamp}` };
1385
1438
  },
1386
1439
  false
1387
1440
  );
@@ -1391,8 +1444,9 @@ var OpfsFs = class {
1391
1444
  return this.retrying(
1392
1445
  path,
1393
1446
  async () => {
1447
+ const stamp = await this.stamp(path);
1394
1448
  const file = await handle.getFile();
1395
- return `${file.lastModified}:${file.size}`;
1449
+ return `${file.lastModified}:${file.size}:${stamp}`;
1396
1450
  },
1397
1451
  false
1398
1452
  );
@@ -1407,14 +1461,24 @@ var OpfsFs = class {
1407
1461
  }
1408
1462
  async pwrite(path, offset, bytes) {
1409
1463
  const [handle, , target] = await this.fileFor(path);
1410
- await this.retrying(path, () => positioned(handle, (io) => io.write(offset, bytes)), true);
1464
+ await this.counting(target);
1465
+ try {
1466
+ await this.retrying(path, () => positioned(handle, (io) => io.write(offset, bytes)), true);
1467
+ } finally {
1468
+ await this.recounting(handle, target);
1469
+ }
1411
1470
  this.written(target);
1412
1471
  }
1413
1472
  async truncate(path, size) {
1414
1473
  const handle = await this.existing(path, true);
1415
1474
  const resolve2 = this.ranged || this.remote?.watched();
1416
- const target = resolve2 ? (await this.locate(path, true)).path : path;
1417
- await this.retrying(path, () => positioned(handle, (io) => io.truncate(size)), true);
1475
+ const target = resolve2 || this.counted.length > 0 ? (await this.locate(path, true)).path : path;
1476
+ await this.counting(target);
1477
+ try {
1478
+ await this.retrying(path, () => positioned(handle, (io) => io.truncate(size)), true);
1479
+ } finally {
1480
+ await this.recounting(handle, target);
1481
+ }
1418
1482
  this.written(target);
1419
1483
  }
1420
1484
  async writeSession(path, truncate) {
@@ -2339,7 +2403,9 @@ function id(value) {
2339
2403
  return value;
2340
2404
  }
2341
2405
  function changeCred(cur, change) {
2342
- if (typeof change !== "object" || change === null) throw fsError("EINVAL", "not a change");
2406
+ if (typeof change !== "object" || change === null || Array.isArray(change)) {
2407
+ throw fsError("EINVAL", "not a change");
2408
+ }
2343
2409
  const privileged = cur.euid === 0;
2344
2410
  const next2 = copyCred(cur);
2345
2411
  for (const keys of [UIDS, GIDS]) {
@@ -2514,6 +2580,10 @@ var KernelError = class extends Error {
2514
2580
  if (detail !== void 0) this.detail = detail;
2515
2581
  }
2516
2582
  };
2583
+ function openAccess(flags2) {
2584
+ const mode = flags2 & 3;
2585
+ return mode === 0 ? "read" : mode === 1 ? "write" : "both";
2586
+ }
2517
2587
  function pollFile(file) {
2518
2588
  return file.poll?.() ?? { readable: !!file.read, writable: !!file.write, hangup: false };
2519
2589
  }
@@ -13189,17 +13259,15 @@ var WasmProcess = class {
13189
13259
  const file = tty.file();
13190
13260
  return this.fds.install(this.options.outputFilter?.(file) ?? file, 3);
13191
13261
  }
13192
- openSealed() {
13262
+ openSealed(flags2) {
13193
13263
  const broker = this.options.sealed;
13194
13264
  if (!broker) throw new KernelError("ENOENT");
13195
- return this.fds.install(broker.open(), 3);
13265
+ return this.fds.install(broker.open(openAccess(flags2)), 3);
13196
13266
  }
13197
13267
  async openClipboard(flags2) {
13198
13268
  const broker = this.options.clipboard;
13199
13269
  if (!broker) throw new KernelError("ENOENT");
13200
- const mode = flags2 & WRITE_ACCESS;
13201
- const access2 = mode === 0 ? "read" : mode === 1 ? "write" : "both";
13202
- const file = broker.open(access2, await this.clipboardStamp());
13270
+ const file = broker.open(openAccess(flags2), await this.clipboardStamp());
13203
13271
  return this.fds.install(this.options.outputFilter?.(file) ?? file, 3);
13204
13272
  }
13205
13273
  async clipboardStamp() {
@@ -13358,7 +13426,7 @@ var WasmProcess = class {
13358
13426
  case "fd-tty-names":
13359
13427
  return { ok: true, kind: "json", json: this.options.jobs?.terminalNames() ?? [] };
13360
13428
  case "fd-open-sealed":
13361
- return { ok: true, kind: "json", json: this.openSealed() };
13429
+ return { ok: true, kind: "json", json: this.openSealed(req.flags) };
13362
13430
  case "fd-open-clipboard":
13363
13431
  return { ok: true, kind: "json", json: await this.openClipboard(req.flags) };
13364
13432
  case "fd-tty-owner":
@@ -15077,14 +15145,19 @@ var SealedChannel = class {
15077
15145
  offset = 0;
15078
15146
  inflight;
15079
15147
  broker;
15080
- constructor(broker) {
15148
+ access;
15149
+ constructor(broker, access2) {
15081
15150
  this.broker = broker;
15151
+ this.access = access2;
15082
15152
  }
15083
15153
  send(bytes, caller, gone) {
15154
+ if (this.access === "read") {
15155
+ bytes.fill(0);
15156
+ throw new KernelError("EBADF");
15157
+ }
15084
15158
  const length2 = bytes.length;
15085
15159
  const fields = parseSealed(bytes);
15086
- this.reply = void 0;
15087
- this.offset = 0;
15160
+ this.drop();
15088
15161
  this.inflight?.control.abort("replaced");
15089
15162
  const control = new AbortController();
15090
15163
  const withdraw = () => control.abort("withdrawn");
@@ -15120,15 +15193,18 @@ var SealedChannel = class {
15120
15193
  if (!signal.aborted) return reply;
15121
15194
  return signal.reason === "timeout" ? errorReply("ETIMEDOUT", "the vault did not answer within 10 s") : void 0;
15122
15195
  }
15196
+ drop() {
15197
+ this.reply?.fill(0);
15198
+ this.reply = void 0;
15199
+ this.offset = 0;
15200
+ }
15123
15201
  async read(max, signal) {
15202
+ if (this.access === "write") throw new KernelError("EBADF");
15124
15203
  for (; ; ) {
15125
15204
  if (this.reply) {
15126
15205
  const out = this.reply.slice(this.offset, this.offset + max);
15127
15206
  this.offset += out.length;
15128
- if (this.offset >= this.reply.length) {
15129
- this.reply = void 0;
15130
- this.offset = 0;
15131
- }
15207
+ if (this.offset >= this.reply.length) this.drop();
15132
15208
  return out;
15133
15209
  }
15134
15210
  const flight = this.inflight;
@@ -15161,7 +15237,7 @@ var SealedChannel = class {
15161
15237
  close() {
15162
15238
  this.inflight?.control.abort("withdrawn");
15163
15239
  this.inflight = void 0;
15164
- this.reply = void 0;
15240
+ this.drop();
15165
15241
  }
15166
15242
  };
15167
15243
  function swapOf(reply) {
@@ -15218,12 +15294,14 @@ var SealedBroker = class {
15218
15294
  control.abort("done");
15219
15295
  }
15220
15296
  }
15221
- open() {
15222
- const channel = new SealedChannel(this);
15297
+ open(access2 = "both") {
15298
+ const channel = new SealedChannel(this, access2);
15223
15299
  return new OpenFile({
15224
- read: (max, signal) => channel.read(max, signal),
15225
- write: async () => {
15226
- throw new KernelError("EINVAL");
15300
+ ...access2 === "write" ? {} : { read: (max, signal) => channel.read(max, signal) },
15301
+ ...access2 === "read" ? {} : {
15302
+ write: async () => {
15303
+ throw new KernelError("EINVAL");
15304
+ }
15227
15305
  },
15228
15306
  poll: () => channel.poll(),
15229
15307
  changed: (signal) => channel.changed(signal),
@@ -16842,7 +16920,10 @@ var SECURE_PATH = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
16842
16920
  var DEFAULT_SUDOERS = "%sudo Cmnd *\n";
16843
16921
  var SUDO_TIMEOUT_MS = 3e5;
16844
16922
  var NAME3 = /^[a-z_][a-z0-9_.-]*$/;
16845
- var RULE = /^(NOPASSWD\s+)?(\S+)\s+(?:\(([^()\s]+)\)\s+)?Cmnd(?:\s+(.*))?$/;
16923
+ var WORD = String.raw`(?:'[^']*'|"(?:[^"\\]|\\[\s\S])*"|\\[\s\S]|[^\s'"\\])+`;
16924
+ var RULE = new RegExp(
16925
+ String.raw`^(NOPASSWD\s+)?(\S+)\s+(?:\(([^()\s]+)\)\s+)?(?:Cwd\s+(${WORD})\s+)?Cmnd(?:\s+(.*))?$`
16926
+ );
16846
16927
  var TERM = /^[A-Za-z0-9._+-]+$/;
16847
16928
  var PLAIN = /^[A-Za-z0-9_@%+=:,./-]+$/;
16848
16929
  var USAGE = "usage: sudo [-n] [-u user] [--] command [args...]\n sudo -l";
@@ -16877,17 +16958,25 @@ function validWho(who) {
16877
16958
  if (who === "ALL") return true;
16878
16959
  return NAME3.test(who.startsWith("%") ? who.slice(1) : who);
16879
16960
  }
16961
+ function cwdOf(word) {
16962
+ const words2 = splitWords(word);
16963
+ const path = words2?.length === 1 ? words2[0] : "";
16964
+ return path.startsWith("/") ? normalizePath(path) : "";
16965
+ }
16880
16966
  function parseRule(line) {
16881
16967
  const text = line.trim();
16882
16968
  if (text === "" || text.startsWith("#")) return null;
16883
16969
  const m = RULE.exec(text);
16884
16970
  if (!m) return void 0;
16885
- const [, nopasswd, who = "", target = "root", rest2 = ""] = m;
16971
+ const [, nopasswd, who = "", target = "root", dir, rest2 = ""] = m;
16886
16972
  if (!validWho(who) || target !== "ALL" && !NAME3.test(target)) return void 0;
16887
- if (rest2.trim() === "*") return { nopasswd: !!nopasswd, who, target, cmnd: "*" };
16973
+ const cwd = dir === void 0 ? void 0 : cwdOf(dir);
16974
+ if (cwd === "") return void 0;
16975
+ const rule = { nopasswd: !!nopasswd, who, target, ...cwd !== void 0 ? { cwd } : {} };
16976
+ if (rest2.trim() === "*") return { ...rule, cmnd: "*" };
16888
16977
  const cmnd = splitWords(rest2);
16889
16978
  if (!cmnd?.[0]?.startsWith("/")) return void 0;
16890
- return { nopasswd: !!nopasswd, who, target, cmnd };
16979
+ return { ...rule, cmnd };
16891
16980
  }
16892
16981
  function flags(parsed2, arg, next2) {
16893
16982
  for (let j = 1; j < arg.length; j++) {
@@ -16926,7 +17015,8 @@ function parseArgs(args) {
16926
17015
  }
16927
17016
  function described(rule) {
16928
17017
  const cmnd = rule.cmnd === "*" ? "ALL" : rule.cmnd.map(quoteWord).join(" ");
16929
- return ` (${rule.target}) ${rule.nopasswd ? "NOPASSWD: " : ""}${cmnd}`;
17018
+ const cwd = rule.cwd === void 0 ? "" : `CWD=${quoteWord(rule.cwd)} `;
17019
+ return ` (${rule.target}) ${cwd}${rule.nopasswd ? "NOPASSWD: " : ""}${cmnd}`;
16930
17020
  }
16931
17021
  var Sudo = class {
16932
17022
  o;
@@ -17066,7 +17156,7 @@ var Sudo = class {
17066
17156
  if (untrusted) return untrusted;
17067
17157
  const argv = [path, ...args];
17068
17158
  const matching = rules.filter(
17069
- (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]))
17159
+ (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]))
17070
17160
  );
17071
17161
  const grant = () => this.grant(self, target, cred, parsed2.command, path, cwd, call.term);
17072
17162
  if (root || matching.some((rule) => rule.nopasswd)) return grant();
@@ -17088,7 +17178,7 @@ var Sudo = class {
17088
17178
  }
17089
17179
  const changed = await this.untrusted(path, cwd, word);
17090
17180
  if (changed) return changed;
17091
- if (answer === "always" && !await this.remember(self, target, argv)) {
17181
+ if (answer === "always" && !await this.remember(self, target, cwd, argv)) {
17092
17182
  return failed2("EIO", "sudo: the rule could not be recorded");
17093
17183
  }
17094
17184
  return grant();
@@ -17153,7 +17243,7 @@ ${lines2}
17153
17243
  requester.removeEventListener("abort", withdraw);
17154
17244
  }
17155
17245
  }
17156
- async remember(self, target, argv) {
17246
+ async remember(self, target, cwd, argv) {
17157
17247
  const { fs } = this.o;
17158
17248
  const path = `${SUDOERS_D}/${self.name}`;
17159
17249
  if (!NAME3.test(self.name)) return false;
@@ -17163,7 +17253,8 @@ ${lines2}
17163
17253
  const st = await fs.lstat(path).catch(() => void 0);
17164
17254
  if (st && !await this.trusted(path)) return false;
17165
17255
  const old = st ? await fs.readFile(path) : "";
17166
- const line = `NOPASSWD ${self.name} (${target.name}) Cmnd ${argv.map(quoteWord).join(" ")}
17256
+ const words2 = argv.map(quoteWord).join(" ");
17257
+ const line = `NOPASSWD ${self.name} (${target.name}) Cwd ${quoteWord(cwd)} Cmnd ${words2}
17167
17258
  `;
17168
17259
  const text = old === "" || old.endsWith("\n") ? `${old}${line}` : `${old}
17169
17260
  ${line}`;
@@ -18613,17 +18704,21 @@ var Launcher = class {
18613
18704
  const real = await this.realLayoutPath("/tmp");
18614
18705
  const st = real === void 0 ? void 0 : await this.base.lstat(real).catch(() => void 0);
18615
18706
  if (real !== void 0 && st?.isDirectory && this.base.hasOwner && !await this.base.hasOwner(real)) {
18707
+ const names = await this.base.readdir(real).catch(() => void 0);
18616
18708
  let removed = 0;
18617
- for (const name2 of await this.base.readdir(real).catch(() => [])) {
18709
+ let left = names === void 0 ? 1 : 0;
18710
+ for (const name2 of names ?? []) {
18618
18711
  const path = joinName(real, name2);
18619
- if (await this.base.hasOwner(path) || this.inUse(path)) continue;
18620
- const failed3 = await this.base.rm(path, { recursive: true, force: true }).catch(String);
18712
+ if (await this.base.hasOwner(path)) continue;
18713
+ const failed3 = this.inUse(path) ? "busy" : await this.base.rm(path, { recursive: true, force: true }).catch(String);
18621
18714
  if (failed3 === void 0) removed++;
18715
+ else left++;
18622
18716
  }
18623
18717
  if (removed > 0) {
18624
18718
  console.warn(`slicc-kernel users: removed ${removed} entries from /tmp that had no owner`);
18625
18719
  }
18626
- await this.base.chown?.(real, { uid: 0, gid: 0, mode: 1023, follow: false });
18720
+ if (left === 0)
18721
+ await this.base.chown?.(real, { uid: 0, gid: 0, mode: 1023, follow: false });
18627
18722
  }
18628
18723
  if (st?.isDirectory) await this.base.chmod(real, 1023);
18629
18724
  }
@@ -19076,7 +19171,12 @@ var USERS_OPS = /* @__PURE__ */ new Set([
19076
19171
  "groups-add-member",
19077
19172
  "groups-remove-member"
19078
19173
  ]);
19079
- var ATTEST_OPS = /* @__PURE__ */ new Set(["attest", "watch-files", "unwatch-files"]);
19174
+ var ATTEST_OPS = /* @__PURE__ */ new Set([
19175
+ "attest",
19176
+ "watch-files",
19177
+ "unwatch-files",
19178
+ "chmod"
19179
+ ]);
19080
19180
  function isAttestRequest(req) {
19081
19181
  return ATTEST_OPS.has(req.op);
19082
19182
  }
@@ -19092,6 +19192,7 @@ function attestOps(port) {
19092
19192
  const watches = /* @__PURE__ */ new Map();
19093
19193
  return async (l, req) => {
19094
19194
  if (req.op === "attest") return l.attest(req.path);
19195
+ if (req.op === "chmod") return l.fs.chmod(req.path, modeOption(req.mode)).then(() => true);
19095
19196
  if (req.op === "unwatch-files") {
19096
19197
  watches.get(req.watch)?.();
19097
19198
  return watches.delete(req.watch);