terminalhire 0.42.40 → 0.42.42

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.
@@ -26435,7 +26435,19 @@ function isGreen(outcome) {
26435
26435
  function isOurFault(outcome) {
26436
26436
  return LIMIT_OWNER[outcome] === "ours";
26437
26437
  }
26438
- var VERIFICATION_OUTCOMES, int, withSuiteFailures, countLines, READERS, SUPPORTED_RUNNERS, COVERAGE_TABLE, EXEC_FAILURE, SUITE_REPORTED_FAILURE, MISSING_SYSTEM_DEPENDENCY, OFFLINE_BUILD_GAP, LIMIT_OWNER;
26438
+ function readStoppedAt(stdout, stderr = "") {
26439
+ return lastStepMarker(stdout) ?? lastStepMarker(stderr);
26440
+ }
26441
+ function isStepName(value) {
26442
+ return value.length > 0 && value.length <= STOPPED_AT_MAX && SCRIPT_NAME.test(value);
26443
+ }
26444
+ function lastStepMarker(text) {
26445
+ let last = null;
26446
+ for (const m of text.replace(/\r\n/g, "\n").matchAll(NPM_STEP_MARKER))
26447
+ last = m[1];
26448
+ return last !== null && isStepName(last) ? last : null;
26449
+ }
26450
+ var VERIFICATION_OUTCOMES, int, withSuiteFailures, countLines, READERS, SUPPORTED_RUNNERS, COVERAGE_TABLE, EXEC_FAILURE, SUITE_REPORTED_FAILURE, MISSING_SYSTEM_DEPENDENCY, OFFLINE_BUILD_GAP, LIMIT_OWNER, NPM_STEP_MARKER, STOPPED_AT_MAX, SCRIPT_NAME;
26439
26451
  var init_classify2 = __esm({
26440
26452
  "../../packages/envrun/dist/classify.js"() {
26441
26453
  "use strict";
@@ -26858,6 +26870,9 @@ var init_classify2 = __esm({
26858
26870
  "environment-exhausted": "ours",
26859
26871
  "budget-exceeded": "ours"
26860
26872
  };
26873
+ NPM_STEP_MARKER = /^> (?:@[^\s@/]+\/)?[^\s@]+@\S+ (\S+)/gm;
26874
+ STOPPED_AT_MAX = 200;
26875
+ SCRIPT_NAME = /^[A-Za-z0-9:_.\-/+@]+$/;
26861
26876
  }
26862
26877
  });
26863
26878
 
@@ -31033,7 +31048,9 @@ var init_result = __esm({
31033
31048
  "leaksClean",
31034
31049
  "leakState",
31035
31050
  "venue",
31036
- "refusalOrigin"
31051
+ "refusalOrigin",
31052
+ "stoppedAt",
31053
+ "installFrozen"
31037
31054
  ];
31038
31055
  RENDER_NONE = null;
31039
31056
  FIELD_VIEWS = {
@@ -31096,7 +31113,12 @@ var init_result = __esm({
31096
31113
  venue: (r) => r.venue === null ? null : renderVenueLine(r.venue),
31097
31114
  // For the worker and the baseline intake, not the terminal: `reason` already says which
31098
31115
  // side refused in words a developer reads.
31099
- refusalOrigin: RENDER_NONE
31116
+ refusalOrigin: RENDER_NONE,
31117
+ // Only when the run did not pass: on a green run "the last step" is just the last step.
31118
+ stoppedAt: (r) => r.stoppedAt === null || r.outcome === "completed" ? null : `last step ${r.stoppedAt} (the last package script named in the output)`,
31119
+ // The run already prints the unfrozen note while it runs (`installUnfrozenNote`); this is
31120
+ // the record of it for the worker and the intake.
31121
+ installFrozen: RENDER_NONE
31100
31122
  };
31101
31123
  }
31102
31124
  });
@@ -31472,6 +31494,317 @@ var init_lockfileCheck = __esm({
31472
31494
  }
31473
31495
  });
31474
31496
 
31497
+ // ../../packages/envrun/dist/binaryPatch.js
31498
+ import { createHash as createHash11 } from "crypto";
31499
+ import { inflateSync } from "zlib";
31500
+ function decodeBase85Line(line) {
31501
+ const first = line.charCodeAt(0);
31502
+ let length;
31503
+ if (first >= 65 && first <= 90)
31504
+ length = first - 64;
31505
+ else if (first >= 97 && first <= 122)
31506
+ length = first - 96 + 26;
31507
+ else
31508
+ return null;
31509
+ const groups = Math.ceil(length / 4);
31510
+ if (line.length !== 1 + groups * 5)
31511
+ return null;
31512
+ const out = new Uint8Array(groups * 4);
31513
+ for (let g = 0; g < groups; g += 1) {
31514
+ let acc = 0;
31515
+ for (let k = 0; k < 5; k += 1) {
31516
+ const code = line.charCodeAt(1 + g * 5 + k);
31517
+ const value = code < 128 ? B85_VALUE[code] ?? -1 : -1;
31518
+ if (value < 0)
31519
+ return null;
31520
+ acc = acc * 85 + value;
31521
+ }
31522
+ if (acc > 4294967295)
31523
+ return null;
31524
+ out[g * 4] = acc >>> 24 & 255;
31525
+ out[g * 4 + 1] = acc >>> 16 & 255;
31526
+ out[g * 4 + 2] = acc >>> 8 & 255;
31527
+ out[g * 4 + 3] = acc & 255;
31528
+ }
31529
+ return out.subarray(0, length);
31530
+ }
31531
+ function readBinaryPayload(lines) {
31532
+ const blocks = [];
31533
+ let i = 0;
31534
+ while (i < lines.length) {
31535
+ const line = lines[i] ?? "";
31536
+ if (line === "") {
31537
+ i += 1;
31538
+ continue;
31539
+ }
31540
+ if (blocks.length === 2)
31541
+ return null;
31542
+ const header = BLOCK_HEADER.exec(line);
31543
+ if (header === null)
31544
+ return null;
31545
+ const size = Number(header[2]);
31546
+ if (!Number.isSafeInteger(size))
31547
+ return null;
31548
+ i += 1;
31549
+ const parts = [];
31550
+ let total = 0;
31551
+ while (i < lines.length && (lines[i] ?? "") !== "") {
31552
+ const decoded = decodeBase85Line(lines[i] ?? "");
31553
+ if (decoded === null)
31554
+ return null;
31555
+ total += decoded.length;
31556
+ if (total > MAX_BINARY_DEFLATED_BLOCK_BYTES)
31557
+ return null;
31558
+ parts.push(decoded);
31559
+ i += 1;
31560
+ }
31561
+ if (parts.length === 0)
31562
+ return null;
31563
+ if (i >= lines.length)
31564
+ return null;
31565
+ const deflated = new Uint8Array(total);
31566
+ let at = 0;
31567
+ for (const p of parts) {
31568
+ deflated.set(p, at);
31569
+ at += p.length;
31570
+ }
31571
+ blocks.push({ kind: header[1] === "delta" ? "delta" : "literal", size, deflated });
31572
+ }
31573
+ const forward = blocks[0];
31574
+ if (forward === void 0)
31575
+ return null;
31576
+ return { forward, reverse: blocks[1] ?? null };
31577
+ }
31578
+ function inflateExact(block, limit2) {
31579
+ if (block.size > limit2)
31580
+ return null;
31581
+ try {
31582
+ const out = inflateSync(block.deflated, { maxOutputLength: block.size + 1 });
31583
+ return out.length === block.size ? new Uint8Array(out) : null;
31584
+ } catch {
31585
+ return null;
31586
+ }
31587
+ }
31588
+ function readVarint(delta, from) {
31589
+ let value = 0;
31590
+ let shift = 0;
31591
+ let pos = from;
31592
+ for (; ; ) {
31593
+ const byte = delta[pos];
31594
+ if (byte === void 0 || shift > 42)
31595
+ return null;
31596
+ value += (byte & 127) * 2 ** shift;
31597
+ pos += 1;
31598
+ shift += 7;
31599
+ if ((byte & 128) === 0)
31600
+ return { value, next: pos };
31601
+ }
31602
+ }
31603
+ function deltaHeader(delta) {
31604
+ const source = readVarint(delta, 0);
31605
+ if (source === null)
31606
+ return null;
31607
+ const target = readVarint(delta, source.next);
31608
+ if (target === null)
31609
+ return null;
31610
+ return { source: source.value, target: target.value, opsAt: target.next };
31611
+ }
31612
+ function deltaLeadingInserts(delta, opsAt) {
31613
+ const parts = [];
31614
+ let pos = opsAt;
31615
+ while (pos < delta.length && parts.length < 64) {
31616
+ const op = delta[pos] ?? 0;
31617
+ if (op === 0 || (op & 128) !== 0)
31618
+ break;
31619
+ const end = Math.min(pos + 1 + op, delta.length);
31620
+ for (let i = pos + 1; i < end; i += 1)
31621
+ parts.push(delta[i] ?? 0);
31622
+ pos = end;
31623
+ }
31624
+ return Uint8Array.from(parts);
31625
+ }
31626
+ function startsWith(bytes, magic, at = 0) {
31627
+ if (bytes.length < at + magic.length)
31628
+ return false;
31629
+ return magic.every((b, i) => bytes[at + i] === b);
31630
+ }
31631
+ function sniffBinaryType(bytes) {
31632
+ if (startsWith(bytes, [127, 69, 76, 70]))
31633
+ return { label: "ELF", program: true };
31634
+ for (const magic of [
31635
+ [254, 237, 250, 206],
31636
+ [254, 237, 250, 207],
31637
+ [206, 250, 237, 254],
31638
+ [207, 250, 237, 254],
31639
+ [202, 254, 186, 191],
31640
+ [190, 186, 254, 202],
31641
+ [191, 186, 254, 202]
31642
+ ]) {
31643
+ if (startsWith(bytes, magic))
31644
+ return { label: "Mach-O", program: true };
31645
+ }
31646
+ if (startsWith(bytes, [202, 254, 186, 190])) {
31647
+ const major = bytes.length >= 8 ? (bytes[6] ?? 0) << 8 | (bytes[7] ?? 0) : 0;
31648
+ return { label: major >= 45 ? "Java class" : "Mach-O", program: true };
31649
+ }
31650
+ if (startsWith(bytes, [0, 97, 115, 109]))
31651
+ return { label: "WebAssembly", program: true };
31652
+ if (startsWith(bytes, [77, 90]))
31653
+ return { label: "PE", program: true };
31654
+ if (startsWith(bytes, [137, 80, 78, 71, 13, 10, 26, 10])) {
31655
+ return { label: "PNG", program: false };
31656
+ }
31657
+ if (startsWith(bytes, [255, 216, 255]))
31658
+ return { label: "JPEG", program: false };
31659
+ if (startsWith(bytes, [71, 73, 70, 56, 55, 97]) || startsWith(bytes, [71, 73, 70, 56, 57, 97])) {
31660
+ return { label: "GIF", program: false };
31661
+ }
31662
+ if (startsWith(bytes, [82, 73, 70, 70]) && startsWith(bytes, [87, 69, 66, 80], 8)) {
31663
+ return { label: "WebP", program: false };
31664
+ }
31665
+ if (startsWith(bytes, [37, 80, 68, 70, 45]))
31666
+ return { label: "PDF", program: false };
31667
+ if (startsWith(bytes, [119, 79, 70, 70]))
31668
+ return { label: "WOFF", program: false };
31669
+ if (startsWith(bytes, [119, 79, 70, 50]))
31670
+ return { label: "WOFF2", program: false };
31671
+ if (startsWith(bytes, [0, 1, 0, 0]) || startsWith(bytes, [116, 114, 117, 101])) {
31672
+ return { label: "TTF", program: false };
31673
+ }
31674
+ if (startsWith(bytes, [79, 84, 84, 79]))
31675
+ return { label: "OTF", program: false };
31676
+ if (startsWith(bytes, [80, 75, 3, 4]) || startsWith(bytes, [80, 75, 5, 6]) || startsWith(bytes, [80, 75, 7, 8])) {
31677
+ return { label: "ZIP", program: false };
31678
+ }
31679
+ return { label: "unknown binary", program: false };
31680
+ }
31681
+ function formatByteSize(bytes) {
31682
+ if (bytes < 1024)
31683
+ return `${bytes} B`;
31684
+ if (bytes < 1024 * 1024)
31685
+ return `${(bytes / 1024).toFixed(1)} KB`;
31686
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
31687
+ }
31688
+ function parseFullIndexLine(line) {
31689
+ const m = /^index ([0-9a-f]+)\.\.([0-9a-f]+)(?: ([0-7]{6}))?$/.exec(line);
31690
+ if (m === null)
31691
+ return null;
31692
+ const pre = m[1] ?? "";
31693
+ const post = m[2] ?? "";
31694
+ if (!FULL_ID.test(pre) || !FULL_ID.test(post))
31695
+ return null;
31696
+ return { pre, post, mode: m[3] ?? null };
31697
+ }
31698
+ function isZeroId(id) {
31699
+ return id === ZERO_ID;
31700
+ }
31701
+ function refuse4(code, detail) {
31702
+ return { kind: "refuse", code, detail };
31703
+ }
31704
+ function judgeBinaryEntry(input) {
31705
+ const { path: path5 } = input;
31706
+ if (input.stub) {
31707
+ return refuse4("binary-content-missing", `${path5} is a binary file, but the patch leaves out its contents. Update terminalhire (npm install -g terminalhire@latest) and submit again.`);
31708
+ }
31709
+ if (input.textContent || input.indexLines.length !== 1)
31710
+ return { kind: "unreadable" };
31711
+ const index = parseFullIndexLine(input.indexLines[0] ?? "");
31712
+ if (index === null)
31713
+ return { kind: "unreadable" };
31714
+ if (isZeroId(index.pre) !== input.createsFile)
31715
+ return { kind: "unreadable" };
31716
+ if (isZeroId(index.post) !== input.deletesFile)
31717
+ return { kind: "unreadable" };
31718
+ const payload = readBinaryPayload(input.payload);
31719
+ if (payload === null)
31720
+ return { kind: "unreadable" };
31721
+ if (input.deletesFile) {
31722
+ const forward2 = payload.forward;
31723
+ if (forward2.kind !== "literal" || inflateExact(forward2, 0) === null) {
31724
+ return { kind: "unreadable" };
31725
+ }
31726
+ const reverse = payload.reverse;
31727
+ const removed = reverse !== null && reverse.kind === "literal" ? inflateExact(reverse, MAX_BINARY_FILE_BYTES) : null;
31728
+ return {
31729
+ kind: "hold",
31730
+ type: removed === null ? "unknown binary" : sniffBinaryType(removed).label,
31731
+ size: reverse !== null && reverse.kind === "literal" ? reverse.size : 0,
31732
+ writes: 0,
31733
+ hash: index.pre
31734
+ };
31735
+ }
31736
+ const mode = input.newMode ?? index.mode ?? "100644";
31737
+ if (mode === "100755")
31738
+ return refuse4("binary-executable", binaryExecutableDetail(path5));
31739
+ if (mode !== "100644")
31740
+ return refuse4("binary-mode", unsupportedModeDetail(path5, mode));
31741
+ const tooLarge = (size2) => refuse4("binary-too-large", `${path5} is ${formatByteSize(size2)}. One binary file can be at most 2 MB.`);
31742
+ let size;
31743
+ let head;
31744
+ const forward = payload.forward;
31745
+ if (forward.kind === "literal") {
31746
+ if (forward.size > MAX_BINARY_FILE_BYTES) {
31747
+ return tooLarge(forward.size);
31748
+ }
31749
+ const bytes = inflateExact(forward, MAX_BINARY_FILE_BYTES);
31750
+ if (bytes === null)
31751
+ return { kind: "unreadable" };
31752
+ size = bytes.length;
31753
+ head = bytes;
31754
+ } else {
31755
+ if (input.createsFile)
31756
+ return { kind: "unreadable" };
31757
+ const delta = inflateExact(forward, 2 * MAX_BINARY_FILE_BYTES);
31758
+ if (delta === null)
31759
+ return { kind: "unreadable" };
31760
+ const header = deltaHeader(delta);
31761
+ if (header === null)
31762
+ return { kind: "unreadable" };
31763
+ if (header.target > MAX_BINARY_FILE_BYTES)
31764
+ return tooLarge(header.target);
31765
+ size = header.target;
31766
+ head = deltaLeadingInserts(delta, header.opsAt);
31767
+ }
31768
+ const type = sniffBinaryType(head);
31769
+ if (type.program) {
31770
+ return refuse4("binary-program", binaryProgramDetail(path5, type.label));
31771
+ }
31772
+ return { kind: "hold", type: type.label, size, writes: size, hash: index.post };
31773
+ }
31774
+ function binaryTotalRefusal(totalBytes) {
31775
+ if (totalBytes <= MAX_BINARY_PATCH_BYTES)
31776
+ return null;
31777
+ return `The binary files in this patch add up to ${formatByteSize(totalBytes)}. A patch can carry at most 3 MB of binary files.`;
31778
+ }
31779
+ function binaryExecutableDetail(path5) {
31780
+ return `${path5} is a binary file marked executable. Binary files can't be marked executable in a patch.`;
31781
+ }
31782
+ function unsupportedModeDetail(path5, mode) {
31783
+ return `The patch changes ${path5} to a file type this platform will not apply on your behalf (mode ${mode}). Nothing on your repository was changed.`;
31784
+ }
31785
+ function binaryProgramDetail(path5, label) {
31786
+ return `${path5} is a program (${label}). Patches can't add or change programs, because the install or test step could run them.`;
31787
+ }
31788
+ var MAX_BINARY_FILE_BYTES, MAX_BINARY_PATCH_BYTES, MAX_BINARY_DEFLATED_BLOCK_BYTES, FULL_ID, ZERO_ID, B85_ALPHABET, B85_VALUE, BLOCK_HEADER;
31789
+ var init_binaryPatch = __esm({
31790
+ "../../packages/envrun/dist/binaryPatch.js"() {
31791
+ "use strict";
31792
+ MAX_BINARY_FILE_BYTES = 2 * 1024 * 1024;
31793
+ MAX_BINARY_PATCH_BYTES = 3 * 1024 * 1024;
31794
+ MAX_BINARY_DEFLATED_BLOCK_BYTES = 2 * MAX_BINARY_FILE_BYTES + 64 * 1024;
31795
+ FULL_ID = /^[0-9a-f]{40}$/;
31796
+ ZERO_ID = "0".repeat(40);
31797
+ B85_ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!#$%&()*+-;<=>?@^_`{|}~";
31798
+ B85_VALUE = (() => {
31799
+ const table = new Array(128).fill(-1);
31800
+ for (let i = 0; i < B85_ALPHABET.length; i += 1)
31801
+ table[B85_ALPHABET.charCodeAt(i)] = i;
31802
+ return table;
31803
+ })();
31804
+ BLOCK_HEADER = /^(literal|delta) (0|[1-9][0-9]{0,15})$/;
31805
+ }
31806
+ });
31807
+
31475
31808
  // ../../packages/envrun/dist/boundary.js
31476
31809
  function lockfileRefusal(base) {
31477
31810
  const kind = LOCKFILE_KINDS[base];
@@ -31596,6 +31929,11 @@ function parsePatchPaths(patch) {
31596
31929
  entry = {
31597
31930
  rawPaths: [],
31598
31931
  binary: false,
31932
+ binaryStub: false,
31933
+ binaryPayload: [],
31934
+ indexLines: [],
31935
+ newMode: null,
31936
+ textContent: false,
31599
31937
  headerOld: null,
31600
31938
  headerNew: null,
31601
31939
  plusPath: null,
@@ -31629,8 +31967,10 @@ function parsePatchPaths(patch) {
31629
31967
  }
31630
31968
  if (entry === null)
31631
31969
  continue;
31632
- if (skippingBinary)
31970
+ if (skippingBinary) {
31971
+ entry.binaryPayload.push(line);
31633
31972
  continue;
31973
+ }
31634
31974
  if (inHunk) {
31635
31975
  const marker = line === "" ? " " : line[0];
31636
31976
  if (marker === "\\")
@@ -31657,6 +31997,7 @@ function parsePatchPaths(patch) {
31657
31997
  if (line === "-- " || line === "--")
31658
31998
  break;
31659
31999
  if (line.startsWith("@@")) {
32000
+ entry.textContent = true;
31660
32001
  const m = HUNK_HEADER.exec(line);
31661
32002
  if (m === null) {
31662
32003
  return fail3("a change block header is malformed, so we cannot tell which lines belong to it");
@@ -31673,13 +32014,20 @@ function parsePatchPaths(patch) {
31673
32014
  const mode = /^(?:old mode|new mode|new file mode|deleted file mode) (\d+)$/.exec(line)?.[1] ?? /^index [0-9a-f]+\.\.[0-9a-f]+ (\d+)$/.exec(line)?.[1];
31674
32015
  if (mode !== void 0)
31675
32016
  entry.modes.push(mode);
32017
+ if (line.startsWith("index "))
32018
+ entry.indexLines.push(line);
32019
+ if (line.startsWith("new file mode ") || line.startsWith("new mode ")) {
32020
+ entry.newMode = line.slice(line.lastIndexOf(" ") + 1).trim();
32021
+ }
31676
32022
  if (line === "GIT binary patch" || line.startsWith("Binary files ")) {
31677
32023
  entry.binary = true;
32024
+ entry.binaryStub = line !== "GIT binary patch";
31678
32025
  skippingBinary = true;
31679
32026
  continue;
31680
32027
  }
31681
32028
  const sideMarker = line.startsWith("--- ") ? "a/" : line.startsWith("+++ ") ? "b/" : null;
31682
32029
  if (sideMarker !== null) {
32030
+ entry.textContent = true;
31683
32031
  const rest = line.slice(4);
31684
32032
  const tab = rest.indexOf(" ");
31685
32033
  const token = tab === -1 ? rest : rest.slice(0, tab);
@@ -31754,16 +32102,14 @@ function preflightBoundary(input) {
31754
32102
  const changedManifests = /* @__PURE__ */ new Set();
31755
32103
  const deniedPaths = /* @__PURE__ */ new Set();
31756
32104
  const nonFileLockfiles = /* @__PURE__ */ new Set();
32105
+ let binaryBytes = 0;
31757
32106
  for (const entry of parsed.entries) {
31758
- if (entry.binary) {
31759
- const name = entry.rawPaths[0] ?? "a file";
31760
- refusals.push(refusal("binary-patch", normalizePath(name)?.path ?? null, `${name} is changed as binary content, which nobody can review line by line. Binary changes are refused.`));
31761
- continue;
31762
- }
32107
+ let refusedByPath = false;
31763
32108
  const entryPaths = [];
31764
32109
  for (const raw of entry.rawPaths) {
31765
32110
  const normalized = normalizePath(raw);
31766
32111
  if (normalized === null) {
32112
+ refusedByPath = true;
31767
32113
  if (!seenPaths.has(raw)) {
31768
32114
  seenPaths.add(raw);
31769
32115
  refusals.push(refusal("unsafe-path", raw, `${raw} is not a file name a patch may name \u2014 it points outside the repository, or it is written in a form we cannot check.`));
@@ -31781,6 +32127,7 @@ function preflightBoundary(input) {
31781
32127
  seenPaths.add(path5);
31782
32128
  const denied = DENY_RULES.find((rule) => rule.matches(path5, segments, base));
31783
32129
  if (denied !== void 0) {
32130
+ refusedByPath = true;
31784
32131
  deniedPaths.add(path5);
31785
32132
  const why = typeof denied.why === "string" ? denied.why : denied.why(base);
31786
32133
  refusals.push(refusal(denied.code, path5, `${path5} ${why}.`));
@@ -31806,7 +32153,29 @@ function preflightBoundary(input) {
31806
32153
  if (contentPath !== null && contentPath.split("/").pop() === "package.json" && entry.changed) {
31807
32154
  changedManifests.add(contentPath);
31808
32155
  }
32156
+ if (entry.binary && !refusedByPath && contentPath !== null) {
32157
+ const verdict = judgeBinaryEntry({
32158
+ path: contentPath,
32159
+ stub: entry.binaryStub,
32160
+ payload: entry.binaryPayload,
32161
+ indexLines: entry.indexLines,
32162
+ newMode: entry.newMode,
32163
+ createsFile: entry.createsFile,
32164
+ deletesFile: entry.deletesFile,
32165
+ textContent: entry.textContent
32166
+ });
32167
+ if (verdict.kind === "refuse") {
32168
+ refusals.push(refusal(verdict.code, contentPath, verdict.detail));
32169
+ } else if (verdict.kind === "unreadable") {
32170
+ refusals.push(refusal("unparseable", contentPath, `The patch could not be read: the entry for ${contentPath} carries no changes we can apply. A patch we cannot read in full is refused rather than applied in part.`));
32171
+ } else {
32172
+ binaryBytes += verdict.writes;
32173
+ }
32174
+ }
31809
32175
  }
32176
+ const overBinaryCap = binaryTotalRefusal(binaryBytes);
32177
+ if (overBinaryCap !== null)
32178
+ refusals.push(refusal("binary-too-large", null, overBinaryCap));
31810
32179
  for (const [path5, { touches, edits }] of lockfileTouches) {
31811
32180
  if (deniedPaths.has(path5))
31812
32181
  continue;
@@ -31830,10 +32199,18 @@ var init_boundary = __esm({
31830
32199
  "../../packages/envrun/dist/boundary.js"() {
31831
32200
  "use strict";
31832
32201
  init_lockfileCheck();
32202
+ init_binaryPatch();
31833
32203
  PATH_REFUSAL_CODES = [
31834
32204
  "empty-patch",
31835
32205
  "unparseable",
31836
32206
  "binary-patch",
32207
+ // Not path-derived, but refused on both sides for the same bytes, so they belong on the
32208
+ // surface `boundary-oracle.test.mjs` compares (TERM-1342).
32209
+ "binary-content-missing",
32210
+ "binary-executable",
32211
+ "binary-mode",
32212
+ "binary-program",
32213
+ "binary-too-large",
31837
32214
  "unsafe-path",
31838
32215
  "out-of-slice",
31839
32216
  "ci-config",
@@ -32190,7 +32567,7 @@ var init_emptyGitConfig = __esm({
32190
32567
 
32191
32568
  // ../../packages/envrun/dist/hostedVenue.js
32192
32569
  import { spawn as spawn4, spawnSync as spawnSync5 } from "child_process";
32193
- import { createHash as createHash11, X509Certificate } from "crypto";
32570
+ import { createHash as createHash12, X509Certificate } from "crypto";
32194
32571
  import { chmodSync as chmodSync3, existsSync as existsSync12, mkdtempSync as mkdtempSync3, readFileSync as readFileSync12, rmSync as rmSync8, writeFileSync as writeFileSync14 } from "fs";
32195
32572
  import { request as httpsRequest } from "https";
32196
32573
  import { createServer } from "net";
@@ -32860,7 +33237,7 @@ function iapPortTunnelArgv(vm, project, zone, remotePort, localPort) {
32860
33237
  }
32861
33238
  function spkiSha256Hex(certDer) {
32862
33239
  const spki = new X509Certificate(certDer).publicKey.export({ type: "spki", format: "der" });
32863
- return createHash11("sha256").update(spki).digest("hex");
33240
+ return createHash12("sha256").update(spki).digest("hex");
32864
33241
  }
32865
33242
  function certMetadataValue(pem) {
32866
33243
  return new X509Certificate(pem).raw.toString("base64url");
@@ -34779,6 +35156,10 @@ function nodeInstallCommand(repo) {
34779
35156
  return null;
34780
35157
  }
34781
35158
  function lockfileGaps(repo) {
35159
+ const read = readLockfileGaps(repo);
35160
+ return read === null || read.missing.length === 0 ? null : { lockfile: read.lockfile, missing: read.missing };
35161
+ }
35162
+ function readLockfileGaps(repo) {
34782
35163
  const pkg = readJsonObject(repo, "package.json");
34783
35164
  if (pkg === null)
34784
35165
  return null;
@@ -34805,7 +35186,7 @@ function lockfileGaps(repo) {
34805
35186
  missing.push(`${n}@${r} (${dir})`);
34806
35187
  }
34807
35188
  }
34808
- return missing.length === 0 ? null : { lockfile, missing };
35189
+ return { lockfile, missing, rootRead: rootListing !== null };
34809
35190
  }
34810
35191
  function wantedDependencies(pkg) {
34811
35192
  const wanted = [];
@@ -37304,7 +37685,11 @@ function refusedRun(fields) {
37304
37685
  // reasoning as `leaksClean` above: null because nothing happened, and it must
37305
37686
  // not read as a venue we looked at and could not name.
37306
37687
  venue: null,
37307
- refusalOrigin: fields.origin
37688
+ refusalOrigin: fields.origin,
37689
+ // No test step ran, so there is no output to read a step from.
37690
+ stoppedAt: null,
37691
+ // Refused before any install step ran.
37692
+ installFrozen: null
37308
37693
  };
37309
37694
  }
37310
37695
  function refusalOriginOf(refusal2) {
@@ -37388,7 +37773,29 @@ function installUnfrozenNote(spec) {
37388
37773
  const u = spec.installUnfrozen;
37389
37774
  if (u === void 0)
37390
37775
  return null;
37391
- return `${u.lockfile} does not list ${u.missing.join(", ")}, so the install ran as \`${String(spec.installCommand)}\` without its frozen-lockfile check. Regenerate ${u.lockfile} before merging.`;
37776
+ return `${u.lockfile} does not list ${u.missing.join(", ")}, so the install ran as \`${String(spec.installCommand)}\` without its frozen-lockfile check. Include the updated ${u.lockfile} in the patch, or regenerate it before merging.`;
37777
+ }
37778
+ function isFrozenInstallCommand(command) {
37779
+ const lead = /^(?:(?:sudo|time)\s+|env(?:\s+[A-Za-z_]\w*=\S*)*\s+)*/.exec(command)?.[0] ?? "";
37780
+ const install = command.slice(lead.length).split(/;|&&|\|\|/)[0]?.trim() ?? "";
37781
+ if (/^npm ci(\s|$)/.test(install))
37782
+ return true;
37783
+ if (/^(yarn|pnpm|bun)\b/.test(install) && /\s--frozen-lockfile(?=\s|$)/.test(install)) {
37784
+ return true;
37785
+ }
37786
+ return /^yarn\b/.test(install) && /\s--immutable(?=\s|$)/.test(install);
37787
+ }
37788
+ function installFrozenFor(spec, install) {
37789
+ if (install === null)
37790
+ return null;
37791
+ const launched = install.exitCode !== INSTALL_NEVER_STARTED && (install.exitCode !== null || install.timedOut);
37792
+ if (!launched)
37793
+ return null;
37794
+ if (spec.runtime !== "node")
37795
+ return null;
37796
+ if (spec.installUnfrozen !== void 0)
37797
+ return false;
37798
+ return isFrozenInstallCommand(install.command);
37392
37799
  }
37393
37800
  async function verifyWorkingDiff(req) {
37394
37801
  const ctx = {
@@ -37755,7 +38162,10 @@ async function runVerification(req, ctx) {
37755
38162
  // reasoning and the #735 failure that makes the distinction load-bearing.
37756
38163
  venue: describeVenue(lease),
37757
38164
  // A run that reached a verdict was not refused, so neither side refused it.
37758
- refusalOrigin: null
38165
+ refusalOrigin: null,
38166
+ // Read from the full test output, not `outputTail`, which is an excerpt.
38167
+ stoppedAt: readStoppedAt(verdict.test?.stdout ?? "", verdict.test?.stderr ?? ""),
38168
+ installFrozen: installFrozenFor(spec, verdict.install)
37759
38169
  };
37760
38170
  let later = null;
37761
38171
  if (req.screenshots !== void 0) {
@@ -37954,7 +38364,7 @@ async function prepareBaseTree(o) {
37954
38364
  throw err;
37955
38365
  }
37956
38366
  }
37957
- var ThRunError, OUTPUT_TAIL_BYTES, LOCKFILE_READ_CAP, FULL_COMMIT_ID, ALLOWED_URL_SCHEMES, SCP_STYLE, URL_SCHEME, WINDOWS_ABSOLUTE, UNC_PATH, TRANSPORT_REASON, SHA_REASON, FULL_SHA, MIN_GIT_VERSION_FOR_END_OF_OPTIONS, CloneUnavailableError, GIT_ENV_ALLOWLIST, credentialFreeHomeDir, SSH_ISOLATION_ARGS, WITHHELD_SEGMENT, UNPARSEABLE_TARGET, DEFERRED_GATE_TIMEOUT_MS, FAILURE_LINE, REDACTED_TARGET_REPO, REDACTED_TARGET_SHA;
38367
+ var ThRunError, OUTPUT_TAIL_BYTES, LOCKFILE_READ_CAP, FULL_COMMIT_ID, ALLOWED_URL_SCHEMES, SCP_STYLE, URL_SCHEME, WINDOWS_ABSOLUTE, UNC_PATH, TRANSPORT_REASON, SHA_REASON, FULL_SHA, MIN_GIT_VERSION_FOR_END_OF_OPTIONS, CloneUnavailableError, GIT_ENV_ALLOWLIST, credentialFreeHomeDir, SSH_ISOLATION_ARGS, WITHHELD_SEGMENT, UNPARSEABLE_TARGET, DEFERRED_GATE_TIMEOUT_MS, FAILURE_LINE, REDACTED_TARGET_REPO, REDACTED_TARGET_SHA, INSTALL_NEVER_STARTED;
37958
38368
  var init_thrun = __esm({
37959
38369
  "../../packages/envrun/dist/thrun.js"() {
37960
38370
  "use strict";
@@ -37962,6 +38372,7 @@ var init_thrun = __esm({
37962
38372
  init_screenshots();
37963
38373
  init_dist3();
37964
38374
  init_attestation2();
38375
+ init_classify2();
37965
38376
  init_boundary();
37966
38377
  init_lockfileCheck();
37967
38378
  init_emptyGitConfig();
@@ -38042,6 +38453,7 @@ var init_thrun = __esm({
38042
38453
  FAILURE_LINE = /^(?:[ \t]*(?:not ok |FAILED |FAIL )|E {3}|# fail [1-9])/m;
38043
38454
  REDACTED_TARGET_REPO = "(refused before the target was accepted)";
38044
38455
  REDACTED_TARGET_SHA = "(refused)";
38456
+ INSTALL_NEVER_STARTED = 125;
38045
38457
  }
38046
38458
  });
38047
38459
 
@@ -38943,6 +39355,7 @@ __export(dist_exports, {
38943
39355
  imageRepo: () => imageRepo,
38944
39356
  imageVariantFor: () => imageVariantFor,
38945
39357
  installCommandFor: () => installCommandFor,
39358
+ installFrozenFor: () => installFrozenFor,
38946
39359
  installLocalMigrationTooling: () => installLocalMigrationTooling,
38947
39360
  installUnfrozenNote: () => installUnfrozenNote,
38948
39361
  isBookkeepingTable: () => isBookkeepingTable,
@@ -38971,6 +39384,7 @@ __export(dist_exports, {
38971
39384
  readAlembicChain: () => readAlembicChain,
38972
39385
  readCounts: () => readCounts,
38973
39386
  readSchema: () => readSchema,
39387
+ readStoppedAt: () => readStoppedAt,
38974
39388
  recordedApplied: () => recordedApplied,
38975
39389
  refuseSshTransport: () => refuseSshTransport,
38976
39390
  refuseUnbuildableSpec: () => refuseUnbuildableSpec,
@@ -39314,7 +39728,7 @@ import {
39314
39728
  closeSync as closeSync6
39315
39729
  } from "fs";
39316
39730
  import { join as join31, dirname as dirname9, isAbsolute as isAbsolute5, resolve as pathResolve } from "path";
39317
- import { createHash as createHash12 } from "crypto";
39731
+ import { createHash as createHash13 } from "crypto";
39318
39732
  import { homedir as homedir14, hostname as osHostname } from "os";
39319
39733
  import { execFile as execFile3, execFileSync as execFileSync2, spawnSync as spawnSync8 } from "child_process";
39320
39734
  import { promisify as promisify3 } from "util";
@@ -41127,7 +41541,7 @@ async function registerFounderClaim(b) {
41127
41541
  let clearedLocalCredential = false;
41128
41542
  let refusedForPurpose = false;
41129
41543
  let preserveBackgroundToken = false;
41130
- const refuse4 = (reason) => {
41544
+ const refuse5 = (reason) => {
41131
41545
  console.error(
41132
41546
  `
41133
41547
  terminalhire claim: refusing to record \u2014 ${reason}
@@ -41152,7 +41566,7 @@ terminalhire claim: refusing to record \u2014 ${reason}
41152
41566
  try {
41153
41567
  expectLogin = await localLoginForPaidBrowserClaim();
41154
41568
  } catch (err) {
41155
- refuse4(
41569
+ refuse5(
41156
41570
  `could not verify which GitHub account this machine is signed in as: ${err instanceof Error ? err.message : String(err)}.
41157
41571
  Run \`terminalhire login\`, then retry this claim. Browser verification was
41158
41572
  not opened and no claim was sent.`
@@ -41168,16 +41582,16 @@ terminalhire claim: refusing to record \u2014 ${reason}
41168
41582
  claimRef: opportunityShortToken(b.bountyId)
41169
41583
  });
41170
41584
  if (!proofToken) {
41171
- refuse4("could not verify your GitHub identity with terminalhire (see above).");
41585
+ refuse5("could not verify your GitHub identity with terminalhire (see above).");
41172
41586
  }
41173
41587
  const verifiedLogin = proofTokenLogin(proofToken);
41174
41588
  if (!verifiedLogin) {
41175
- refuse4(
41589
+ refuse5(
41176
41590
  "the browser verification response did not name a GitHub account, so it could not be compared with this machine\u2019s sign-in. Nothing was sent; retry the claim."
41177
41591
  );
41178
41592
  }
41179
41593
  if (verifiedLogin.toLowerCase() !== expectLogin.toLowerCase()) {
41180
- refuse4(
41594
+ refuse5(
41181
41595
  `you verified in the browser as @${verifiedLogin}, but this machine is signed in as @${expectLogin}.
41182
41596
  Nothing was sent. Sign the browser into @${expectLogin} and run this again, or
41183
41597
  run \`terminalhire login\` if you meant to work as @${verifiedLogin}.`
@@ -41209,7 +41623,7 @@ terminalhire claim: refusing to record \u2014 ${reason}
41209
41623
  try {
41210
41624
  res = await sendRegistration(true);
41211
41625
  } catch (err) {
41212
- refuse4(
41626
+ refuse5(
41213
41627
  `terminalhire is unreachable (${err instanceof Error ? err.message : String(err)}), so the posting could not be revalidated.`
41214
41628
  );
41215
41629
  }
@@ -41260,7 +41674,7 @@ terminalhire claim: refusing to record \u2014 ${reason}
41260
41674
  try {
41261
41675
  res = await sendRegistration(true);
41262
41676
  } catch (err) {
41263
- refuse4(
41677
+ refuse5(
41264
41678
  `terminalhire is unreachable (${err instanceof Error ? err.message : String(err)}), so the posting could not be revalidated.`
41265
41679
  );
41266
41680
  }
@@ -41271,14 +41685,14 @@ terminalhire claim: refusing to record \u2014 ${reason}
41271
41685
  res = await sendRegistration(false);
41272
41686
  refusalBody = await readRefusal(res);
41273
41687
  } catch (err) {
41274
- refuse4(
41688
+ refuse5(
41275
41689
  `terminalhire is unreachable (${err instanceof Error ? err.message : String(err)}), so the posting could not be revalidated.`
41276
41690
  );
41277
41691
  }
41278
41692
  }
41279
41693
  if (!res.ok) {
41280
41694
  const detail = refusalBody?.message || refusalBody?.error || "";
41281
- refuse4(
41695
+ refuse5(
41282
41696
  detail ? `the server refused this claim (${res.status}): ${detail}` : `the server refused this claim (${res.status}).`
41283
41697
  );
41284
41698
  }
@@ -41289,7 +41703,7 @@ terminalhire claim: refusing to record \u2014 ${reason}
41289
41703
  body = null;
41290
41704
  }
41291
41705
  if (!body || body.ok !== true) {
41292
- refuse4("malformed registration response from the server.");
41706
+ refuse5("malformed registration response from the server.");
41293
41707
  }
41294
41708
  const mintedToken = typeof body.pushToken === "string" && body.pushToken.length > 0 ? body.pushToken : null;
41295
41709
  if (refusedForPurpose && mintedToken && !preserveBackgroundToken) {
@@ -42821,7 +43235,7 @@ function writePackFile(destDir, relPath, content, what) {
42821
43235
  return { written: true, reason: null, sha256: sha256OfUtf8(content) };
42822
43236
  }
42823
43237
  function sha256OfUtf8(content) {
42824
- return createHash12("sha256").update(content, "utf8").digest("hex");
43238
+ return createHash13("sha256").update(content, "utf8").digest("hex");
42825
43239
  }
42826
43240
  function writeWorkspacePack(destDir, spec, claim, delivery) {
42827
43241
  if (delivery !== "full" && delivery !== "sparse") {
@@ -43724,7 +44138,7 @@ function loadClaimScreenshots(specArg) {
43724
44138
  items.push({
43725
44139
  file,
43726
44140
  caption: shot.caption,
43727
- sha256: createHash12("sha256").update(bytes).digest("hex"),
44141
+ sha256: createHash13("sha256").update(bytes).digest("hex"),
43728
44142
  bytes: bytes.byteLength,
43729
44143
  width,
43730
44144
  height
@@ -43790,6 +44204,67 @@ async function uploadClaimScreenshots(intake, uploads) {
43790
44204
  );
43791
44205
  }
43792
44206
  }
44207
+ var DEPENDENCY_SECTIONS2 = [
44208
+ "dependencies",
44209
+ "devDependencies",
44210
+ "optionalDependencies",
44211
+ "peerDependencies"
44212
+ ];
44213
+ var SENDABLE_LOCKFILES = [
44214
+ ["package-lock.json", "npm install"],
44215
+ ["yarn.lock", "yarn install"]
44216
+ ];
44217
+ async function packageJsonAt(wt, rev, path5) {
44218
+ let text;
44219
+ try {
44220
+ ({ stdout: text } = await pExecFile("git", ["-C", wt, "show", `${rev}:${path5}`], {
44221
+ shell: false,
44222
+ maxBuffer: 16 * 1024 * 1024
44223
+ }));
44224
+ } catch {
44225
+ return {};
44226
+ }
44227
+ try {
44228
+ const parsed = JSON.parse(String(text));
44229
+ return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
44230
+ } catch {
44231
+ return null;
44232
+ }
44233
+ }
44234
+ function sectionKey(pkg, section) {
44235
+ const value = pkg[section];
44236
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
44237
+ return JSON.stringify(value ?? null);
44238
+ }
44239
+ return JSON.stringify(Object.keys(value).sort().map((k) => [k, value[k]]));
44240
+ }
44241
+ async function patchChangesDependencies(wt, base, touched) {
44242
+ for (const path5 of touched) {
44243
+ if (path5.split("/").pop() !== "package.json") continue;
44244
+ const before = await packageJsonAt(wt, base, path5);
44245
+ const after = await packageJsonAt(wt, "HEAD", path5);
44246
+ if (before === null || after === null) continue;
44247
+ if (DEPENDENCY_SECTIONS2.some((s) => sectionKey(before, s) !== sectionKey(after, s))) return true;
44248
+ }
44249
+ return false;
44250
+ }
44251
+ async function dependencyLockfileWarnings(wt, base, touched) {
44252
+ if (!await patchChangesDependencies(wt, base, touched)) return [];
44253
+ const warnings = [];
44254
+ for (const [lockfile, cmd] of SENDABLE_LOCKFILES) {
44255
+ let present;
44256
+ try {
44257
+ present = (await sh("git", ["-C", wt, "ls-tree", "--name-only", base, "--", lockfile])).trim() === lockfile;
44258
+ } catch {
44259
+ present = false;
44260
+ }
44261
+ if (!present || touched.includes(lockfile)) continue;
44262
+ warnings.push(
44263
+ ` \u26A0 package.json changes dependencies, but ${lockfile} is not in this patch. The run will install without its frozen-lockfile check. Run \`${cmd}\` and commit ${lockfile} to keep it frozen.`
44264
+ );
44265
+ }
44266
+ return warnings;
44267
+ }
43793
44268
  async function submitFounderPatch({ claims, claim, id, wt, flags }) {
43794
44269
  let roots;
43795
44270
  try {
@@ -43809,10 +44284,14 @@ async function submitFounderPatch({ claims, claim, id, wt, flags }) {
43809
44284
  const base = roots[0];
43810
44285
  let patch;
43811
44286
  try {
43812
- ({ stdout: patch } = await pExecFile("git", ["-C", wt, "diff", base, "HEAD"], {
43813
- shell: false,
43814
- maxBuffer: 16 * 1024 * 1024
43815
- }));
44287
+ ({ stdout: patch } = await pExecFile(
44288
+ "git",
44289
+ ["-C", wt, "diff", "--binary", "--full-index", base, "HEAD"],
44290
+ {
44291
+ shell: false,
44292
+ maxBuffer: 16 * 1024 * 1024
44293
+ }
44294
+ ));
43816
44295
  patch = String(patch);
43817
44296
  } catch (err) {
43818
44297
  console.error(
@@ -43836,6 +44315,9 @@ async function submitFounderPatch({ claims, claim, id, wt, flags }) {
43836
44315
  );
43837
44316
  process.exit(1);
43838
44317
  }
44318
+ const lockfileWarnings = await dependencyLockfileWarnings(wt, base, touched);
44319
+ if (lockfileWarnings.length > 0) console.log(`
44320
+ ${lockfileWarnings.join("\n")}`);
43839
44321
  const authorName = await sh("git", ["-C", wt, "log", "-1", "--format=%an"]);
43840
44322
  const authorEmail = await sh("git", ["-C", wt, "log", "-1", "--format=%ae"]);
43841
44323
  const shots = flags.screenshots === void 0 ? null : loadClaimScreenshots(flags.screenshots);