@sjawhar/pi-legion-envoy 5.17.3 → 5.17.5

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.
Files changed (2) hide show
  1. package/dist/legion.js +723 -91
  2. package/package.json +1 -1
package/dist/legion.js CHANGED
@@ -33473,7 +33473,7 @@ import { logger } from "@oh-my-pi/pi-utils";
33473
33473
  // package.json
33474
33474
  var package_default = {
33475
33475
  name: "@sjawhar/pi-legion-envoy",
33476
- version: "5.17.3",
33476
+ version: "5.17.5",
33477
33477
  type: "module",
33478
33478
  omp: {
33479
33479
  extensions: [
@@ -34776,8 +34776,11 @@ function createGoLegionTool(deps) {
34776
34776
  import {
34777
34777
  closeSync,
34778
34778
  existsSync,
34779
+ lstatSync,
34780
+ opendirSync,
34779
34781
  openSync,
34780
34782
  readFileSync as readFileSync3,
34783
+ readlinkSync,
34781
34784
  readSync,
34782
34785
  realpathSync,
34783
34786
  statSync
@@ -41435,9 +41438,15 @@ function isUnset(value) {
41435
41438
  }
41436
41439
  var NO_ELEMENTS = { kind: "literal", text: "", noElements: true };
41437
41440
  function stored(value) {
41438
- if (!value.some((piece) => piece.noElements === true))
41439
- return value;
41440
- return value.map((piece) => piece.noElements === true ? literal2("") : piece);
41441
+ const counted = value.map((piece) => {
41442
+ if (piece.unknownCount !== true)
41443
+ return piece;
41444
+ const { unknownCount: _, ...rest } = piece;
41445
+ return rest;
41446
+ });
41447
+ if (!counted.some((piece) => piece.noElements === true))
41448
+ return counted;
41449
+ return counted.map((piece) => piece.noElements === true ? literal2("") : piece);
41441
41450
  }
41442
41451
  var ANY_NAME = "\x00";
41443
41452
  var IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
@@ -41477,7 +41486,7 @@ function wrapRefusal(error48, site, label) {
41477
41486
  }
41478
41487
  var MAX_DEPTH = 8;
41479
41488
  var MAX_WALK_STEPS = 1e5;
41480
- var WALK_LIMIT = `the guard reached its walk limit of ${MAX_WALK_STEPS} nodes; run a smaller script`;
41489
+ var WALK_LIMIT = `the guard reached its walk limit of ${MAX_WALK_STEPS} steps (syntax nodes, pattern matching, and the destination entries a copy inspects); run a smaller script, or copy into a smaller destination`;
41481
41490
  var MAX_ALTERNATIVES = 64;
41482
41491
  var MAX_PATTERN_VALUE = 512;
41483
41492
  var PATTERN_WORK_PER_STEP = 1024;
@@ -41500,6 +41509,10 @@ var FILE_COMMANDS = new Set([
41500
41509
  "rm",
41501
41510
  "unlink",
41502
41511
  "mv",
41512
+ "cp",
41513
+ "dd",
41514
+ "install",
41515
+ "ln",
41503
41516
  "shred",
41504
41517
  "truncate",
41505
41518
  "chmod",
@@ -41612,6 +41625,70 @@ function realish(abs, followFinal) {
41612
41625
  return realExisting(resolved);
41613
41626
  return path4.join(realExisting(path4.dirname(resolved)), path4.basename(resolved));
41614
41627
  }
41628
+ function readlinkTarget(link) {
41629
+ try {
41630
+ return readlinkSync(link);
41631
+ } catch {
41632
+ return "a target the guard cannot read";
41633
+ }
41634
+ }
41635
+ function physical(cwd, text, followFinal) {
41636
+ const joined = text.startsWith("/") ? text : `${cwd}/${text}`;
41637
+ const parts = joined.split("/");
41638
+ const last = parts.findLastIndex((part) => part !== "" && part !== ".");
41639
+ const pending = [];
41640
+ let real = "/";
41641
+ for (let i = 0;i < parts.length; i += 1) {
41642
+ const part = parts[i];
41643
+ if (part === "" || part === ".")
41644
+ continue;
41645
+ if (pending.length > 0) {
41646
+ if (part === "..") {
41647
+ return {
41648
+ unknown: `\`${path4.join(real, ...pending)}\`, which does not exist yet, so the guard ` + "cannot know where the `..` after it leads"
41649
+ };
41650
+ }
41651
+ pending.push(part);
41652
+ continue;
41653
+ }
41654
+ if (part === "..") {
41655
+ real = path4.dirname(real);
41656
+ continue;
41657
+ }
41658
+ const candidate = real === "/" ? `/${part}` : `${real}/${part}`;
41659
+ if (!followFinal && i === last) {
41660
+ return { real: candidate };
41661
+ }
41662
+ let link;
41663
+ try {
41664
+ link = lstatSync(candidate).isSymbolicLink();
41665
+ } catch (error48) {
41666
+ if (error48.code !== "ENOENT") {
41667
+ return { unknown: `\`${candidate}\`, which the guard cannot read (${messageFor(error48)})` };
41668
+ }
41669
+ pending.push(part);
41670
+ continue;
41671
+ }
41672
+ if (!link) {
41673
+ real = candidate;
41674
+ continue;
41675
+ }
41676
+ try {
41677
+ real = realpathSync(candidate);
41678
+ } catch (error48) {
41679
+ const code = error48.code;
41680
+ const why = code === "ENOENT" ? `points at \`${readlinkTarget(candidate)}\`, which is not there` : `cannot be followed (${messageFor(error48)})`;
41681
+ return { unknown: `the link \`${candidate}\` ${why}, so the guard cannot resolve this path` };
41682
+ }
41683
+ }
41684
+ return { real: pending.length === 0 ? real : path4.join(real, ...pending) };
41685
+ }
41686
+ function kernelPath(from, text) {
41687
+ if (from === undefined && !text.startsWith("/"))
41688
+ return;
41689
+ const resolved = physical(from ?? "/", text, true);
41690
+ return "unknown" in resolved ? undefined : resolved.real;
41691
+ }
41615
41692
  function isProtectedScratchComponent(component, roots, partial2 = false) {
41616
41693
  return roots.protectedScratch.some((name) => component.startsWith(name) || partial2 && name.startsWith(component)) || roots.protectedDirectories.some((name) => component === name || partial2 && name.startsWith(component));
41617
41694
  }
@@ -41644,12 +41721,16 @@ function judgePath(exp, st, ctx, options) {
41644
41721
  resolution: `relative to a working directory unknown after ${st.cwdWhy}`
41645
41722
  };
41646
41723
  }
41647
- const abs = path4.resolve(st.cwd ?? "/", text);
41648
- const real2 = realish(abs, options.follow || text.endsWith("/"));
41724
+ const resolved2 = physical(st.cwd ?? "/", text, options.follow || text.endsWith("/"));
41725
+ if ("unknown" in resolved2) {
41726
+ return { ok: false, resolution: resolved2.unknown, unresolved: true };
41727
+ }
41728
+ const real2 = resolved2.real;
41649
41729
  if (inWorkspace(real2, roots) || inScratch(real2, roots))
41650
41730
  return { ok: true };
41651
41731
  if (options.overwrite && !existsSync(real2))
41652
41732
  return { ok: true };
41733
+ const abs = path4.resolve(st.cwd ?? "/", text);
41653
41734
  return { ok: false, resolution: real2 === abs ? real2 : `${abs}, which resolves to ${real2}` };
41654
41735
  }
41655
41736
  const piece = exp[open];
@@ -41664,11 +41745,13 @@ function judgePath(exp, st, ctx, options) {
41664
41745
  return { ok: false, resolution: `relative to a working directory unknown after ${st.cwdWhy}` };
41665
41746
  }
41666
41747
  const component = prefix.slice(slash + 1);
41667
- let stem = path4.resolve(st.cwd ?? "/", slash === -1 ? "." : slash === 0 ? "/" : prefix.slice(0, slash));
41668
- if (piece.kind === "glob" && (component === "" ? piece.text : component).startsWith(".")) {
41669
- stem = path4.dirname(stem);
41748
+ const stemText = slash === -1 ? "." : slash === 0 ? "/" : prefix.slice(0, slash);
41749
+ const resolved = physical(st.cwd ?? "/", stemText, true);
41750
+ if ("unknown" in resolved) {
41751
+ return { ok: false, resolution: resolved.unknown, unresolved: true };
41670
41752
  }
41671
- const real = realish(stem, true);
41753
+ const dotGlob = piece.kind === "glob" && (component === "" ? piece.text : component).startsWith(".");
41754
+ const real = dotGlob ? path4.dirname(resolved.real) : resolved.real;
41672
41755
  const allowed = inWorkspace(real, roots) || inScratch(real, roots) || real === roots.scratch && component !== "" && !isProtectedScratchComponent(component, roots, true);
41673
41756
  if (allowed)
41674
41757
  return { ok: true };
@@ -41804,8 +41887,10 @@ function resolveArrayIndex(index, st, ctx) {
41804
41887
  }
41805
41888
  function parameter(name, quoted, st, ctx) {
41806
41889
  if (name === "@" || name === "*") {
41807
- if (st.positional === undefined)
41808
- return [[unknown2(`\`$${name}\` (the positional parameters)`)]];
41890
+ if (st.positional === undefined) {
41891
+ const piece = unknown2(`\`$${name}\` (the positional parameters)`);
41892
+ return [[name === "*" && quoted ? piece : { ...piece, unknownCount: true }]];
41893
+ }
41809
41894
  return elements(st.positional, name === "*" && quoted);
41810
41895
  }
41811
41896
  if (/^[0-9]+$/.test(name)) {
@@ -41880,11 +41965,14 @@ function parameterExpansion(part, quoted, st, ctx) {
41880
41965
  if (part.index === "@" || part.index === "*") {
41881
41966
  if (array2 !== undefined) {
41882
41967
  const unknownElement = array2.get(UNKNOWN_ARRAY_INDEX);
41883
- if (unknownElement !== undefined)
41884
- return [[...unknownElement]];
41968
+ if (unknownElement !== undefined) {
41969
+ if (part.index === "*" && quoted)
41970
+ return [[...unknownElement]];
41971
+ return [unknownElement.map((piece) => ({ ...piece, unknownCount: true }))];
41972
+ }
41885
41973
  return elements([...array2.values()], part.index === "*" && quoted);
41886
41974
  }
41887
- return parameter(name, quoted, st, ctx);
41975
+ return parameter(name, quoted, st, ctx).map((alt) => !(part.index === "*" && quoted) && alt.some((piece) => piece.kind === "unknown") ? alt.map((piece) => ({ ...piece, unknownCount: true })) : alt);
41888
41976
  }
41889
41977
  if (part.index !== undefined) {
41890
41978
  const index = resolveArrayIndex(part.index, st, ctx);
@@ -41997,12 +42085,13 @@ function patternExpansion(part, quoted, st, ctx) {
41997
42085
  return [unquotedLiteral(result.replaceAll("\\", "\\\\"), undefined, st, ctx)];
41998
42086
  }
41999
42087
  function substitution(script, text, st, ctx) {
42000
- const pidRead = /^\$\(<(.+)\)$/.exec(text);
42001
- const variableRead = /^\$\(<"?\$([A-Za-z_][A-Za-z0-9_]*)"?\)$/.exec(text);
42088
+ const pidRead = /^\$\(<\s*(.+?)\s*\)$/.exec(text);
42089
+ const variableRead = /^\$\(<\s*"?\$([A-Za-z_][A-Za-z0-9_]*)"?\s*\)$/.exec(text);
42002
42090
  const directFile = pidRead?.[1]?.includes("$") === true ? undefined : pidRead?.[1];
42003
42091
  const file2 = directFile ?? (variableRead === null ? undefined : literalText(lookup(variableRead[1], st, ctx)));
42004
- if (file2 !== undefined && st.cwd !== undefined) {
42005
- const pid = st.pidFiles.get(path4.resolve(st.cwd, file2));
42092
+ const pidFile = file2 === undefined ? undefined : kernelPath(st.cwd, file2);
42093
+ if (pidFile !== undefined) {
42094
+ const pid = st.pidFiles.get(pidFile);
42006
42095
  if (pid !== undefined)
42007
42096
  return [[...pid]];
42008
42097
  }
@@ -42056,22 +42145,70 @@ function substitutionInvocation(node, st, ctx) {
42056
42145
  return;
42057
42146
  return { base: path4.basename(name), command: node, rest: argv.slice(1) };
42058
42147
  }
42148
+ function gnuDirname(text) {
42149
+ const trimmed = text.replace(/\/+$/, "");
42150
+ if (trimmed === "")
42151
+ return "/";
42152
+ const cut = trimmed.replace(/\/+[^/]+$/, "");
42153
+ if (cut === trimmed)
42154
+ return ".";
42155
+ return cut === "" ? "/" : cut;
42156
+ }
42157
+ function gnuBasename(text) {
42158
+ const trimmed = text.replace(/\/+$/, "");
42159
+ if (trimmed === "")
42160
+ return text === "" ? "" : "/";
42161
+ return trimmed.slice(trimmed.lastIndexOf("/") + 1);
42162
+ }
42059
42163
  function substitutionPath(base, list, st) {
42060
42164
  if (!["dirname", "basename", "realpath", "readlink"].includes(base))
42061
42165
  return;
42062
- const target = literalText(operands(list, "").operands.at(-1)?.exp);
42063
- if (target === undefined || st.cwd === undefined)
42166
+ const found = operands(list, "");
42167
+ if (base === "basename" && found.options.some((option) => option === "--multiple" || option.startsWith("--suffix") || /^-[^-]*[as]/.test(option))) {
42168
+ return;
42169
+ }
42170
+ const suffixed = base === "basename" && found.operands.length === 2;
42171
+ if (found.operands.length !== 1 && !suffixed)
42172
+ return;
42173
+ const target = literalText(found.operands[0]?.exp);
42174
+ const suffix = suffixed ? literalText(found.operands[1]?.exp) : "";
42175
+ if (target === undefined || suffix === undefined || st.cwd === undefined)
42064
42176
  return;
42065
42177
  if (target === "")
42066
42178
  return base === "dirname" ? "." : "";
42067
- const abs = path4.resolve(st.cwd, target);
42068
42179
  if (base === "dirname")
42069
- return path4.dirname(abs);
42070
- if (base === "basename")
42071
- return path4.basename(abs);
42072
- if (base === "readlink" && !list.some((arg) => literalText(arg.exp)?.includes("f")))
42073
- return;
42074
- return realExisting(abs);
42180
+ return gnuDirname(target);
42181
+ if (base === "basename") {
42182
+ const name = gnuBasename(target);
42183
+ return suffix !== "" && name !== suffix && name.endsWith(suffix) ? name.slice(0, -suffix.length) : name;
42184
+ }
42185
+ if (base === "readlink") {
42186
+ const mode = lastModeFlag(found.options, "fem", {
42187
+ "--canonicalize": "f",
42188
+ "--canonicalize-existing": "e",
42189
+ "--canonicalize-missing": "m"
42190
+ });
42191
+ if (mode !== "f" && mode !== "m")
42192
+ return;
42193
+ }
42194
+ if (base === "realpath") {
42195
+ if (found.options.some((option) => option.startsWith("--relative")))
42196
+ return;
42197
+ const mode = lastModeFlag(found.options, "sLP", {
42198
+ "--strip": "s",
42199
+ "--no-symlinks": "s",
42200
+ "--logical": "L",
42201
+ "--physical": "P"
42202
+ });
42203
+ if (mode === "s" || mode === "L") {
42204
+ const here = kernelPath("/", st.cwd);
42205
+ if (here === undefined)
42206
+ return;
42207
+ const lexical = path4.resolve(here, target);
42208
+ return mode === "s" ? lexical : kernelPath("/", lexical);
42209
+ }
42210
+ }
42211
+ return kernelPath(st.cwd, target);
42075
42212
  }
42076
42213
  function mktempPath(command, st, ctx) {
42077
42214
  const words = command.suffix;
@@ -42090,7 +42227,7 @@ function mktempPath(command, st, ctx) {
42090
42227
  else if (value.startsWith("-p") && value.length > 2)
42091
42228
  directory = [literal2(value.slice(2))];
42092
42229
  else if (!value.startsWith("-") && value.includes("/")) {
42093
- directory = [literal2(path4.resolve(st.cwd ?? "/", path4.dirname(value)))];
42230
+ directory = [literal2(path4.dirname(value))];
42094
42231
  }
42095
42232
  }
42096
42233
  const base = directory ?? [literal2(ctx.env.TMPDIR ?? "/tmp")];
@@ -42103,10 +42240,18 @@ function braceAlternatives(part) {
42103
42240
  return [[{ kind: "glob", text: part.text }]];
42104
42241
  return inner.split(",").map((alternative) => [literal2(alternative)]);
42105
42242
  }
42243
+ function tildeAt(text) {
42244
+ if (text.startsWith("~"))
42245
+ return 0;
42246
+ const equals = text.indexOf("=");
42247
+ if (equals <= 0 || !IDENTIFIER.test(text.slice(0, equals)))
42248
+ return;
42249
+ return text.charAt(equals + 1) === "~" ? equals + 1 : undefined;
42250
+ }
42106
42251
  function expandPart(part, first, st, ctx) {
42107
42252
  switch (part.type) {
42108
42253
  case "Literal":
42109
- return [unquotedLiteral(part.text, first ? 0 : undefined, st, ctx)];
42254
+ return [unquotedLiteral(part.text, first ? tildeAt(part.text) : undefined, st, ctx)];
42110
42255
  case "SingleQuoted":
42111
42256
  case "AnsiCQuoted":
42112
42257
  return [[literal2(part.value)]];
@@ -42162,7 +42307,7 @@ function textLength(pieces) {
42162
42307
  function expandWord(word, st, ctx) {
42163
42308
  let alternatives = [[]];
42164
42309
  if (word.parts === undefined)
42165
- alternatives = [unquotedLiteral(word.text, 0, st, ctx)];
42310
+ alternatives = [unquotedLiteral(word.text, tildeAt(word.text), st, ctx)];
42166
42311
  else {
42167
42312
  word.parts.forEach((part, index) => {
42168
42313
  alternatives = product(alternatives, expandPart(part, index === 0, st, ctx));
@@ -42188,6 +42333,9 @@ function args(words, st, ctx) {
42188
42333
  if (exp.some((piece) => piece.kind === "glob")) {
42189
42334
  return { text: word.text, exp, fields: "unknown" };
42190
42335
  }
42336
+ if (exp.some((piece) => piece.unknownCount === true)) {
42337
+ return { text: word.text, exp, fields: "unknown" };
42338
+ }
42191
42339
  if (unquoted && exp.some((piece) => piece.kind === "unknown" || /[ \t\n]/.test(piece.text))) {
42192
42340
  return { text: word.text, exp, fields: "unknown" };
42193
42341
  }
@@ -42227,6 +42375,9 @@ function mayBeOption(arg, matches) {
42227
42375
  const { text, whole } = readableWord(arg);
42228
42376
  if (whole)
42229
42377
  return matches(text);
42378
+ return mayBeSomeOption(text);
42379
+ }
42380
+ function mayBeSomeOption(text) {
42230
42381
  return text === "" || /^--?[a-zA-Z-]*$/.test(text);
42231
42382
  }
42232
42383
  function valueInWord(arg, count) {
@@ -42522,6 +42673,10 @@ function modelPidFile(st, ctx, file2, pid) {
42522
42673
  model.set(file2, [unknown2("a pid a branch or loop body wrote to this file", true)]);
42523
42674
  }
42524
42675
  }
42676
+ function forgetFileModels(st, ctx, why) {
42677
+ st.pidFiles.clear();
42678
+ ctx.unnameableWrite.why ??= why;
42679
+ }
42525
42680
  function handlerVars(trap, vars) {
42526
42681
  const { vars: branch, merged } = trap;
42527
42682
  if (branch === undefined || merged === undefined)
@@ -42682,6 +42837,15 @@ function spendPatternWork(work, ctx) {
42682
42837
  function outputChanged(before, after) {
42683
42838
  return before !== undefined && (after === undefined || JSON.stringify(before) !== JSON.stringify(after));
42684
42839
  }
42840
+ function loopValue(type, name, words) {
42841
+ const pids = words.every((word) => {
42842
+ const text = literalText(word.exp);
42843
+ return text === "" || word.exp.length > 0 && word.exp.every((piece) => piece.descendantPid);
42844
+ });
42845
+ if (pids)
42846
+ return [unknown2(`\`$${name}\`, a loop variable`, true)];
42847
+ return type === "For" && words.length === 1 && words[0]?.exp[0]?.kind === "literal" && words[0].exp[0].text !== "" && !words[0].exp[0].text.startsWith("-") && words[0]?.exp.some((piece) => piece.kind === "glob") ? words[0].exp : [unknown2(`\`$${name}\`, a loop variable`)];
42848
+ }
42685
42849
  function walkNode(node, st, ctx, pipeIn) {
42686
42850
  if (++ctx.steps.count > MAX_WALK_STEPS) {
42687
42851
  const site = siteOf(node, st);
@@ -42805,11 +42969,7 @@ function walkNode(node, st, ctx, pipeIn) {
42805
42969
  }
42806
42970
  } else {
42807
42971
  const body = clone2(st);
42808
- const signalSafe = words.every((word) => {
42809
- const text = literalText(word.exp);
42810
- return text === "" || word.exp.length > 0 && word.exp.every((piece) => piece.descendantPid);
42811
- });
42812
- assignScalar(body, name, signalSafe ? [unknown2(`\`$${name}\`, a loop variable`, true)] : [unknown2(`\`$${name}\`, a loop variable`)]);
42972
+ assignScalar(body, name, loopValue(node.type, name, words));
42813
42973
  if (node.type === "Select")
42814
42974
  assignScalar(body, "REPLY", [unknown2("`$REPLY`, read from input")]);
42815
42975
  uncertainly(ctx, () => walkList(node.body.commands, body, ctx));
@@ -42908,13 +43068,15 @@ function checkRedirects(redirects, command, site, st, ctx) {
42908
43068
  if (writes || operator === ">&") {
42909
43069
  const verdict = judgePath(exp, st, ctx, { follow: true, overwrite: true });
42910
43070
  if (!verdict.ok) {
42911
- throw refusal(site, `the redirection \`${operator}\` would overwrite \`${redirect.target.text}\` (${verdict.resolution})`, ctx);
43071
+ throw refusal(site, `the redirection \`${operator}\` would overwrite \`${redirect.target.text}\` (${verdict.resolution})`, ctx, verdict.unresolved);
42912
43072
  }
42913
43073
  }
42914
43074
  const target = literalText(exp);
42915
- if (target === undefined || st.cwd === undefined && !target.startsWith("/"))
43075
+ const file2 = target === undefined ? undefined : kernelPath(st.cwd, target);
43076
+ if (file2 === undefined) {
43077
+ forgetFileModels(st, ctx, `the redirection \`${operator} ${redirect.target.text}\``);
42916
43078
  continue;
42917
- const file2 = path4.resolve(st.cwd ?? "/", target);
43079
+ }
42918
43080
  let content = null;
42919
43081
  if (command?.name === "cat" && command.args.length === 0) {
42920
43082
  if (heredoc !== undefined)
@@ -42948,9 +43110,9 @@ function groupOutputFile(statement, st, ctx) {
42948
43110
  }
42949
43111
  const targets = expandWord(redirect.target, st, ctx);
42950
43112
  const target = targets.length === 1 ? literalText(targets[0]) : undefined;
42951
- if (target === undefined || st.cwd === undefined && !target.startsWith("/"))
43113
+ if (target === undefined)
42952
43114
  return;
42953
- return path4.resolve(st.cwd ?? "/", target);
43115
+ return kernelPath(st.cwd, target);
42954
43116
  }
42955
43117
  function walkRenderingGroup(group, st, ctx) {
42956
43118
  let content = "";
@@ -43069,8 +43231,10 @@ function printfText(list, limit, script = false) {
43069
43231
  } while (conversions > 0 && index < values.length);
43070
43232
  return out;
43071
43233
  }
43072
- function refusal(site, detail, ctx) {
43073
- return new Refusal(site.snippet, site.line, `${detail}, outside ${describeRoots(ctx.roots)}. ${HINT}`);
43234
+ function refusal(site, detail, ctx, unresolved = false) {
43235
+ const where = unresolved ? `. Every target is resolved as the kernel resolves it, against ${describeRoots(ctx.roots)}` : `, outside ${describeRoots(ctx.roots)}`;
43236
+ const hint = unresolved ? "Make the unresolved path component resolvable in an earlier command \u2014 create it, make " + "it readable, or fix a dangling or looping symlink \u2014 then run this command." : HINT;
43237
+ return new Refusal(site.snippet, site.line, `${detail}${where}. ${hint}`);
43074
43238
  }
43075
43239
  function handleCommand(command, st, ctx, pipeIn) {
43076
43240
  const site = siteOf(command, st);
@@ -43168,6 +43332,16 @@ function operands(list, valued, longValued = []) {
43168
43332
  }
43169
43333
  return { options, operands: found };
43170
43334
  }
43335
+ function lastModeFlag(options, letters, long = {}) {
43336
+ return options.flatMap((option) => {
43337
+ const named = long[option];
43338
+ if (named !== undefined)
43339
+ return [named];
43340
+ if (!/^-[a-zA-Z]+$/.test(option))
43341
+ return [];
43342
+ return [...option.slice(1)].filter((letter) => letters.includes(letter));
43343
+ }).at(-1);
43344
+ }
43171
43345
  function unwrap2(argv, st, ctx, site) {
43172
43346
  const alts = [];
43173
43347
  let list = argv;
@@ -43251,10 +43425,10 @@ function unwrap2(argv, st, ctx, site) {
43251
43425
  i += 1;
43252
43426
  } else if (text === "-C" || text === "--chdir") {
43253
43427
  const dir = literalText(rest[i + 1]?.exp);
43254
- cwd = dir === undefined || state.cwd === undefined ? undefined : path4.resolve(state.cwd, dir);
43428
+ cwd = dir === undefined ? undefined : kernelPath(state.cwd, dir);
43255
43429
  i += 1;
43256
43430
  } else if (text?.startsWith("--chdir=")) {
43257
- cwd = state.cwd === undefined ? undefined : path4.resolve(state.cwd, text.slice(8));
43431
+ cwd = kernelPath(state.cwd, text.slice(8));
43258
43432
  } else if (text === "-S" || text === "--split-string") {
43259
43433
  const split = literalText(rest[i + 1]?.exp);
43260
43434
  const tail = rest.slice(i + 2);
@@ -43421,10 +43595,25 @@ function dispatch(invocation, outer, ctx) {
43421
43595
  outer.output = rest.length === 1 && rest[0] !== undefined ? [...outer.output, rest[0].exp] : undefined;
43422
43596
  }
43423
43597
  return;
43424
- case "cd":
43598
+ case "cd": {
43599
+ const found = operands(rest, "");
43600
+ if (found.options.some((option) => !/^-[LPe]+$/.test(option))) {
43601
+ outer.cwd = undefined;
43602
+ outer.cwdWhy = "`cd` with an option the guard does not model";
43603
+ return;
43604
+ }
43605
+ changeDirectory(found.operands[0], lastModeFlag(found.options, "LP") === "P", outer, ctx);
43606
+ return;
43607
+ }
43425
43608
  case "pushd": {
43426
- const target = operands(rest, "").operands[0];
43427
- changeDirectory(target, outer, ctx);
43609
+ const found = operands(rest, "");
43610
+ const operand = literalText(found.operands[0]?.exp);
43611
+ if (found.options.length > 0 || found.operands[0] === undefined || operand?.startsWith("+") === true) {
43612
+ outer.cwd = undefined;
43613
+ outer.cwdWhy = "`pushd` with a directory stack or option the guard does not model";
43614
+ return;
43615
+ }
43616
+ changeDirectory(found.operands[0], false, outer, ctx);
43428
43617
  return;
43429
43618
  }
43430
43619
  case "popd":
@@ -43624,24 +43813,78 @@ function dispatch(invocation, outer, ctx) {
43624
43813
  return;
43625
43814
  case "rm":
43626
43815
  case "unlink":
43627
- checkTargets(base, "delete", operands(rest, "").operands, false, st, ctx, site);
43816
+ checkTargets(base, "delete", operands(rest, "").operands, ACTS_ON_LINK, st, ctx, site);
43628
43817
  return;
43629
43818
  case "mv": {
43630
- const found = operands(rest, "tS", ["--target-directory", "--suffix"]);
43631
- const targets = [...found.operands];
43632
- const at = rest.findIndex((arg) => {
43633
- const text = literalText(arg.exp);
43634
- return text === "-t" || text === "--target-directory";
43635
- });
43636
- if (at !== -1 && rest[at + 1] !== undefined)
43637
- targets.push(rest[at + 1]);
43819
+ const found = writeArguments(rest, "tS", MV_OPTIONS);
43820
+ if (found.flags.has("--help") || found.flags.has("--version"))
43821
+ return;
43822
+ const targets = [...found.operands, ...writeDestinations(found, site)];
43823
+ checkTargets("mv", "move", targets, ACTS_ON_LINK, st, ctx, site);
43824
+ return;
43825
+ }
43826
+ case "cp": {
43827
+ const found = writeArguments(rest, "tS", CP_OPTIONS);
43828
+ if (found.flags.has("--help") || found.flags.has("--version"))
43829
+ return;
43830
+ const unlinks = found.flags.has("b") || found.flags.has("--backup") || found.flags.has("--remove-destination") || found.uncertain;
43831
+ const destinations = writeDestinations(found, site);
43832
+ checkTargets("cp", "overwrite", destinations, unlinks ? WRITES_EITHER : WRITES_THROUGH_LINK, st, ctx, site);
43833
+ checkCopyContents(found, destinations, st, ctx, site);
43834
+ return;
43835
+ }
43836
+ case "install": {
43837
+ const found = writeArguments(rest, "mogtS", INSTALL_OPTIONS);
43838
+ if (found.flags.has("--help") || found.flags.has("--version"))
43839
+ return;
43840
+ const directories = found.flags.has("d") || found.flags.has("--directory");
43841
+ checkTargets("install", directories ? "create or change the mode of" : "overwrite", directories ? found.operands : writeDestinations(found, site), WRITES_EITHER, st, ctx, site);
43842
+ return;
43843
+ }
43844
+ case "dd": {
43845
+ const targets = [];
43638
43846
  for (const arg of rest) {
43639
- const text = literalText(arg.exp);
43640
- if (text?.startsWith("--target-directory=")) {
43641
- targets.push({ text: arg.text, exp: [literal2(text.slice(19))] });
43847
+ const { text, whole } = readableWord(arg);
43848
+ if (whole && longOption(text, ["--help", "--version"]) !== undefined)
43849
+ return;
43850
+ if (text.startsWith("of=")) {
43851
+ const carried = valueInWord(arg, 3);
43852
+ targets.push({
43853
+ text: arg.text,
43854
+ exp: carried ?? [unknown2("an `of=` operand the guard cannot read")]
43855
+ });
43856
+ continue;
43642
43857
  }
43858
+ if (!whole && "of=".startsWith(text)) {
43859
+ targets.push({
43860
+ text: arg.text,
43861
+ exp: [unknown2("a `dd` operand the guard cannot read, which may name its `of=` file")]
43862
+ });
43863
+ }
43864
+ }
43865
+ checkTargets("dd", "write", targets, WRITES_THROUGH_LINK, st, ctx, site);
43866
+ return;
43867
+ }
43868
+ case "ln": {
43869
+ const found = writeArguments(rest, "St", LN_OPTIONS);
43870
+ if (found.flags.has("--help") || found.flags.has("--version"))
43871
+ return;
43872
+ const sole = found.operands[0];
43873
+ const single = found.operands.length === 1 && found.directory === undefined && !found.uncertain && sole !== undefined && readableWord(sole).whole;
43874
+ const destinations = single ? [{ text: ".", exp: [literal2(".")] }] : writeDestinations(found, site);
43875
+ checkTargets("ln", "overwrite", destinations, WRITES_EITHER, st, ctx, site);
43876
+ const symbolic = found.flags.has("s") || found.flags.has("--symbolic");
43877
+ if (!symbolic) {
43878
+ const sources = found.operands.filter((arg) => !destinations.includes(arg));
43879
+ checkTargets("ln", "hard-link", sources, LINKS_TO, st, ctx, site);
43643
43880
  }
43644
- checkTargets("mv", "move", targets, false, st, ctx, site);
43881
+ return;
43882
+ }
43883
+ case "sed": {
43884
+ const writes = sedWrites(rest);
43885
+ const program = writes.inferred ? "sed, whose option word the guard cannot read and which may be `-i`," : "sed -i";
43886
+ checkTargets(program, "rewrite in place", writes.replaced, WRITES_LINK, st, ctx, site);
43887
+ checkTargets(program, "rewrite in place", writes.followed, WRITES_EITHER, st, ctx, site);
43645
43888
  return;
43646
43889
  }
43647
43890
  case "rsync": {
@@ -43663,9 +43906,9 @@ function dispatch(invocation, outer, ctx) {
43663
43906
  "--remote-option",
43664
43907
  "--rsync-path",
43665
43908
  "--temp-dir"
43666
- ]).operands.at(-1);
43909
+ ]).operands.filter(passesAnArgument).at(-1);
43667
43910
  if (destination !== undefined) {
43668
- checkTargets("rsync", "synchronize and delete into", [destination], true, st, ctx, site);
43911
+ checkTargets("rsync", "synchronize and delete into", [destination], ACTS_THROUGH_LINK, st, ctx, site);
43669
43912
  }
43670
43913
  return;
43671
43914
  }
@@ -43697,14 +43940,14 @@ function dispatch(invocation, outer, ctx) {
43697
43940
  targetDirectories.push(target);
43698
43941
  }
43699
43942
  }
43700
- checkTargets(base, "extract and overwrite in", targetDirectories, true, st, ctx, site);
43943
+ checkTargets(base, "extract and overwrite in", targetDirectories, ACTS_THROUGH_LINK, st, ctx, site);
43701
43944
  return;
43702
43945
  }
43703
43946
  case "shred":
43704
- checkTargets("shred", "shred", operands(rest, "ns", ["--iterations", "--size", "--random-source"]).operands, false, st, ctx, site);
43947
+ checkTargets("shred", "shred", operands(rest, "ns", ["--iterations", "--size", "--random-source"]).operands, ACTS_ON_BOTH, st, ctx, site);
43705
43948
  return;
43706
43949
  case "truncate":
43707
- checkTargets("truncate", "truncate", operands(rest, "sr", ["--size", "--reference"]).operands, true, st, ctx, site);
43950
+ checkTargets("truncate", "truncate", operands(rest, "sr", ["--size", "--reference"]).operands, ACTS_THROUGH_LINK, st, ctx, site);
43708
43951
  return;
43709
43952
  case "chmod":
43710
43953
  case "chown":
@@ -43722,16 +43965,18 @@ function dispatch(invocation, outer, ctx) {
43722
43965
  if (!append) {
43723
43966
  const verdict = judgePath(arg.exp, st, ctx, { follow: true, overwrite: true });
43724
43967
  if (!verdict.ok) {
43725
- throw refusal(site, `tee would overwrite \`${arg.text}\` (${verdict.resolution})`, ctx);
43968
+ throw refusal(site, `tee would overwrite \`${arg.text}\` (${verdict.resolution})`, ctx, verdict.unresolved);
43726
43969
  }
43727
43970
  }
43728
43971
  const target = literalText(arg.exp);
43729
- if (target !== undefined && st.cwd !== undefined) {
43730
- const file2 = path4.resolve(st.cwd, target);
43731
- st.pidFiles.delete(file2);
43732
- const text = heredoc === undefined ? null : heredocText(heredoc);
43733
- modelWrite(st, ctx, file2, append ? UNREAD_FILE : text === null ? UNRENDERED_WRITE : readable(text) ?? OVERLONG_WRITE);
43972
+ const file2 = target === undefined ? undefined : kernelPath(st.cwd, target);
43973
+ if (file2 === undefined) {
43974
+ forgetFileModels(st, ctx, `\`tee ${arg.text}\``);
43975
+ continue;
43734
43976
  }
43977
+ st.pidFiles.delete(file2);
43978
+ const text = heredoc === undefined ? null : heredocText(heredoc);
43979
+ modelWrite(st, ctx, file2, append ? UNREAD_FILE : text === null ? UNRENDERED_WRITE : readable(text) ?? OVERLONG_WRITE);
43735
43980
  }
43736
43981
  return;
43737
43982
  }
@@ -43778,8 +44023,8 @@ function dispatch(invocation, outer, ctx) {
43778
44023
  return false;
43779
44024
  const { text, whole } = readableWord(word);
43780
44025
  const compact = text.replace("-", "");
43781
- const reachable = commands.filter((command) => command.replace("-", "").startsWith(compact));
43782
- return whole ? reachable.length === 1 : reachable.length > 0;
44026
+ const normalized = commands.map((command) => command.replace("-", ""));
44027
+ return whole ? completeName(compact, normalized) !== undefined : normalized.some((command) => command.startsWith(compact));
43783
44028
  };
43784
44029
  let killing;
43785
44030
  for (const segment of segments) {
@@ -43856,7 +44101,7 @@ function dispatch(invocation, outer, ctx) {
43856
44101
  runFile(argv[0], rest, childState(st, invocation.overlay), ctx, site, "auto");
43857
44102
  }
43858
44103
  }
43859
- function changeDirectory(target, st, ctx) {
44104
+ function changeDirectory(target, physicalMode, st, ctx) {
43860
44105
  if (target === undefined) {
43861
44106
  const home = literalText(lookup("HOME", st, ctx));
43862
44107
  st.cwd = home;
@@ -43869,7 +44114,13 @@ function changeDirectory(target, st, ctx) {
43869
44114
  st.cwdWhy = `\`cd ${target.text}\``;
43870
44115
  return;
43871
44116
  }
43872
- st.cwd = path4.resolve(st.cwd, text);
44117
+ if (!physicalMode) {
44118
+ st.cwd = path4.resolve(st.cwd, text);
44119
+ return;
44120
+ }
44121
+ st.cwd = kernelPath(st.cwd, text);
44122
+ if (st.cwd === undefined)
44123
+ st.cwdWhy = `\`cd -P ${target.text}\``;
43873
44124
  }
43874
44125
  function declaration(builtin, list, st, ctx, site) {
43875
44126
  const options = list.map((arg) => literalText(arg.exp) ?? "").filter((text) => /^[-+][a-zA-Z]+$/.test(text));
@@ -44023,17 +44274,389 @@ function nameAndValue(arg) {
44023
44274
  }
44024
44275
  return { name, value: undefined };
44025
44276
  }
44026
- function checkTargets(program, verb, targets, follow, st, ctx, site) {
44277
+ var ACTS_ON_LINK = {
44278
+ followed: false,
44279
+ unfollowed: true,
44280
+ overwrite: false
44281
+ };
44282
+ var ACTS_THROUGH_LINK = {
44283
+ followed: true,
44284
+ unfollowed: false,
44285
+ overwrite: false
44286
+ };
44287
+ var ACTS_ON_BOTH = {
44288
+ followed: true,
44289
+ unfollowed: true,
44290
+ overwrite: false
44291
+ };
44292
+ var WRITES_THROUGH_LINK = {
44293
+ followed: true,
44294
+ unfollowed: false,
44295
+ overwrite: true
44296
+ };
44297
+ var WRITES_LINK = {
44298
+ followed: false,
44299
+ unfollowed: true,
44300
+ overwrite: true
44301
+ };
44302
+ var WRITES_EITHER = {
44303
+ followed: true,
44304
+ unfollowed: true,
44305
+ overwrite: true
44306
+ };
44307
+ var LINKS_TO = {
44308
+ followed: true,
44309
+ unfollowed: false,
44310
+ overwrite: true
44311
+ };
44312
+ function checkTargets(program, verb, targets, reach, st, ctx, site) {
44027
44313
  for (const target of targets) {
44028
- const verdict = judgePath(target.exp, st, ctx, { follow, overwrite: false });
44029
- if (!verdict.ok) {
44030
- throw refusal(site, `${program} would ${verb} \`${target.text}\` (${verdict.resolution})`, ctx);
44314
+ for (const follow of [true, false]) {
44315
+ if (!(follow ? reach.followed : reach.unfollowed))
44316
+ continue;
44317
+ const verdict = judgePath(target.exp, st, ctx, { follow, overwrite: reach.overwrite });
44318
+ if (!verdict.ok) {
44319
+ throw refusal(site, `${program} would ${verb} \`${target.text}\` (${verdict.resolution})`, ctx, verdict.unresolved);
44320
+ }
44031
44321
  }
44032
44322
  const text = literalText(target.exp);
44033
- if (text !== undefined && (text.startsWith("/") || st.cwd !== undefined)) {
44034
- st.pidFiles.delete(path4.resolve(st.cwd ?? "/", text));
44323
+ const file2 = text === undefined ? undefined : kernelPath(st.cwd, text);
44324
+ if (file2 !== undefined)
44325
+ st.pidFiles.delete(file2);
44326
+ }
44327
+ }
44328
+ function completeName(prefix, names) {
44329
+ if (names.includes(prefix))
44330
+ return prefix;
44331
+ const reachable = names.filter((name) => name.startsWith(prefix));
44332
+ return reachable.length === 1 ? reachable[0] : undefined;
44333
+ }
44334
+ function longOption(text, names) {
44335
+ return completeName(text.split("=", 1)[0], names);
44336
+ }
44337
+ var GNU_INFORMATION = { "--help": false, "--version": false };
44338
+ var MV_OPTIONS = {
44339
+ "--backup": false,
44340
+ "--context": false,
44341
+ "--debug": false,
44342
+ "--force": false,
44343
+ "--interactive": false,
44344
+ "--no-clobber": false,
44345
+ "--no-copy": false,
44346
+ "--no-target-directory": false,
44347
+ "--strip-trailing-slashes": false,
44348
+ "--suffix": true,
44349
+ "--target-directory": true,
44350
+ "--update": false,
44351
+ "--verbose": false,
44352
+ ...GNU_INFORMATION
44353
+ };
44354
+ var CP_OPTIONS = {
44355
+ "--archive": false,
44356
+ "--attributes-only": false,
44357
+ "--backup": false,
44358
+ "--context": false,
44359
+ "--copy-contents": false,
44360
+ "--debug": false,
44361
+ "--dereference": false,
44362
+ "--force": false,
44363
+ "--interactive": false,
44364
+ "--link": false,
44365
+ "--no-clobber": false,
44366
+ "--no-dereference": false,
44367
+ "--no-preserve": true,
44368
+ "--no-target-directory": false,
44369
+ "--one-file-system": false,
44370
+ "--parents": false,
44371
+ "--preserve": false,
44372
+ "--recursive": false,
44373
+ "--reflink": false,
44374
+ "--remove-destination": false,
44375
+ "--sparse": true,
44376
+ "--strip-trailing-slashes": false,
44377
+ "--suffix": true,
44378
+ "--symbolic-link": false,
44379
+ "--target-directory": true,
44380
+ "--update": false,
44381
+ "--verbose": false,
44382
+ ...GNU_INFORMATION
44383
+ };
44384
+ var INSTALL_OPTIONS = {
44385
+ "--backup": false,
44386
+ "--compare": false,
44387
+ "--context": false,
44388
+ "--debug": false,
44389
+ "--directory": false,
44390
+ "--group": true,
44391
+ "--mode": true,
44392
+ "--no-target-directory": false,
44393
+ "--owner": true,
44394
+ "--preserve-context": false,
44395
+ "--preserve-timestamps": false,
44396
+ "--strip": false,
44397
+ "--strip-program": true,
44398
+ "--suffix": true,
44399
+ "--target-directory": true,
44400
+ "--verbose": false,
44401
+ ...GNU_INFORMATION
44402
+ };
44403
+ var LN_OPTIONS = {
44404
+ "--backup": false,
44405
+ "--directory": false,
44406
+ "--force": false,
44407
+ "--interactive": false,
44408
+ "--logical": false,
44409
+ "--no-dereference": false,
44410
+ "--no-target-directory": false,
44411
+ "--physical": false,
44412
+ "--relative": false,
44413
+ "--suffix": true,
44414
+ "--symbolic": false,
44415
+ "--target-directory": true,
44416
+ "--verbose": false,
44417
+ ...GNU_INFORMATION
44418
+ };
44419
+ function passesAnArgument(arg) {
44420
+ return arg.fields !== "none";
44421
+ }
44422
+ function writeArguments(rest, valued, long) {
44423
+ const names = Object.keys(long);
44424
+ const operands2 = [];
44425
+ const flags = new Set;
44426
+ let directory;
44427
+ let uncertain;
44428
+ for (let i = 0;i < rest.length; i += 1) {
44429
+ const arg = rest[i];
44430
+ const { text, whole } = readableWord(arg);
44431
+ if (whole && text === "--") {
44432
+ operands2.push(...rest.slice(i + 1).filter(passesAnArgument));
44433
+ break;
44434
+ }
44435
+ if (!whole || !text.startsWith("-") || text === "-") {
44436
+ if (!whole && mayBeSomeOption(text))
44437
+ uncertain = arg;
44438
+ if (passesAnArgument(arg))
44439
+ operands2.push(arg);
44440
+ continue;
44035
44441
  }
44442
+ let takesValue = false;
44443
+ let offset;
44444
+ if (text.startsWith("--")) {
44445
+ const option = longOption(text, names);
44446
+ if (option === undefined)
44447
+ continue;
44448
+ flags.add(option);
44449
+ takesValue = long[option] === true && !text.includes("=");
44450
+ if (option === "--target-directory")
44451
+ offset = text.indexOf("=") + 1 || text.length;
44452
+ } else {
44453
+ for (let index = 1;index < text.length; index += 1) {
44454
+ const letter = text.charAt(index);
44455
+ flags.add(letter);
44456
+ if (!valued.includes(letter))
44457
+ continue;
44458
+ takesValue = index === text.length - 1;
44459
+ if (letter === "t")
44460
+ offset = index + 1;
44461
+ break;
44462
+ }
44463
+ }
44464
+ const next = takesValue ? rest[i + 1] : undefined;
44465
+ if (takesValue)
44466
+ i += 1;
44467
+ if (offset === undefined)
44468
+ continue;
44469
+ const value = valueInWord(arg, offset);
44470
+ directory = value === undefined ? next : { text: arg.text, exp: value };
44471
+ }
44472
+ return { operands: operands2, flags, directory, uncertain };
44473
+ }
44474
+ function writeDestinations(found, site) {
44475
+ if (found.operands.length > 1 && (found.flags.has("T") || found.flags.has("--no-target-directory"))) {
44476
+ return [found.operands.at(-1)];
44036
44477
  }
44478
+ if (found.directory !== undefined && !found.uncertain)
44479
+ return [found.directory];
44480
+ if (found.operands.length < 2 || found.uncertain) {
44481
+ const word = found.uncertain?.text ?? found.operands.at(-1)?.text ?? "missing destination";
44482
+ throw new Refusal(site.snippet, site.line, `the guard cannot read a write destination from \`${word}\`; put \`--\` before operands, or use \`-T\` to name the last operand as the destination`);
44483
+ }
44484
+ return [found.operands.at(-1)];
44485
+ }
44486
+ function checkCopyContents(found, destinations, st, ctx, site) {
44487
+ const recursive = found.uncertain || ["r", "R", "a", "--recursive", "--archive"].some((flag) => found.flags.has(flag));
44488
+ const direct = found.flags.has("T") || found.flags.has("--no-target-directory");
44489
+ for (const destination of destinations) {
44490
+ const text = literalText(destination.exp);
44491
+ if (text === undefined || !path4.isAbsolute(text) && st.cwd === undefined)
44492
+ continue;
44493
+ const dir = realExisting(path4.resolve(st.cwd ?? "/", text));
44494
+ if (!statSync(dir, { throwIfNoEntry: false })?.isDirectory())
44495
+ continue;
44496
+ for (const source of found.operands) {
44497
+ if (destinations.includes(source))
44498
+ continue;
44499
+ const name = literalText(source.exp);
44500
+ const from = name === undefined ? undefined : path4.resolve(st.cwd ?? "/", name);
44501
+ const knownTree = from !== undefined && statSync(from, { throwIfNoEntry: false })?.isDirectory();
44502
+ const root = direct || name === undefined ? dir : path4.join(dir, found.flags.has("--parents") ? name : path4.basename(name));
44503
+ const pending = [root];
44504
+ while (pending.length > 0) {
44505
+ const target = pending.pop();
44506
+ checkTargets("cp", "overwrite", [{ text: name === undefined ? source.text : target, exp: [literal2(target)] }], WRITES_THROUGH_LINK, st, ctx, site);
44507
+ if (!recursive && !(name === undefined && target === root))
44508
+ continue;
44509
+ if (!lstatSync(target, { throwIfNoEntry: false })?.isDirectory())
44510
+ continue;
44511
+ const entries = opendirSync(target);
44512
+ try {
44513
+ for (let entry = entries.readSync();entry !== null; entry = entries.readSync()) {
44514
+ if (++ctx.steps.count > MAX_WALK_STEPS) {
44515
+ throw new Refusal(site.snippet, site.line, WALK_LIMIT);
44516
+ }
44517
+ const child = path4.join(target, entry.name);
44518
+ if (knownTree && !existsSync(path4.join(from, path4.relative(root, child))))
44519
+ continue;
44520
+ pending.push(child);
44521
+ }
44522
+ } finally {
44523
+ entries.closeSync();
44524
+ }
44525
+ }
44526
+ }
44527
+ }
44528
+ }
44529
+ var SED_OPTIONS = {
44530
+ "--debug": false,
44531
+ "--expression": true,
44532
+ "--file": true,
44533
+ "--follow-symlinks": false,
44534
+ "--in-place": false,
44535
+ "--line-length": true,
44536
+ "--null-data": false,
44537
+ "--posix": false,
44538
+ "--quiet": false,
44539
+ "--regexp-extended": false,
44540
+ "--sandbox": false,
44541
+ "--separate": false,
44542
+ "--silent": false,
44543
+ "--unbuffered": false,
44544
+ ...GNU_INFORMATION
44545
+ };
44546
+ function sedWrites(list) {
44547
+ const names = Object.keys(SED_OPTIONS);
44548
+ let inPlace = false;
44549
+ let follow = false;
44550
+ let expression = false;
44551
+ let splitOption = false;
44552
+ let ended = false;
44553
+ let reopened = false;
44554
+ let takesNext = false;
44555
+ const unread = [];
44556
+ const operands2 = [];
44557
+ for (let i = 0;i < list.length; i += 1) {
44558
+ const arg = list[i];
44559
+ const { text, whole } = readableWord(arg);
44560
+ const afterUnread = takesNext;
44561
+ takesNext = false;
44562
+ const several = arg.fields === "unknown";
44563
+ if (ended) {
44564
+ operands2.push({ arg, option: false, several });
44565
+ continue;
44566
+ }
44567
+ if (whole && text === "--") {
44568
+ if (reopened)
44569
+ operands2.push({ arg, option: false, several: false });
44570
+ if (afterUnread)
44571
+ reopened = true;
44572
+ else
44573
+ ended = true;
44574
+ continue;
44575
+ }
44576
+ if (several) {
44577
+ splitOption = true;
44578
+ takesNext = true;
44579
+ operands2.push({ arg, option: true, several });
44580
+ continue;
44581
+ }
44582
+ if (!text.startsWith("-") || whole && text === "-") {
44583
+ const option = !whole && text === "";
44584
+ if (option) {
44585
+ unread.push(arg);
44586
+ takesNext = true;
44587
+ }
44588
+ operands2.push({ arg, option, several });
44589
+ continue;
44590
+ }
44591
+ if (reopened)
44592
+ operands2.push({ arg, option: false, several });
44593
+ if (text.startsWith("--")) {
44594
+ const option = longOption(text, names);
44595
+ if (whole && !reopened && (option === "--help" || option === "--version")) {
44596
+ return { replaced: [], followed: [], inPlace: false, inferred: false };
44597
+ }
44598
+ if (option === "--in-place")
44599
+ inPlace = true;
44600
+ if (option === "--follow-symlinks")
44601
+ follow = true;
44602
+ if (option === "--expression" || option === "--file")
44603
+ expression = true;
44604
+ if (option === undefined && !whole)
44605
+ unread.push(arg);
44606
+ if ((option === undefined ? !whole : SED_OPTIONS[option]) && !text.includes("=")) {
44607
+ if (whole && !reopened)
44608
+ i += 1;
44609
+ else
44610
+ takesNext = true;
44611
+ }
44612
+ continue;
44613
+ }
44614
+ let settled = false;
44615
+ for (const [offset, letter] of [...text.slice(1)].entries()) {
44616
+ if (letter === "i") {
44617
+ inPlace = true;
44618
+ settled = true;
44619
+ break;
44620
+ }
44621
+ if ("efl".includes(letter)) {
44622
+ if (letter !== "l")
44623
+ expression = true;
44624
+ if (offset === text.length - 2) {
44625
+ if (whole && !reopened)
44626
+ i += 1;
44627
+ else
44628
+ takesNext = true;
44629
+ }
44630
+ settled = true;
44631
+ break;
44632
+ }
44633
+ }
44634
+ if (!settled && !whole) {
44635
+ unread.push(arg);
44636
+ takesNext = true;
44637
+ }
44638
+ }
44639
+ const unsettled = (settled) => settled ? 0 : 1;
44640
+ const replaced = [];
44641
+ const followed = [];
44642
+ let before = false;
44643
+ for (const operand of operands2) {
44644
+ const maybe = operand.option && !operand.several;
44645
+ const others = unread.length - (maybe ? 1 : 0);
44646
+ const needed = unsettled(inPlace || splitOption) + unsettled(expression || splitOption || operand.several || before);
44647
+ if (others >= needed + unsettled(follow || splitOption))
44648
+ followed.push(operand.arg);
44649
+ else if (others >= needed)
44650
+ replaced.push(operand.arg);
44651
+ if (!maybe)
44652
+ before = true;
44653
+ }
44654
+ return {
44655
+ replaced,
44656
+ followed,
44657
+ inPlace: inPlace || splitOption || unread.length > 0,
44658
+ inferred: !inPlace
44659
+ };
44037
44660
  }
44038
44661
  function checkRecursiveMode(program, list, st, ctx, site) {
44039
44662
  const flags = program === "chmod" ? "cfvR" : "cfvRhHLP";
@@ -44063,7 +44686,7 @@ function checkRecursiveMode(program, list, st, ctx, site) {
44063
44686
  }
44064
44687
  const verb = program === "chmod" ? "recursively change the mode of" : "recursively change the owner of";
44065
44688
  if (recursive) {
44066
- checkTargets(`${program} -R`, verb, reference ? found : found.slice(1), true, st, ctx, site);
44689
+ checkTargets(`${program} -R`, verb, reference ? found : found.slice(1), ACTS_THROUGH_LINK, st, ctx, site);
44067
44690
  return;
44068
44691
  }
44069
44692
  for (const [index, arg] of found.entries()) {
@@ -44073,7 +44696,7 @@ function checkRecursiveMode(program, list, st, ctx, site) {
44073
44696
  const targets = reference || arg.fields === "unknown" ? others : others.slice(1);
44074
44697
  if (targets.length === 0)
44075
44698
  continue;
44076
- checkTargets(`${program} -R`, verb, targets, true, st, ctx, site);
44699
+ checkTargets(`${program} -R`, verb, targets, ACTS_THROUGH_LINK, st, ctx, site);
44077
44700
  }
44078
44701
  }
44079
44702
  function checkFind(list, st, ctx, site) {
@@ -44119,8 +44742,9 @@ function checkFind(list, st, ctx, site) {
44119
44742
  continue;
44120
44743
  const inner = unwrap2(list.slice(i + index + 1, i + end), st, ctx, site);
44121
44744
  const program = path4.basename(literalText(inner.argv[0]?.exp) ?? "");
44122
- if (FILE_COMMANDS.has(program) || program === "xargs")
44745
+ if (FILE_COMMANDS.has(program) || program === "xargs" || program === "sed" && sedWrites(inner.argv.slice(1)).inPlace) {
44123
44746
  action = `find ${word} ${program}`;
44747
+ }
44124
44748
  if (SHELLS.has(program)) {
44125
44749
  action = `find ${word} ${program}`;
44126
44750
  shell = inner.argv;
@@ -44130,10 +44754,10 @@ function checkFind(list, st, ctx, site) {
44130
44754
  if (rootsBeforePredicate === undefined)
44131
44755
  return;
44132
44756
  const under = rootsBeforePredicate.length > 0 ? rootsBeforePredicate : [{ text: ".", exp: [literal2(".")] }];
44133
- checkTargets("find with a root the guard cannot read, which may be a predicate", "delete or change what it finds under", under, false, st, ctx, site);
44757
+ checkTargets("find with a root the guard cannot read, which may be a predicate", "delete or change what it finds under", under, ACTS_ON_LINK, st, ctx, site);
44134
44758
  return;
44135
44759
  }
44136
- checkTargets(action, "delete or change what it finds under", targets, false, st, ctx, site);
44760
+ checkTargets(action, "delete or change what it finds under", targets, ACTS_ON_LINK, st, ctx, site);
44137
44761
  if (shell === undefined)
44138
44762
  return;
44139
44763
  const found = targets.length === 1 ? targets[0]?.exp ?? [] : [unknown2("a path `find` found")];
@@ -44171,7 +44795,7 @@ function checkXargs(list, st, ctx, site) {
44171
44795
  };
44172
44796
  });
44173
44797
  const program = path4.basename(literalText(argv[0]?.exp) ?? "");
44174
- if (FILE_COMMANDS.has(program) || SIGNAL_COMMANDS.has(program)) {
44798
+ if (FILE_COMMANDS.has(program) || SIGNAL_COMMANDS.has(program) || program === "sed" && sedWrites(argv.slice(1)).inPlace) {
44175
44799
  throw new Refusal(site.snippet, site.line, `\`xargs ${program}\` takes its targets from standard input, which the guard cannot see; name ` + `the paths or pids on the command line (or use \`find <dir> -delete\`) so it can check them`);
44176
44800
  }
44177
44801
  const overlay = new Map;
@@ -44454,7 +45078,14 @@ function runFile(file2, positional, st, ctx, site, kind, source = false) {
44454
45078
  if (text === undefined || !text.startsWith("/") && st.cwd === undefined) {
44455
45079
  throw new Refusal(site.snippet, site.line, `the guard cannot resolve the script \`${file2.text}\` (${file2.exp.find((p) => p.kind !== "literal")?.why ?? `a working directory unknown after ${st.cwdWhy}`}), so it cannot read what it runs`);
44456
45080
  }
44457
- const abs = path4.resolve(st.cwd ?? "/", text);
45081
+ if (ctx.unnameableWrite.why !== undefined) {
45082
+ throw new Refusal(site.snippet, site.line, `${ctx.unnameableWrite.why} writes a file the guard cannot name, so it cannot tell whether \`${file2.text}\` is the file that was written`);
45083
+ }
45084
+ const resolved = physical(st.cwd ?? "/", text, true);
45085
+ if ("unknown" in resolved) {
45086
+ throw new Refusal(site.snippet, site.line, `the guard cannot resolve the script \`${file2.text}\` (${resolved.unknown}), so it cannot read what it runs`);
45087
+ }
45088
+ const abs = resolved.real;
44458
45089
  let content;
44459
45090
  if (st.files.has(abs)) {
44460
45091
  const written = st.files.get(abs);
@@ -44582,7 +45213,7 @@ function checkCode(language, code, st, ctx, ipython = false) {
44582
45213
  overwrite: verb === "overwrite"
44583
45214
  });
44584
45215
  if (!verdict.ok) {
44585
- throw refusal({ snippet: call, line: 1 }, `${call} would ${verb} ${verdict.resolution}`, ctx);
45216
+ throw refusal({ snippet: call, line: 1 }, `${call} would ${verb} ${verdict.resolution}`, ctx, verdict.unresolved);
44586
45217
  }
44587
45218
  },
44588
45219
  signal: (call, pid, group) => {
@@ -44653,6 +45284,7 @@ function createPaneGuard(options) {
44653
45284
  ompPid: options.ompPid,
44654
45285
  parentOf: options.parentOf ?? procParent,
44655
45286
  steps: { count: 0 },
45287
+ unnameableWrite: { why: undefined },
44656
45288
  uncertain: { depth: 0 }
44657
45289
  });
44658
45290
  const initial = (cwd, source) => ({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sjawhar/pi-legion-envoy",
3
- "version": "5.17.3",
3
+ "version": "5.17.5",
4
4
  "type": "module",
5
5
  "omp": {
6
6
  "extensions": [