@ai-ecoverse/slicc-kernel 1.65.2 → 1.66.1

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.
@@ -1313,7 +1313,7 @@ function portCdpHook(ask, gone) {
1313
1313
  }
1314
1314
 
1315
1315
  // src/client/protocol.ts
1316
- var PROTOCOL = [1, 8];
1316
+ var PROTOCOL = [1, 9];
1317
1317
  var FS_METHODS = [
1318
1318
  "readFile",
1319
1319
  "writeFile",
@@ -1339,6 +1339,11 @@ function versionError(theirs) {
1339
1339
  return `slicc-kernel client protocol ${named} is not supported: this side speaks ${PROTOCOL.join(".")}`;
1340
1340
  }
1341
1341
 
1342
+ // src/bytes.ts
1343
+ function fresh(bytes, start2 = 0, end = bytes.length) {
1344
+ return new Uint8Array(bytes.subarray(start2, end));
1345
+ }
1346
+
1342
1347
  // src/fs/walk.ts
1343
1348
  var MAX_LINKS2 = 40;
1344
1349
  function parentOf4(path) {
@@ -1685,6 +1690,64 @@ async function openable(g, op, path, want, create) {
1685
1690
  await need(g, op, path, want, { directory: st?.isDirectory ?? false });
1686
1691
  }
1687
1692
 
1693
+ // src/kernel/cred.ts
1694
+ var ROOT = Object.freeze({
1695
+ ruid: 0,
1696
+ euid: 0,
1697
+ suid: 0,
1698
+ rgid: 0,
1699
+ egid: 0,
1700
+ sgid: 0,
1701
+ groups: [0]
1702
+ });
1703
+ var MAX_ID = 4294967294;
1704
+ var MAX_GROUPS = 65536;
1705
+ var UIDS = ["ruid", "euid", "suid"];
1706
+ var GIDS = ["rgid", "egid", "sgid"];
1707
+ function userCred(uid, gid, groups = [gid]) {
1708
+ return { ruid: uid, euid: uid, suid: uid, rgid: gid, egid: gid, sgid: gid, groups: [...groups] };
1709
+ }
1710
+ function copyCred(cred) {
1711
+ return { ...cred, groups: [...cred.groups] };
1712
+ }
1713
+ function id(value) {
1714
+ if (value === void 0 || value === -1 || value === 4294967295) return void 0;
1715
+ if (!Number.isInteger(value) || value < 0 || value > MAX_ID) {
1716
+ throw fsError("EINVAL", `not a user or group id: ${String(value)}`);
1717
+ }
1718
+ return value;
1719
+ }
1720
+ function changeCred(cur, change) {
1721
+ const privileged = cur.euid === 0;
1722
+ const next2 = copyCred(cur);
1723
+ for (const keys of [UIDS, GIDS]) {
1724
+ const own = keys.map((key) => cur[key]);
1725
+ for (const key of keys) {
1726
+ const value = id(change[key]);
1727
+ if (value === void 0) continue;
1728
+ if (!privileged && !own.includes(value)) throw fsError("EPERM", `${key} ${value}`);
1729
+ next2[key] = value;
1730
+ }
1731
+ }
1732
+ if (change.groups !== void 0) {
1733
+ if (!Array.isArray(change.groups) || change.groups.length > MAX_GROUPS) {
1734
+ throw fsError("EINVAL", "not a group list");
1735
+ }
1736
+ const groups = change.groups.map((g) => id(g) ?? Number.NaN);
1737
+ if (groups.some(Number.isNaN)) throw fsError("EINVAL", "not a group list");
1738
+ if (!privileged) throw fsError("EPERM", "setgroups");
1739
+ next2.groups = groups;
1740
+ }
1741
+ return next2;
1742
+ }
1743
+ function maySignal(sender, target) {
1744
+ if (sender.euid === 0) return true;
1745
+ return [sender.ruid, sender.euid].some((uid) => uid === target.ruid || uid === target.suid);
1746
+ }
1747
+ function maySee(viewer, target) {
1748
+ return viewer.euid === 0 || target.ruid === viewer.euid || target.euid === viewer.euid;
1749
+ }
1750
+
1688
1751
  // src/kernel/pipe.ts
1689
1752
  var PIPE_CAPACITY = 64 * 1024;
1690
1753
  var PipeError = class extends Error {
@@ -1992,9 +2055,9 @@ var FdTable = class _FdTable {
1992
2055
  closesOnExec(fd) {
1993
2056
  return this.cloexec.has(fd);
1994
2057
  }
1995
- setStatusFlags(fd, flags) {
2058
+ setStatusFlags(fd, flags2) {
1996
2059
  this.get(fd);
1997
- this.status.set(fd, flags);
2060
+ this.status.set(fd, flags2);
1998
2061
  }
1999
2062
  statusFlags(fd) {
2000
2063
  return this.status.get(fd);
@@ -2039,7 +2102,7 @@ var FdTable = class _FdTable {
2039
2102
  const child = new _FdTable();
2040
2103
  for (const [fd, file] of this.fds) child.fds.set(fd, file.retain());
2041
2104
  for (const fd of this.cloexec) child.cloexec.add(fd);
2042
- for (const [fd, flags] of this.status) child.status.set(fd, flags);
2105
+ for (const [fd, flags2] of this.status) child.status.set(fd, flags2);
2043
2106
  return child;
2044
2107
  }
2045
2108
  async closeAll() {
@@ -2506,9 +2569,9 @@ var KernelSocket = class _KernelSocket {
2506
2569
  case SO_TYPE:
2507
2570
  return SOCK_STREAM;
2508
2571
  case SO_ERROR: {
2509
- const failed2 = this.failed;
2572
+ const failed3 = this.failed;
2510
2573
  this.failed = void 0;
2511
- return failed2 ? wasiErrno(failed2) : 0;
2574
+ return failed3 ? wasiErrno(failed3) : 0;
2512
2575
  }
2513
2576
  case SO_ACCEPTCONN:
2514
2577
  return this.state === "listening" ? 1 : 0;
@@ -2944,7 +3007,7 @@ function serveTransport(peer, transport, { closeWaitMs = CLOSE_WAIT_MS } = {}) {
2944
3007
  }
2945
3008
  if (typeof data === "string") peer.postMessage({ net: "ws-message", nid, data });
2946
3009
  else {
2947
- const bytes = data.slice();
3010
+ const bytes = fresh(data);
2948
3011
  peer.postMessage({ net: "ws-message", nid, data: bytes }, [bytes.buffer]);
2949
3012
  }
2950
3013
  flow.inflight += payloadSize(data);
@@ -2982,7 +3045,7 @@ function serveTransport(peer, transport, { closeWaitMs = CLOSE_WAIT_MS } = {}) {
2982
3045
  open.delete(nid);
2983
3046
  peer.postMessage({ net: "end", nid });
2984
3047
  } else {
2985
- const bytes = next2.value.slice();
3048
+ const bytes = fresh(next2.value);
2986
3049
  peer.postMessage({ net: "chunk", nid, bytes }, [bytes.buffer]);
2987
3050
  }
2988
3051
  } catch (e) {
@@ -3015,64 +3078,6 @@ function serveTransport(peer, transport, { closeWaitMs = CLOSE_WAIT_MS } = {}) {
3015
3078
  };
3016
3079
  }
3017
3080
 
3018
- // src/kernel/cred.ts
3019
- var ROOT = Object.freeze({
3020
- ruid: 0,
3021
- euid: 0,
3022
- suid: 0,
3023
- rgid: 0,
3024
- egid: 0,
3025
- sgid: 0,
3026
- groups: [0]
3027
- });
3028
- var MAX_ID = 4294967294;
3029
- var MAX_GROUPS = 65536;
3030
- var UIDS = ["ruid", "euid", "suid"];
3031
- var GIDS = ["rgid", "egid", "sgid"];
3032
- function userCred(uid, gid, groups = [gid]) {
3033
- return { ruid: uid, euid: uid, suid: uid, rgid: gid, egid: gid, sgid: gid, groups: [...groups] };
3034
- }
3035
- function copyCred(cred) {
3036
- return { ...cred, groups: [...cred.groups] };
3037
- }
3038
- function id(value) {
3039
- if (value === void 0 || value === -1 || value === 4294967295) return void 0;
3040
- if (!Number.isInteger(value) || value < 0 || value > MAX_ID) {
3041
- throw fsError("EINVAL", `not a user or group id: ${String(value)}`);
3042
- }
3043
- return value;
3044
- }
3045
- function changeCred(cur, change) {
3046
- const privileged = cur.euid === 0;
3047
- const next2 = copyCred(cur);
3048
- for (const keys of [UIDS, GIDS]) {
3049
- const own = keys.map((key) => cur[key]);
3050
- for (const key of keys) {
3051
- const value = id(change[key]);
3052
- if (value === void 0) continue;
3053
- if (!privileged && !own.includes(value)) throw fsError("EPERM", `${key} ${value}`);
3054
- next2[key] = value;
3055
- }
3056
- }
3057
- if (change.groups !== void 0) {
3058
- if (!Array.isArray(change.groups) || change.groups.length > MAX_GROUPS) {
3059
- throw fsError("EINVAL", "not a group list");
3060
- }
3061
- const groups = change.groups.map((g) => id(g) ?? Number.NaN);
3062
- if (groups.some(Number.isNaN)) throw fsError("EINVAL", "not a group list");
3063
- if (!privileged) throw fsError("EPERM", "setgroups");
3064
- next2.groups = groups;
3065
- }
3066
- return next2;
3067
- }
3068
- function maySignal(sender, target) {
3069
- if (sender.euid === 0) return true;
3070
- return [sender.ruid, sender.euid].some((uid) => uid === target.ruid || uid === target.suid);
3071
- }
3072
- function maySee(viewer, target) {
3073
- return viewer.euid === 0 || target.ruid === viewer.euid || target.euid === viewer.euid;
3074
- }
3075
-
3076
3081
  // src/users.ts
3077
3082
  var PASSWD = "/etc/passwd";
3078
3083
  var GROUP = "/etc/group";
@@ -3334,7 +3339,7 @@ var UserDb = class {
3334
3339
  joined(name2, extra);
3335
3340
  group.push({ name: name2, gid: uid, members: [] });
3336
3341
  passwd.push(row);
3337
- const fresh = !await this.fs.exists(home);
3342
+ const fresh2 = !await this.fs.exists(home);
3338
3343
  await this.fs.mkdir(home, { recursive: true });
3339
3344
  try {
3340
3345
  await this.provision(row);
@@ -3342,7 +3347,7 @@ var UserDb = class {
3342
3347
  await this.fs.mkdir("/etc", { recursive: true });
3343
3348
  await this.commit(formatPasswd(passwd), formatGroup(group));
3344
3349
  } catch (err) {
3345
- if (fresh) await this.fs.rm(home, { recursive: true, force: true });
3350
+ if (fresh2) await this.fs.rm(home, { recursive: true, force: true });
3346
3351
  throw err;
3347
3352
  }
3348
3353
  remember();
@@ -3460,7 +3465,7 @@ async function fsCall(fs, method, args, owner, g) {
3460
3465
  }
3461
3466
  switch (method) {
3462
3467
  case "readFile": {
3463
- const bytes = (await fs.readFileBuffer(path(0))).slice();
3468
+ const bytes = fresh(await fs.readFileBuffer(path(0)));
3464
3469
  return { result: bytes, transfer: [bytes.buffer] };
3465
3470
  }
3466
3471
  case "writeFile":
@@ -3537,6 +3542,12 @@ async function runAs(l, self, requested) {
3537
3542
  }
3538
3543
  return target;
3539
3544
  }
3545
+ function clientCred(self) {
3546
+ return self ? credOf(self) : copyCred(ROOT);
3547
+ }
3548
+ async function runTagged(l, self, requested, tag) {
3549
+ return { user: await runAs(l, self, requested), tag };
3550
+ }
3540
3551
  function processes(l, self) {
3541
3552
  const viewer = credOf(self);
3542
3553
  return l.list(viewer).map(({ tid: _, cred = viewer, umask: _umask, ...entry }) => ({
@@ -3671,7 +3682,7 @@ function serveClient(port, host) {
3671
3682
  async function terminal(req, l) {
3672
3683
  const session = await l.openTerminal(req.argv, {
3673
3684
  ...req.options,
3674
- user: await runAs(l, self, req.options.user),
3685
+ ...await runTagged(l, self, req.options.user, host.tag),
3675
3686
  onData: (bytes) => reply(req.id, { fd: 1, bytes: bytes.slice() })
3676
3687
  });
3677
3688
  if (state === "closed") {
@@ -3693,7 +3704,7 @@ function serveClient(port, host) {
3693
3704
  let leader;
3694
3705
  const result = await l.run(req.argv, {
3695
3706
  ...req.options,
3696
- user: await runAs(l, self, req.options.user),
3707
+ ...await runTagged(l, self, req.options.user, host.tag),
3697
3708
  collect: false,
3698
3709
  onStdout: out(1),
3699
3710
  onStderr: out(2),
@@ -3734,9 +3745,9 @@ function serveClient(port, host) {
3734
3745
  case "unwatch":
3735
3746
  return watchOp(l, req, watches, send, guardFor(l, self));
3736
3747
  case "mount":
3737
- return { result: await l.mount(req.spec) };
3748
+ return { result: await l.mountAs(clientCred(self), req.spec) };
3738
3749
  case "umount":
3739
- l.umount(req.target);
3750
+ await l.umountAs(clientCred(self), req.target);
3740
3751
  return { result: true };
3741
3752
  case "mounts":
3742
3753
  return { result: l.mounts.list() };
@@ -4947,8 +4958,8 @@ function ipv6(host) {
4947
4958
  const head = groups(halves[0]);
4948
4959
  const back = halves.length === 2 ? groups(halves[1]) : [];
4949
4960
  if (![...head, ...back].every((g) => /^[0-9A-Fa-f]{1,4}$/.test(g))) return void 0;
4950
- const words = [...head, ...back].map((g) => Number.parseInt(g, 16));
4951
- const missing3 = 8 - words.length - tail.length;
4961
+ const words2 = [...head, ...back].map((g) => Number.parseInt(g, 16));
4962
+ const missing3 = 8 - words2.length - tail.length;
4952
4963
  if (halves.length === 2 ? missing3 < 1 : missing3 !== 0) return void 0;
4953
4964
  const all = [
4954
4965
  ...head.map((g) => Number.parseInt(g, 16)),
@@ -6352,9 +6363,8 @@ async function scanFilesystems(fs, modules) {
6352
6363
  }
6353
6364
  var SUFFIX = /^\.[A-Za-z0-9_+-][A-Za-z0-9._+-]*$/;
6354
6365
  async function scanBinfmts(fs, modules, warn = () => {
6355
- }) {
6366
+ }, owners = /* @__PURE__ */ new Map()) {
6356
6367
  const found = /* @__PURE__ */ new Map();
6357
- const owners = /* @__PURE__ */ new Map();
6358
6368
  for (const pkg of await packagesIn(fs, modules)) {
6359
6369
  let manifest;
6360
6370
  try {
@@ -6379,7 +6389,7 @@ async function scanBinfmts(fs, modules, warn = () => {
6379
6389
  }
6380
6390
  return found;
6381
6391
  }
6382
- function binfmtOf(binfmts, path) {
6392
+ function binfmtSuffix(binfmts, path) {
6383
6393
  const name2 = path.slice(path.lastIndexOf("/") + 1);
6384
6394
  let best;
6385
6395
  for (const suffix of binfmts.keys()) {
@@ -6387,6 +6397,10 @@ function binfmtOf(binfmts, path) {
6387
6397
  if (best === void 0 || suffix.length > best.length) best = suffix;
6388
6398
  }
6389
6399
  }
6400
+ return best;
6401
+ }
6402
+ function binfmtOf(binfmts, path) {
6403
+ const best = binfmtSuffix(binfmts, path);
6390
6404
  return best === void 0 ? void 0 : binfmts.get(best);
6391
6405
  }
6392
6406
 
@@ -6963,8 +6977,9 @@ var ChildTable = class {
6963
6977
  if (!this.forker) throw new SpawnError("ENOSYS");
6964
6978
  return this.track(state, this.parentFds.fork(), /* @__PURE__ */ new Map(), this.forker);
6965
6979
  }
6966
- async spawn(req, stdio, inherit2 = []) {
6967
- if (!this.spawner) throw new SpawnError("ENOSYS");
6980
+ async spawn(req, stdio, inherit2 = [], grant) {
6981
+ const { spawner } = this;
6982
+ if (!spawner) throw new SpawnError("ENOSYS");
6968
6983
  const fds = new FdTable();
6969
6984
  const captured = /* @__PURE__ */ new Map();
6970
6985
  try {
@@ -6982,7 +6997,7 @@ var ChildTable = class {
6982
6997
  await fds.closeAll();
6983
6998
  throw e;
6984
6999
  }
6985
- return this.track(req, fds, captured, this.spawner);
7000
+ return this.track(req, fds, captured, (r, f) => spawner(r, f, grant));
6986
7001
  }
6987
7002
  async track(req, fds, captured, start2) {
6988
7003
  let handle;
@@ -7029,34 +7044,34 @@ var ChildTable = class {
7029
7044
  }
7030
7045
  return deviceFile("null");
7031
7046
  }
7032
- async wait(pid, nohang, signal, flags = {}) {
7047
+ async wait(pid, nohang, signal, flags2 = {}) {
7033
7048
  let interrupt;
7034
7049
  try {
7035
7050
  for (; ; ) {
7036
- const candidates = this.candidates(pid, flags);
7051
+ const candidates = this.candidates(pid, flags2);
7037
7052
  if (candidates.length === 0) throw new KernelError("ECHILD");
7038
7053
  const done4 = candidates.find(([, child]) => child.code !== void 0);
7039
7054
  if (done4) return this.reap(done4[0], done4[1].code);
7040
- const changed = this.stateReport(candidates, flags);
7055
+ const changed = this.stateReport(candidates, flags2);
7041
7056
  if (changed) return changed;
7042
7057
  if (nohang) return [0, 0];
7043
7058
  interrupt ??= interrupted(signal);
7044
- await this.nextChange(candidates, flags, interrupt.promise);
7059
+ await this.nextChange(candidates, flags2, interrupt.promise);
7045
7060
  }
7046
7061
  } finally {
7047
7062
  interrupt?.done();
7048
7063
  }
7049
7064
  }
7050
- candidates(pid, flags) {
7065
+ candidates(pid, flags2) {
7051
7066
  const all = [...this.children];
7052
7067
  if (pid > 0) return all.filter(([p]) => p === pid);
7053
- const inGroup2 = flags.inGroup;
7068
+ const inGroup2 = flags2.inGroup;
7054
7069
  return pid === -1 || !inGroup2 ? all : all.filter(([p]) => inGroup2(p));
7055
7070
  }
7056
- async nextChange(candidates, flags, interrupt) {
7071
+ async nextChange(candidates, flags2, interrupt) {
7057
7072
  let wake;
7058
7073
  const stateChange = new Promise((resolve) => wake = resolve);
7059
- const watching = flags.untraced || flags.continued;
7074
+ const watching = flags2.untraced || flags2.continued;
7060
7075
  if (watching && wake) this.stateChanged.push(wake);
7061
7076
  try {
7062
7077
  await Promise.race([
@@ -7068,14 +7083,14 @@ var ChildTable = class {
7068
7083
  this.stateChanged = this.stateChanged.filter((w) => w !== wake);
7069
7084
  }
7070
7085
  }
7071
- stateReport(candidates, flags) {
7086
+ stateReport(candidates, flags2) {
7072
7087
  for (const [p, child] of candidates) {
7073
- if (flags.untraced && child.stopReport !== void 0) {
7088
+ if (flags2.untraced && child.stopReport !== void 0) {
7074
7089
  const sig = child.stopReport;
7075
7090
  child.stopReport = void 0;
7076
7091
  return [p, stoppedStatus(sig)];
7077
7092
  }
7078
- if (flags.continued && child.continueReport) {
7093
+ if (flags2.continued && child.continueReport) {
7079
7094
  child.continueReport = false;
7080
7095
  return [p, CONTINUED_STATUS];
7081
7096
  }
@@ -7284,9 +7299,9 @@ var HeldPaths = class extends Map {
7284
7299
  return this.keptHandles.get(handle);
7285
7300
  }
7286
7301
  keep(path, file) {
7287
- const fresh = this.handlesAt(path).filter((id2) => !this.keptHandles.has(id2));
7288
- for (const id2 of fresh) this.keptHandles.set(id2, file);
7289
- this.uses.set(file, Math.max(fresh.length, 1));
7302
+ const fresh2 = this.handlesAt(path).filter((id2) => !this.keptHandles.has(id2));
7303
+ for (const id2 of fresh2) this.keptHandles.set(id2, file);
7304
+ this.uses.set(file, Math.max(fresh2.length, 1));
7290
7305
  this.unlinked.set(path, file);
7291
7306
  }
7292
7307
  unkeep(path, file) {
@@ -7842,7 +7857,7 @@ var MountFs = class {
7842
7857
  const chunk2 = mount.caps.maxIo ?? DEFAULT_IO;
7843
7858
  try {
7844
7859
  for (let offset = 0; offset < bytes.length; offset += chunk2) {
7845
- const part = bytes.slice(offset, offset + chunk2);
7860
+ const part = fresh(bytes, offset, offset + chunk2);
7846
7861
  await mount.conn.call({ op: "write", fh, offset, bytes: part }, [part.buffer]);
7847
7862
  }
7848
7863
  } finally {
@@ -7939,7 +7954,7 @@ var MountFs = class {
7939
7954
  async writeRuns(mount, fh, offset, bytes, transfer) {
7940
7955
  const chunk2 = mount.caps.maxIo ?? DEFAULT_IO;
7941
7956
  for (let at = 0; at < bytes.length; at += chunk2) {
7942
- const part = handed(bytes, transfer, chunk2) ? bytes : bytes.slice(at, at + chunk2);
7957
+ const part = handed(bytes, transfer, chunk2) ? bytes : fresh(bytes, at, at + chunk2);
7943
7958
  await mount.conn.call({ op: "write", fh, offset: offset + at, bytes: part }, [part.buffer]);
7944
7959
  }
7945
7960
  }
@@ -8171,8 +8186,8 @@ var MountFs = class {
8171
8186
  }
8172
8187
  this.writable(mount, real);
8173
8188
  const { uid, gid, mode } = change;
8174
- const fresh = change.kind === "new" && { uid, gid, mode };
8175
- const set2 = fresh ? await this.driverOwner(found, real, fresh, false) : change;
8189
+ const fresh2 = change.kind === "new" && { uid, gid, mode };
8190
+ const set2 = fresh2 ? await this.driverOwner(found, real, fresh2, false) : change;
8176
8191
  this.table.forget(mount, real);
8177
8192
  const attrs = Object.entries({ uid: set2.uid, gid: set2.gid, mode: set2.mode });
8178
8193
  await mount.conn.call({
@@ -8775,9 +8790,9 @@ var Pages = class {
8775
8790
  const page = (start2 + at) / PAGE;
8776
8791
  if (this.pages.has(page)) continue;
8777
8792
  const part = bytes.subarray(at, Math.min(at + PAGE, bytes.length));
8778
- const fresh = this.take(part.length);
8779
- fresh.set(part);
8780
- this.pages.set(page, fresh);
8793
+ const fresh2 = this.take(part.length);
8794
+ fresh2.set(part);
8795
+ this.pages.set(page, fresh2);
8781
8796
  }
8782
8797
  }
8783
8798
  read(at, length2) {
@@ -11036,8 +11051,8 @@ function errCode(err) {
11036
11051
  async function targetDir(fs, path) {
11037
11052
  return parentOf4(await realTarget(fs, path).catch(() => path));
11038
11053
  }
11039
- async function refuseReadonly(fs, path, flags, opts = {}) {
11040
- const writes = (flags & O_ACCMODE) !== 0 || opts.truncate === true;
11054
+ async function refuseReadonly(fs, path, flags2, opts = {}) {
11055
+ const writes = (flags2 & O_ACCMODE) !== 0 || opts.truncate === true;
11041
11056
  if (!writes && !opts.create || !fs.stat) return;
11042
11057
  const stat = (p) => fs.stat?.(p).catch((err) => {
11043
11058
  const code = err?.code;
@@ -11252,6 +11267,7 @@ var SYSCALL_OPS = /* @__PURE__ */ new Set([
11252
11267
  "proc-spawn",
11253
11268
  "proc-wait",
11254
11269
  "proc-captured",
11270
+ "proc-sudo",
11255
11271
  "proc-run",
11256
11272
  "proc-exit",
11257
11273
  "proc-fork",
@@ -11284,6 +11300,12 @@ var MAX_READ = 1024 * 1024;
11284
11300
  var WRITE_ACCESS = 3;
11285
11301
  var KEYBOARD = /* @__PURE__ */ new Set([SIG.INT, SIG.QUIT, SIG.TSTP]);
11286
11302
  var DEFAULT_UMASK = 18;
11303
+ function words(value) {
11304
+ if (!Array.isArray(value) || !value.every((w) => typeof w === "string")) {
11305
+ throw new KernelError("EINVAL");
11306
+ }
11307
+ return [...value];
11308
+ }
11287
11309
  var DYING = { ok: false, errno: "EINTR", message: "the process is being killed" };
11288
11310
  var HOSTS_ONLY = new Resolver({ routes: new Routes() });
11289
11311
  var WasmProcess = class {
@@ -11476,17 +11498,17 @@ var WasmProcess = class {
11476
11498
  if (restart && this.options.hasPending?.()) throw new KernelError("EINTR");
11477
11499
  }
11478
11500
  async procWait(req, cancelled) {
11479
- const flags = {
11501
+ const flags2 = {
11480
11502
  untraced: req.untraced,
11481
11503
  continued: req.continued,
11482
11504
  inGroup: this.waitGroup(req.pid)
11483
11505
  };
11484
11506
  if (this.owed && !req.nohang) {
11485
- const now = await this.children.wait(req.pid, true, this.interrupt.signal, flags);
11507
+ const now = await this.children.wait(req.pid, true, this.interrupt.signal, flags2);
11486
11508
  if (now[0] !== 0) return now;
11487
11509
  }
11488
11510
  const signal = req.nohang ? this.interrupt.signal : this.blockingSignal(cancelled);
11489
- const waited = await this.children.wait(req.pid, req.nohang, signal, flags);
11511
+ const waited = await this.children.wait(req.pid, req.nohang, signal, flags2);
11490
11512
  return waited;
11491
11513
  }
11492
11514
  blockingSignal(cancelled, owing = true) {
@@ -11548,10 +11570,10 @@ var WasmProcess = class {
11548
11570
  this.fds.installAt(fd, this.openVfsFile(open));
11549
11571
  } else if (req.create) {
11550
11572
  await nodes.onEntry(path, async () => {
11551
- const fresh = this.options.owners && !await present2(this.options.fs, path, true);
11573
+ const fresh2 = this.options.owners && !await present2(this.options.fs, path, true);
11552
11574
  this.fds.installAt(fd, this.openVfsFile(open, await nodes.identify(path)));
11553
11575
  await this.fds.get(fd).file.stat?.();
11554
- if (fresh) await this.options.fs.chown?.(path, { ...this.newOwner(), kind: "new" });
11576
+ if (fresh2) await this.options.fs.chown?.(path, { ...this.newOwner(), kind: "new" });
11555
11577
  });
11556
11578
  } else {
11557
11579
  this.fds.installAt(fd, this.openVfsFile(open, await nodes.identify(path)));
@@ -11818,13 +11840,13 @@ var WasmProcess = class {
11818
11840
  }
11819
11841
  fdInfo(fd) {
11820
11842
  const file = this.fds.get(fd).file;
11821
- const flags = this.fds.statusFlags(fd);
11843
+ const flags2 = this.fds.statusFlags(fd);
11822
11844
  const terminal = (file.tty ?? file.pty?.slave)?.name;
11823
11845
  return {
11824
11846
  tty: file.tty !== void 0,
11825
11847
  kind: file instanceof KernelSocket ? "socket" : kernelFdKind(file),
11826
11848
  ...file.heldMeta ? { meta: file.heldMeta } : {},
11827
- ...flags !== void 0 ? { flags } : {},
11849
+ ...flags2 !== void 0 ? { flags: flags2 } : {},
11828
11850
  ...this.fds.closesOnExec(fd) ? { cloexec: true } : {},
11829
11851
  ...file.read && !file.write ? { access: "read" } : {},
11830
11852
  ...file.write && !file.read ? { access: "write" } : {},
@@ -12012,7 +12034,27 @@ var WasmProcess = class {
12012
12034
  return { ok: true, kind: "void" };
12013
12035
  case "proc-captured":
12014
12036
  return { ok: true, kind: "bytes", bytes: this.children.captured(req.pid, req.slot) };
12015
- }
12037
+ case "proc-sudo":
12038
+ return { ok: true, kind: "json", json: await this.sudo(req, cancelled) };
12039
+ }
12040
+ }
12041
+ async sudo(req, cancelled) {
12042
+ const { sudo } = this.options;
12043
+ if (!sudo) throw new KernelError("ENOSYS");
12044
+ const call = {
12045
+ op: "proc-sudo",
12046
+ args: words(req.args),
12047
+ cwd: typeof req.cwd === "string" && req.cwd.startsWith("/") ? req.cwd : "/",
12048
+ ...typeof req.term === "string" ? { term: req.term } : {}
12049
+ };
12050
+ const answer = await sudo(call, this.blockingSignal(cancelled));
12051
+ if (!("grant" in answer)) return answer;
12052
+ if (this.dying) throw new KernelError("EINTR");
12053
+ const { file, argv, env, cwd, cred, caller } = answer.grant;
12054
+ const stdio = [{ fd: 0 }, { fd: 1 }, { fd: 2 }];
12055
+ return {
12056
+ pid: await this.children.spawn({ file, argv, env, cwd }, stdio, [], { cred, caller })
12057
+ };
12016
12058
  }
12017
12059
  async mountSyscall(req, cancelled) {
12018
12060
  const { mount, umount } = this.options;
@@ -12020,9 +12062,9 @@ var WasmProcess = class {
12020
12062
  try {
12021
12063
  if (req.op === "umount") await umount(req.target, req.flags);
12022
12064
  else {
12023
- const { source, target, type, flags, data } = req;
12065
+ const { source, target, type, flags: flags2, data } = req;
12024
12066
  const signal = this.blockingSignal(cancelled);
12025
- await Promise.race([mount({ source, target, type, flags, data }, signal), aborted(signal)]);
12067
+ await Promise.race([mount({ source, target, type, flags: flags2, data }, signal), aborted(signal)]);
12026
12068
  }
12027
12069
  } catch (err) {
12028
12070
  const code = err?.code;
@@ -12165,14 +12207,14 @@ function descId(file) {
12165
12207
  function inheritedFds(fds) {
12166
12208
  return fds.numbers().filter((fd) => fd > 2 && !fds.get(fd).file.held).map((fd) => {
12167
12209
  const open = fds.get(fd);
12168
- const flags = fds.statusFlags(fd);
12210
+ const flags2 = fds.statusFlags(fd);
12169
12211
  const kind = open.file instanceof KernelSocket ? "socket" : kernelFdKind(open.file);
12170
12212
  const meta = open.file.heldMeta;
12171
12213
  return {
12172
12214
  fd,
12173
12215
  kind,
12174
12216
  ...kind === "device" && meta && "device" in meta ? { device: meta } : {},
12175
- ...flags !== void 0 ? { flags } : {},
12217
+ ...flags2 !== void 0 ? { flags: flags2 } : {},
12176
12218
  ...fds.closesOnExec(fd) ? { cloexec: true } : {},
12177
12219
  ...kind === "stream" ? { desc: descId(open) } : {}
12178
12220
  };
@@ -12325,6 +12367,7 @@ function processOptions(opts, header3, hooks, created3) {
12325
12367
  kill: opts.kill,
12326
12368
  ...opts.writesBack ? { writesBack: opts.writesBack } : {},
12327
12369
  processes: opts.processes,
12370
+ ...opts.sudo ? { sudo: opts.sudo } : {},
12328
12371
  openFiles: opts.openFiles,
12329
12372
  ...opts.nodes ? { nodes: opts.nodes } : {},
12330
12373
  mounts: opts.mounts,
@@ -12974,7 +13017,7 @@ function serveFilesystem(port, handlers, capabilities = {}, options = {}) {
12974
13017
  (result) => {
12975
13018
  if (result instanceof Uint8Array) {
12976
13019
  const whole = result.byteOffset === 0 && result.byteLength === result.buffer.byteLength;
12977
- const bytes = options.owned && whole ? result : result.slice();
13020
+ const bytes = options.owned && whole ? result : fresh(result);
12978
13021
  reply({ id: data.id, result: bytes }, [bytes.buffer]);
12979
13022
  } else reply({ id: data.id, result: result ?? null });
12980
13023
  },
@@ -13083,12 +13126,12 @@ function kernelFsHandlers(fs) {
13083
13126
  const attr2 = attrOf(st);
13084
13127
  return [{ name: name2, kind: attr2.kind, attr: attr2 }];
13085
13128
  }),
13086
- async open(path, flags) {
13129
+ async open(path, flags2) {
13087
13130
  const st = await existing(path);
13088
- if (st && flags.exclusive) throw fsError2("EEXIST", path);
13089
- if (st?.isDirectory && flags.write) throw fsError2("EISDIR", path);
13090
- if (!st && !flags.create) throw fsError2("ENOENT", path);
13091
- if (!st || flags.truncate) await fs.writeFile(path, new Uint8Array(0));
13131
+ if (st && flags2.exclusive) throw fsError2("EEXIST", path);
13132
+ if (st?.isDirectory && flags2.write) throw fsError2("EISDIR", path);
13133
+ if (!st && !flags2.create) throw fsError2("ENOENT", path);
13134
+ if (!st || flags2.truncate) await fs.writeFile(path, new Uint8Array(0));
13092
13135
  handles.set(++nextFh, { path });
13093
13136
  return nextFh;
13094
13137
  },
@@ -13218,24 +13261,24 @@ function absolute(target) {
13218
13261
  return normalizePath(target);
13219
13262
  }
13220
13263
  function mountCall(call) {
13221
- let flags = call.flags >>> 0;
13222
- if ((flags & MS_MGC_MSK) >>> 0 === MS_MGC_VAL) flags &= ~MS_MGC_MSK;
13223
- if (flags & ~(MS_RDONLY | IGNORED_FLAGS)) {
13224
- throw errnoError("EINVAL", `unsupported mount flags ${flags.toString(16)}`);
13264
+ let flags2 = call.flags >>> 0;
13265
+ if ((flags2 & MS_MGC_MSK) >>> 0 === MS_MGC_VAL) flags2 &= ~MS_MGC_MSK;
13266
+ if (flags2 & ~(MS_RDONLY | IGNORED_FLAGS)) {
13267
+ throw errnoError("EINVAL", `unsupported mount flags ${flags2.toString(16)}`);
13225
13268
  }
13226
13269
  if (!call.type) throw errnoError("EINVAL", "no file system type");
13227
13270
  const target = absolute(call.target);
13228
13271
  refusedTarget(target);
13229
13272
  const options = parseMountOptions(call.data);
13230
- if (flags & MS_RDONLY) options.ro = "";
13273
+ if (flags2 & MS_RDONLY) options.ro = "";
13231
13274
  return { type: call.type, source: call.source || "none", target, options };
13232
13275
  }
13233
- function umountCall(target, flags) {
13276
+ function umountCall(target, flags2) {
13234
13277
  const known = MNT_FORCE | MNT_DETACH | UMOUNT_NOFOLLOW;
13235
- if (flags >>> 0 & ~known) throw errnoError("EINVAL", `unsupported umount flags ${flags}`);
13278
+ if (flags2 >>> 0 & ~known) throw errnoError("EINVAL", `unsupported umount flags ${flags2}`);
13236
13279
  const at = absolute(target);
13237
13280
  refusedTarget(at);
13238
- return { target: at, detach: (flags & (MNT_FORCE | MNT_DETACH)) !== 0 };
13281
+ return { target: at, detach: (flags2 & (MNT_FORCE | MNT_DETACH)) !== 0 };
13239
13282
  }
13240
13283
 
13241
13284
  // src/mount/fstab.ts
@@ -13293,22 +13336,22 @@ async function mountFstab(lines2, mount, signal, retries = FSTAB_RETRIES, discar
13293
13336
  );
13294
13337
  }
13295
13338
  async function mountLine({ line, spec, invalid }, mount, signal, retries, discard) {
13296
- let failed2 = invalid;
13339
+ let failed3 = invalid;
13297
13340
  for (let attempt = 0; !invalid && attempt <= retries.length && !signal.aborted; attempt++) {
13298
13341
  try {
13299
13342
  const entry = await mount(spec);
13300
13343
  if (!signal.aborted) return { line, target: spec.target, entry };
13301
13344
  discard(entry);
13302
- failed2 = Object.assign(new Error("the kernel stopped"), { code: "ECANCELED" });
13345
+ failed3 = Object.assign(new Error("the kernel stopped"), { code: "ECANCELED" });
13303
13346
  break;
13304
13347
  } catch (err) {
13305
- failed2 = err;
13348
+ failed3 = err;
13306
13349
  if (FINAL.has(codeOf3(err)) || attempt === retries.length) break;
13307
13350
  await wait(retries[attempt], signal);
13308
13351
  }
13309
13352
  }
13310
- const code = codeOf3(failed2);
13311
- return { line, target: spec.target, code, error: String(failed2?.message ?? code) };
13353
+ const code = codeOf3(failed3);
13354
+ return { line, target: spec.target, code, error: String(failed3?.message ?? code) };
13312
13355
  }
13313
13356
 
13314
13357
  // src/mount/hostfs.ts
@@ -13462,14 +13505,14 @@ var RESTORE_AGAIN_MS = 3e4;
13462
13505
  function hostfsHandlers(current, againMs = RESTORE_AGAIN_MS) {
13463
13506
  const handles = /* @__PURE__ */ new Map();
13464
13507
  let nextFh = 0;
13465
- const fresh = (remote) => {
13508
+ const fresh2 = (remote) => {
13466
13509
  if (remote.client.token === remote.token && !remote.client.gone) return remote;
13467
13510
  throw fsError2("ESTALE", "the folder was granted again, so this open file is gone");
13468
13511
  };
13469
13512
  const live = (fh) => {
13470
13513
  const remote = handles.get(fh);
13471
13514
  if (!remote) throw fsError2("EBADF", `handle ${fh}`);
13472
- return fresh(remote);
13515
+ return fresh2(remote);
13473
13516
  };
13474
13517
  const call = (op, body = {}) => current().call(op, body);
13475
13518
  const same = (was, mode, seen) => seen?.mode === (mode | 128) && (was.ino !== void 0 && seen.ino !== void 0 ? seen.ino === was.ino : seen.size === was.size && seen.mtime === was.mtime);
@@ -13485,13 +13528,13 @@ function hostfsHandlers(current, againMs = RESTORE_AGAIN_MS) {
13485
13528
  }, againMs);
13486
13529
  timer.unref?.();
13487
13530
  });
13488
- async function writable2(client, path, flags) {
13489
- const { ifMatch: _, ...asked } = flags;
13531
+ async function writable2(client, path, flags2) {
13532
+ const { ifMatch: _, ...asked } = flags2;
13490
13533
  const opening2 = () => client.call("open", { path: rel(path), ...asked });
13491
13534
  try {
13492
13535
  return { opened: await opening2(), restore: void 0 };
13493
13536
  } catch (err) {
13494
- if (!flags.write || codeOf4(err) !== "EACCES") throw err;
13537
+ if (!flags2.write || codeOf4(err) !== "EACCES") throw err;
13495
13538
  const was = await client.call("stat", { path: rel(path) });
13496
13539
  const mode = was.mode ?? 0;
13497
13540
  if (mode & 128) throw err;
@@ -13513,11 +13556,11 @@ function hostfsHandlers(current, againMs = RESTORE_AGAIN_MS) {
13513
13556
  );
13514
13557
  return entries.map(({ name: name2, attr: attr2 }) => ({ name: name2, kind: attr2.kind, attr: attr2 }));
13515
13558
  },
13516
- async open(path, flags) {
13559
+ async open(path, flags2) {
13517
13560
  const client = current();
13518
13561
  const token = client.token;
13519
- const { opened: opened2, restore } = await writable2(client, path, flags);
13520
- if (flags.ifMatch !== void 0 && opened2.attr?.etag !== flags.ifMatch) {
13562
+ const { opened: opened2, restore } = await writable2(client, path, flags2);
13563
+ if (flags2.ifMatch !== void 0 && opened2.attr?.etag !== flags2.ifMatch) {
13521
13564
  await client.call("release", { fh: opened2.fh }, true);
13522
13565
  throw fsError2("ESTALE", `${path} changed on the host while it was being read`);
13523
13566
  }
@@ -13525,7 +13568,7 @@ function hostfsHandlers(current, againMs = RESTORE_AGAIN_MS) {
13525
13568
  client,
13526
13569
  token,
13527
13570
  fh: opened2.fh,
13528
- etag: flags.write ? void 0 : opened2.attr?.etag,
13571
+ etag: flags2.write ? void 0 : opened2.attr?.etag,
13529
13572
  restore
13530
13573
  });
13531
13574
  return nextFh;
@@ -13551,7 +13594,7 @@ function hostfsHandlers(current, againMs = RESTORE_AGAIN_MS) {
13551
13594
  if (!remote) throw fsError2("EBADF", `handle ${fh}`);
13552
13595
  handles.delete(fh);
13553
13596
  try {
13554
- await fresh(remote).client.call("release", { fh: remote.fh }, true);
13597
+ await fresh2(remote).client.call("release", { fh: remote.fh }, true);
13555
13598
  } finally {
13556
13599
  if (remote.restore) await settle3(current(), remote.restore);
13557
13600
  }
@@ -14102,14 +14145,14 @@ function tmpfs() {
14102
14145
  if (!found) throw fsError2("EBADF", `handle ${fh}`);
14103
14146
  return found;
14104
14147
  };
14105
- function opened2(path, flags) {
14148
+ function opened2(path, flags2) {
14106
14149
  const { dir, name: name2 } = parent(path);
14107
14150
  const existing = dir.children.get(name2);
14108
- if (existing && flags.exclusive) throw fsError2("EEXIST", path);
14109
- if (existing?.kind === "directory" && flags.write) throw fsError2("EISDIR", path);
14151
+ if (existing && flags2.exclusive) throw fsError2("EEXIST", path);
14152
+ if (existing?.kind === "directory" && flags2.write) throw fsError2("EISDIR", path);
14110
14153
  if (existing) return existing;
14111
- if (!flags.create) throw fsError2("ENOENT", path);
14112
- const node = make("file", 33188, dir, flags.owner);
14154
+ if (!flags2.create) throw fsError2("ENOENT", path);
14155
+ const node = make("file", 33188, dir, flags2.owner);
14113
14156
  place(dir, name2, node);
14114
14157
  return node;
14115
14158
  }
@@ -14129,13 +14172,13 @@ function tmpfs() {
14129
14172
  ([name2, node]) => ({ name: name2, kind: node.kind, attr: attr(node) })
14130
14173
  );
14131
14174
  },
14132
- async open(path, flags) {
14133
- const node = opened2(path, flags);
14134
- if (flags.truncate && node.kind === "file") {
14175
+ async open(path, flags2) {
14176
+ const node = opened2(path, flags2);
14177
+ if (flags2.truncate && node.kind === "file") {
14135
14178
  node.data.truncate(0);
14136
14179
  node.mtime = Date.now();
14137
14180
  }
14138
- handles.set(++nextFh, { node, write: flags.write });
14181
+ handles.set(++nextFh, { node, write: flags2.write });
14139
14182
  return nextFh;
14140
14183
  },
14141
14184
  async read(fh, offset, size) {
@@ -14251,10 +14294,10 @@ function importedMemory(bytes) {
14251
14294
  return s;
14252
14295
  };
14253
14296
  const limits = () => {
14254
- const flags = bytes[at++];
14297
+ const flags2 = bytes[at++];
14255
14298
  const initial = u32();
14256
- const maximum = flags & 1 ? u32() : void 0;
14257
- return { initial, maximum, shared: (flags & 2) !== 0 };
14299
+ const maximum = flags2 & 1 ? u32() : void 0;
14300
+ return { initial, maximum, shared: (flags2 & 2) !== 0 };
14258
14301
  };
14259
14302
  while (at < bytes.length) {
14260
14303
  const id2 = bytes[at++];
@@ -14333,9 +14376,9 @@ var Reader = class {
14333
14376
  return code;
14334
14377
  }
14335
14378
  limits() {
14336
- const flags = this.byte();
14379
+ const flags2 = this.byte();
14337
14380
  this.u32();
14338
- if (flags & 1) this.u32();
14381
+ if (flags2 & 1) this.u32();
14339
14382
  }
14340
14383
  };
14341
14384
  function readTypes(r) {
@@ -14390,6 +14433,412 @@ function foreignImports(bytes) {
14390
14433
  return {};
14391
14434
  }
14392
14435
 
14436
+ // src/sudo.ts
14437
+ var SUDOERS = "/etc/sudoers";
14438
+ var SUDOERS_D = "/etc/sudoers.d";
14439
+ var SECURE_PATH = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";
14440
+ var DEFAULT_SUDOERS = "%sudo Cmnd *\n";
14441
+ var SUDO_TIMEOUT_MS = 3e5;
14442
+ var NAME3 = /^[a-z_][a-z0-9_.-]*$/;
14443
+ var RULE = /^(NOPASSWD\s+)?(\S+)\s+(?:\(([^()\s]+)\)\s+)?Cmnd(?:\s+(.*))?$/;
14444
+ var TERM = /^[A-Za-z0-9._+-]+$/;
14445
+ var PLAIN = /^[A-Za-z0-9_@%+=:,./-]+$/;
14446
+ var USAGE = "usage: sudo [-n] [-u user] [--] command [args...]\n sudo -l";
14447
+ function failed2(errno, error) {
14448
+ return { error, errno };
14449
+ }
14450
+ function unescaped(double) {
14451
+ return double.replace(/\\(["\\$`])/g, "$1");
14452
+ }
14453
+ function splitWords(text) {
14454
+ const piece = /'([^']*)'|"((?:[^"\\]|\\[\s\S])*)"|\\([\s\S])|([^\s'"\\]+)|(\s+)/y;
14455
+ const words2 = [];
14456
+ let word;
14457
+ while (piece.lastIndex < text.length) {
14458
+ const m = piece.exec(text);
14459
+ if (!m) return void 0;
14460
+ const [, single, double, escaped, plain, space] = m;
14461
+ if (space !== void 0) {
14462
+ if (word !== void 0) words2.push(word);
14463
+ word = void 0;
14464
+ continue;
14465
+ }
14466
+ word = (word ?? "") + (single ?? (double !== void 0 ? unescaped(double) : escaped ?? plain));
14467
+ }
14468
+ if (word !== void 0) words2.push(word);
14469
+ return words2;
14470
+ }
14471
+ function quoteWord(word) {
14472
+ return PLAIN.test(word) ? word : `'${word.replaceAll("'", `'\\''`)}'`;
14473
+ }
14474
+ function validWho(who) {
14475
+ if (who === "ALL") return true;
14476
+ return NAME3.test(who.startsWith("%") ? who.slice(1) : who);
14477
+ }
14478
+ function parseRule(line) {
14479
+ const text = line.trim();
14480
+ if (text === "" || text.startsWith("#")) return null;
14481
+ const m = RULE.exec(text);
14482
+ if (!m) return void 0;
14483
+ const [, nopasswd, who = "", target = "root", rest2 = ""] = m;
14484
+ if (!validWho(who) || target !== "ALL" && !NAME3.test(target)) return void 0;
14485
+ if (rest2.trim() === "*") return { nopasswd: !!nopasswd, who, target, cmnd: "*" };
14486
+ const cmnd = splitWords(rest2);
14487
+ if (!cmnd?.[0]?.startsWith("/")) return void 0;
14488
+ return { nopasswd: !!nopasswd, who, target, cmnd };
14489
+ }
14490
+ function flags(parsed2, arg, next2) {
14491
+ for (let j = 1; j < arg.length; j++) {
14492
+ const flag = arg[j];
14493
+ if (flag === "n") parsed2.nonInteractive = true;
14494
+ else if (flag === "l") parsed2.list = true;
14495
+ else if (flag !== "u") return `sudo: invalid option -- '${flag}'
14496
+ ${USAGE}`;
14497
+ else {
14498
+ const user = arg.slice(j + 1) || next2;
14499
+ if (!user) return `sudo: option requires an argument -- 'u'
14500
+ ${USAGE}`;
14501
+ parsed2.user = user;
14502
+ return arg.slice(j + 1) ? 0 : 1;
14503
+ }
14504
+ }
14505
+ return 0;
14506
+ }
14507
+ function parseArgs(args) {
14508
+ const parsed2 = { list: false, nonInteractive: false, user: "root", command: [] };
14509
+ let i = 0;
14510
+ for (; i < args.length; i++) {
14511
+ const arg = args[i];
14512
+ if (arg === "--") {
14513
+ i++;
14514
+ break;
14515
+ }
14516
+ if (!arg.startsWith("-") || arg === "-") break;
14517
+ const used = flags(parsed2, arg, args[i + 1]);
14518
+ if (typeof used === "string") return used;
14519
+ i += used;
14520
+ }
14521
+ parsed2.command = args.slice(i);
14522
+ if (parsed2.list ? parsed2.command.length > 0 : parsed2.command.length === 0) return USAGE;
14523
+ return parsed2;
14524
+ }
14525
+ function described(rule) {
14526
+ const cmnd = rule.cmnd === "*" ? "ALL" : rule.cmnd.map(quoteWord).join(" ");
14527
+ return ` (${rule.target}) ${rule.nopasswd ? "NOPASSWD: " : ""}${cmnd}`;
14528
+ }
14529
+ var Sudo = class {
14530
+ o;
14531
+ warned = /* @__PURE__ */ new Set();
14532
+ pending = /* @__PURE__ */ new Set();
14533
+ next = 0;
14534
+ constructor(options) {
14535
+ this.o = options;
14536
+ }
14537
+ warn(message3) {
14538
+ if (this.warned.has(message3)) return;
14539
+ this.warned.add(message3);
14540
+ (this.o.warn ?? console.warn)(message3);
14541
+ }
14542
+ async provision() {
14543
+ const { fs } = this.o;
14544
+ if (!await fs.exists(SUDOERS)) {
14545
+ await fs.writeFile(SUDOERS, DEFAULT_SUDOERS);
14546
+ await fs.chown?.(SUDOERS, { uid: 0, gid: 0, mode: 288, follow: false });
14547
+ }
14548
+ if (!await fs.exists(SUDOERS_D)) {
14549
+ await fs.mkdir(SUDOERS_D, { recursive: true });
14550
+ await fs.chown?.(SUDOERS_D, { uid: 0, gid: 0, mode: 488, follow: false });
14551
+ }
14552
+ }
14553
+ async trusted(path) {
14554
+ const parts2 = normalizePath(path).split("/").filter(Boolean);
14555
+ for (let n = 0; n <= parts2.length; n++) {
14556
+ const at = `/${parts2.slice(0, n).join("/")}`;
14557
+ const st = await this.o.fs.lstat(at).catch(() => void 0);
14558
+ if (!st || st.isSymbolicLink || st.uid !== 0 || (st.mode & 18) !== 0) return false;
14559
+ }
14560
+ return true;
14561
+ }
14562
+ async policyFile(path) {
14563
+ if (!await this.trusted(path)) {
14564
+ this.warn(`sudo: ${path} skipped: it must be root's, no symlink, and writable by root only`);
14565
+ return [];
14566
+ }
14567
+ const text = await this.o.fs.readFile(path).catch(() => "");
14568
+ const rules = [];
14569
+ for (const [n, line] of text.split("\n").entries()) {
14570
+ const rule = parseRule(line);
14571
+ if (rule) rules.push(rule);
14572
+ else if (rule === void 0) this.warn(`sudo: ${path}:${n + 1}: bad line skipped: ${line}`);
14573
+ }
14574
+ return rules;
14575
+ }
14576
+ async policy() {
14577
+ const rules = await this.policyFile(SUDOERS);
14578
+ const names = await this.o.fs.readdir(SUDOERS_D).catch(() => []);
14579
+ if (names.length > 0 && !await this.trusted(SUDOERS_D)) {
14580
+ this.warn(`sudo: ${SUDOERS_D} skipped: it must be root's and writable by root only`);
14581
+ return rules;
14582
+ }
14583
+ for (const name2 of [...names].sort()) {
14584
+ if (name2.startsWith(".") || name2.endsWith("~")) continue;
14585
+ rules.push(...await this.policyFile(`${SUDOERS_D}/${name2}`));
14586
+ }
14587
+ return rules;
14588
+ }
14589
+ async groupIds() {
14590
+ return new Map((await this.o.users.groups()).map((g) => [g.name, g.gid]));
14591
+ }
14592
+ async applies(caller, cred) {
14593
+ const gids = await this.groupIds();
14594
+ const mine = /* @__PURE__ */ new Set([cred.rgid, cred.egid, ...cred.groups]);
14595
+ return (rule) => {
14596
+ if (rule.who === "ALL" || rule.who === caller.name) return true;
14597
+ const gid = rule.who.startsWith("%") ? gids.get(rule.who.slice(1)) : void 0;
14598
+ return gid !== void 0 && mine.has(gid);
14599
+ };
14600
+ }
14601
+ async lookup(user) {
14602
+ const key = /^#\d+$/.test(user) ? Number(user.slice(1)) : user;
14603
+ return this.o.users.lookup(key);
14604
+ }
14605
+ async canonical(cwd) {
14606
+ const { fs } = this.o;
14607
+ const path = normalizePath(cwd);
14608
+ return fs.realpath ? fs.realpath(path, true).catch(() => void 0) : path;
14609
+ }
14610
+ async reachable(cred, cwd) {
14611
+ const walked = await access(this.o.fs, cred, cwd, X_OK, { directory: true });
14612
+ if (walked.denied !== void 0 || walked.missing) return false;
14613
+ return (await this.o.fs.stat(cwd).catch(() => void 0))?.isDirectory === true;
14614
+ }
14615
+ async untrusted(path, cwd) {
14616
+ const files = await this.o.backing?.(path, cwd) ?? [];
14617
+ for (const file of [path, ...files]) if (!await this.trusted(file)) return file;
14618
+ return void 0;
14619
+ }
14620
+ async locate(word, cwd) {
14621
+ const { fs } = this.o;
14622
+ const candidates = word.includes("/") ? [fs.resolvePath(cwd, word)] : SECURE_PATH.split(":").map((dir) => `${dir}/${word}`);
14623
+ for (const path of candidates) {
14624
+ const st = await fs.stat(path).catch(() => void 0);
14625
+ if (!st?.isFile || (st.mode & 73) === 0) continue;
14626
+ return fs.realpath ? fs.realpath(path, true).catch(() => path) : path;
14627
+ }
14628
+ return void 0;
14629
+ }
14630
+ async request(caller, call, signal) {
14631
+ const parsed2 = parseArgs(call.args);
14632
+ if (typeof parsed2 === "string") return failed2("EINVAL", parsed2);
14633
+ if (this.o.mode !== "enforce") {
14634
+ const mode = this.o.mode;
14635
+ if (parsed2.list) return { list: `sudo: users are '${mode}': commands run as the caller
14636
+ ` };
14637
+ return { run: parsed2.command };
14638
+ }
14639
+ const { cred } = caller;
14640
+ const self = await this.o.users.lookup(cred.ruid);
14641
+ if (!self) return failed2("EPERM", `sudo: uid ${cred.ruid} is not in the passwd database`);
14642
+ const target = await this.lookup(parsed2.user);
14643
+ if (!target) return failed2("ENOENT", `sudo: unknown user ${parsed2.user}`);
14644
+ const root = cred.euid === 0;
14645
+ const rules = root ? [] : await this.policy();
14646
+ const applies = await this.applies(self, cred);
14647
+ if (parsed2.list) return this.listed(self, root, rules.filter(applies));
14648
+ const [word = "", ...args] = parsed2.command;
14649
+ const cwd = await this.canonical(call.cwd);
14650
+ if (cwd === void 0 || !await this.reachable(cred, cwd)) {
14651
+ return failed2(
14652
+ "EACCES",
14653
+ `sudo: cannot run in ${call.cwd}: it isn't a directory you can enter`
14654
+ );
14655
+ }
14656
+ const path = await this.locate(word, cwd);
14657
+ if (!path) return failed2("ENOENT", `sudo: ${word}: command not found`);
14658
+ const untrusted = await this.untrusted(path, cwd);
14659
+ if (untrusted) {
14660
+ return failed2("EPERM", `sudo: ${untrusted} must be root's, and writable by root only`);
14661
+ }
14662
+ const argv = [path, ...args];
14663
+ const matching = rules.filter(
14664
+ (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]))
14665
+ );
14666
+ const grant = () => this.grant(self, target, cred, parsed2.command, path, cwd, call.term);
14667
+ if (root || matching.some((rule) => rule.nopasswd)) return grant();
14668
+ const quoted = argv.map(quoteWord).join(" ");
14669
+ if (matching.length === 0) {
14670
+ return failed2(
14671
+ "EPERM",
14672
+ `sudo: ${self.name} is not allowed to run ${quoted} as ${target.name}`
14673
+ );
14674
+ }
14675
+ if (parsed2.nonInteractive) return failed2("EPERM", "sudo: a password is required");
14676
+ if (this.pending.has(cred.ruid)) return failed2("EBUSY", "sudo: another request is pending");
14677
+ this.pending.add(cred.ruid);
14678
+ try {
14679
+ const answer = await this.ask(caller, self, target, parsed2.command, path, cwd, signal);
14680
+ if (typeof answer !== "string") return answer;
14681
+ if (await this.canonical(call.cwd) !== cwd || !await this.reachable(cred, cwd)) {
14682
+ return failed2("EACCES", `sudo: ${call.cwd} changed while the request waited`);
14683
+ }
14684
+ const changed = await this.untrusted(path, cwd);
14685
+ if (changed) {
14686
+ return failed2("EPERM", `sudo: ${changed} must be root's, and writable by root only`);
14687
+ }
14688
+ if (answer === "always" && !await this.remember(self, target, argv)) {
14689
+ return failed2("EIO", "sudo: the rule could not be recorded");
14690
+ }
14691
+ return grant();
14692
+ } finally {
14693
+ this.pending.delete(cred.ruid);
14694
+ }
14695
+ }
14696
+ listed(self, root, rules) {
14697
+ const host = this.o.hostname;
14698
+ if (root) {
14699
+ return {
14700
+ list: `User ${self.name} may run the following commands on ${host}:
14701
+ (ALL) NOPASSWD: ALL
14702
+ `
14703
+ };
14704
+ }
14705
+ if (rules.length === 0) {
14706
+ return failed2("EPERM", `User ${self.name} is not allowed to run sudo on ${host}.`);
14707
+ }
14708
+ const lines2 = rules.map(described).join("\n");
14709
+ return { list: `User ${self.name} may run the following commands on ${host}:
14710
+ ${lines2}
14711
+ ` };
14712
+ }
14713
+ async ask(caller, self, target, argv, path, cwd, requester) {
14714
+ const { approve } = this.o;
14715
+ if (!approve || requester.aborted) return failed2("EACCES", "sudo: approval denied");
14716
+ const createdAt = Date.now();
14717
+ const timeout = this.o.timeoutMs ?? SUDO_TIMEOUT_MS;
14718
+ const req = {
14719
+ id: `sudo-${++this.next}-${crypto.randomUUID()}`,
14720
+ kind: "sudo",
14721
+ user: self.name,
14722
+ uid: self.uid,
14723
+ target: target.name,
14724
+ argv: [...argv],
14725
+ path,
14726
+ cwd,
14727
+ pid: caller.pid,
14728
+ ...caller.tag !== void 0 ? { tag: caller.tag } : {},
14729
+ ...caller.tty ? { tty: caller.tty } : {},
14730
+ createdAt,
14731
+ expiresAt: createdAt + timeout,
14732
+ always: true
14733
+ };
14734
+ const control = new AbortController();
14735
+ const timer = setTimeout(() => control.abort("expired"), timeout);
14736
+ const withdraw = () => control.abort("withdrawn");
14737
+ requester.addEventListener("abort", withdraw, { once: true });
14738
+ const aborted2 = new Promise(
14739
+ (resolve) => control.signal.addEventListener("abort", () => resolve("aborted"), { once: true })
14740
+ );
14741
+ try {
14742
+ const answered2 = Promise.resolve().then(() => approve(req, { signal: control.signal })).catch(() => "deny");
14743
+ const answer = await Promise.race([answered2, aborted2]);
14744
+ if (answer === "aborted" || control.signal.aborted) {
14745
+ return control.signal.reason === "expired" ? failed2("ETIMEDOUT", "sudo: the request expired") : failed2("EINTR", "sudo: the request was withdrawn");
14746
+ }
14747
+ return answer === "once" || answer === "always" ? answer : failed2("EACCES", "sudo: approval denied");
14748
+ } finally {
14749
+ clearTimeout(timer);
14750
+ requester.removeEventListener("abort", withdraw);
14751
+ }
14752
+ }
14753
+ async remember(self, target, argv) {
14754
+ const { fs } = this.o;
14755
+ const path = `${SUDOERS_D}/${self.name}`;
14756
+ if (!NAME3.test(self.name)) return false;
14757
+ try {
14758
+ await this.provision();
14759
+ if (!await this.trusted(SUDOERS_D)) return false;
14760
+ const st = await fs.lstat(path).catch(() => void 0);
14761
+ if (st && !await this.trusted(path)) return false;
14762
+ const old = st ? await fs.readFile(path) : "";
14763
+ const line = `NOPASSWD ${self.name} (${target.name}) Cmnd ${argv.map(quoteWord).join(" ")}
14764
+ `;
14765
+ const text = old === "" || old.endsWith("\n") ? `${old}${line}` : `${old}
14766
+ ${line}`;
14767
+ await fs.writeFile(path, text);
14768
+ await fs.chown?.(path, { uid: 0, gid: 0, mode: 288, follow: false });
14769
+ return true;
14770
+ } catch {
14771
+ return false;
14772
+ }
14773
+ }
14774
+ grant(self, target, cred, argv, file, cwd, term) {
14775
+ const env = {
14776
+ ...this.o.env(),
14777
+ ...userEnv(target, this.o.pnpmHome),
14778
+ SHELL: target.shell,
14779
+ PATH: SECURE_PATH,
14780
+ PWD: cwd,
14781
+ ...term !== void 0 && TERM.test(term) ? { TERM: term } : {},
14782
+ SUDO_USER: self.name,
14783
+ SUDO_UID: String(cred.ruid),
14784
+ SUDO_GID: String(cred.rgid),
14785
+ SUDO_COMMAND: [file, ...argv.slice(1)].join(" ")
14786
+ };
14787
+ return { grant: { file, argv: [...argv], env, cwd, cred: credOf(target), caller: cred } };
14788
+ }
14789
+ };
14790
+
14791
+ // src/sudo-command.ts
14792
+ var SUDO_PATH = "/usr/bin/sudo";
14793
+ var SUDO_COMMAND = {
14794
+ name: "sudo",
14795
+ abi: "js",
14796
+ glue: SUDO_PATH,
14797
+ wasm: SUDO_PATH,
14798
+ argv0: "sudo"
14799
+ };
14800
+ var SUDO_SOURCE = [
14801
+ "export default async function sudo(ctx) {",
14802
+ " const say = (fd, text) => ctx.write(fd, text).catch(() => undefined);",
14803
+ " let pid;",
14804
+ " let held;",
14805
+ " const relay = (sig) => {",
14806
+ " if (pid === undefined) held = sig;",
14807
+ " else return ctx.kill(pid, sig).catch(() => undefined);",
14808
+ " };",
14809
+ ' for (const sig of ["SIGTERM", "SIGHUP"]) await ctx.signals.on(sig, relay);',
14810
+ " const failed = () => (held === undefined ? 1 : 128 + held);",
14811
+ " let answer;",
14812
+ " try {",
14813
+ " answer = await ctx.sudo(ctx.argv.slice(1));",
14814
+ " } catch (err) {",
14815
+ ' await say(2, "sudo: " + (err && err.code ? err.code : String(err)) + "\\n");',
14816
+ " return failed();",
14817
+ " }",
14818
+ " if (answer.error !== undefined) {",
14819
+ ' await say(2, answer.error + "\\n");',
14820
+ " return failed();",
14821
+ " }",
14822
+ " if (answer.list !== undefined) {",
14823
+ " await say(1, answer.list);",
14824
+ " return 0;",
14825
+ " }",
14826
+ ' for (const sig of ["SIGINT", "SIGQUIT"]) await ctx.signals.on(sig, () => undefined);',
14827
+ " if (answer.run !== undefined) {",
14828
+ " try {",
14829
+ " pid = (await ctx.spawn({ argv: answer.run })).pid;",
14830
+ " } catch (err) {",
14831
+ ' const why = err && err.code === "ENOENT" ? "command not found" : String(err && err.code);',
14832
+ ' await say(2, "sudo: " + answer.run[0] + ": " + why + "\\n");',
14833
+ " return failed();",
14834
+ " }",
14835
+ " } else pid = answer.pid;",
14836
+ " if (held !== undefined) await relay(held);",
14837
+ " return (await ctx.wait(pid)).status;",
14838
+ "}",
14839
+ ""
14840
+ ].join("\n");
14841
+
14393
14842
  // src/launcher.ts
14394
14843
  var COMMAND2 = /^\/(?:usr\/)?bin\/([^/]+)$/;
14395
14844
  var PACKAGE_ROOT = /^(.*\/node_modules\/(?:@[^/]+\/)?[^/]+)\//;
@@ -14417,9 +14866,21 @@ function usersMode(mode = "off") {
14417
14866
  if (USERS_MODES.includes(mode)) return mode;
14418
14867
  throw new Error(`users must be 'off', 'report' or 'enforce', not ${String(mode)}`);
14419
14868
  }
14869
+ function tagOption(tag) {
14870
+ if (tag !== void 0 && typeof tag !== "string") throw new TypeError("a tag must be a string");
14871
+ }
14420
14872
  function baseName(path) {
14421
14873
  return path.slice(path.lastIndexOf("/") + 1);
14422
14874
  }
14875
+ function packageRootOf(path, roots) {
14876
+ for (const root of roots) {
14877
+ const base = normalizePath(root);
14878
+ if (!path.startsWith(`${base}/`)) continue;
14879
+ const [first = "", second] = path.slice(base.length + 1).split("/");
14880
+ return first.startsWith("@") && second !== void 0 ? `${base}/${first}/${second}` : `${base}/${first}`;
14881
+ }
14882
+ return void 0;
14883
+ }
14423
14884
  function modulePath(glue) {
14424
14885
  return glue.endsWith(".js") ? `${glue.slice(0, -3)}.wasm` : `${glue}.wasm`;
14425
14886
  }
@@ -14431,11 +14892,11 @@ function shebang(head) {
14431
14892
  return new TextDecoder().decode(head.subarray(0, SHEBANG_MAX)).slice(2).split("\n")[0].trim().split(/[ \t]+/);
14432
14893
  }
14433
14894
  function interpreter(head) {
14434
- const words = shebang(head);
14435
- if (!words) return "node";
14436
- if (baseName(words[0]) === "env") words.shift();
14437
- if (words[0] === "-S") words.shift();
14438
- return baseName(words[0] ?? "");
14895
+ const words2 = shebang(head);
14896
+ if (!words2) return "node";
14897
+ if (baseName(words2[0]) === "env") words2.shift();
14898
+ if (words2[0] === "-S") words2.shift();
14899
+ return baseName(words2[0] ?? "");
14439
14900
  }
14440
14901
  function expandDefaults(defaults, env) {
14441
14902
  const out = {};
@@ -14509,7 +14970,10 @@ var Launcher = class {
14509
14970
  orphans = /* @__PURE__ */ new Set();
14510
14971
  described = /* @__PURE__ */ new Map();
14511
14972
  creds = /* @__PURE__ */ new Map();
14973
+ tags = /* @__PURE__ */ new Map();
14974
+ granted = /* @__PURE__ */ new Map();
14512
14975
  users;
14976
+ sudoer;
14513
14977
  jobs = new JobTable();
14514
14978
  locks = new LockTable();
14515
14979
  settling = new SettlingChildren((pid) => this.jobs.pgidOf(pid));
@@ -14610,6 +15074,17 @@ var Launcher = class {
14610
15074
  );
14611
15075
  this.fs = this.unlinked.wrap(keepingOpen(fs, this.nodes));
14612
15076
  this.users = new UserDb(this.fs, this.owners);
15077
+ this.sudoer = new Sudo({
15078
+ fs: this.fs,
15079
+ users: this.users,
15080
+ mode: this.usersMode,
15081
+ hostname,
15082
+ env: () => this.env,
15083
+ pnpmHome: this.pnpmHome,
15084
+ backing: (path, cwd) => this.backing(path, cwd),
15085
+ approve: options.approve,
15086
+ timeoutMs: options.sudoTimeoutMs
15087
+ });
14613
15088
  void this.unlinked.sweep("/");
14614
15089
  this.watchers.watch(
14615
15090
  [this.modulesDir, this.pnpmHome],
@@ -14630,7 +15105,7 @@ var Launcher = class {
14630
15105
  commands(scope = "") {
14631
15106
  let found = this.catalogs.get(scope);
14632
15107
  if (!found) {
14633
- found = this.roots(scope).then((roots) => scanCommands(this.base, roots));
15108
+ found = this.roots(scope).then((roots) => scanCommands(this.base, roots)).then((commands) => commands.set(SUDO_COMMAND.name, SUDO_COMMAND));
14634
15109
  this.catalogs.set(scope, found);
14635
15110
  }
14636
15111
  return found;
@@ -14753,10 +15228,10 @@ var Launcher = class {
14753
15228
  const head = await this.head(script);
14754
15229
  if (!head) return void 0;
14755
15230
  const binfmt = isWasm(head) ? void 0 : binfmtOf(await this.interpreters(scope), script);
14756
- const words = shebang(head) ?? (binfmt ? [binfmt] : void 0);
14757
- if (!words) return void 0;
14758
- if (baseName(words[0]) === "env") words.shift();
14759
- const [interp, ...rest2] = words;
15231
+ const words2 = shebang(head) ?? (binfmt ? [binfmt] : void 0);
15232
+ if (!words2) return void 0;
15233
+ if (baseName(words2[0]) === "env") words2.shift();
15234
+ const [interp, ...rest2] = words2;
14760
15235
  const arg = rest2.join(" ");
14761
15236
  const passed = [...arg ? [arg] : [], command ? script : file, ...argv.slice(1)];
14762
15237
  const found = interp ? await this.resolve(interp, interp, cwd, scope) : void 0;
@@ -14793,6 +15268,41 @@ var Launcher = class {
14793
15268
  await need(g, "exec", this.fs.resolvePath(req.cwd, file), want);
14794
15269
  }
14795
15270
  }
15271
+ async grantedCwd(caller, cwd) {
15272
+ const real = await physicalPathAsync(this.base, cwd).catch(() => void 0);
15273
+ return real === cwd && await this.sudoer.reachable(caller, cwd);
15274
+ }
15275
+ async binfmtManifests(scripts) {
15276
+ const sources = /* @__PURE__ */ new Map();
15277
+ const binfmts = await scanBinfmts(this.base, await this.roots(), void 0, sources);
15278
+ const manifests = [];
15279
+ for (const script of scripts) {
15280
+ const head = await this.head(script);
15281
+ if (!head || isWasm(head) || shebang(head)) continue;
15282
+ const pkg = sources.get(binfmtSuffix(binfmts, script) ?? "");
15283
+ if (pkg !== void 0) manifests.push(`${pkg}/package.json`);
15284
+ }
15285
+ return manifests;
15286
+ }
15287
+ async backing(file, cwd) {
15288
+ const planned = await this.plan(file, [file], cwd, "");
15289
+ if (!planned) return [];
15290
+ const { glue, wasm, imports } = planned.target;
15291
+ const files = [glue, wasm, ...imports ? [imports] : []];
15292
+ const scripts = [];
15293
+ for (const name2 of [file, ...planned.via ?? []]) {
15294
+ if (name2.includes("/") && !COMMAND2.test(name2)) scripts.push(this.fs.resolvePath(cwd, name2));
15295
+ const script = (await this.scriptCommand(name2, cwd, ""))?.script;
15296
+ if (script) scripts.push(script);
15297
+ }
15298
+ files.push(...scripts, ...await this.binfmtManifests(scripts));
15299
+ const roots = await this.roots();
15300
+ for (const at of [...files]) {
15301
+ const root = packageRootOf(at, roots) ?? PACKAGE_ROOT.exec(at)?.[1];
15302
+ if (root !== void 0) files.push(`${root}/package.json`);
15303
+ }
15304
+ return [...new Set(files)].filter((path) => path !== "" && path !== SUDO_PATH);
15305
+ }
14796
15306
  async unrunnable(file, cwd, scope) {
14797
15307
  const path = this.fs.resolvePath(cwd, file);
14798
15308
  const head = await this.base.readFileBuffer(path).catch(() => null);
@@ -14830,6 +15340,9 @@ var Launcher = class {
14830
15340
  return await this.base.exists(optional) ? optional : void 0;
14831
15341
  }
14832
15342
  async program(target, env) {
15343
+ if (target.abi === "js" && target.wasm === SUDO_PATH) {
15344
+ return { abi: "js", glue: SUDO_SOURCE, path: SUDO_PATH };
15345
+ }
14833
15346
  if (target.abi === "js") {
14834
15347
  return { abi: "js", glue: await this.base.readFile(target.wasm), path: target.wasm };
14835
15348
  }
@@ -14916,6 +15429,7 @@ var Launcher = class {
14916
15429
  kill: (target, sig) => this.kill(target, sig, this.credOf(pid), pid),
14917
15430
  writesBack: (target) => this.processes.get(target)?.writesBack() === true,
14918
15431
  processes: () => ({ boot: this.boot, processes: this.list(this.credOf(pid)) }),
15432
+ sudo: (call, signal) => this.sudo(pid, call, signal),
14919
15433
  openFiles: this.openFiles,
14920
15434
  nodes: this.nodes,
14921
15435
  held: this.held,
@@ -14923,7 +15437,7 @@ var Launcher = class {
14923
15437
  statfs: (path) => this.mounts.statfs(path),
14924
15438
  mounts: () => this.mounts.list(),
14925
15439
  mount: (call, signal) => this.processMount(pid, call, signal),
14926
- umount: (target, flags) => this.processUmount(pid, target, flags),
15440
+ umount: (target, flags2) => this.processUmount(pid, target, flags2),
14927
15441
  jobs: this.jobs,
14928
15442
  ptys: this.ptys,
14929
15443
  net: this.net,
@@ -14937,6 +15451,7 @@ var Launcher = class {
14937
15451
  this.processes.set(pid, handle);
14938
15452
  const { cred } = req;
14939
15453
  this.creds.set(pid, () => handle.cred?.() ?? copyCred(cred));
15454
+ if (req.tag !== void 0) this.tags.set(pid, req.tag);
14940
15455
  if (req.ppid !== void 0 && this.controlsJobs(req.ppid)) this.settling.started(pid, req.ppid);
14941
15456
  this.jobs.add(pid, req.ppid, (sig) => handle.signal(sig), terminal);
14942
15457
  if (req.exec && req.ppid !== void 0) this.jobs.exec(req.ppid, pid, true);
@@ -14964,6 +15479,8 @@ var Launcher = class {
14964
15479
  this.jobs.remove(pid);
14965
15480
  this.described.delete(pid);
14966
15481
  this.creds.delete(pid);
15482
+ this.tags.delete(pid);
15483
+ this.granted.delete(pid);
14967
15484
  }
14968
15485
  reaped(pid) {
14969
15486
  if (this.zombies.delete(pid)) this.forget(pid);
@@ -14985,8 +15502,8 @@ var Launcher = class {
14985
15502
  };
14986
15503
  const listed = [];
14987
15504
  for (const member of members.values()) {
14988
- const described = this.described.get(member.pid);
14989
- if (member.execed || !described) continue;
15505
+ const described2 = this.described.get(member.pid);
15506
+ if (member.execed || !described2) continue;
14990
15507
  const cred = this.credOf(member.pid);
14991
15508
  if (!maySee(viewer, cred)) continue;
14992
15509
  const root = rootOf(member);
@@ -14996,8 +15513,8 @@ var Launcher = class {
14996
15513
  ppid: shown(root.ppid),
14997
15514
  pgid: member.pgid,
14998
15515
  sid: member.sid,
14999
- ...described,
15000
- started: (this.described.get(root.pid) ?? described).started,
15516
+ ...described2,
15517
+ started: (this.described.get(root.pid) ?? described2).started,
15001
15518
  state: this.processes.has(member.pid) ? "S" : "Z",
15002
15519
  memory: this.processes.get(member.pid)?.memory() ?? 0,
15003
15520
  umask: this.umaskOf(member.pid),
@@ -15007,12 +15524,26 @@ var Launcher = class {
15007
15524
  }
15008
15525
  return listed;
15009
15526
  }
15527
+ sudo(pid, call, signal) {
15528
+ const caller = {
15529
+ cred: this.credOf(pid),
15530
+ pid: this.jobs.shown(pid),
15531
+ tag: this.tagOf(pid),
15532
+ tty: this.described.get(pid)?.tty
15533
+ };
15534
+ return this.sudoer.request(caller, call, signal);
15535
+ }
15010
15536
  spawner(ppid, report) {
15011
- return async (req, fds) => {
15012
- const scope = await this.scopeOf(this.credOf(ppid).euid);
15537
+ return async (req, fds, grant) => {
15538
+ if (grant && !await this.grantedCwd(grant.caller, req.cwd)) {
15539
+ await fds.closeAll();
15540
+ throw new SpawnError("EACCES");
15541
+ }
15542
+ const cred = grant ? grant.cred : this.credOf(ppid);
15543
+ const scope = grant ? "" : await this.scopeOf(cred.euid);
15013
15544
  const planned = await this.plan(req.file, req.argv, req.cwd, scope);
15014
15545
  try {
15015
- await this.mayExec(this.credOf(ppid), { ...req, pid: ppid }, planned);
15546
+ await this.mayExec(cred, { ...req, pid: ppid }, planned);
15016
15547
  } catch (err) {
15017
15548
  await fds.closeAll();
15018
15549
  throw err.code === "EACCES" ? new SpawnError("EACCES") : err;
@@ -15032,10 +15563,12 @@ var Launcher = class {
15032
15563
  ppid,
15033
15564
  ignored: this.ignoredBy(ppid),
15034
15565
  umask: this.umaskOf(ppid),
15035
- cred: this.credOf(ppid),
15566
+ cred,
15567
+ tag: this.tagOf(ppid),
15036
15568
  ...exec
15037
15569
  });
15038
15570
  deciding?.attach();
15571
+ if (grant) this.granted.set(handle.pid, ppid);
15039
15572
  return childHandle(handle);
15040
15573
  };
15041
15574
  }
@@ -15079,7 +15612,8 @@ var Launcher = class {
15079
15612
  fork: state,
15080
15613
  ignored: this.ignoredBy(ppid, true),
15081
15614
  umask: this.umaskOf(ppid),
15082
- cred: this.credOf(ppid)
15615
+ cred: this.credOf(ppid),
15616
+ tag: this.tagOf(ppid)
15083
15617
  })
15084
15618
  );
15085
15619
  }
@@ -15104,6 +15638,9 @@ var Launcher = class {
15104
15638
  credOf(pid) {
15105
15639
  return this.creds.get(pid)?.() ?? copyCred(ROOT);
15106
15640
  }
15641
+ tagOf(pid) {
15642
+ return this.tags.get(pid);
15643
+ }
15107
15644
  released(path) {
15108
15645
  const at = normalizePath(path);
15109
15646
  this.unlinked.closed(at);
@@ -15122,7 +15659,7 @@ var Launcher = class {
15122
15659
  }
15123
15660
  kill(pid, sig, sender, from) {
15124
15661
  const session = from === void 0 ? void 0 : this.jobs.sidOf(from);
15125
- 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);
15662
+ 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;
15126
15663
  if (pid < 0) return this.jobs.killGroup(-pid, sig, (member) => may(this.imageOf(member)));
15127
15664
  const handle = this.processes.get(pid);
15128
15665
  if (!handle) return false;
@@ -15239,11 +15776,12 @@ var Launcher = class {
15239
15776
  ];
15240
15777
  for (const [path, mode] of layout) {
15241
15778
  const real = await this.realLayoutPath(path);
15242
- const fresh = real !== void 0 && !await this.base.hasOwner?.(real);
15243
- if (fresh) await this.base.chown?.(real, { uid: 0, gid: 0, mode, follow: false });
15779
+ const fresh2 = real !== void 0 && !await this.base.hasOwner?.(real);
15780
+ if (fresh2) await this.base.chown?.(real, { uid: 0, gid: 0, mode, follow: false });
15244
15781
  }
15245
15782
  }
15246
- async realLayoutPath(path) {
15783
+ async realLayoutPath(given) {
15784
+ const path = normalizePath(given);
15247
15785
  if (!await this.base.exists(path)) return void 0;
15248
15786
  if (path === "/") return path;
15249
15787
  return joinName(await realDir(this.base, parentOf4(path)), baseName(path));
@@ -15254,7 +15792,8 @@ var Launcher = class {
15254
15792
  mountedBy(spec, cred) {
15255
15793
  if (!this.owners || cred.euid === 0 && cred.egid === 0) return spec;
15256
15794
  const ids = { uid: String(cred.euid), gid: String(cred.egid) };
15257
- return { ...spec, options: { ...ids, ...spec.options } };
15795
+ const options = cred.euid === 0 ? { ...ids, ...spec.options } : { ...spec.options, ...ids };
15796
+ return { ...spec, options };
15258
15797
  }
15259
15798
  mountRoot(path) {
15260
15799
  let root = "/";
@@ -15299,13 +15838,36 @@ var Launcher = class {
15299
15838
  async processMount(pid, call, signal) {
15300
15839
  const spec = mountCall(call);
15301
15840
  await this.permit({ op: "mount", pid, ...spec, options: { ...spec.options } });
15841
+ await this.mountsOwn(
15842
+ this.credOf(pid),
15843
+ spec.target,
15844
+ async () => (await this.fs.stat(spec.target)).uid
15845
+ );
15302
15846
  const entry = await this.mount(this.mountedBy(spec, this.credOf(pid)));
15303
15847
  if (signal.aborted) this.umount(entry.target, true);
15304
15848
  }
15305
- async processUmount(pid, target, flags) {
15306
- const call = umountCall(target, flags);
15849
+ async mountAs(cred, spec) {
15850
+ const target = normalizePath(spec.target);
15851
+ await this.mountsOwn(cred, target, async () => (await this.fs.stat(target)).uid);
15852
+ return this.mount(this.mountedBy(spec, cred));
15853
+ }
15854
+ async umountAs(cred, target) {
15855
+ const at = normalizePath(target);
15856
+ await this.mountsOwn(cred, at, async () => Number(this.mounts.mounted(at)?.options.uid));
15857
+ this.umount(at);
15858
+ }
15859
+ async mountsOwn(cred, target, owner) {
15860
+ const { euid } = cred;
15861
+ if (this.usersMode !== "enforce" || euid === 0) return;
15862
+ if (await owner().catch(() => void 0) !== euid) {
15863
+ throw fsError("EPERM", `${target} is not the caller's to mount or unmount`);
15864
+ }
15865
+ }
15866
+ async processUmount(pid, target, flags2) {
15867
+ const call = umountCall(target, flags2);
15868
+ let mounted;
15307
15869
  for (; ; ) {
15308
- const mounted = this.mounts.mounted(call.target);
15870
+ mounted = this.mounts.mounted(call.target);
15309
15871
  await this.permit({
15310
15872
  op: "umount",
15311
15873
  pid,
@@ -15314,6 +15876,7 @@ var Launcher = class {
15314
15876
  });
15315
15877
  if (this.mounts.mounted(call.target) === mounted) break;
15316
15878
  }
15879
+ await this.mountsOwn(this.credOf(pid), call.target, async () => Number(mounted?.options.uid));
15317
15880
  this.umount(call.target, call.detach);
15318
15881
  }
15319
15882
  async prepare() {
@@ -15322,6 +15885,7 @@ var Launcher = class {
15322
15885
  await this.provisionPnpmHome();
15323
15886
  await this.users.ensure();
15324
15887
  if (this.owners) await this.stampLayout();
15888
+ if (this.owners) await this.sudoer.provision();
15325
15889
  await writeCaFile(this.base, this.ca).catch(() => void 0);
15326
15890
  this.fstab = this.mountFstab();
15327
15891
  }
@@ -15359,8 +15923,9 @@ var Launcher = class {
15359
15923
  if (!found) throw fsError("ENOENT", `no user ${String(user)}`);
15360
15924
  return found;
15361
15925
  }
15362
- identity(account) {
15926
+ identity(account, tag) {
15363
15927
  return {
15928
+ tag,
15364
15929
  cred: credOf(account),
15365
15930
  ...account.umask !== void 0 ? { umask: account.umask } : {}
15366
15931
  };
@@ -15377,6 +15942,7 @@ var Launcher = class {
15377
15942
  return { cwd, file, planned };
15378
15943
  }
15379
15944
  async openTerminal(argv, options) {
15945
+ tagOption(options.tag);
15380
15946
  const { account, tty: ttyGroup } = await this.terminalAccount(options.user);
15381
15947
  const { cwd, file, planned } = await this.starting(argv, options.cwd, account);
15382
15948
  if (!planned) throw new Error(`${file}: command not found`);
@@ -15405,7 +15971,13 @@ var Launcher = class {
15405
15971
  );
15406
15972
  const report = (message3) => options.onData(encoder3.encode(`${message3}\r
15407
15973
  `));
15408
- const handle = await this.launch(planned, { env, cwd, fds, report, ...this.identity(account) });
15974
+ const handle = await this.launch(planned, {
15975
+ env,
15976
+ cwd,
15977
+ fds,
15978
+ report,
15979
+ ...this.identity(account, options.tag)
15980
+ });
15409
15981
  leader = handle.pid;
15410
15982
  void handle.exited.then(() => tty.hangup());
15411
15983
  return {
@@ -15421,6 +15993,7 @@ var Launcher = class {
15421
15993
  };
15422
15994
  }
15423
15995
  async run(argv, options = {}) {
15996
+ tagOption(options.tag);
15424
15997
  const account = await this.account(options.user);
15425
15998
  const { cwd, file, planned } = await this.starting(argv, options.cwd, account);
15426
15999
  const env = this.environment(cwd, options.env, account);
@@ -15450,7 +16023,7 @@ var Launcher = class {
15450
16023
  fds,
15451
16024
  report,
15452
16025
  ...group,
15453
- ...this.identity(account)
16026
+ ...this.identity(account, options.tag)
15454
16027
  });
15455
16028
  options.onStarted?.(handle.pid);
15456
16029
  const status = await handle.exited;
@@ -15511,8 +16084,37 @@ function remotes(port, req) {
15511
16084
  ...req.uplink ? { uplink: new RemoteUplink(port, req.uplink.traits, req.uplink.routes) } : {}
15512
16085
  };
15513
16086
  }
15514
- function answered(data, remote, uplink) {
15515
- if (data.net !== void 0) remote?.receive(data);
16087
+ function dial(l, req) {
16088
+ const socket = dialSocket(l.net, req.port, req.host);
16089
+ const { port1, port2 } = new MessageChannel();
16090
+ serveSocket(socket, port1);
16091
+ return port2;
16092
+ }
16093
+ function sudoApprovals(port) {
16094
+ const waiting = /* @__PURE__ */ new Map();
16095
+ return {
16096
+ ask: (req, { signal }) => new Promise((resolve) => {
16097
+ waiting.set(req.id, resolve);
16098
+ signal.addEventListener(
16099
+ "abort",
16100
+ () => {
16101
+ waiting.delete(req.id);
16102
+ port.postMessage({ sudoAbort: { id: req.id, reason: String(signal.reason) } });
16103
+ resolve("deny");
16104
+ },
16105
+ { once: true }
16106
+ );
16107
+ port.postMessage({ sudoAsk: req });
16108
+ }),
16109
+ answer({ id: id2, answer }) {
16110
+ waiting.get(id2)?.(answer);
16111
+ waiting.delete(id2);
16112
+ }
16113
+ };
16114
+ }
16115
+ function answered(data, remote, uplink, approvals) {
16116
+ if (data.sudoAnswer) approvals.answer(data.sudoAnswer);
16117
+ else if (data.net !== void 0) remote?.receive(data);
15516
16118
  else if (typeof data.uplink === "string") uplink?.receive(data);
15517
16119
  else return false;
15518
16120
  return true;
@@ -15557,6 +16159,7 @@ function serveKernel(port, deps) {
15557
16159
  const terminals = /* @__PURE__ */ new Map();
15558
16160
  const reply = (id2, body, transfer) => port.postMessage({ id: id2, ...body }, transfer);
15559
16161
  const clients = /* @__PURE__ */ new Set();
16162
+ const approvals = sudoApprovals(port);
15560
16163
  const locks = deps.locks === null ? void 0 : deps.locks ?? locksOf();
15561
16164
  let holding;
15562
16165
  function holdLock(locks2) {
@@ -15570,8 +16173,9 @@ function serveKernel(port, deps) {
15570
16173
  });
15571
16174
  return holding;
15572
16175
  }
15573
- async function connect(user) {
16176
+ async function connect(user, tag) {
15574
16177
  const l = await ready2();
16178
+ tagOption(tag);
15575
16179
  if (user !== void 0 && !await l.users.lookup(user)) {
15576
16180
  throw fsError("ENOENT", `no user ${String(user)}`);
15577
16181
  }
@@ -15581,6 +16185,7 @@ function serveKernel(port, deps) {
15581
16185
  launcher: ready2,
15582
16186
  signal: signalNumber,
15583
16187
  ...user !== void 0 ? { user } : {},
16188
+ ...tag !== void 0 ? { tag } : {},
15584
16189
  ...locks ? { locks } : {},
15585
16190
  ...held ? { lock: held } : {}
15586
16191
  });
@@ -15588,12 +16193,6 @@ function serveKernel(port, deps) {
15588
16193
  void served.closed.then(() => clients.delete(served));
15589
16194
  return port2;
15590
16195
  }
15591
- async function dial(req) {
15592
- const socket = dialSocket((await ready2()).net, req.port, req.host);
15593
- const { port1, port2 } = new MessageChannel();
15594
- serveSocket(socket, port1);
15595
- return port2;
15596
- }
15597
16196
  async function ready2() {
15598
16197
  if (!launcher) throw new Error("the kernel is not initialized");
15599
16198
  return launcher;
@@ -15625,6 +16224,7 @@ function serveKernel(port, deps) {
15625
16224
  ...req.modules ? { modules: req.modules } : {},
15626
16225
  ...req.env ? { env: req.env } : {},
15627
16226
  ...hostOptions(req, port),
16227
+ ...req.approve ? { approve: approvals.ask } : {},
15628
16228
  ...net,
15629
16229
  ...media ? { media } : {},
15630
16230
  onMountPending: (medium) => port.postMessage({ medium }),
@@ -15649,13 +16249,13 @@ function serveKernel(port, deps) {
15649
16249
  async function handle(req) {
15650
16250
  if (req.op === "init") return init(req);
15651
16251
  if (req.op === "open-terminal") return terminal(req);
15652
- if (req.op === "connect") return connect(req.user);
16252
+ if (req.op === "connect") return connect(req.user, req.tag);
15653
16253
  if (isUsersRequest(req)) return usersOp(await ready2(), req);
15654
16254
  if (req.op === "routes") {
15655
16255
  (await ready2()).setRoutes(req.routes);
15656
16256
  return true;
15657
16257
  }
15658
- if (req.op === "dial") return dial(req);
16258
+ if (req.op === "dial") return dial(await ready2(), req);
15659
16259
  if (req.op === "mount" || req.op === "umount" || req.op === "mounts" || req.op === "insert")
15660
16260
  return mountOp(req);
15661
16261
  if (req.op === "terminal") {
@@ -15703,7 +16303,7 @@ function serveKernel(port, deps) {
15703
16303
  policies.delete(answer.id);
15704
16304
  return;
15705
16305
  }
15706
- if (answered(event.data, remote, uplink)) return;
16306
+ if (answered(event.data, remote, uplink, approvals)) return;
15707
16307
  const req = event.data;
15708
16308
  handle(req).then(
15709
16309
  (result) => reply(req.id, { result }, result instanceof MessagePort ? [result] : void 0),