@ai-ecoverse/slicc-kernel 1.178.1 → 1.178.2

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.
@@ -7518,7 +7518,7 @@ var init_version = __esm({
7518
7518
  "src/version.ts"() {
7519
7519
  "use strict";
7520
7520
  init_captured_globals();
7521
- KERNEL_VERSION = true ? "1.178.1" : "0.0.0-dev";
7521
+ KERNEL_VERSION = true ? "1.178.2" : "0.0.0-dev";
7522
7522
  OS_RELEASE = "6.0.0-slicc";
7523
7523
  }
7524
7524
  });
@@ -7517,7 +7517,7 @@ var init_version = __esm({
7517
7517
  "src/version.ts"() {
7518
7518
  "use strict";
7519
7519
  init_captured_globals();
7520
- KERNEL_VERSION = true ? "1.178.1" : "0.0.0-dev";
7520
+ KERNEL_VERSION = true ? "1.178.2" : "0.0.0-dev";
7521
7521
  OS_RELEASE = "6.0.0-slicc";
7522
7522
  }
7523
7523
  });
package/dist/node.js CHANGED
@@ -3709,7 +3709,7 @@ function satisfies(version, range2) {
3709
3709
  }
3710
3710
 
3711
3711
  // src/version.ts
3712
- var KERNEL_VERSION = true ? "1.178.1" : "0.0.0-dev";
3712
+ var KERNEL_VERSION = true ? "1.178.2" : "0.0.0-dev";
3713
3713
  var OS_RELEASE = "6.0.0-slicc";
