@ai-ecoverse/slicc-kernel 1.65.1 → 1.66.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.
@@ -376,15 +376,15 @@ function translate(err, path, op = "lookup", mapped = true) {
376
376
  if (typeof err?.code === "string") return err;
377
377
  const raw = err?.name;
378
378
  const name2 = typeof raw === "string" ? raw : "Error";
379
- const code = mapped && ERRNO_BY_DOM_ERROR[name2] || "EIO";
379
+ const code = name2 === "QuotaExceededError" ? "ENOSPC" : mapped && ERRNO_BY_DOM_ERROR[name2] || "EIO";
380
380
  const error = Object.assign(fsError(code, path), { domName: name2, op, cause: err });
381
- if (code !== "EIO") return error;
381
+ if (code !== "EIO" && code !== "ENOSPC") return error;
382
382
  const detail = err?.message;
383
383
  error.message += ` (OPFS ${op}: ${name2}${typeof detail === "string" && detail ? `: ${detail}` : ""})`;
384
384
  if (!reported.has(`${name2}/${op}`)) {
385
385
  reported.add(`${name2}/${op}`);
386
386
  console.warn(
387
- `slicc-kernel: ${error.message}, reported as EIO; logged once per error and operation`
387
+ `slicc-kernel: ${error.message}, reported as ${code}; logged once per error and operation`
388
388
  );
389
389
  }
390
390
  return error;
@@ -894,11 +894,13 @@ var OpfsFs = class {
894
894
  const [file, existed, target] = await this.fileFor(path);
895
895
  if (!existed) await this.stampNew(target, owner, false);
896
896
  if (!existed && content.length === 0) return;
897
+ let writable2;
897
898
  try {
898
- const writable2 = await file.createWritable();
899
+ writable2 = await file.createWritable();
899
900
  await writable2.write(content);
900
901
  await writable2.close();
901
902
  } catch (err) {
903
+ await writable2?.abort().catch(() => void 0);
902
904
  throw translate(err, path, "commit", false);
903
905
  }
904
906
  }
@@ -1311,7 +1313,7 @@ function portCdpHook(ask, gone) {
1311
1313
  }
1312
1314
 
1313
1315
  // src/client/protocol.ts
1314
- var PROTOCOL = [1, 8];
1316
+ var PROTOCOL = [1, 9];
1315
1317
  var FS_METHODS = [
1316
1318
  "readFile",
1317
1319
  "writeFile",
@@ -1683,6 +1685,64 @@ async function openable(g, op, path, want, create) {
1683
1685
  await need(g, op, path, want, { directory: st?.isDirectory ?? false });
1684
1686
  }
1685
1687
 
1688
+ // src/kernel/cred.ts
1689
+ var ROOT = Object.freeze({
1690
+ ruid: 0,
1691
+ euid: 0,
1692
+ suid: 0,
1693
+ rgid: 0,
1694
+ egid: 0,
1695
+ sgid: 0,
1696
+ groups: [0]
1697
+ });
1698
+ var MAX_ID = 4294967294;
1699
+ var MAX_GROUPS = 65536;
1700
+ var UIDS = ["ruid", "euid", "suid"];
1701
+ var GIDS = ["rgid", "egid", "sgid"];
1702
+ function userCred(uid, gid, groups = [gid]) {
1703
+ return { ruid: uid, euid: uid, suid: uid, rgid: gid, egid: gid, sgid: gid, groups: [...groups] };
1704
+ }
1705
+ function copyCred(cred) {
1706
+ return { ...cred, groups: [...cred.groups] };
1707
+ }
1708
+ function id(value) {
1709
+ if (value === void 0 || value === -1 || value === 4294967295) return void 0;
1710
+ if (!Number.isInteger(value) || value < 0 || value > MAX_ID) {
1711
+ throw fsError("EINVAL", `not a user or group id: ${String(value)}`);
1712
+ }
1713
+ return value;
1714
+ }
1715
+ function changeCred(cur, change) {
1716
+ const privileged = cur.euid === 0;
1717
+ const next2 = copyCred(cur);
1718
+ for (const keys of [UIDS, GIDS]) {
1719
+ const own = keys.map((key) => cur[key]);
1720
+ for (const key of keys) {
1721
+ const value = id(change[key]);
1722
+ if (value === void 0) continue;
1723
+ if (!privileged && !own.includes(value)) throw fsError("EPERM", `${key} ${value}`);
1724
+ next2[key] = value;
1725
+ }
1726
+ }
1727
+ if (change.groups !== void 0) {
1728
+ if (!Array.isArray(change.groups) || change.groups.length > MAX_GROUPS) {
1729
+ throw fsError("EINVAL", "not a group list");
1730
+ }
1731
+ const groups = change.groups.map((g) => id(g) ?? Number.NaN);
1732
+ if (groups.some(Number.isNaN)) throw fsError("EINVAL", "not a group list");
1733
+ if (!privileged) throw fsError("EPERM", "setgroups");
1734
+ next2.groups = groups;
1735
+ }
1736
+ return next2;
1737
+ }
1738
+ function maySignal(sender, target) {
1739
+ if (sender.euid === 0) return true;
1740
+ return [sender.ruid, sender.euid].some((uid) => uid === target.ruid || uid === target.suid);
1741
+ }
1742
+ function maySee(viewer, target) {
1743
+ return viewer.euid === 0 || target.ruid === viewer.euid || target.euid === viewer.euid;
1744
+ }
1745
+
1686
1746
  // src/kernel/pipe.ts
1687
1747
  var PIPE_CAPACITY = 64 * 1024;
1688
1748
  var PipeError = class extends Error {
@@ -1990,9 +2050,9 @@ var FdTable = class _FdTable {
1990
2050
  closesOnExec(fd) {
1991
2051
  return this.cloexec.has(fd);
1992
2052
  }
1993
- setStatusFlags(fd, flags) {
2053
+ setStatusFlags(fd, flags2) {
1994
2054
  this.get(fd);
1995
- this.status.set(fd, flags);
2055
+ this.status.set(fd, flags2);
1996
2056
  }
1997
2057
  statusFlags(fd) {
1998
2058
  return this.status.get(fd);
@@ -2037,7 +2097,7 @@ var FdTable = class _FdTable {
2037
2097
  const child = new _FdTable();
2038
2098
  for (const [fd, file] of this.fds) child.fds.set(fd, file.retain());
2039
2099
  for (const fd of this.cloexec) child.cloexec.add(fd);
2040
- for (const [fd, flags] of this.status) child.status.set(fd, flags);
2100
+ for (const [fd, flags2] of this.status) child.status.set(fd, flags2);
2041
2101
  return child;
2042
2102
  }
2043
2103
  async closeAll() {
@@ -2504,9 +2564,9 @@ var KernelSocket = class _KernelSocket {
2504
2564
  case SO_TYPE:
2505
2565
  return SOCK_STREAM;
2506
2566
  case SO_ERROR: {
2507
- const failed2 = this.failed;
2567
+ const failed3 = this.failed;
2508
2568
  this.failed = void 0;
2509
- return failed2 ? wasiErrno(failed2) : 0;
2569
+ return failed3 ? wasiErrno(failed3) : 0;
2510
2570
  }
2511
2571
  case SO_ACCEPTCONN:
2512
2572
  return this.state === "listening" ? 1 : 0;
@@ -3013,64 +3073,6 @@ function serveTransport(peer, transport, { closeWaitMs = CLOSE_WAIT_MS } = {}) {
3013
3073
  };
3014
3074
  }
3015
3075
 
3016
- // src/kernel/cred.ts
3017
- var ROOT = Object.freeze({
3018
- ruid: 0,
3019
- euid: 0,
3020
- suid: 0,
3021
- rgid: 0,
3022
- egid: 0,
3023
- sgid: 0,
3024
- groups: [0]
3025
- });
3026
- var MAX_ID = 4294967294;
3027
- var MAX_GROUPS = 65536;
3028
- var UIDS = ["ruid", "euid", "suid"];
3029
- var GIDS = ["rgid", "egid", "sgid"];
3030
- function userCred(uid, gid, groups = [gid]) {
3031
- return { ruid: uid, euid: uid, suid: uid, rgid: gid, egid: gid, sgid: gid, groups: [...groups] };
3032
- }
3033
- function copyCred(cred) {
3034
- return { ...cred, groups: [...cred.groups] };
3035
- }
3036
- function id(value) {
3037
- if (value === void 0 || value === -1 || value === 4294967295) return void 0;
3038
- if (!Number.isInteger(value) || value < 0 || value > MAX_ID) {
3039
- throw fsError("EINVAL", `not a user or group id: ${String(value)}`);
3040
- }
3041
- return value;
3042
- }
3043
- function changeCred(cur, change) {
3044
- const privileged = cur.euid === 0;
3045
- const next2 = copyCred(cur);
3046
- for (const keys of [UIDS, GIDS]) {
3047
- const own = keys.map((key) => cur[key]);
3048
- for (const key of keys) {
3049
- const value = id(change[key]);
3050
- if (value === void 0) continue;
3051
- if (!privileged && !own.includes(value)) throw fsError("EPERM", `${key} ${value}`);
3052
- next2[key] = value;
3053
- }
3054
- }
3055
- if (change.groups !== void 0) {
3056
- if (!Array.isArray(change.groups) || change.groups.length > MAX_GROUPS) {
3057
- throw fsError("EINVAL", "not a group list");
3058
- }
3059
- const groups = change.groups.map((g) => id(g) ?? Number.NaN);
3060
- if (groups.some(Number.isNaN)) throw fsError("EINVAL", "not a group list");
3061
- if (!privileged) throw fsError("EPERM", "setgroups");
3062
- next2.groups = groups;
3063
- }
3064
- return next2;
3065
- }
3066
- function maySignal(sender, target) {
3067
- if (sender.euid === 0) return true;
3068
- return [sender.ruid, sender.euid].some((uid) => uid === target.ruid || uid === target.suid);
3069
- }
3070
- function maySee(viewer, target) {
3071
- return viewer.euid === 0 || target.ruid === viewer.euid || target.euid === viewer.euid;
3072
- }
3073
-
3074
3076
  // src/users.ts
3075
3077
  var PASSWD = "/etc/passwd";
3076
3078
  var GROUP = "/etc/group";
@@ -3535,6 +3537,12 @@ async function runAs(l, self, requested) {
3535
3537
  }
3536
3538
  return target;
3537
3539
  }
3540
+ function clientCred(self) {
3541
+ return self ? credOf(self) : copyCred(ROOT);
3542
+ }
3543
+ async function runTagged(l, self, requested, tag) {
3544
+ return { user: await runAs(l, self, requested), tag };
3545
+ }
3538
3546
  function processes(l, self) {
3539
3547
  const viewer = credOf(self);
3540
3548
  return l.list(viewer).map(({ tid: _, cred = viewer, umask: _umask, ...entry }) => ({
@@ -3669,7 +3677,7 @@ function serveClient(port, host) {
3669
3677
  async function terminal(req, l) {
3670
3678
  const session = await l.openTerminal(req.argv, {
3671
3679
  ...req.options,
3672
- user: await runAs(l, self, req.options.user),
3680
+ ...await runTagged(l, self, req.options.user, host.tag),
3673
3681
  onData: (bytes) => reply(req.id, { fd: 1, bytes: bytes.slice() })
3674
3682
  });
3675
3683
  if (state === "closed") {
@@ -3691,7 +3699,7 @@ function serveClient(port, host) {
3691
3699
  let leader;
3692
3700
  const result = await l.run(req.argv, {
3693
3701
  ...req.options,
3694
- user: await runAs(l, self, req.options.user),
3702
+ ...await runTagged(l, self, req.options.user, host.tag),
3695
3703
  collect: false,
3696
3704
  onStdout: out(1),
3697
3705
  onStderr: out(2),
@@ -3732,9 +3740,9 @@ function serveClient(port, host) {
3732
3740
  case "unwatch":
3733
3741
  return watchOp(l, req, watches, send, guardFor(l, self));
3734
3742
  case "mount":
3735
- return { result: await l.mount(req.spec) };
3743
+ return { result: await l.mountAs(clientCred(self), req.spec) };
3736
3744
  case "umount":
3737
- l.umount(req.target);
3745
+ await l.umountAs(clientCred(self), req.target);
3738
3746
  return { result: true };
3739
3747
  case "mounts":
3740
3748
  return { result: l.mounts.list() };
@@ -4945,8 +4953,8 @@ function ipv6(host) {
4945
4953
  const head = groups(halves[0]);
4946
4954
  const back = halves.length === 2 ? groups(halves[1]) : [];
4947
4955
  if (![...head, ...back].every((g) => /^[0-9A-Fa-f]{1,4}$/.test(g))) return void 0;
4948
- const words = [...head, ...back].map((g) => Number.parseInt(g, 16));
4949
- const missing3 = 8 - words.length - tail.length;
4956
+ const words2 = [...head, ...back].map((g) => Number.parseInt(g, 16));
4957
+ const missing3 = 8 - words2.length - tail.length;
4950
4958
  if (halves.length === 2 ? missing3 < 1 : missing3 !== 0) return void 0;
4951
4959
  const all = [
4952
4960
  ...head.map((g) => Number.parseInt(g, 16)),
@@ -6350,9 +6358,8 @@ async function scanFilesystems(fs, modules) {
6350
6358
  }
6351
6359
  var SUFFIX = /^\.[A-Za-z0-9_+-][A-Za-z0-9._+-]*$/;
6352
6360
  async function scanBinfmts(fs, modules, warn = () => {
6353
- }) {
6361
+ }, owners = /* @__PURE__ */ new Map()) {
6354
6362
  const found = /* @__PURE__ */ new Map();
6355
- const owners = /* @__PURE__ */ new Map();
6356
6363
  for (const pkg of await packagesIn(fs, modules)) {
6357
6364
  let manifest;
6358
6365
  try {
@@ -6377,7 +6384,7 @@ async function scanBinfmts(fs, modules, warn = () => {
6377
6384
  }
6378
6385
  return found;
6379
6386
  }
6380
- function binfmtOf(binfmts, path) {
6387
+ function binfmtSuffix(binfmts, path) {
6381
6388
  const name2 = path.slice(path.lastIndexOf("/") + 1);
6382
6389
  let best;
6383
6390
  for (const suffix of binfmts.keys()) {
@@ -6385,6 +6392,10 @@ function binfmtOf(binfmts, path) {
6385
6392
  if (best === void 0 || suffix.length > best.length) best = suffix;
6386
6393
  }
6387
6394
  }
6395
+ return best;
6396
+ }
6397
+ function binfmtOf(binfmts, path) {
6398
+ const best = binfmtSuffix(binfmts, path);
6388
6399
  return best === void 0 ? void 0 : binfmts.get(best);
6389
6400
  }
6390
6401
 
@@ -6961,8 +6972,9 @@ var ChildTable = class {
6961
6972
  if (!this.forker) throw new SpawnError("ENOSYS");
6962
6973
  return this.track(state, this.parentFds.fork(), /* @__PURE__ */ new Map(), this.forker);
6963
6974
  }
6964
- async spawn(req, stdio, inherit2 = []) {
6965
- if (!this.spawner) throw new SpawnError("ENOSYS");
6975
+ async spawn(req, stdio, inherit2 = [], grant) {
6976
+ const { spawner } = this;
6977
+ if (!spawner) throw new SpawnError("ENOSYS");
6966
6978
  const fds = new FdTable();
6967
6979
  const captured = /* @__PURE__ */ new Map();
6968
6980
  try {
@@ -6980,7 +6992,7 @@ var ChildTable = class {
6980
6992
  await fds.closeAll();
6981
6993
  throw e;
6982
6994
  }
6983
- return this.track(req, fds, captured, this.spawner);
6995
+ return this.track(req, fds, captured, (r, f) => spawner(r, f, grant));
6984
6996
  }
6985
6997
  async track(req, fds, captured, start2) {
6986
6998
  let handle;
@@ -7027,34 +7039,34 @@ var ChildTable = class {
7027
7039
  }
7028
7040
  return deviceFile("null");
7029
7041
  }
7030
- async wait(pid, nohang, signal, flags = {}) {
7042
+ async wait(pid, nohang, signal, flags2 = {}) {
7031
7043
  let interrupt;
7032
7044
  try {
7033
7045
  for (; ; ) {
7034
- const candidates = this.candidates(pid, flags);
7046
+ const candidates = this.candidates(pid, flags2);
7035
7047
  if (candidates.length === 0) throw new KernelError("ECHILD");
7036
7048
  const done4 = candidates.find(([, child]) => child.code !== void 0);
7037
7049
  if (done4) return this.reap(done4[0], done4[1].code);
7038
- const changed = this.stateReport(candidates, flags);
7050
+ const changed = this.stateReport(candidates, flags2);
7039
7051
  if (changed) return changed;
7040
7052
  if (nohang) return [0, 0];
7041
7053
  interrupt ??= interrupted(signal);
7042
- await this.nextChange(candidates, flags, interrupt.promise);
7054
+ await this.nextChange(candidates, flags2, interrupt.promise);
7043
7055
  }
7044
7056
  } finally {
7045
7057
  interrupt?.done();
7046
7058
  }
7047
7059
  }
7048
- candidates(pid, flags) {
7060
+ candidates(pid, flags2) {
7049
7061
  const all = [...this.children];
7050
7062
  if (pid > 0) return all.filter(([p]) => p === pid);
7051
- const inGroup2 = flags.inGroup;
7063
+ const inGroup2 = flags2.inGroup;
7052
7064
  return pid === -1 || !inGroup2 ? all : all.filter(([p]) => inGroup2(p));
7053
7065
  }
7054
- async nextChange(candidates, flags, interrupt) {
7066
+ async nextChange(candidates, flags2, interrupt) {
7055
7067
  let wake;
7056
7068
  const stateChange = new Promise((resolve) => wake = resolve);
7057
- const watching = flags.untraced || flags.continued;
7069
+ const watching = flags2.untraced || flags2.continued;
7058
7070
  if (watching && wake) this.stateChanged.push(wake);
7059
7071
  try {
7060
7072
  await Promise.race([
@@ -7066,14 +7078,14 @@ var ChildTable = class {
7066
7078
  this.stateChanged = this.stateChanged.filter((w) => w !== wake);
7067
7079
  }
7068
7080
  }
7069
- stateReport(candidates, flags) {
7081
+ stateReport(candidates, flags2) {
7070
7082
  for (const [p, child] of candidates) {
7071
- if (flags.untraced && child.stopReport !== void 0) {
7083
+ if (flags2.untraced && child.stopReport !== void 0) {
7072
7084
  const sig = child.stopReport;
7073
7085
  child.stopReport = void 0;
7074
7086
  return [p, stoppedStatus(sig)];
7075
7087
  }
7076
- if (flags.continued && child.continueReport) {
7088
+ if (flags2.continued && child.continueReport) {
7077
7089
  child.continueReport = false;
7078
7090
  return [p, CONTINUED_STATUS];
7079
7091
  }
@@ -11034,8 +11046,8 @@ function errCode(err) {
11034
11046
  async function targetDir(fs, path) {
11035
11047
  return parentOf4(await realTarget(fs, path).catch(() => path));
11036
11048
  }
11037
- async function refuseReadonly(fs, path, flags, opts = {}) {
11038
- const writes = (flags & O_ACCMODE) !== 0 || opts.truncate === true;
11049
+ async function refuseReadonly(fs, path, flags2, opts = {}) {
11050
+ const writes = (flags2 & O_ACCMODE) !== 0 || opts.truncate === true;
11039
11051
  if (!writes && !opts.create || !fs.stat) return;
11040
11052
  const stat = (p) => fs.stat?.(p).catch((err) => {
11041
11053
  const code = err?.code;
@@ -11250,6 +11262,7 @@ var SYSCALL_OPS = /* @__PURE__ */ new Set([
11250
11262
  "proc-spawn",
11251
11263
  "proc-wait",
11252
11264
  "proc-captured",
11265
+ "proc-sudo",
11253
11266
  "proc-run",
11254
11267
  "proc-exit",
11255
11268
  "proc-fork",
@@ -11282,6 +11295,12 @@ var MAX_READ = 1024 * 1024;
11282
11295
  var WRITE_ACCESS = 3;
11283
11296
  var KEYBOARD = /* @__PURE__ */ new Set([SIG.INT, SIG.QUIT, SIG.TSTP]);
11284
11297
  var DEFAULT_UMASK = 18;
11298
+ function words(value) {
11299
+ if (!Array.isArray(value) || !value.every((w) => typeof w === "string")) {
11300
+ throw new KernelError("EINVAL");
11301
+ }
11302
+ return [...value];
11303
+ }
11285
11304
  var DYING = { ok: false, errno: "EINTR", message: "the process is being killed" };
11286
11305
  var HOSTS_ONLY = new Resolver({ routes: new Routes() });
11287
11306
  var WasmProcess = class {
@@ -11474,17 +11493,17 @@ var WasmProcess = class {
11474
11493
  if (restart && this.options.hasPending?.()) throw new KernelError("EINTR");
11475
11494
  }
11476
11495
  async procWait(req, cancelled) {
11477
- const flags = {
11496
+ const flags2 = {
11478
11497
  untraced: req.untraced,
11479
11498
  continued: req.continued,
11480
11499
  inGroup: this.waitGroup(req.pid)
11481
11500
  };
11482
11501
  if (this.owed && !req.nohang) {
11483
- const now = await this.children.wait(req.pid, true, this.interrupt.signal, flags);
11502
+ const now = await this.children.wait(req.pid, true, this.interrupt.signal, flags2);
11484
11503
  if (now[0] !== 0) return now;
11485
11504
  }
11486
11505
  const signal = req.nohang ? this.interrupt.signal : this.blockingSignal(cancelled);
11487
- const waited = await this.children.wait(req.pid, req.nohang, signal, flags);
11506
+ const waited = await this.children.wait(req.pid, req.nohang, signal, flags2);
11488
11507
  return waited;
11489
11508
  }
11490
11509
  blockingSignal(cancelled, owing = true) {
@@ -11816,13 +11835,13 @@ var WasmProcess = class {
11816
11835
  }
11817
11836
  fdInfo(fd) {
11818
11837
  const file = this.fds.get(fd).file;
11819
- const flags = this.fds.statusFlags(fd);
11838
+ const flags2 = this.fds.statusFlags(fd);
11820
11839
  const terminal = (file.tty ?? file.pty?.slave)?.name;
11821
11840
  return {
11822
11841
  tty: file.tty !== void 0,
11823
11842
  kind: file instanceof KernelSocket ? "socket" : kernelFdKind(file),
11824
11843
  ...file.heldMeta ? { meta: file.heldMeta } : {},
11825
- ...flags !== void 0 ? { flags } : {},
11844
+ ...flags2 !== void 0 ? { flags: flags2 } : {},
11826
11845
  ...this.fds.closesOnExec(fd) ? { cloexec: true } : {},
11827
11846
  ...file.read && !file.write ? { access: "read" } : {},
11828
11847
  ...file.write && !file.read ? { access: "write" } : {},
@@ -12010,7 +12029,27 @@ var WasmProcess = class {
12010
12029
  return { ok: true, kind: "void" };
12011
12030
  case "proc-captured":
12012
12031
  return { ok: true, kind: "bytes", bytes: this.children.captured(req.pid, req.slot) };
12013
- }
12032
+ case "proc-sudo":
12033
+ return { ok: true, kind: "json", json: await this.sudo(req, cancelled) };
12034
+ }
12035
+ }
12036
+ async sudo(req, cancelled) {
12037
+ const { sudo } = this.options;
12038
+ if (!sudo) throw new KernelError("ENOSYS");
12039
+ const call = {
12040
+ op: "proc-sudo",
12041
+ args: words(req.args),
12042
+ cwd: typeof req.cwd === "string" && req.cwd.startsWith("/") ? req.cwd : "/",
12043
+ ...typeof req.term === "string" ? { term: req.term } : {}
12044
+ };
12045
+ const answer = await sudo(call, this.blockingSignal(cancelled));
12046
+ if (!("grant" in answer)) return answer;
12047
+ if (this.dying) throw new KernelError("EINTR");
12048
+ const { file, argv, env, cwd, cred, caller } = answer.grant;
12049
+ const stdio = [{ fd: 0 }, { fd: 1 }, { fd: 2 }];
12050
+ return {
12051
+ pid: await this.children.spawn({ file, argv, env, cwd }, stdio, [], { cred, caller })
12052
+ };
12014
12053
  }
12015
12054
  async mountSyscall(req, cancelled) {
12016
12055
  const { mount, umount } = this.options;
@@ -12018,9 +12057,9 @@ var WasmProcess = class {
12018
12057
  try {
12019
12058
  if (req.op === "umount") await umount(req.target, req.flags);
12020
12059
  else {
12021
- const { source, target, type, flags, data } = req;
12060
+ const { source, target, type, flags: flags2, data } = req;
12022
12061
  const signal = this.blockingSignal(cancelled);
12023
- await Promise.race([mount({ source, target, type, flags, data }, signal), aborted(signal)]);
12062
+ await Promise.race([mount({ source, target, type, flags: flags2, data }, signal), aborted(signal)]);
12024
12063
  }
12025
12064
  } catch (err) {
12026
12065
  const code = err?.code;
@@ -12163,14 +12202,14 @@ function descId(file) {
12163
12202
  function inheritedFds(fds) {
12164
12203
  return fds.numbers().filter((fd) => fd > 2 && !fds.get(fd).file.held).map((fd) => {
12165
12204
  const open = fds.get(fd);
12166
- const flags = fds.statusFlags(fd);
12205
+ const flags2 = fds.statusFlags(fd);
12167
12206
  const kind = open.file instanceof KernelSocket ? "socket" : kernelFdKind(open.file);
12168
12207
  const meta = open.file.heldMeta;
12169
12208
  return {
12170
12209
  fd,
12171
12210
  kind,
12172
12211
  ...kind === "device" && meta && "device" in meta ? { device: meta } : {},
12173
- ...flags !== void 0 ? { flags } : {},
12212
+ ...flags2 !== void 0 ? { flags: flags2 } : {},
12174
12213
  ...fds.closesOnExec(fd) ? { cloexec: true } : {},
12175
12214
  ...kind === "stream" ? { desc: descId(open) } : {}
12176
12215
  };
@@ -12323,6 +12362,7 @@ function processOptions(opts, header3, hooks, created3) {
12323
12362
  kill: opts.kill,
12324
12363
  ...opts.writesBack ? { writesBack: opts.writesBack } : {},
12325
12364
  processes: opts.processes,
12365
+ ...opts.sudo ? { sudo: opts.sudo } : {},
12326
12366
  openFiles: opts.openFiles,
12327
12367
  ...opts.nodes ? { nodes: opts.nodes } : {},
12328
12368
  mounts: opts.mounts,
@@ -13081,12 +13121,12 @@ function kernelFsHandlers(fs) {
13081
13121
  const attr2 = attrOf(st);
13082
13122
  return [{ name: name2, kind: attr2.kind, attr: attr2 }];
13083
13123
  }),
13084
- async open(path, flags) {
13124
+ async open(path, flags2) {
13085
13125
  const st = await existing(path);
13086
- if (st && flags.exclusive) throw fsError2("EEXIST", path);
13087
- if (st?.isDirectory && flags.write) throw fsError2("EISDIR", path);
13088
- if (!st && !flags.create) throw fsError2("ENOENT", path);
13089
- if (!st || flags.truncate) await fs.writeFile(path, new Uint8Array(0));
13126
+ if (st && flags2.exclusive) throw fsError2("EEXIST", path);
13127
+ if (st?.isDirectory && flags2.write) throw fsError2("EISDIR", path);
13128
+ if (!st && !flags2.create) throw fsError2("ENOENT", path);
13129
+ if (!st || flags2.truncate) await fs.writeFile(path, new Uint8Array(0));
13090
13130
  handles.set(++nextFh, { path });
13091
13131
  return nextFh;
13092
13132
  },
@@ -13216,24 +13256,24 @@ function absolute(target) {
13216
13256
  return normalizePath(target);
13217
13257
  }
13218
13258
  function mountCall(call) {
13219
- let flags = call.flags >>> 0;
13220
- if ((flags & MS_MGC_MSK) >>> 0 === MS_MGC_VAL) flags &= ~MS_MGC_MSK;
13221
- if (flags & ~(MS_RDONLY | IGNORED_FLAGS)) {
13222
- throw errnoError("EINVAL", `unsupported mount flags ${flags.toString(16)}`);
13259
+ let flags2 = call.flags >>> 0;
13260
+ if ((flags2 & MS_MGC_MSK) >>> 0 === MS_MGC_VAL) flags2 &= ~MS_MGC_MSK;
13261
+ if (flags2 & ~(MS_RDONLY | IGNORED_FLAGS)) {
13262
+ throw errnoError("EINVAL", `unsupported mount flags ${flags2.toString(16)}`);
13223
13263
  }
13224
13264
  if (!call.type) throw errnoError("EINVAL", "no file system type");
13225
13265
  const target = absolute(call.target);
13226
13266
  refusedTarget(target);
13227
13267
  const options = parseMountOptions(call.data);
13228
- if (flags & MS_RDONLY) options.ro = "";
13268
+ if (flags2 & MS_RDONLY) options.ro = "";
13229
13269
  return { type: call.type, source: call.source || "none", target, options };
13230
13270
  }
13231
- function umountCall(target, flags) {
13271
+ function umountCall(target, flags2) {
13232
13272
  const known = MNT_FORCE | MNT_DETACH | UMOUNT_NOFOLLOW;
13233
- if (flags >>> 0 & ~known) throw errnoError("EINVAL", `unsupported umount flags ${flags}`);
13273
+ if (flags2 >>> 0 & ~known) throw errnoError("EINVAL", `unsupported umount flags ${flags2}`);
13234
13274
  const at = absolute(target);
13235
13275
  refusedTarget(at);
13236
- return { target: at, detach: (flags & (MNT_FORCE | MNT_DETACH)) !== 0 };
13276
+ return { target: at, detach: (flags2 & (MNT_FORCE | MNT_DETACH)) !== 0 };
13237
13277
  }
13238
13278
 
13239
13279
  // src/mount/fstab.ts
@@ -13291,22 +13331,22 @@ async function mountFstab(lines2, mount, signal, retries = FSTAB_RETRIES, discar
13291
13331
  );
13292
13332
  }
13293
13333
  async function mountLine({ line, spec, invalid }, mount, signal, retries, discard) {
13294
- let failed2 = invalid;
13334
+ let failed3 = invalid;
13295
13335
  for (let attempt = 0; !invalid && attempt <= retries.length && !signal.aborted; attempt++) {
13296
13336
  try {
13297
13337
  const entry = await mount(spec);
13298
13338
  if (!signal.aborted) return { line, target: spec.target, entry };
13299
13339
  discard(entry);
13300
- failed2 = Object.assign(new Error("the kernel stopped"), { code: "ECANCELED" });
13340
+ failed3 = Object.assign(new Error("the kernel stopped"), { code: "ECANCELED" });
13301
13341
  break;
13302
13342
  } catch (err) {
13303
- failed2 = err;
13343
+ failed3 = err;
13304
13344
  if (FINAL.has(codeOf3(err)) || attempt === retries.length) break;
13305
13345
  await wait(retries[attempt], signal);
13306
13346
  }
13307
13347
  }
13308
- const code = codeOf3(failed2);
13309
- return { line, target: spec.target, code, error: String(failed2?.message ?? code) };
13348
+ const code = codeOf3(failed3);
13349
+ return { line, target: spec.target, code, error: String(failed3?.message ?? code) };
13310
13350
  }
13311
13351
 
13312
13352
  // src/mount/hostfs.ts
@@ -13483,13 +13523,13 @@ function hostfsHandlers(current, againMs = RESTORE_AGAIN_MS) {
13483
13523
  }, againMs);
13484
13524
  timer.unref?.();
13485
13525
  });
13486
- async function writable2(client, path, flags) {
13487
- const { ifMatch: _, ...asked } = flags;
13526
+ async function writable2(client, path, flags2) {
13527
+ const { ifMatch: _, ...asked } = flags2;
13488
13528
  const opening2 = () => client.call("open", { path: rel(path), ...asked });
13489
13529
  try {
13490
13530
  return { opened: await opening2(), restore: void 0 };
13491
13531
  } catch (err) {
13492
- if (!flags.write || codeOf4(err) !== "EACCES") throw err;
13532
+ if (!flags2.write || codeOf4(err) !== "EACCES") throw err;
13493
13533
  const was = await client.call("stat", { path: rel(path) });
13494
13534
  const mode = was.mode ?? 0;
13495
13535
  if (mode & 128) throw err;
@@ -13511,11 +13551,11 @@ function hostfsHandlers(current, againMs = RESTORE_AGAIN_MS) {
13511
13551
  );
13512
13552
  return entries.map(({ name: name2, attr: attr2 }) => ({ name: name2, kind: attr2.kind, attr: attr2 }));
13513
13553
  },
13514
- async open(path, flags) {
13554
+ async open(path, flags2) {
13515
13555
  const client = current();
13516
13556
  const token = client.token;
13517
- const { opened: opened2, restore } = await writable2(client, path, flags);
13518
- if (flags.ifMatch !== void 0 && opened2.attr?.etag !== flags.ifMatch) {
13557
+ const { opened: opened2, restore } = await writable2(client, path, flags2);
13558
+ if (flags2.ifMatch !== void 0 && opened2.attr?.etag !== flags2.ifMatch) {
13519
13559
  await client.call("release", { fh: opened2.fh }, true);
13520
13560
  throw fsError2("ESTALE", `${path} changed on the host while it was being read`);
13521
13561
  }
@@ -13523,7 +13563,7 @@ function hostfsHandlers(current, againMs = RESTORE_AGAIN_MS) {
13523
13563
  client,
13524
13564
  token,
13525
13565
  fh: opened2.fh,
13526
- etag: flags.write ? void 0 : opened2.attr?.etag,
13566
+ etag: flags2.write ? void 0 : opened2.attr?.etag,
13527
13567
  restore
13528
13568
  });
13529
13569
  return nextFh;
@@ -14100,14 +14140,14 @@ function tmpfs() {
14100
14140
  if (!found) throw fsError2("EBADF", `handle ${fh}`);
14101
14141
  return found;
14102
14142
  };
14103
- function opened2(path, flags) {
14143
+ function opened2(path, flags2) {
14104
14144
  const { dir, name: name2 } = parent(path);
14105
14145
  const existing = dir.children.get(name2);
14106
- if (existing && flags.exclusive) throw fsError2("EEXIST", path);
14107
- if (existing?.kind === "directory" && flags.write) throw fsError2("EISDIR", path);
14146
+ if (existing && flags2.exclusive) throw fsError2("EEXIST", path);
14147
+ if (existing?.kind === "directory" && flags2.write) throw fsError2("EISDIR", path);
14108
14148
  if (existing) return existing;
14109
- if (!flags.create) throw fsError2("ENOENT", path);
14110
- const node = make("file", 33188, dir, flags.owner);
14149
+ if (!flags2.create) throw fsError2("ENOENT", path);
14150
+ const node = make("file", 33188, dir, flags2.owner);
14111
14151
  place(dir, name2, node);
14112
14152
  return node;
14113
14153
  }
@@ -14127,13 +14167,13 @@ function tmpfs() {
14127
14167
  ([name2, node]) => ({ name: name2, kind: node.kind, attr: attr(node) })
14128
14168
  );
14129
14169
  },
14130
- async open(path, flags) {
14131
- const node = opened2(path, flags);
14132
- if (flags.truncate && node.kind === "file") {
14170
+ async open(path, flags2) {
14171
+ const node = opened2(path, flags2);
14172
+ if (flags2.truncate && node.kind === "file") {
14133
14173
  node.data.truncate(0);
14134
14174
  node.mtime = Date.now();
14135
14175
  }
14136
- handles.set(++nextFh, { node, write: flags.write });
14176
+ handles.set(++nextFh, { node, write: flags2.write });
14137
14177
  return nextFh;
14138
14178
  },
14139
14179
  async read(fh, offset, size) {
@@ -14249,10 +14289,10 @@ function importedMemory(bytes) {
14249
14289
  return s;
14250
14290
  };
14251
14291
  const limits = () => {
14252
- const flags = bytes[at++];
14292
+ const flags2 = bytes[at++];
14253
14293
  const initial = u32();
14254
- const maximum = flags & 1 ? u32() : void 0;
14255
- return { initial, maximum, shared: (flags & 2) !== 0 };
14294
+ const maximum = flags2 & 1 ? u32() : void 0;
14295
+ return { initial, maximum, shared: (flags2 & 2) !== 0 };
14256
14296
  };
14257
14297
  while (at < bytes.length) {
14258
14298
  const id2 = bytes[at++];
@@ -14331,9 +14371,9 @@ var Reader = class {
14331
14371
  return code;
14332
14372
  }
14333
14373
  limits() {
14334
- const flags = this.byte();
14374
+ const flags2 = this.byte();
14335
14375
  this.u32();
14336
- if (flags & 1) this.u32();
14376
+ if (flags2 & 1) this.u32();
14337
14377
  }
14338
14378
  };
14339
14379
  function readTypes(r) {
@@ -14388,6 +14428,412 @@ function foreignImports(bytes) {
14388
14428
  return {};
14389
14429
  }
14390
14430
 
14431
+ // src/sudo.ts
14432
+ var SUDOERS = "/etc/sudoers";
14433
+ var SUDOERS_D = "/etc/sudoers.d";
14434
+ var SECURE_PATH = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";
14435
+ var DEFAULT_SUDOERS = "%sudo Cmnd *\n";
14436
+ var SUDO_TIMEOUT_MS = 3e5;
14437
+ var NAME3 = /^[a-z_][a-z0-9_.-]*$/;
14438
+ var RULE = /^(NOPASSWD\s+)?(\S+)\s+(?:\(([^()\s]+)\)\s+)?Cmnd(?:\s+(.*))?$/;
14439
+ var TERM = /^[A-Za-z0-9._+-]+$/;
14440
+ var PLAIN = /^[A-Za-z0-9_@%+=:,./-]+$/;
14441
+ var USAGE = "usage: sudo [-n] [-u user] [--] command [args...]\n sudo -l";
14442
+ function failed2(errno, error) {
14443
+ return { error, errno };
14444
+ }
14445
+ function unescaped(double) {
14446
+ return double.replace(/\\(["\\$`])/g, "$1");
14447
+ }
14448
+ function splitWords(text) {
14449
+ const piece = /'([^']*)'|"((?:[^"\\]|\\[\s\S])*)"|\\([\s\S])|([^\s'"\\]+)|(\s+)/y;
14450
+ const words2 = [];
14451
+ let word;
14452
+ while (piece.lastIndex < text.length) {
14453
+ const m = piece.exec(text);
14454
+ if (!m) return void 0;
14455
+ const [, single, double, escaped, plain, space] = m;
14456
+ if (space !== void 0) {
14457
+ if (word !== void 0) words2.push(word);
14458
+ word = void 0;
14459
+ continue;
14460
+ }
14461
+ word = (word ?? "") + (single ?? (double !== void 0 ? unescaped(double) : escaped ?? plain));
14462
+ }
14463
+ if (word !== void 0) words2.push(word);
14464
+ return words2;
14465
+ }
14466
+ function quoteWord(word) {
14467
+ return PLAIN.test(word) ? word : `'${word.replaceAll("'", `'\\''`)}'`;
14468
+ }
14469
+ function validWho(who) {
14470
+ if (who === "ALL") return true;
14471
+ return NAME3.test(who.startsWith("%") ? who.slice(1) : who);
14472
+ }
14473
+ function parseRule(line) {
14474
+ const text = line.trim();
14475
+ if (text === "" || text.startsWith("#")) return null;
14476
+ const m = RULE.exec(text);
14477
+ if (!m) return void 0;
14478
+ const [, nopasswd, who = "", target = "root", rest2 = ""] = m;
14479
+ if (!validWho(who) || target !== "ALL" && !NAME3.test(target)) return void 0;
14480
+ if (rest2.trim() === "*") return { nopasswd: !!nopasswd, who, target, cmnd: "*" };
14481
+ const cmnd = splitWords(rest2);
14482
+ if (!cmnd?.[0]?.startsWith("/")) return void 0;
14483
+ return { nopasswd: !!nopasswd, who, target, cmnd };
14484
+ }
14485
+ function flags(parsed2, arg, next2) {
14486
+ for (let j = 1; j < arg.length; j++) {
14487
+ const flag = arg[j];
14488
+ if (flag === "n") parsed2.nonInteractive = true;
14489
+ else if (flag === "l") parsed2.list = true;
14490
+ else if (flag !== "u") return `sudo: invalid option -- '${flag}'
14491
+ ${USAGE}`;
14492
+ else {
14493
+ const user = arg.slice(j + 1) || next2;
14494
+ if (!user) return `sudo: option requires an argument -- 'u'
14495
+ ${USAGE}`;
14496
+ parsed2.user = user;
14497
+ return arg.slice(j + 1) ? 0 : 1;
14498
+ }
14499
+ }
14500
+ return 0;
14501
+ }
14502
+ function parseArgs(args) {
14503
+ const parsed2 = { list: false, nonInteractive: false, user: "root", command: [] };
14504
+ let i = 0;
14505
+ for (; i < args.length; i++) {
14506
+ const arg = args[i];
14507
+ if (arg === "--") {
14508
+ i++;
14509
+ break;
14510
+ }
14511
+ if (!arg.startsWith("-") || arg === "-") break;
14512
+ const used = flags(parsed2, arg, args[i + 1]);
14513
+ if (typeof used === "string") return used;
14514
+ i += used;
14515
+ }
14516
+ parsed2.command = args.slice(i);
14517
+ if (parsed2.list ? parsed2.command.length > 0 : parsed2.command.length === 0) return USAGE;
14518
+ return parsed2;
14519
+ }
14520
+ function described(rule) {
14521
+ const cmnd = rule.cmnd === "*" ? "ALL" : rule.cmnd.map(quoteWord).join(" ");
14522
+ return ` (${rule.target}) ${rule.nopasswd ? "NOPASSWD: " : ""}${cmnd}`;
14523
+ }
14524
+ var Sudo = class {
14525
+ o;
14526
+ warned = /* @__PURE__ */ new Set();
14527
+ pending = /* @__PURE__ */ new Set();
14528
+ next = 0;
14529
+ constructor(options) {
14530
+ this.o = options;
14531
+ }
14532
+ warn(message3) {
14533
+ if (this.warned.has(message3)) return;
14534
+ this.warned.add(message3);
14535
+ (this.o.warn ?? console.warn)(message3);
14536
+ }
14537
+ async provision() {
14538
+ const { fs } = this.o;
14539
+ if (!await fs.exists(SUDOERS)) {
14540
+ await fs.writeFile(SUDOERS, DEFAULT_SUDOERS);
14541
+ await fs.chown?.(SUDOERS, { uid: 0, gid: 0, mode: 288, follow: false });
14542
+ }
14543
+ if (!await fs.exists(SUDOERS_D)) {
14544
+ await fs.mkdir(SUDOERS_D, { recursive: true });
14545
+ await fs.chown?.(SUDOERS_D, { uid: 0, gid: 0, mode: 488, follow: false });
14546
+ }
14547
+ }
14548
+ async trusted(path) {
14549
+ const parts2 = normalizePath(path).split("/").filter(Boolean);
14550
+ for (let n = 0; n <= parts2.length; n++) {
14551
+ const at = `/${parts2.slice(0, n).join("/")}`;
14552
+ const st = await this.o.fs.lstat(at).catch(() => void 0);
14553
+ if (!st || st.isSymbolicLink || st.uid !== 0 || (st.mode & 18) !== 0) return false;
14554
+ }
14555
+ return true;
14556
+ }
14557
+ async policyFile(path) {
14558
+ if (!await this.trusted(path)) {
14559
+ this.warn(`sudo: ${path} skipped: it must be root's, no symlink, and writable by root only`);
14560
+ return [];
14561
+ }
14562
+ const text = await this.o.fs.readFile(path).catch(() => "");
14563
+ const rules = [];
14564
+ for (const [n, line] of text.split("\n").entries()) {
14565
+ const rule = parseRule(line);
14566
+ if (rule) rules.push(rule);
14567
+ else if (rule === void 0) this.warn(`sudo: ${path}:${n + 1}: bad line skipped: ${line}`);
14568
+ }
14569
+ return rules;
14570
+ }
14571
+ async policy() {
14572
+ const rules = await this.policyFile(SUDOERS);
14573
+ const names = await this.o.fs.readdir(SUDOERS_D).catch(() => []);
14574
+ if (names.length > 0 && !await this.trusted(SUDOERS_D)) {
14575
+ this.warn(`sudo: ${SUDOERS_D} skipped: it must be root's and writable by root only`);
14576
+ return rules;
14577
+ }
14578
+ for (const name2 of [...names].sort()) {
14579
+ if (name2.startsWith(".") || name2.endsWith("~")) continue;
14580
+ rules.push(...await this.policyFile(`${SUDOERS_D}/${name2}`));
14581
+ }
14582
+ return rules;
14583
+ }
14584
+ async groupIds() {
14585
+ return new Map((await this.o.users.groups()).map((g) => [g.name, g.gid]));
14586
+ }
14587
+ async applies(caller, cred) {
14588
+ const gids = await this.groupIds();
14589
+ const mine = /* @__PURE__ */ new Set([cred.rgid, cred.egid, ...cred.groups]);
14590
+ return (rule) => {
14591
+ if (rule.who === "ALL" || rule.who === caller.name) return true;
14592
+ const gid = rule.who.startsWith("%") ? gids.get(rule.who.slice(1)) : void 0;
14593
+ return gid !== void 0 && mine.has(gid);
14594
+ };
14595
+ }
14596
+ async lookup(user) {
14597
+ const key = /^#\d+$/.test(user) ? Number(user.slice(1)) : user;
14598
+ return this.o.users.lookup(key);
14599
+ }
14600
+ async canonical(cwd) {
14601
+ const { fs } = this.o;
14602
+ const path = normalizePath(cwd);
14603
+ return fs.realpath ? fs.realpath(path, true).catch(() => void 0) : path;
14604
+ }
14605
+ async reachable(cred, cwd) {
14606
+ const walked = await access(this.o.fs, cred, cwd, X_OK, { directory: true });
14607
+ if (walked.denied !== void 0 || walked.missing) return false;
14608
+ return (await this.o.fs.stat(cwd).catch(() => void 0))?.isDirectory === true;
14609
+ }
14610
+ async untrusted(path, cwd) {
14611
+ const files = await this.o.backing?.(path, cwd) ?? [];
14612
+ for (const file of [path, ...files]) if (!await this.trusted(file)) return file;
14613
+ return void 0;
14614
+ }
14615
+ async locate(word, cwd) {
14616
+ const { fs } = this.o;
14617
+ const candidates = word.includes("/") ? [fs.resolvePath(cwd, word)] : SECURE_PATH.split(":").map((dir) => `${dir}/${word}`);
14618
+ for (const path of candidates) {
14619
+ const st = await fs.stat(path).catch(() => void 0);
14620
+ if (!st?.isFile || (st.mode & 73) === 0) continue;
14621
+ return fs.realpath ? fs.realpath(path, true).catch(() => path) : path;
14622
+ }
14623
+ return void 0;
14624
+ }
14625
+ async request(caller, call, signal) {
14626
+ const parsed2 = parseArgs(call.args);
14627
+ if (typeof parsed2 === "string") return failed2("EINVAL", parsed2);
14628
+ if (this.o.mode !== "enforce") {
14629
+ const mode = this.o.mode;
14630
+ if (parsed2.list) return { list: `sudo: users are '${mode}': commands run as the caller
14631
+ ` };
14632
+ return { run: parsed2.command };
14633
+ }
14634
+ const { cred } = caller;
14635
+ const self = await this.o.users.lookup(cred.ruid);
14636
+ if (!self) return failed2("EPERM", `sudo: uid ${cred.ruid} is not in the passwd database`);
14637
+ const target = await this.lookup(parsed2.user);
14638
+ if (!target) return failed2("ENOENT", `sudo: unknown user ${parsed2.user}`);
14639
+ const root = cred.euid === 0;
14640
+ const rules = root ? [] : await this.policy();
14641
+ const applies = await this.applies(self, cred);
14642
+ if (parsed2.list) return this.listed(self, root, rules.filter(applies));
14643
+ const [word = "", ...args] = parsed2.command;
14644
+ const cwd = await this.canonical(call.cwd);
14645
+ if (cwd === void 0 || !await this.reachable(cred, cwd)) {
14646
+ return failed2(
14647
+ "EACCES",
14648
+ `sudo: cannot run in ${call.cwd}: it isn't a directory you can enter`
14649
+ );
14650
+ }
14651
+ const path = await this.locate(word, cwd);
14652
+ if (!path) return failed2("ENOENT", `sudo: ${word}: command not found`);
14653
+ const untrusted = await this.untrusted(path, cwd);
14654
+ if (untrusted) {
14655
+ return failed2("EPERM", `sudo: ${untrusted} must be root's, and writable by root only`);
14656
+ }
14657
+ const argv = [path, ...args];
14658
+ const matching = rules.filter(
14659
+ (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]))
14660
+ );
14661
+ const grant = () => this.grant(self, target, cred, parsed2.command, path, cwd, call.term);
14662
+ if (root || matching.some((rule) => rule.nopasswd)) return grant();
14663
+ const quoted = argv.map(quoteWord).join(" ");
14664
+ if (matching.length === 0) {
14665
+ return failed2(
14666
+ "EPERM",
14667
+ `sudo: ${self.name} is not allowed to run ${quoted} as ${target.name}`
14668
+ );
14669
+ }
14670
+ if (parsed2.nonInteractive) return failed2("EPERM", "sudo: a password is required");
14671
+ if (this.pending.has(cred.ruid)) return failed2("EBUSY", "sudo: another request is pending");
14672
+ this.pending.add(cred.ruid);
14673
+ try {
14674
+ const answer = await this.ask(caller, self, target, parsed2.command, path, cwd, signal);
14675
+ if (typeof answer !== "string") return answer;
14676
+ if (await this.canonical(call.cwd) !== cwd || !await this.reachable(cred, cwd)) {
14677
+ return failed2("EACCES", `sudo: ${call.cwd} changed while the request waited`);
14678
+ }
14679
+ const changed = await this.untrusted(path, cwd);
14680
+ if (changed) {
14681
+ return failed2("EPERM", `sudo: ${changed} must be root's, and writable by root only`);
14682
+ }
14683
+ if (answer === "always" && !await this.remember(self, target, argv)) {
14684
+ return failed2("EIO", "sudo: the rule could not be recorded");
14685
+ }
14686
+ return grant();
14687
+ } finally {
14688
+ this.pending.delete(cred.ruid);
14689
+ }
14690
+ }
14691
+ listed(self, root, rules) {
14692
+ const host = this.o.hostname;
14693
+ if (root) {
14694
+ return {
14695
+ list: `User ${self.name} may run the following commands on ${host}:
14696
+ (ALL) NOPASSWD: ALL
14697
+ `
14698
+ };
14699
+ }
14700
+ if (rules.length === 0) {
14701
+ return failed2("EPERM", `User ${self.name} is not allowed to run sudo on ${host}.`);
14702
+ }
14703
+ const lines2 = rules.map(described).join("\n");
14704
+ return { list: `User ${self.name} may run the following commands on ${host}:
14705
+ ${lines2}
14706
+ ` };
14707
+ }
14708
+ async ask(caller, self, target, argv, path, cwd, requester) {
14709
+ const { approve } = this.o;
14710
+ if (!approve || requester.aborted) return failed2("EACCES", "sudo: approval denied");
14711
+ const createdAt = Date.now();
14712
+ const timeout = this.o.timeoutMs ?? SUDO_TIMEOUT_MS;
14713
+ const req = {
14714
+ id: `sudo-${++this.next}-${crypto.randomUUID()}`,
14715
+ kind: "sudo",
14716
+ user: self.name,
14717
+ uid: self.uid,
14718
+ target: target.name,
14719
+ argv: [...argv],
14720
+ path,
14721
+ cwd,
14722
+ pid: caller.pid,
14723
+ ...caller.tag !== void 0 ? { tag: caller.tag } : {},
14724
+ ...caller.tty ? { tty: caller.tty } : {},
14725
+ createdAt,
14726
+ expiresAt: createdAt + timeout,
14727
+ always: true
14728
+ };
14729
+ const control = new AbortController();
14730
+ const timer = setTimeout(() => control.abort("expired"), timeout);
14731
+ const withdraw = () => control.abort("withdrawn");
14732
+ requester.addEventListener("abort", withdraw, { once: true });
14733
+ const aborted2 = new Promise(
14734
+ (resolve) => control.signal.addEventListener("abort", () => resolve("aborted"), { once: true })
14735
+ );
14736
+ try {
14737
+ const answered2 = Promise.resolve().then(() => approve(req, { signal: control.signal })).catch(() => "deny");
14738
+ const answer = await Promise.race([answered2, aborted2]);
14739
+ if (answer === "aborted" || control.signal.aborted) {
14740
+ return control.signal.reason === "expired" ? failed2("ETIMEDOUT", "sudo: the request expired") : failed2("EINTR", "sudo: the request was withdrawn");
14741
+ }
14742
+ return answer === "once" || answer === "always" ? answer : failed2("EACCES", "sudo: approval denied");
14743
+ } finally {
14744
+ clearTimeout(timer);
14745
+ requester.removeEventListener("abort", withdraw);
14746
+ }
14747
+ }
14748
+ async remember(self, target, argv) {
14749
+ const { fs } = this.o;
14750
+ const path = `${SUDOERS_D}/${self.name}`;
14751
+ if (!NAME3.test(self.name)) return false;
14752
+ try {
14753
+ await this.provision();
14754
+ if (!await this.trusted(SUDOERS_D)) return false;
14755
+ const st = await fs.lstat(path).catch(() => void 0);
14756
+ if (st && !await this.trusted(path)) return false;
14757
+ const old = st ? await fs.readFile(path) : "";
14758
+ const line = `NOPASSWD ${self.name} (${target.name}) Cmnd ${argv.map(quoteWord).join(" ")}
14759
+ `;
14760
+ const text = old === "" || old.endsWith("\n") ? `${old}${line}` : `${old}
14761
+ ${line}`;
14762
+ await fs.writeFile(path, text);
14763
+ await fs.chown?.(path, { uid: 0, gid: 0, mode: 288, follow: false });
14764
+ return true;
14765
+ } catch {
14766
+ return false;
14767
+ }
14768
+ }
14769
+ grant(self, target, cred, argv, file, cwd, term) {
14770
+ const env = {
14771
+ ...this.o.env(),
14772
+ ...userEnv(target, this.o.pnpmHome),
14773
+ SHELL: target.shell,
14774
+ PATH: SECURE_PATH,
14775
+ PWD: cwd,
14776
+ ...term !== void 0 && TERM.test(term) ? { TERM: term } : {},
14777
+ SUDO_USER: self.name,
14778
+ SUDO_UID: String(cred.ruid),
14779
+ SUDO_GID: String(cred.rgid),
14780
+ SUDO_COMMAND: [file, ...argv.slice(1)].join(" ")
14781
+ };
14782
+ return { grant: { file, argv: [...argv], env, cwd, cred: credOf(target), caller: cred } };
14783
+ }
14784
+ };
14785
+
14786
+ // src/sudo-command.ts
14787
+ var SUDO_PATH = "/usr/bin/sudo";
14788
+ var SUDO_COMMAND = {
14789
+ name: "sudo",
14790
+ abi: "js",
14791
+ glue: SUDO_PATH,
14792
+ wasm: SUDO_PATH,
14793
+ argv0: "sudo"
14794
+ };
14795
+ var SUDO_SOURCE = [
14796
+ "export default async function sudo(ctx) {",
14797
+ " const say = (fd, text) => ctx.write(fd, text).catch(() => undefined);",
14798
+ " let pid;",
14799
+ " let held;",
14800
+ " const relay = (sig) => {",
14801
+ " if (pid === undefined) held = sig;",
14802
+ " else return ctx.kill(pid, sig).catch(() => undefined);",
14803
+ " };",
14804
+ ' for (const sig of ["SIGTERM", "SIGHUP"]) await ctx.signals.on(sig, relay);',
14805
+ " const failed = () => (held === undefined ? 1 : 128 + held);",
14806
+ " let answer;",
14807
+ " try {",
14808
+ " answer = await ctx.sudo(ctx.argv.slice(1));",
14809
+ " } catch (err) {",
14810
+ ' await say(2, "sudo: " + (err && err.code ? err.code : String(err)) + "\\n");',
14811
+ " return failed();",
14812
+ " }",
14813
+ " if (answer.error !== undefined) {",
14814
+ ' await say(2, answer.error + "\\n");',
14815
+ " return failed();",
14816
+ " }",
14817
+ " if (answer.list !== undefined) {",
14818
+ " await say(1, answer.list);",
14819
+ " return 0;",
14820
+ " }",
14821
+ ' for (const sig of ["SIGINT", "SIGQUIT"]) await ctx.signals.on(sig, () => undefined);',
14822
+ " if (answer.run !== undefined) {",
14823
+ " try {",
14824
+ " pid = (await ctx.spawn({ argv: answer.run })).pid;",
14825
+ " } catch (err) {",
14826
+ ' const why = err && err.code === "ENOENT" ? "command not found" : String(err && err.code);',
14827
+ ' await say(2, "sudo: " + answer.run[0] + ": " + why + "\\n");',
14828
+ " return failed();",
14829
+ " }",
14830
+ " } else pid = answer.pid;",
14831
+ " if (held !== undefined) await relay(held);",
14832
+ " return (await ctx.wait(pid)).status;",
14833
+ "}",
14834
+ ""
14835
+ ].join("\n");
14836
+
14391
14837
  // src/launcher.ts
14392
14838
  var COMMAND2 = /^\/(?:usr\/)?bin\/([^/]+)$/;
14393
14839
  var PACKAGE_ROOT = /^(.*\/node_modules\/(?:@[^/]+\/)?[^/]+)\//;
@@ -14415,9 +14861,21 @@ function usersMode(mode = "off") {
14415
14861
  if (USERS_MODES.includes(mode)) return mode;
14416
14862
  throw new Error(`users must be 'off', 'report' or 'enforce', not ${String(mode)}`);
14417
14863
  }
14864
+ function tagOption(tag) {
14865
+ if (tag !== void 0 && typeof tag !== "string") throw new TypeError("a tag must be a string");
14866
+ }
14418
14867
  function baseName(path) {
14419
14868
  return path.slice(path.lastIndexOf("/") + 1);
14420
14869
  }
14870
+ function packageRootOf(path, roots) {
14871
+ for (const root of roots) {
14872
+ const base = normalizePath(root);
14873
+ if (!path.startsWith(`${base}/`)) continue;
14874
+ const [first = "", second] = path.slice(base.length + 1).split("/");
14875
+ return first.startsWith("@") && second !== void 0 ? `${base}/${first}/${second}` : `${base}/${first}`;
14876
+ }
14877
+ return void 0;
14878
+ }
14421
14879
  function modulePath(glue) {
14422
14880
  return glue.endsWith(".js") ? `${glue.slice(0, -3)}.wasm` : `${glue}.wasm`;
14423
14881
  }
@@ -14429,11 +14887,11 @@ function shebang(head) {
14429
14887
  return new TextDecoder().decode(head.subarray(0, SHEBANG_MAX)).slice(2).split("\n")[0].trim().split(/[ \t]+/);
14430
14888
  }
14431
14889
  function interpreter(head) {
14432
- const words = shebang(head);
14433
- if (!words) return "node";
14434
- if (baseName(words[0]) === "env") words.shift();
14435
- if (words[0] === "-S") words.shift();
14436
- return baseName(words[0] ?? "");
14890
+ const words2 = shebang(head);
14891
+ if (!words2) return "node";
14892
+ if (baseName(words2[0]) === "env") words2.shift();
14893
+ if (words2[0] === "-S") words2.shift();
14894
+ return baseName(words2[0] ?? "");
14437
14895
  }
14438
14896
  function expandDefaults(defaults, env) {
14439
14897
  const out = {};
@@ -14507,7 +14965,10 @@ var Launcher = class {
14507
14965
  orphans = /* @__PURE__ */ new Set();
14508
14966
  described = /* @__PURE__ */ new Map();
14509
14967
  creds = /* @__PURE__ */ new Map();
14968
+ tags = /* @__PURE__ */ new Map();
14969
+ granted = /* @__PURE__ */ new Map();
14510
14970
  users;
14971
+ sudoer;
14511
14972
  jobs = new JobTable();
14512
14973
  locks = new LockTable();
14513
14974
  settling = new SettlingChildren((pid) => this.jobs.pgidOf(pid));
@@ -14608,6 +15069,17 @@ var Launcher = class {
14608
15069
  );
14609
15070
  this.fs = this.unlinked.wrap(keepingOpen(fs, this.nodes));
14610
15071
  this.users = new UserDb(this.fs, this.owners);
15072
+ this.sudoer = new Sudo({
15073
+ fs: this.fs,
15074
+ users: this.users,
15075
+ mode: this.usersMode,
15076
+ hostname,
15077
+ env: () => this.env,
15078
+ pnpmHome: this.pnpmHome,
15079
+ backing: (path, cwd) => this.backing(path, cwd),
15080
+ approve: options.approve,
15081
+ timeoutMs: options.sudoTimeoutMs
15082
+ });
14611
15083
  void this.unlinked.sweep("/");
14612
15084
  this.watchers.watch(
14613
15085
  [this.modulesDir, this.pnpmHome],
@@ -14628,7 +15100,7 @@ var Launcher = class {
14628
15100
  commands(scope = "") {
14629
15101
  let found = this.catalogs.get(scope);
14630
15102
  if (!found) {
14631
- found = this.roots(scope).then((roots) => scanCommands(this.base, roots));
15103
+ found = this.roots(scope).then((roots) => scanCommands(this.base, roots)).then((commands) => commands.set(SUDO_COMMAND.name, SUDO_COMMAND));
14632
15104
  this.catalogs.set(scope, found);
14633
15105
  }
14634
15106
  return found;
@@ -14751,10 +15223,10 @@ var Launcher = class {
14751
15223
  const head = await this.head(script);
14752
15224
  if (!head) return void 0;
14753
15225
  const binfmt = isWasm(head) ? void 0 : binfmtOf(await this.interpreters(scope), script);
14754
- const words = shebang(head) ?? (binfmt ? [binfmt] : void 0);
14755
- if (!words) return void 0;
14756
- if (baseName(words[0]) === "env") words.shift();
14757
- const [interp, ...rest2] = words;
15226
+ const words2 = shebang(head) ?? (binfmt ? [binfmt] : void 0);
15227
+ if (!words2) return void 0;
15228
+ if (baseName(words2[0]) === "env") words2.shift();
15229
+ const [interp, ...rest2] = words2;
14758
15230
  const arg = rest2.join(" ");
14759
15231
  const passed = [...arg ? [arg] : [], command ? script : file, ...argv.slice(1)];
14760
15232
  const found = interp ? await this.resolve(interp, interp, cwd, scope) : void 0;
@@ -14791,6 +15263,41 @@ var Launcher = class {
14791
15263
  await need(g, "exec", this.fs.resolvePath(req.cwd, file), want);
14792
15264
  }
14793
15265
  }
15266
+ async grantedCwd(caller, cwd) {
15267
+ const real = await physicalPathAsync(this.base, cwd).catch(() => void 0);
15268
+ return real === cwd && await this.sudoer.reachable(caller, cwd);
15269
+ }
15270
+ async binfmtManifests(scripts) {
15271
+ const sources = /* @__PURE__ */ new Map();
15272
+ const binfmts = await scanBinfmts(this.base, await this.roots(), void 0, sources);
15273
+ const manifests = [];
15274
+ for (const script of scripts) {
15275
+ const head = await this.head(script);
15276
+ if (!head || isWasm(head) || shebang(head)) continue;
15277
+ const pkg = sources.get(binfmtSuffix(binfmts, script) ?? "");
15278
+ if (pkg !== void 0) manifests.push(`${pkg}/package.json`);
15279
+ }
15280
+ return manifests;
15281
+ }
15282
+ async backing(file, cwd) {
15283
+ const planned = await this.plan(file, [file], cwd, "");
15284
+ if (!planned) return [];
15285
+ const { glue, wasm, imports } = planned.target;
15286
+ const files = [glue, wasm, ...imports ? [imports] : []];
15287
+ const scripts = [];
15288
+ for (const name2 of [file, ...planned.via ?? []]) {
15289
+ if (name2.includes("/") && !COMMAND2.test(name2)) scripts.push(this.fs.resolvePath(cwd, name2));
15290
+ const script = (await this.scriptCommand(name2, cwd, ""))?.script;
15291
+ if (script) scripts.push(script);
15292
+ }
15293
+ files.push(...scripts, ...await this.binfmtManifests(scripts));
15294
+ const roots = await this.roots();
15295
+ for (const at of [...files]) {
15296
+ const root = packageRootOf(at, roots) ?? PACKAGE_ROOT.exec(at)?.[1];
15297
+ if (root !== void 0) files.push(`${root}/package.json`);
15298
+ }
15299
+ return [...new Set(files)].filter((path) => path !== "" && path !== SUDO_PATH);
15300
+ }
14794
15301
  async unrunnable(file, cwd, scope) {
14795
15302
  const path = this.fs.resolvePath(cwd, file);
14796
15303
  const head = await this.base.readFileBuffer(path).catch(() => null);
@@ -14828,6 +15335,9 @@ var Launcher = class {
14828
15335
  return await this.base.exists(optional) ? optional : void 0;
14829
15336
  }
14830
15337
  async program(target, env) {
15338
+ if (target.abi === "js" && target.wasm === SUDO_PATH) {
15339
+ return { abi: "js", glue: SUDO_SOURCE, path: SUDO_PATH };
15340
+ }
14831
15341
  if (target.abi === "js") {
14832
15342
  return { abi: "js", glue: await this.base.readFile(target.wasm), path: target.wasm };
14833
15343
  }
@@ -14914,6 +15424,7 @@ var Launcher = class {
14914
15424
  kill: (target, sig) => this.kill(target, sig, this.credOf(pid), pid),
14915
15425
  writesBack: (target) => this.processes.get(target)?.writesBack() === true,
14916
15426
  processes: () => ({ boot: this.boot, processes: this.list(this.credOf(pid)) }),
15427
+ sudo: (call, signal) => this.sudo(pid, call, signal),
14917
15428
  openFiles: this.openFiles,
14918
15429
  nodes: this.nodes,
14919
15430
  held: this.held,
@@ -14921,7 +15432,7 @@ var Launcher = class {
14921
15432
  statfs: (path) => this.mounts.statfs(path),
14922
15433
  mounts: () => this.mounts.list(),
14923
15434
  mount: (call, signal) => this.processMount(pid, call, signal),
14924
- umount: (target, flags) => this.processUmount(pid, target, flags),
15435
+ umount: (target, flags2) => this.processUmount(pid, target, flags2),
14925
15436
  jobs: this.jobs,
14926
15437
  ptys: this.ptys,
14927
15438
  net: this.net,
@@ -14935,6 +15446,7 @@ var Launcher = class {
14935
15446
  this.processes.set(pid, handle);
14936
15447
  const { cred } = req;
14937
15448
  this.creds.set(pid, () => handle.cred?.() ?? copyCred(cred));
15449
+ if (req.tag !== void 0) this.tags.set(pid, req.tag);
14938
15450
  if (req.ppid !== void 0 && this.controlsJobs(req.ppid)) this.settling.started(pid, req.ppid);
14939
15451
  this.jobs.add(pid, req.ppid, (sig) => handle.signal(sig), terminal);
14940
15452
  if (req.exec && req.ppid !== void 0) this.jobs.exec(req.ppid, pid, true);
@@ -14962,6 +15474,8 @@ var Launcher = class {
14962
15474
  this.jobs.remove(pid);
14963
15475
  this.described.delete(pid);
14964
15476
  this.creds.delete(pid);
15477
+ this.tags.delete(pid);
15478
+ this.granted.delete(pid);
14965
15479
  }
14966
15480
  reaped(pid) {
14967
15481
  if (this.zombies.delete(pid)) this.forget(pid);
@@ -14983,8 +15497,8 @@ var Launcher = class {
14983
15497
  };
14984
15498
  const listed = [];
14985
15499
  for (const member of members.values()) {
14986
- const described = this.described.get(member.pid);
14987
- if (member.execed || !described) continue;
15500
+ const described2 = this.described.get(member.pid);
15501
+ if (member.execed || !described2) continue;
14988
15502
  const cred = this.credOf(member.pid);
14989
15503
  if (!maySee(viewer, cred)) continue;
14990
15504
  const root = rootOf(member);
@@ -14994,8 +15508,8 @@ var Launcher = class {
14994
15508
  ppid: shown(root.ppid),
14995
15509
  pgid: member.pgid,
14996
15510
  sid: member.sid,
14997
- ...described,
14998
- started: (this.described.get(root.pid) ?? described).started,
15511
+ ...described2,
15512
+ started: (this.described.get(root.pid) ?? described2).started,
14999
15513
  state: this.processes.has(member.pid) ? "S" : "Z",
15000
15514
  memory: this.processes.get(member.pid)?.memory() ?? 0,
15001
15515
  umask: this.umaskOf(member.pid),
@@ -15005,12 +15519,26 @@ var Launcher = class {
15005
15519
  }
15006
15520
  return listed;
15007
15521
  }
15522
+ sudo(pid, call, signal) {
15523
+ const caller = {
15524
+ cred: this.credOf(pid),
15525
+ pid: this.jobs.shown(pid),
15526
+ tag: this.tagOf(pid),
15527
+ tty: this.described.get(pid)?.tty
15528
+ };
15529
+ return this.sudoer.request(caller, call, signal);
15530
+ }
15008
15531
  spawner(ppid, report) {
15009
- return async (req, fds) => {
15010
- const scope = await this.scopeOf(this.credOf(ppid).euid);
15532
+ return async (req, fds, grant) => {
15533
+ if (grant && !await this.grantedCwd(grant.caller, req.cwd)) {
15534
+ await fds.closeAll();
15535
+ throw new SpawnError("EACCES");
15536
+ }
15537
+ const cred = grant ? grant.cred : this.credOf(ppid);
15538
+ const scope = grant ? "" : await this.scopeOf(cred.euid);
15011
15539
  const planned = await this.plan(req.file, req.argv, req.cwd, scope);
15012
15540
  try {
15013
- await this.mayExec(this.credOf(ppid), { ...req, pid: ppid }, planned);
15541
+ await this.mayExec(cred, { ...req, pid: ppid }, planned);
15014
15542
  } catch (err) {
15015
15543
  await fds.closeAll();
15016
15544
  throw err.code === "EACCES" ? new SpawnError("EACCES") : err;
@@ -15030,10 +15558,12 @@ var Launcher = class {
15030
15558
  ppid,
15031
15559
  ignored: this.ignoredBy(ppid),
15032
15560
  umask: this.umaskOf(ppid),
15033
- cred: this.credOf(ppid),
15561
+ cred,
15562
+ tag: this.tagOf(ppid),
15034
15563
  ...exec
15035
15564
  });
15036
15565
  deciding?.attach();
15566
+ if (grant) this.granted.set(handle.pid, ppid);
15037
15567
  return childHandle(handle);
15038
15568
  };
15039
15569
  }
@@ -15077,7 +15607,8 @@ var Launcher = class {
15077
15607
  fork: state,
15078
15608
  ignored: this.ignoredBy(ppid, true),
15079
15609
  umask: this.umaskOf(ppid),
15080
- cred: this.credOf(ppid)
15610
+ cred: this.credOf(ppid),
15611
+ tag: this.tagOf(ppid)
15081
15612
  })
15082
15613
  );
15083
15614
  }
@@ -15102,6 +15633,9 @@ var Launcher = class {
15102
15633
  credOf(pid) {
15103
15634
  return this.creds.get(pid)?.() ?? copyCred(ROOT);
15104
15635
  }
15636
+ tagOf(pid) {
15637
+ return this.tags.get(pid);
15638
+ }
15105
15639
  released(path) {
15106
15640
  const at = normalizePath(path);
15107
15641
  this.unlinked.closed(at);
@@ -15120,7 +15654,7 @@ var Launcher = class {
15120
15654
  }
15121
15655
  kill(pid, sig, sender, from) {
15122
15656
  const session = from === void 0 ? void 0 : this.jobs.sidOf(from);
15123
- 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);
15657
+ 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;
15124
15658
  if (pid < 0) return this.jobs.killGroup(-pid, sig, (member) => may(this.imageOf(member)));
15125
15659
  const handle = this.processes.get(pid);
15126
15660
  if (!handle) return false;
@@ -15241,7 +15775,8 @@ var Launcher = class {
15241
15775
  if (fresh) await this.base.chown?.(real, { uid: 0, gid: 0, mode, follow: false });
15242
15776
  }
15243
15777
  }
15244
- async realLayoutPath(path) {
15778
+ async realLayoutPath(given) {
15779
+ const path = normalizePath(given);
15245
15780
  if (!await this.base.exists(path)) return void 0;
15246
15781
  if (path === "/") return path;
15247
15782
  return joinName(await realDir(this.base, parentOf4(path)), baseName(path));
@@ -15252,7 +15787,8 @@ var Launcher = class {
15252
15787
  mountedBy(spec, cred) {
15253
15788
  if (!this.owners || cred.euid === 0 && cred.egid === 0) return spec;
15254
15789
  const ids = { uid: String(cred.euid), gid: String(cred.egid) };
15255
- return { ...spec, options: { ...ids, ...spec.options } };
15790
+ const options = cred.euid === 0 ? { ...ids, ...spec.options } : { ...spec.options, ...ids };
15791
+ return { ...spec, options };
15256
15792
  }
15257
15793
  mountRoot(path) {
15258
15794
  let root = "/";
@@ -15297,13 +15833,36 @@ var Launcher = class {
15297
15833
  async processMount(pid, call, signal) {
15298
15834
  const spec = mountCall(call);
15299
15835
  await this.permit({ op: "mount", pid, ...spec, options: { ...spec.options } });
15836
+ await this.mountsOwn(
15837
+ this.credOf(pid),
15838
+ spec.target,
15839
+ async () => (await this.fs.stat(spec.target)).uid
15840
+ );
15300
15841
  const entry = await this.mount(this.mountedBy(spec, this.credOf(pid)));
15301
15842
  if (signal.aborted) this.umount(entry.target, true);
15302
15843
  }
15303
- async processUmount(pid, target, flags) {
15304
- const call = umountCall(target, flags);
15844
+ async mountAs(cred, spec) {
15845
+ const target = normalizePath(spec.target);
15846
+ await this.mountsOwn(cred, target, async () => (await this.fs.stat(target)).uid);
15847
+ return this.mount(this.mountedBy(spec, cred));
15848
+ }
15849
+ async umountAs(cred, target) {
15850
+ const at = normalizePath(target);
15851
+ await this.mountsOwn(cred, at, async () => Number(this.mounts.mounted(at)?.options.uid));
15852
+ this.umount(at);
15853
+ }
15854
+ async mountsOwn(cred, target, owner) {
15855
+ const { euid } = cred;
15856
+ if (this.usersMode !== "enforce" || euid === 0) return;
15857
+ if (await owner().catch(() => void 0) !== euid) {
15858
+ throw fsError("EPERM", `${target} is not the caller's to mount or unmount`);
15859
+ }
15860
+ }
15861
+ async processUmount(pid, target, flags2) {
15862
+ const call = umountCall(target, flags2);
15863
+ let mounted;
15305
15864
  for (; ; ) {
15306
- const mounted = this.mounts.mounted(call.target);
15865
+ mounted = this.mounts.mounted(call.target);
15307
15866
  await this.permit({
15308
15867
  op: "umount",
15309
15868
  pid,
@@ -15312,6 +15871,7 @@ var Launcher = class {
15312
15871
  });
15313
15872
  if (this.mounts.mounted(call.target) === mounted) break;
15314
15873
  }
15874
+ await this.mountsOwn(this.credOf(pid), call.target, async () => Number(mounted?.options.uid));
15315
15875
  this.umount(call.target, call.detach);
15316
15876
  }
15317
15877
  async prepare() {
@@ -15320,6 +15880,7 @@ var Launcher = class {
15320
15880
  await this.provisionPnpmHome();
15321
15881
  await this.users.ensure();
15322
15882
  if (this.owners) await this.stampLayout();
15883
+ if (this.owners) await this.sudoer.provision();
15323
15884
  await writeCaFile(this.base, this.ca).catch(() => void 0);
15324
15885
  this.fstab = this.mountFstab();
15325
15886
  }
@@ -15357,8 +15918,9 @@ var Launcher = class {
15357
15918
  if (!found) throw fsError("ENOENT", `no user ${String(user)}`);
15358
15919
  return found;
15359
15920
  }
15360
- identity(account) {
15921
+ identity(account, tag) {
15361
15922
  return {
15923
+ tag,
15362
15924
  cred: credOf(account),
15363
15925
  ...account.umask !== void 0 ? { umask: account.umask } : {}
15364
15926
  };
@@ -15375,6 +15937,7 @@ var Launcher = class {
15375
15937
  return { cwd, file, planned };
15376
15938
  }
15377
15939
  async openTerminal(argv, options) {
15940
+ tagOption(options.tag);
15378
15941
  const { account, tty: ttyGroup } = await this.terminalAccount(options.user);
15379
15942
  const { cwd, file, planned } = await this.starting(argv, options.cwd, account);
15380
15943
  if (!planned) throw new Error(`${file}: command not found`);
@@ -15403,7 +15966,13 @@ var Launcher = class {
15403
15966
  );
15404
15967
  const report = (message3) => options.onData(encoder3.encode(`${message3}\r
15405
15968
  `));
15406
- const handle = await this.launch(planned, { env, cwd, fds, report, ...this.identity(account) });
15969
+ const handle = await this.launch(planned, {
15970
+ env,
15971
+ cwd,
15972
+ fds,
15973
+ report,
15974
+ ...this.identity(account, options.tag)
15975
+ });
15407
15976
  leader = handle.pid;
15408
15977
  void handle.exited.then(() => tty.hangup());
15409
15978
  return {
@@ -15419,6 +15988,7 @@ var Launcher = class {
15419
15988
  };
15420
15989
  }
15421
15990
  async run(argv, options = {}) {
15991
+ tagOption(options.tag);
15422
15992
  const account = await this.account(options.user);
15423
15993
  const { cwd, file, planned } = await this.starting(argv, options.cwd, account);
15424
15994
  const env = this.environment(cwd, options.env, account);
@@ -15448,7 +16018,7 @@ var Launcher = class {
15448
16018
  fds,
15449
16019
  report,
15450
16020
  ...group,
15451
- ...this.identity(account)
16021
+ ...this.identity(account, options.tag)
15452
16022
  });
15453
16023
  options.onStarted?.(handle.pid);
15454
16024
  const status = await handle.exited;
@@ -15509,8 +16079,37 @@ function remotes(port, req) {
15509
16079
  ...req.uplink ? { uplink: new RemoteUplink(port, req.uplink.traits, req.uplink.routes) } : {}
15510
16080
  };
15511
16081
  }
15512
- function answered(data, remote, uplink) {
15513
- if (data.net !== void 0) remote?.receive(data);
16082
+ function dial(l, req) {
16083
+ const socket = dialSocket(l.net, req.port, req.host);
16084
+ const { port1, port2 } = new MessageChannel();
16085
+ serveSocket(socket, port1);
16086
+ return port2;
16087
+ }
16088
+ function sudoApprovals(port) {
16089
+ const waiting = /* @__PURE__ */ new Map();
16090
+ return {
16091
+ ask: (req, { signal }) => new Promise((resolve) => {
16092
+ waiting.set(req.id, resolve);
16093
+ signal.addEventListener(
16094
+ "abort",
16095
+ () => {
16096
+ waiting.delete(req.id);
16097
+ port.postMessage({ sudoAbort: { id: req.id, reason: String(signal.reason) } });
16098
+ resolve("deny");
16099
+ },
16100
+ { once: true }
16101
+ );
16102
+ port.postMessage({ sudoAsk: req });
16103
+ }),
16104
+ answer({ id: id2, answer }) {
16105
+ waiting.get(id2)?.(answer);
16106
+ waiting.delete(id2);
16107
+ }
16108
+ };
16109
+ }
16110
+ function answered(data, remote, uplink, approvals) {
16111
+ if (data.sudoAnswer) approvals.answer(data.sudoAnswer);
16112
+ else if (data.net !== void 0) remote?.receive(data);
15514
16113
  else if (typeof data.uplink === "string") uplink?.receive(data);
15515
16114
  else return false;
15516
16115
  return true;
@@ -15555,6 +16154,7 @@ function serveKernel(port, deps) {
15555
16154
  const terminals = /* @__PURE__ */ new Map();
15556
16155
  const reply = (id2, body, transfer) => port.postMessage({ id: id2, ...body }, transfer);
15557
16156
  const clients = /* @__PURE__ */ new Set();
16157
+ const approvals = sudoApprovals(port);
15558
16158
  const locks = deps.locks === null ? void 0 : deps.locks ?? locksOf();
15559
16159
  let holding;
15560
16160
  function holdLock(locks2) {
@@ -15568,8 +16168,9 @@ function serveKernel(port, deps) {
15568
16168
  });
15569
16169
  return holding;
15570
16170
  }
15571
- async function connect(user) {
16171
+ async function connect(user, tag) {
15572
16172
  const l = await ready2();
16173
+ tagOption(tag);
15573
16174
  if (user !== void 0 && !await l.users.lookup(user)) {
15574
16175
  throw fsError("ENOENT", `no user ${String(user)}`);
15575
16176
  }
@@ -15579,6 +16180,7 @@ function serveKernel(port, deps) {
15579
16180
  launcher: ready2,
15580
16181
  signal: signalNumber,
15581
16182
  ...user !== void 0 ? { user } : {},
16183
+ ...tag !== void 0 ? { tag } : {},
15582
16184
  ...locks ? { locks } : {},
15583
16185
  ...held ? { lock: held } : {}
15584
16186
  });
@@ -15586,12 +16188,6 @@ function serveKernel(port, deps) {
15586
16188
  void served.closed.then(() => clients.delete(served));
15587
16189
  return port2;
15588
16190
  }
15589
- async function dial(req) {
15590
- const socket = dialSocket((await ready2()).net, req.port, req.host);
15591
- const { port1, port2 } = new MessageChannel();
15592
- serveSocket(socket, port1);
15593
- return port2;
15594
- }
15595
16191
  async function ready2() {
15596
16192
  if (!launcher) throw new Error("the kernel is not initialized");
15597
16193
  return launcher;
@@ -15623,6 +16219,7 @@ function serveKernel(port, deps) {
15623
16219
  ...req.modules ? { modules: req.modules } : {},
15624
16220
  ...req.env ? { env: req.env } : {},
15625
16221
  ...hostOptions(req, port),
16222
+ ...req.approve ? { approve: approvals.ask } : {},
15626
16223
  ...net,
15627
16224
  ...media ? { media } : {},
15628
16225
  onMountPending: (medium) => port.postMessage({ medium }),
@@ -15647,13 +16244,13 @@ function serveKernel(port, deps) {
15647
16244
  async function handle(req) {
15648
16245
  if (req.op === "init") return init(req);
15649
16246
  if (req.op === "open-terminal") return terminal(req);
15650
- if (req.op === "connect") return connect(req.user);
16247
+ if (req.op === "connect") return connect(req.user, req.tag);
15651
16248
  if (isUsersRequest(req)) return usersOp(await ready2(), req);
15652
16249
  if (req.op === "routes") {
15653
16250
  (await ready2()).setRoutes(req.routes);
15654
16251
  return true;
15655
16252
  }
15656
- if (req.op === "dial") return dial(req);
16253
+ if (req.op === "dial") return dial(await ready2(), req);
15657
16254
  if (req.op === "mount" || req.op === "umount" || req.op === "mounts" || req.op === "insert")
15658
16255
  return mountOp(req);
15659
16256
  if (req.op === "terminal") {
@@ -15701,7 +16298,7 @@ function serveKernel(port, deps) {
15701
16298
  policies.delete(answer.id);
15702
16299
  return;
15703
16300
  }
15704
- if (answered(event.data, remote, uplink)) return;
16301
+ if (answered(event.data, remote, uplink, approvals)) return;
15705
16302
  const req = event.data;
15706
16303
  handle(req).then(
15707
16304
  (result) => reply(req.id, { result }, result instanceof MessagePort ? [result] : void 0),