3714
3714
  function procVersion() {
3715
3715
  return `Linux version ${OS_RELEASE} (slicc-kernel ${KERNEL_VERSION}) #1 SMP PREEMPT_DYNAMIC
@@ -5004,9 +5004,9 @@ var UnlinkedKeeper = class {
5004
5004
  if (kept.whole) return kept.undoing(async () => void 0);
5005
5005
  await kept.undoing(
5006
5006
  () => replace(target, async (gated, sealed) => {
5007
- const pinned = await keeper.pin(target, gated, sealed);
5008
- if (pinned.moved.has(target)) pinned.commit();
5009
- else await keeper.settle(pinned, () => fs.rm(path, options));
5007
+ const pinned2 = await keeper.pin(target, gated, sealed);
5008
+ if (pinned2.moved.has(target)) pinned2.commit();
5009
+ else await keeper.settle(pinned2, () => fs.rm(path, options));
5010
5010
  })
5011
5011
  );
5012
5012
  },
@@ -5019,8 +5019,8 @@ var UnlinkedKeeper = class {
5019
5019
  () => keeper.marks.around(
5020
5020
  [source],
5021
5021
  () => replace(target, async (gated, sealed) => {
5022
- const pinned = await keeper.pin(target, gated, sealed);
5023
- await keeper.settle(pinned, () => fs.rename(from, to));
5022
+ const pinned2 = await keeper.pin(target, gated, sealed);
5023
+ await keeper.settle(pinned2, () => fs.rename(from, to));
5024
5024
  })
5025
5025
  )
5026
5026
  );
@@ -5055,14 +5055,14 @@ var UnlinkedKeeper = class {
5055
5055
  }
5056
5056
  return false;
5057
5057
  }
5058
- async settle(pinned, op) {
5058
+ async settle(pinned2, op) {
5059
5059
  try {
5060
5060
  await op();
5061
5061
  } catch (err) {
5062
- await pinned.rollback();
5062
+ await pinned2.rollback();
5063
5063
  throw err;
5064
5064
  }
5065
- pinned.commit();
5065
+ pinned2.commit();
5066
5066
  }
5067
5067
  heldWithin(target) {
5068
5068
  const held = /* @__PURE__ */ new Map();
@@ -8294,6 +8294,13 @@ var SpawnError = class extends Error {
8294
8294
  this.code = code;
8295
8295
  }
8296
8296
  };
8297
+ var ChangedError = class extends SpawnError {
8298
+ notice;
8299
+ constructor(notice) {
8300
+ super("EPERM");
8301
+ this.notice = notice;
8302
+ }
8303
+ };
8297
8304
  function interrupted(signal) {
8298
8305
  const { promise, reject } = Promise.withResolvers();
8299
8306
  const fail3 = () => reject(new KernelError("EINTR"));
@@ -15304,8 +15311,10 @@ var SUDOERS_D = "/etc/sudoers.d";
15304
15311
  var NAME3 = /^[a-z_][a-z0-9_.-]*$/;
15305
15312
  var WORD = String.raw`(?:'[^']*'|"(?:[^"\\]|\\[\s\S])*"|\\[\s\S]|[^\s'"\\])+`;
15306
15313
  var KERNEL_FORM = new RegExp(
15307
- String.raw`^(NOPASSWD\s+)?(\S+)\s+(?:\(([^()]*)\)\s+)?(?:Cwd\s+(${WORD})\s+)?(?:Stdin\s+(\S+)\s+)?Cmnd(?:\s+(.*))?$`
15314
+ String.raw`^(NOPASSWD\s+)?(\S+)\s+(?:\(([^()]*)\)\s+)?(?:Cwd\s+(${WORD})\s+)?(?:Stdin\s+(\S+)\s+)?((?:Sha256\s+${WORD}\s+)*)Cmnd(?:\s+(.*))?$`
15308
15315
  );
15316
+ var SHA256_CLAUSE = new RegExp(String.raw`Sha256\s+(${WORD})`, "g");
15317
+ var HEX = /^[0-9a-f]{64}$/;
15309
15318
  var SUDOERS_FORM = /^(\S+)\s+([^\s=]+)\s*=\s*(.*)$/;
15310
15319
  var RUNAS = /^\(([^()]*)\)\s*/;
15311
15320
  var TAG = /^([A-Z_]+)\s*:\s*/;
@@ -15314,7 +15323,7 @@ var PLAIN = /^[A-Za-z0-9_@%+=:,./-]+$/;
15314
15323
  var CONTROL2 = /[\p{Cc}\u2028\u2029]/u;
15315
15324
  var CONTROLS = /[\p{Cc}\u2028\u2029]/gu;
15316
15325
  var SHOWN_MAX = 60;
15317
- var KERNEL_SYNTAX = "[NOPASSWD] <user|%group|ALL> [(<target>)] [Cwd <dir>] [Stdin none] Cmnd <command\u2026|*>";
15326
+ var KERNEL_SYNTAX = "[NOPASSWD] <user|%group|ALL> [(<target>)] [Cwd <dir>] [Stdin none] [Sha256 <file>=<sha256>\u2026] Cmnd <command\u2026|*>";
15318
15327
  var SUDOERS_SYNTAX = "<user|%group|ALL> ALL=[(<target>[,<target>\u2026])] [NOPASSWD:] <command\u2026|ALL>";
15319
15328
  var EXPECTED = `expected '${KERNEL_SYNTAX}' or '${SUDOERS_SYNTAX}'`;
15320
15329
  var TIMESTAMP_TIMEOUT = 5;
@@ -15481,6 +15490,23 @@ function commandOf2(text, after) {
15481
15490
  if (only && /\s""$/.test(text)) return { path, args: [] };
15482
15491
  return args.length === 0 ? { path } : { path, args };
15483
15492
  }
15493
+ function sha256Of(clauses) {
15494
+ const files = {};
15495
+ for (const [, word = ""] of clauses.matchAll(SHA256_CLAUSE)) {
15496
+ const [text = ""] = splitWords(word) ?? [];
15497
+ const at = text.lastIndexOf("=");
15498
+ const path = text.slice(0, at);
15499
+ const hex4 = text.slice(at + 1).toLowerCase();
15500
+ if (!path.startsWith("/") || !HEX.test(hex4)) {
15501
+ return `Sha256 expects <absolute file>=<sha256 in hex>, not ${shown(word)}`;
15502
+ }
15503
+ files[normalizePath(path)] = hex4;
15504
+ }
15505
+ return files;
15506
+ }
15507
+ function sha256Text(files) {
15508
+ return Object.entries(files).map(([path, hex4]) => `Sha256 ${quoteWord(`${path}=${hex4}`)} `).join("");
15509
+ }
15484
15510
  function targetError(list) {
15485
15511
  if (list.includes(",")) {
15486
15512
  return `(${list}) names one user in this form: write one line per user, or use '${SUDOERS_SYNTAX}'`;
@@ -15488,19 +15514,22 @@ function targetError(list) {
15488
15514
  return `expected one user name or ALL in (\u2026), not ${shown(`(${list})`)}`;
15489
15515
  }
15490
15516
  function kernelRules(m) {
15491
- const [, nopasswd, who = "", target = "root", dir, stdin, rest2 = ""] = m;
15517
+ const [, nopasswd, who = "", target = "root", dir, stdin, sums = "", rest2 = ""] = m;
15492
15518
  const bad = whoError(who);
15493
15519
  if (bad !== void 0) return bad;
15494
15520
  if (target !== "ALL" && !NAME3.test(target)) return targetError(target);
15495
15521
  const cwd = dir === void 0 ? void 0 : cwdOf(dir);
15496
15522
  if (cwd === "") return `Cwd expects one absolute directory, not ${shown(dir)}`;
15497
15523
  if (stdin !== void 0 && stdin !== "none") return `Stdin takes none, not ${shown(stdin)}`;
15524
+ const sha2564 = sha256Of(sums);
15525
+ if (typeof sha2564 === "string") return sha2564;
15498
15526
  const rule = {
15499
15527
  nopasswd: !!nopasswd,
15500
15528
  who,
15501
15529
  target,
15502
15530
  ...cwd !== void 0 ? { cwd } : {},
15503
- ...stdin === "none" ? { stdin: "none" } : {}
15531
+ ...stdin === "none" ? { stdin: "none" } : {},
15532
+ ...sums === "" ? {} : { sha256: sha2564 }
15504
15533
  };
15505
15534
  const text = rest2.trim();
15506
15535
  if (text === "*") return [{ ...rule, cmnd: "*" }];
@@ -16130,12 +16159,26 @@ var RAN_ONCE = "sudo: ran once; this command line can't be recorded as always";
16130
16159
  var ROOT_ONLY_MAX = 64;
16131
16160
  var OPERANDS_MAX = 256;
16132
16161
  var PATH_MAX = 4096;
16162
+ function userWritable(bound3) {
16163
+ const files = Object.entries(bound3).map(([path, hex4]) => ({ path, sha256: hex4 }));
16164
+ return files.length > 0 ? { userWritable: files } : {};
16165
+ }
16133
16166
  function stdinMatches(rule, stdin) {
16134
16167
  return rule.stdin === void 0 || stdin === "tty" || stdin === "none";
16135
16168
  }
16136
16169
  function recordable(cwd, argv) {
16137
16170
  return !hasControl(cwd) && !argv.some(hasControl);
16138
16171
  }
16172
+ function changedText(file) {
16173
+ return `sudo: ${file} changed since it was approved`;
16174
+ }
16175
+ function changedFile(approved, now) {
16176
+ const files = /* @__PURE__ */ new Set([...Object.keys(approved), ...Object.keys(now)]);
16177
+ return [...files].find((file) => approved[file] !== now[file]);
16178
+ }
16179
+ function pinned(rule, bound3) {
16180
+ return rule.sha256 === void 0 || changedFile(rule.sha256, bound3) === void 0;
16181
+ }
16139
16182
  var VISUDO_FILES_MAX = 64;
16140
16183
  var VISUDO_TEXT_MAX = 1024 * 1024;
16141
16184
  function visudoCall(req) {
@@ -16266,11 +16309,13 @@ function usable(parsed2) {
16266
16309
  function policyOf(settings) {
16267
16310
  return { type: settings.timestampType, minutes: settings.timestampTimeout };
16268
16311
  }
16269
- function described(rule) {
16312
+ function described(rule, writable2) {
16270
16313
  const cmnd = rule.cmnd === "*" ? "ALL" : commandText(rule.cmnd);
16271
16314
  const cwd = rule.cwd === void 0 ? "" : `CWD=${quoteWord(rule.cwd)} `;
16272
16315
  const stdin = rule.stdin === void 0 ? "" : "STDIN=none ";
16273
- return ` (${rule.target}) ${cwd}${stdin}${rule.nopasswd ? "NOPASSWD: " : ""}${cmnd}`;
16316
+ const sums = Object.entries(rule.sha256 ?? {}).map(([path, hex4]) => `SHA256=${quoteWord(path)}:${hex4} `).join("");
16317
+ const flag = writable2 ? " # user-writable command" : "";
16318
+ return ` (${rule.target}) ${cwd}${stdin}${sums}${rule.nopasswd ? "NOPASSWD: " : ""}${cmnd}${flag}`;
16274
16319
  }
16275
16320
  var Sudo = class {
16276
16321
  o;
@@ -16461,15 +16506,31 @@ var Sudo = class {
16461
16506
  if (walked.denied !== void 0 || walked.missing) return false;
16462
16507
  return (await this.o.fs.stat(cwd).catch(() => void 0))?.isDirectory === true;
16463
16508
  }
16464
- async untrusted(path, cwd, word) {
16465
- const files = this.o.backing ? await this.o.backing(path, cwd) : [];
16466
- if (!files) return failed2("ENOENT", `sudo: ${word}: command not found`);
16467
- for (const file of [path, ...files]) {
16468
- if (!await this.trusted(file)) {
16469
- return failed2("EPERM", `sudo: ${file} must be root's, and writable by root only`);
16470
- }
16509
+ async real(file) {
16510
+ const { fs } = this.o;
16511
+ return await fs.realpath?.(file, true).catch(() => file) ?? file;
16512
+ }
16513
+ async digests(files) {
16514
+ const { fs } = this.o;
16515
+ const found = {};
16516
+ for (const file of new Set(files)) {
16517
+ if (await this.trusted(file)) continue;
16518
+ const real = await this.real(file);
16519
+ found[real] = await fs.readFileBuffer(real).then(sha2562, () => "");
16471
16520
  }
16472
- return void 0;
16521
+ return found;
16522
+ }
16523
+ async bound(path, cwd) {
16524
+ const files = this.o.backing ? await this.o.backing(path, cwd) : [];
16525
+ const bound3 = files && await this.digests([path, ...files]);
16526
+ return bound3 && !Object.values(bound3).includes("") ? bound3 : void 0;
16527
+ }
16528
+ async writable(rule) {
16529
+ if (rule.cmnd === "*") return false;
16530
+ const { path } = rule.cmnd;
16531
+ const bound3 = await this.bound(path, "/");
16532
+ if (bound3) return Object.keys(bound3).length > 0;
16533
+ return !await this.trusted(await this.existing(path));
16473
16534
  }
16474
16535
  async locate(word, cwd) {
16475
16536
  const { fs } = this.o;
@@ -16514,7 +16575,7 @@ var Sudo = class {
16514
16575
  const root = cred.euid === 0;
16515
16576
  const rules = root ? [] : policy.rules;
16516
16577
  const mine = rules.filter((rule) => applies(rule.who));
16517
- if (parsed2.list) return this.listed(self, root, mine, settings.matching);
16578
+ if (parsed2.list) return await this.listed(self, root, mine, settings.matching);
16518
16579
  const session = { caller, self, target, owner, policy: policyOf(settings), parsed: parsed2, signal };
16519
16580
  if (parsed2.validate) return this.validate(session, root, mine, call.cwd);
16520
16581
  return this.command(session, root, mine, call);
@@ -16531,14 +16592,13 @@ var Sudo = class {
16531
16592
  );
16532
16593
  }
16533
16594
  const path = await this.locate(word, cwd);
16534
- if (!path) return failed2("ENOENT", `sudo: ${word}: command not found`);
16535
- const untrusted2 = await this.untrusted(path, cwd, word);
16536
- if (untrusted2) return untrusted2;
16595
+ const bound3 = path && await this.bound(path, cwd);
16596
+ if (!path || !bound3) return failed2("ENOENT", `sudo: ${word}: command not found`);
16537
16597
  const argv = [path, ...args];
16538
16598
  const matching = mine.filter(
16539
- (rule) => (rule.target === "ALL" || rule.target === target.name) && (rule.cwd === void 0 || rule.cwd === cwd) && stdinMatches(rule, caller.stdin) && commandMatches(rule.cmnd, argv)
16599
+ (rule) => (rule.target === "ALL" || rule.target === target.name) && (rule.cwd === void 0 || rule.cwd === cwd) && stdinMatches(rule, caller.stdin) && commandMatches(rule.cmnd, argv) && pinned(rule, bound3)
16540
16600
  );
16541
- const grant = () => this.grant(self, target, cred, parsed2.command, path, cwd, call.term);
16601
+ const grant = () => this.grant(self, target, cred, parsed2.command, path, cwd, call.term, bound3);
16542
16602
  if (root || matching.some((rule) => rule.nopasswd)) return grant();
16543
16603
  const quoted = argv.map(quoteWord).join(" ");
16544
16604
  if (matching.length === 0) {
@@ -16554,14 +16614,15 @@ var Sudo = class {
16554
16614
  }
16555
16615
  if (parsed2.nonInteractive) return failed2("EPERM", "sudo: a password is required");
16556
16616
  return this.pendingFor(cred.ruid, async () => {
16557
- const answer2 = await this.ask(s, { argv: parsed2.command, path, cwd });
16617
+ const answer2 = await this.ask(s, { argv: parsed2.command, path, cwd, bound: bound3 });
16558
16618
  if ("error" in answer2) return answer2;
16559
16619
  if (await this.canonical(call.cwd) !== cwd || !await this.reachable(cred, cwd)) {
16560
16620
  return failed2("EACCES", `sudo: ${call.cwd} changed while the request waited`);
16561
16621
  }
16562
- const changed = await this.untrusted(path, cwd, word);
16563
- if (changed) return changed;
16564
- const result = answer2.answer === "once" ? grant() : await this.always(self, target, cwd, argv, recordable(cwd, parsed2.command), grant);
16622
+ const now = await this.bound(path, cwd);
16623
+ if (!now) return failed2("ENOENT", `sudo: ${word}: command not found`);
16624
+ const shown2 = recordable(cwd, [...parsed2.command, ...Object.keys(bound3)]);
16625
+ const result = answer2.answer === "once" ? grant() : await this.always(self, target, { cwd, argv, bound: bound3 }, now, shown2, grant);
16565
16626
  if (!("error" in result)) this.stamped(s, answer2);
16566
16627
  return result;
16567
16628
  });
@@ -16580,7 +16641,7 @@ var Sudo = class {
16580
16641
  if (parsed2.nonInteractive) return failed2("EPERM", "sudo: a password is required");
16581
16642
  const cwd = await this.canonical(dir) ?? normalizePath(dir);
16582
16643
  return this.pendingFor(s.caller.cred.ruid, async () => {
16583
- const answer2 = await this.ask(s, { argv: [], path: "", cwd, validate: true });
16644
+ const answer2 = await this.ask(s, { argv: [], path: "", cwd, bound: {}, validate: true });
16584
16645
  if ("error" in answer2) return answer2;
16585
16646
  if (!answer2.person) await s.caller.report?.(NOT_STAMPED);
16586
16647
  this.stamped(s, answer2);
@@ -16608,14 +16669,17 @@ var Sudo = class {
16608
16669
  `sudo: ${self.name} : TTY=${tty} ; PWD=${quoteWord(cwd)} ; USER=${target.name} ; TIMESTAMP=${type} ; COMMAND=${command}`
16609
16670
  );
16610
16671
  }
16611
- async always(self, target, cwd, argv, shown2, grant) {
16672
+ async always(self, target, line, now, shown2, grant) {
16673
+ const { cwd, argv, bound: bound3 } = line;
16612
16674
  if (!shown2 || !recordable(cwd, argv)) return { ...grant(), notice: RAN_ONCE };
16613
- if (!await this.remember(self, target, cwd, argv)) {
16675
+ const changed = changedFile(bound3, now);
16676
+ if (changed !== void 0) return failed2("EPERM", changedText(changed));
16677
+ if (!await this.remember(self, target, line)) {
16614
16678
  return failed2("EIO", "sudo: the rule could not be recorded");
16615
16679
  }
16616
16680
  return grant();
16617
16681
  }
16618
- listed(self, root, rules, defaults2) {
16682
+ async listed(self, root, rules, defaults2) {
16619
16683
  const host = this.o.hostname;
16620
16684
  if (root) {
16621
16685
  return {
@@ -16627,7 +16691,9 @@ var Sudo = class {
16627
16691
  if (rules.length === 0) {
16628
16692
  return failed2("EPERM", `User ${self.name} is not allowed to run sudo on ${host}.`);
16629
16693
  }
16630
- const lines2 = rules.map(described).join("\n");
16694
+ const shown2 = [];
16695
+ for (const rule of rules) shown2.push(described(rule, await this.writable(rule)));
16696
+ const lines2 = shown2.join("\n");
16631
16697
  const params = defaults2.flatMap((d) => d.params).join(", ");
16632
16698
  const matching = params === "" ? "" : `Matching Defaults entries for ${self.name} on ${host}:
16633
16699
  ${params}
@@ -16660,9 +16726,10 @@ ${lines2}
16660
16726
  ...caller.tty ? { tty: caller.tty } : {},
16661
16727
  createdAt,
16662
16728
  expiresAt: createdAt + timeout,
16663
- always: asked.validate ? false : recordable(cwd, [path, ...argv]),
16729
+ always: asked.validate ? false : recordable(cwd, [path, ...argv, ...Object.keys(asked.bound)]),
16664
16730
  stdin: caller.stdin,
16665
16731
  rootOnly: asked.validate ? {} : await this.rootOnly(cwd, argv.slice(1)),
16732
+ ...userWritable(asked.bound),
16666
16733
  ...asked.validate ? { validate: true } : {},
16667
16734
  timestamp: true
16668
16735
  };
@@ -16682,7 +16749,7 @@ ${lines2}
16682
16749
  requester.removeEventListener("abort", withdraw);
16683
16750
  }
16684
16751
  }
16685
- async remember(self, target, cwd, argv) {
16752
+ async remember(self, target, { cwd, argv, bound: bound3 }) {
16686
16753
  const { fs } = this.o;
16687
16754
  const path = `${SUDOERS_D}/${self.name}`;
16688
16755
  if (!NAME3.test(self.name)) return false;
@@ -16693,7 +16760,7 @@ ${lines2}
16693
16760
  if (st && !await this.trusted(path)) return false;
16694
16761
  const old = st ? await fs.readFile(path) : "";
16695
16762
  const words2 = recordedCommand(argv);
16696
- const line = `NOPASSWD ${self.name} (${target.name}) Cwd ${quoteWord(cwd)} Stdin none Cmnd ${words2}
16763
+ const line = `NOPASSWD ${self.name} (${target.name}) Cwd ${quoteWord(cwd)} Stdin none ${sha256Text(bound3)}Cmnd ${words2}
16697
16764
  `;
16698
16765
  const text = old === "" || old.endsWith("\n") ? `${old}${line}` : `${old}
16699
16766
  ${line}`;
@@ -16704,7 +16771,7 @@ ${line}`;
16704
16771
  return false;
16705
16772
  }
16706
16773
  }
16707
- grant(self, target, cred, argv, file, cwd, term) {
16774
+ grant(self, target, cred, argv, file, cwd, term, files) {
16708
16775
  const env = {
16709
16776
  ...this.o.env(),
16710
16777
  ...userEnv(target, this.o.pnpmHome),
@@ -16717,7 +16784,9 @@ ${line}`;
16717
16784
  SUDO_GID: String(cred.rgid),
16718
16785
  SUDO_COMMAND: [file, ...argv.slice(1)].join(" ")
16719
16786
  };
16720
- return { grant: { file, argv: [...argv], env, cwd, cred: credOf(target), caller: cred } };
16787
+ return {
16788
+ grant: { file, argv: [...argv], env, cwd, cred: credOf(target), caller: cred, files }
16789
+ };
16721
16790
  }
16722
16791
  };
16723
16792
 
@@ -24887,14 +24956,20 @@ var WasmProcess = class {
24887
24956
  if (!("grant" in answer2)) return answer2;
24888
24957
  if (this.dying) throw new KernelError("EINTR");
24889
24958
  if (answer2.notice !== void 0) await this.complainLine(answer2.notice);
24890
- const { file, argv, env, cwd, cred, caller } = answer2.grant;
24959
+ const { file, argv, env, cwd, cred, caller, files } = answer2.grant;
24891
24960
  const stdio = [{ fd: 0 }, { fd: 1 }, { fd: 2 }];
24892
- return {
24893
- pid: await this.children.spawn({ file, argv, env, cwd, pgid: 0 }, stdio, [], {
24894
- cred,
24895
- caller
24896
- })
24897
- };
24961
+ try {
24962
+ return {
24963
+ pid: await this.children.spawn({ file, argv, env, cwd, pgid: 0 }, stdio, [], {
24964
+ cred,
24965
+ caller,
24966
+ files
24967
+ })
24968
+ };
24969
+ } catch (err) {
24970
+ if (err instanceof ChangedError) return { error: err.notice, errno: err.code };
24971
+ throw err;
24972
+ }
24898
24973
  }
24899
24974
  stdinKind() {
24900
24975
  if (!this.fds.has(0)) return "none";
@@ -32466,6 +32541,10 @@ function describe(req, tty) {
32466
32541
  };
32467
32542
  return { argv: [req.argv0, ...req.args], tty, started: Date.now(), ...exe };
32468
32543
  }
32544
+ function refuseChanged(approved, now) {
32545
+ const changed = changedFile(approved, now);
32546
+ if (changed !== void 0) throw new ChangedError(changedText(changed));
32547
+ }
32469
32548
  function linksOf2(req) {
32470
32549
  return req.links === void 0 ? {} : { links: req.links };
32471
32550
  }
@@ -33599,6 +33678,27 @@ var Launcher = class {
33599
33678
  }
33600
33679
  return true;
33601
33680
  }
33681
+ async unchanged(planned, file, cwd, grant) {
33682
+ const approved = grant?.files;
33683
+ if (!approved) return {};
33684
+ const files = await this.backingOf(planned, file, cwd, "");
33685
+ refuseChanged(approved, await this.sudoer.digests([file, ...files]));
33686
+ return { pinned: approved };
33687
+ }
33688
+ async pinOf(path, caller) {
33689
+ const { pinned: pinned2 } = caller;
33690
+ if (!pinned2) return {};
33691
+ const real = await this.sudoer.real(path);
33692
+ const hex4 = pinned2[real];
33693
+ if (hex4 !== void 0) return { [real]: hex4 };
33694
+ if (await this.sudoer.trusted(real)) return {};
33695
+ throw new ChangedError(changedText(real));
33696
+ }
33697
+ async pinnedBytes(pin, bytes) {
33698
+ const [real = ""] = Object.keys(pin);
33699
+ refuseChanged(pin, { [real]: await sha2562(bytes) });
33700
+ return bytes;
33701
+ }
33602
33702
  async grantedCwd(caller, cwd) {
33603
33703
  const real = await physicalPathAsync(this.base, cwd).catch(() => void 0);
33604
33704
  return real === cwd && await this.sudoer.reachable(caller, cwd);
@@ -33687,13 +33787,19 @@ var Launcher = class {
33687
33787
  return found;
33688
33788
  }
33689
33789
  async module(path, caller, asked) {
33690
- const key = await this.cacheKey(path, asked);
33790
+ const pin = await this.pinOf(path, caller);
33791
+ const sum = Object.values(pin)[0];
33792
+ const key = sum === void 0 ? await this.cacheKey(path, asked) : `${path}:sha256:${sum}`;
33691
33793
  return this.cached(this.compiled, key, async () => {
33692
- const { bytes, key: read } = await this.keyed(path, caller, asked);
33794
+ const { bytes: read, key: readKey } = await this.keyed(path, caller, asked);
33795
+ const bytes = sum === void 0 ? read : await this.pinnedBytes(pin, read);
33693
33796
  const memory = importedMemory(bytes);
33694
33797
  const foreign = foreignImports(bytes);
33695
33798
  const compiled = await WebAssembly.compile(bytes);
33696
- return { value: { module: compiled, ...memory ? { memory } : {}, foreign }, key: read };
33799
+ return {
33800
+ value: { module: compiled, ...memory ? { memory } : {}, foreign },
33801
+ key: sum === void 0 ? readKey : key
33802
+ };
33697
33803
  });
33698
33804
  }
33699
33805
  async checkedSources(planned, caller) {
@@ -33718,6 +33824,11 @@ var Launcher = class {
33718
33824
  }
33719
33825
  }
33720
33826
  async text(path, caller, asked) {
33827
+ const pin = await this.pinOf(path, caller);
33828
+ if (Object.keys(pin).length > 0) {
33829
+ const { bytes } = await this.keyed(path, caller, asked);
33830
+ return new TextDecoder().decode(await this.pinnedBytes(pin, bytes));
33831
+ }
33721
33832
  if (!this.integrity) return this.base.readFile(path);
33722
33833
  const key = await this.cacheKey(path, asked);
33723
33834
  return this.cached(this.texts, key, async () => {
@@ -33906,7 +34017,12 @@ var Launcher = class {
33906
34017
  const own = env.HOSTNAME;
33907
34018
  if (asked.target.abi === "wasi") env.HOSTNAME = this.hostname;
33908
34019
  const planned = chosen2(asked, env);
33909
- const caller = { pid: req.ppid ?? 0, uid: req.cred.euid, tag: req.tags?.[0] };
34020
+ const caller = {
34021
+ pid: req.ppid ?? 0,
34022
+ uid: req.cred.euid,
34023
+ tag: req.tags?.[0],
34024
+ pinned: req.pinned
34025
+ };
33910
34026
  if (this.integrity) await this.checkedSources(planned, caller);
33911
34027
  const [program, exeId] = await this.loaded(planned.target, env, caller);
33912
34028
  if (readsEtcHostname(program)) setOrDrop(env, "HOSTNAME", own);
@@ -34421,6 +34537,7 @@ var Launcher = class {
34421
34537
  const scope = grant ? "" : await this.scopeOf(cred.euid);
34422
34538
  const file = booked === void 0 ? req.file : await this.ticketFile(req, fds);
34423
34539
  const planned = await this.checkedPlan({ ...req, file }, { ppid, cred, scope }, fds);
34540
+ const pin = await this.unchanged(planned, file, req.cwd, grant);
34424
34541
  const { recheck, ...redeemed } = await this.redeemed(
34425
34542
  { ...req, file },
34426
34543
  { ppid, cred, scope, booked },
@@ -34432,6 +34549,7 @@ var Launcher = class {
34432
34549
  const replan = () => this.planOrSearch({ ...req, file }, { ppid, cred, scope });
34433
34550
  const again = { ...planned, replan, recheck };
34434
34551
  const handle = await this.launch(again, {
34552
+ ...pin,
34435
34553
  ...deciding ? { decided: deciding.promise } : {},
34436
34554
  cwd: req.cwd,
34437
34555
  cwdFrom: inheritedCwd(req.chdir, this.cwdSlots.get(ppid)),