@lousy-agents/lint 5.14.8 → 5.14.10

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/index.js +237 -123
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -566,7 +566,7 @@ module.exports = webpackEmptyAsyncContext;
566
566
 
567
567
 
568
568
  },
569
- 2202(__unused_rspack_module, __webpack_exports__, __webpack_require__) {
569
+ 4403(__unused_rspack_module, __webpack_exports__, __webpack_require__) {
570
570
 
571
571
  // EXPORTS
572
572
  __webpack_require__.d(__webpack_exports__, {
@@ -1031,6 +1031,9 @@ function path_isPathInside(root, target) {
1031
1031
  const firstSegment = relative.split(external_node_path_.posix.sep)[0];
1032
1032
  return relative === "" || (firstSegment !== ".." && !external_node_path_.isAbsolute(relative));
1033
1033
  }
1034
+ function isPathRelativeEscape(relativePath) {
1035
+ return relativePath === ".." || relativePath.startsWith(`..${external_node_path_.sep}`) || external_node_path_.isAbsolute(relativePath);
1036
+ }
1034
1037
  function resolveSafeBaseDir(rootDir) {
1035
1038
  const resolved = path.resolve(rootDir);
1036
1039
  return resolved.endsWith(path.sep) ? resolved : `${resolved}${path.sep}`;
@@ -1096,7 +1099,7 @@ function splitSafeRelativePath(relativePath) {
1096
1099
  function resolveSafeRelativePath(rootDir, relativePath) {
1097
1100
  const root = path.resolve(rootDir);
1098
1101
  const target = path.resolve(root, ...splitSafeRelativePath(relativePath));
1099
- if (target !== root && !target.startsWith(root + path.sep)) {
1102
+ if (!path_isPathInside(root, target)) {
1100
1103
  throw new FsSafeError("outside-workspace", "relative path escapes root");
1101
1104
  }
1102
1105
  return target;
@@ -1179,17 +1182,17 @@ function directory_guard_createNearestExistingSyncDirectoryGuard(rootReal, targe
1179
1182
 
1180
1183
 
1181
1184
 
1185
+
1182
1186
  function isSameOrChildPath(candidate, parent) {
1183
- return candidate === parent || candidate.startsWith(`${parent}${external_node_path_.sep}`);
1184
- }
1185
- function isPathEscape(relativePath) {
1186
- return relativePath === ".." || relativePath.startsWith(`..${external_node_path_.sep}`) || external_node_path_.isAbsolute(relativePath);
1187
+ const parentPrefix = parent.endsWith(external_node_path_.sep) ? parent : `${parent}${external_node_path_.sep}`;
1188
+ return candidate === parent || candidate.startsWith(parentPrefix);
1187
1189
  }
1188
1190
  async function mkdirPathComponentsWithGuards(params) {
1189
1191
  const root = external_node_path_.resolve(params.rootReal);
1192
+ const rootCanonical = external_node_path_.resolve(await promises_.realpath(root));
1190
1193
  const target = external_node_path_.resolve(params.targetPath);
1191
1194
  const relative = external_node_path_.relative(root, target);
1192
- if (isPathEscape(relative)) {
1195
+ if (isPathRelativeEscape(relative)) {
1193
1196
  throw new errors_FsSafeError("outside-workspace", "directory is outside workspace root");
1194
1197
  }
1195
1198
  let current = root;
@@ -1212,7 +1215,7 @@ async function mkdirPathComponentsWithGuards(params) {
1212
1215
  }
1213
1216
  // Node's recursive mkdir follows symlinks in missing components. Build one
1214
1217
  // segment at a time and realpath-check each segment before descending.
1215
- if (!isSameOrChildPath(external_node_path_.resolve(await promises_.realpath(next)), root)) {
1218
+ if (!isSameOrChildPath(external_node_path_.resolve(await promises_.realpath(next)), rootCanonical)) {
1216
1219
  throw new errors_FsSafeError("outside-workspace", "directory escaped workspace root");
1217
1220
  }
1218
1221
  await directory_guard_createAsyncDirectoryGuard(next);
@@ -1347,26 +1350,20 @@ var external_node_child_process_ = __webpack_require__(1421);
1347
1350
 
1348
1351
 
1349
1352
  const PINNED_PYTHON_WORKER_SOURCE = String.raw `
1350
- import base64
1351
- import errno
1352
- import json
1353
- import os
1354
- import secrets
1355
- import stat
1356
- import sys
1357
-
1353
+ import base64, errno, json, os, secrets, stat, sys
1358
1354
  DIR_FLAGS = os.O_RDONLY
1359
1355
  if hasattr(os, "O_DIRECTORY"):
1360
1356
  DIR_FLAGS |= os.O_DIRECTORY
1361
1357
  if hasattr(os, "O_NOFOLLOW"):
1362
1358
  DIR_FLAGS |= os.O_NOFOLLOW
1363
1359
  READ_FLAGS = os.O_RDONLY
1360
+ if hasattr(os, "O_NONBLOCK"):
1361
+ READ_FLAGS |= os.O_NONBLOCK
1364
1362
  if hasattr(os, "O_NOFOLLOW"):
1365
1363
  READ_FLAGS |= os.O_NOFOLLOW
1366
1364
  WRITE_FLAGS = os.O_WRONLY | os.O_CREAT | os.O_EXCL
1367
1365
  if hasattr(os, "O_NOFOLLOW"):
1368
1366
  WRITE_FLAGS |= os.O_NOFOLLOW
1369
-
1370
1367
  def split_relative(value):
1371
1368
  if value in ("", "."):
1372
1369
  return []
@@ -1379,10 +1376,8 @@ def split_relative(value):
1379
1376
  if part == "..":
1380
1377
  raise OSError(errno.EPERM, "path traversal is not allowed")
1381
1378
  return parts
1382
-
1383
1379
  def open_dir(path_value, dir_fd=None):
1384
1380
  return os.open(path_value, DIR_FLAGS, dir_fd=dir_fd)
1385
-
1386
1381
  def walk_dir(root_fd, segments, mkdir_enabled=False):
1387
1382
  current_fd = os.dup(root_fd)
1388
1383
  try:
@@ -1400,14 +1395,12 @@ def walk_dir(root_fd, segments, mkdir_enabled=False):
1400
1395
  except Exception:
1401
1396
  os.close(current_fd)
1402
1397
  raise
1403
-
1404
1398
  def parent_and_basename(root_fd, relative):
1405
1399
  segments = split_relative(relative)
1406
1400
  if not segments:
1407
1401
  raise OSError(errno.EPERM, "operation requires a non-root path")
1408
1402
  parent_fd = walk_dir(root_fd, segments[:-1])
1409
1403
  return parent_fd, segments[-1]
1410
-
1411
1404
  def encode_stat(st):
1412
1405
  mode = st.st_mode
1413
1406
  return {
@@ -1423,14 +1416,108 @@ def encode_stat(st):
1423
1416
  "size": st.st_size,
1424
1417
  "uid": st.st_uid,
1425
1418
  }
1426
-
1427
1419
  def reject_unsafe_endpoint(st):
1428
1420
  mode = st.st_mode
1429
1421
  if stat.S_ISLNK(mode):
1430
1422
  raise OSError(errno.ELOOP, "symlink endpoint is not allowed")
1431
1423
  if stat.S_ISREG(mode) and st.st_nlink > 1:
1432
1424
  raise OSError(errno.EPERM, "hardlinked file endpoint is not allowed")
1433
-
1425
+ def copy_bytes(source_fd, dest_fd):
1426
+ while True:
1427
+ chunk = os.read(source_fd, 65536)
1428
+ if not chunk:
1429
+ break
1430
+ view = memoryview(chunk)
1431
+ while view:
1432
+ written = os.write(dest_fd, view)
1433
+ if written <= 0:
1434
+ raise OSError(errno.EIO, "short write")
1435
+ view = view[written:]
1436
+ def write_all(fd, data):
1437
+ view = memoryview(data)
1438
+ while view:
1439
+ written = os.write(fd, view)
1440
+ if written <= 0:
1441
+ raise OSError(errno.EIO, "short write")
1442
+ view = view[written:]
1443
+ def link_unsupported(exc):
1444
+ unsupported = (errno.EPERM, errno.EOPNOTSUPP, getattr(errno, "ENOTSUP", errno.EOPNOTSUPP))
1445
+ return getattr(exc, "errno", None) in unsupported
1446
+ def link_no_replace(name, new_name, source_fd, target_fd):
1447
+ linked = False
1448
+ try:
1449
+ os.link(name, new_name, src_dir_fd=source_fd, dst_dir_fd=target_fd, follow_symlinks=False)
1450
+ linked = True
1451
+ os.unlink(name, dir_fd=source_fd)
1452
+ except Exception:
1453
+ if linked:
1454
+ try: os.unlink(new_name, dir_fd=target_fd)
1455
+ except FileNotFoundError: pass
1456
+ raise
1457
+ os.fsync(source_fd)
1458
+ if source_fd != target_fd:
1459
+ os.fsync(target_fd)
1460
+ def copy_file_no_replace(source_parent_fd, source_name, target_parent_fd, basename, mode, expected=None, unlink_source=False):
1461
+ source_fd = os.open(source_name, READ_FLAGS, dir_fd=source_parent_fd)
1462
+ dest_fd = None; success = False; dest_stat = None
1463
+ try:
1464
+ if expected is not None:
1465
+ source_stat = os.fstat(source_fd)
1466
+ if source_stat.st_dev != expected.st_dev or source_stat.st_ino != expected.st_ino:
1467
+ raise RuntimeError("fs-safe-source-mismatch")
1468
+ dest_fd = os.open(basename, WRITE_FLAGS, mode, dir_fd=target_parent_fd)
1469
+ copy_bytes(source_fd, dest_fd)
1470
+ os.fsync(dest_fd)
1471
+ dest_stat = os.fstat(dest_fd)
1472
+ success = True
1473
+ finally:
1474
+ os.close(source_fd)
1475
+ if dest_fd is not None:
1476
+ os.close(dest_fd)
1477
+ if dest_fd is not None and not success:
1478
+ try: os.unlink(basename, dir_fd=target_parent_fd)
1479
+ except FileNotFoundError: pass
1480
+ if unlink_source:
1481
+ try:
1482
+ os.unlink(source_name, dir_fd=source_parent_fd)
1483
+ except Exception:
1484
+ try: os.unlink(basename, dir_fd=target_parent_fd)
1485
+ except FileNotFoundError: pass
1486
+ raise
1487
+ return dest_stat
1488
+ def same_identity(left, right):
1489
+ return left.st_dev == right.st_dev and left.st_ino == right.st_ino
1490
+ def verify_temp_name(parent_fd, temp_name, expected_stat):
1491
+ current_stat = os.lstat(temp_name, dir_fd=parent_fd)
1492
+ if stat.S_ISLNK(current_stat.st_mode) or not same_identity(current_stat, expected_stat):
1493
+ raise RuntimeError("fs-safe-temp-mismatch")
1494
+ def verify_committed_temp(parent_fd, basename, expected_stat):
1495
+ final_stat = os.lstat(basename, dir_fd=parent_fd)
1496
+ if not stat.S_ISLNK(final_stat.st_mode) and same_identity(final_stat, expected_stat):
1497
+ return final_stat
1498
+ try: os.unlink(basename, dir_fd=parent_fd)
1499
+ except FileNotFoundError: pass
1500
+ raise RuntimeError("fs-safe-temp-mismatch")
1501
+ def commit_temp_file(parent_fd, temp_name, basename, overwrite, mode, expected_stat):
1502
+ verify_temp_name(parent_fd, temp_name, expected_stat)
1503
+ if overwrite:
1504
+ os.replace(temp_name, basename, src_dir_fd=parent_fd, dst_dir_fd=parent_fd)
1505
+ return verify_committed_temp(parent_fd, basename, expected_stat)
1506
+ else:
1507
+ try:
1508
+ os.link(temp_name, basename, src_dir_fd=parent_fd, dst_dir_fd=parent_fd, follow_symlinks=False)
1509
+ final_stat = verify_committed_temp(parent_fd, basename, expected_stat)
1510
+ os.unlink(temp_name, dir_fd=parent_fd)
1511
+ return final_stat
1512
+ except OSError as exc:
1513
+ if not link_unsupported(exc):
1514
+ raise
1515
+ return copy_file_no_replace(parent_fd, temp_name, parent_fd, basename, mode, expected_stat, True)
1516
+ def assert_expected_root(root_fd, payload):
1517
+ if "rootDev" in payload or "rootIno" in payload:
1518
+ root_stat = os.fstat(root_fd)
1519
+ if root_stat.st_dev != int(payload["rootDev"]) or root_stat.st_ino != int(payload["rootIno"]):
1520
+ raise RuntimeError("fs-safe-root-mismatch")
1434
1521
  def stat_path(root_fd, payload):
1435
1522
  relative = payload.get("relativePath", "")
1436
1523
  segments = split_relative(relative)
@@ -1444,7 +1531,6 @@ def stat_path(root_fd, payload):
1444
1531
  return encode_stat(st)
1445
1532
  finally:
1446
1533
  os.close(parent_fd)
1447
-
1448
1534
  def readdir_path(root_fd, payload):
1449
1535
  dir_fd = walk_dir(root_fd, split_relative(payload.get("relativePath", "")))
1450
1536
  try:
@@ -1460,12 +1546,9 @@ def readdir_path(root_fd, payload):
1460
1546
  return entries
1461
1547
  finally:
1462
1548
  os.close(dir_fd)
1463
-
1464
1549
  def mkdirp_path(root_fd, payload):
1465
1550
  dir_fd = walk_dir(root_fd, split_relative(payload.get("relativePath", "")), mkdir_enabled=True)
1466
- os.close(dir_fd)
1467
- return None
1468
-
1551
+ os.close(dir_fd); return None
1469
1552
  def remove_tree(parent_fd, basename):
1470
1553
  st = os.lstat(basename, dir_fd=parent_fd)
1471
1554
  if stat.S_ISDIR(st.st_mode) and not stat.S_ISLNK(st.st_mode):
@@ -1478,7 +1561,6 @@ def remove_tree(parent_fd, basename):
1478
1561
  os.rmdir(basename, dir_fd=parent_fd)
1479
1562
  else:
1480
1563
  os.unlink(basename, dir_fd=parent_fd)
1481
-
1482
1564
  def remove_path(root_fd, payload):
1483
1565
  parent_fd, basename = parent_and_basename(root_fd, payload.get("relativePath", ""))
1484
1566
  try:
@@ -1505,7 +1587,18 @@ def rename_path(root_fd, payload):
1505
1587
  try:
1506
1588
  from_stat = os.lstat(from_base, dir_fd=from_parent_fd)
1507
1589
  reject_unsafe_endpoint(from_stat)
1508
- if not payload.get("overwrite", True):
1590
+ overwrite = payload.get("overwrite", True)
1591
+ if not overwrite and stat.S_ISREG(from_stat.st_mode):
1592
+ try:
1593
+ link_no_replace(from_base, to_base, from_parent_fd, to_parent_fd)
1594
+ except OSError as exc:
1595
+ if not link_unsupported(exc):
1596
+ raise
1597
+ copy_file_no_replace(from_parent_fd, from_base, to_parent_fd, to_base, stat.S_IMODE(from_stat.st_mode), from_stat, True)
1598
+ return None
1599
+ if not overwrite and stat.S_ISDIR(from_stat.st_mode):
1600
+ raise RuntimeError("fs-safe-directory-noreplace-unsupported")
1601
+ if not overwrite:
1509
1602
  try:
1510
1603
  os.lstat(to_base, dir_fd=to_parent_fd)
1511
1604
  raise FileExistsError(errno.EEXIST, "destination exists", to_base)
@@ -1550,19 +1643,15 @@ def write_path(root_fd, payload):
1550
1643
  except FileNotFoundError:
1551
1644
  pass
1552
1645
  temp_name, temp_fd = create_temp_file(parent_fd, basename, mode)
1553
- view = memoryview(data)
1554
- while view:
1555
- written = os.write(temp_fd, view)
1556
- if written <= 0:
1557
- raise OSError(errno.EIO, "short write")
1558
- view = view[written:]
1646
+ os.fchmod(temp_fd, mode)
1647
+ write_all(temp_fd, data)
1559
1648
  os.fsync(temp_fd)
1649
+ temp_stat = os.fstat(temp_fd)
1560
1650
  os.close(temp_fd)
1561
1651
  temp_fd = None
1562
- os.replace(temp_name, basename, src_dir_fd=parent_fd, dst_dir_fd=parent_fd)
1652
+ result_stat = commit_temp_file(parent_fd, temp_name, basename, overwrite, mode, temp_stat)
1563
1653
  temp_name = None
1564
1654
  os.fsync(parent_fd)
1565
- result_stat = os.stat(basename, dir_fd=parent_fd, follow_symlinks=False)
1566
1655
  return {"dev": result_stat.st_dev, "ino": result_stat.st_ino}
1567
1656
  finally:
1568
1657
  if temp_fd is not None:
@@ -1592,13 +1681,8 @@ def copy_path(root_fd, payload):
1592
1681
  if max_bytes >= 0 and source_stat.st_size > max_bytes:
1593
1682
  raise RuntimeError("fs-safe-too-large:%d:%d" % (max_bytes, source_stat.st_size))
1594
1683
  parent_fd = walk_dir(root_fd, split_relative(payload.get("relativeParentPath", "")), bool(payload.get("mkdir", True)))
1595
- if not overwrite:
1596
- try:
1597
- os.lstat(basename, dir_fd=parent_fd)
1598
- raise FileExistsError(errno.EEXIST, "destination exists", basename)
1599
- except FileNotFoundError:
1600
- pass
1601
1684
  temp_name, temp_fd = create_temp_file(parent_fd, basename, mode)
1685
+ os.fchmod(temp_fd, mode)
1602
1686
  written_bytes = 0
1603
1687
  while True:
1604
1688
  chunk = os.read(source_fd, 65536)
@@ -1614,12 +1698,12 @@ def copy_path(root_fd, payload):
1614
1698
  raise OSError(errno.EIO, "short write")
1615
1699
  view = view[written:]
1616
1700
  os.fsync(temp_fd)
1701
+ temp_stat = os.fstat(temp_fd)
1617
1702
  os.close(temp_fd)
1618
1703
  temp_fd = None
1619
- os.replace(temp_name, basename, src_dir_fd=parent_fd, dst_dir_fd=parent_fd)
1704
+ result_stat = commit_temp_file(parent_fd, temp_name, basename, overwrite, mode, temp_stat)
1620
1705
  temp_name = None
1621
1706
  os.fsync(parent_fd)
1622
- result_stat = os.stat(basename, dir_fd=parent_fd, follow_symlinks=False)
1623
1707
  return {"dev": result_stat.st_dev, "ino": result_stat.st_ino}
1624
1708
  finally:
1625
1709
  os.close(source_fd)
@@ -1636,6 +1720,7 @@ def copy_path(root_fd, payload):
1636
1720
  def run_operation(operation, root_path, payload):
1637
1721
  root_fd = open_dir(root_path)
1638
1722
  try:
1723
+ assert_expected_root(root_fd, payload)
1639
1724
  if operation == "stat":
1640
1725
  return stat_path(root_fd, payload)
1641
1726
  if operation == "readdir":
@@ -1740,6 +1825,15 @@ function mapWorkerError(response) {
1740
1825
  if (message.includes("fs-safe-source-mismatch")) {
1741
1826
  return new errors_FsSafeError("path-mismatch", "source path changed during copy");
1742
1827
  }
1828
+ if (message.includes("fs-safe-temp-mismatch")) {
1829
+ return new errors_FsSafeError("path-mismatch", "temp path changed during write");
1830
+ }
1831
+ if (message.includes("fs-safe-root-mismatch")) {
1832
+ return new errors_FsSafeError("path-mismatch", "root path changed during operation");
1833
+ }
1834
+ if (message.includes("fs-safe-directory-noreplace-unsupported")) {
1835
+ return new errors_FsSafeError("invalid-path", "directory moves require overwrite: true");
1836
+ }
1743
1837
  if (code === "FileNotFoundError" || errno === 2) {
1744
1838
  return new errors_FsSafeError("not-found", "file not found");
1745
1839
  }
@@ -1757,19 +1851,19 @@ function mapWorkerError(response) {
1757
1851
  }
1758
1852
  return new errors_FsSafeError("helper-failed", message);
1759
1853
  }
1760
- function rejectPending(error) {
1761
- if (!worker) {
1854
+ function rejectPending(error, targetWorker = worker) {
1855
+ if (!targetWorker || worker !== targetWorker) {
1762
1856
  return;
1763
1857
  }
1764
- setWorkerRef(worker, false);
1765
- for (const pending of worker.pending.values()) {
1858
+ setWorkerRef(targetWorker, false);
1859
+ for (const pending of targetWorker.pending.values()) {
1766
1860
  pending.reject(error);
1767
1861
  }
1768
- worker.pending.clear();
1862
+ targetWorker.pending.clear();
1769
1863
  worker = null;
1770
1864
  }
1771
- function handleWorkerLine(line) {
1772
- if (!worker || !line.trim()) {
1865
+ function handleWorkerLine(currentWorker, line) {
1866
+ if (worker !== currentWorker || !line.trim()) {
1773
1867
  return;
1774
1868
  }
1775
1869
  let decoded;
@@ -1777,11 +1871,11 @@ function handleWorkerLine(line) {
1777
1871
  decoded = JSON.parse(line);
1778
1872
  }
1779
1873
  catch {
1780
- rejectPending(new errors_FsSafeError("helper-failed", `pinned helper returned invalid JSON: ${line}`));
1874
+ rejectPending(new errors_FsSafeError("helper-failed", `pinned helper returned invalid JSON: ${line}`), currentWorker);
1781
1875
  return;
1782
1876
  }
1783
1877
  if (typeof decoded !== "object" || decoded === null || !("id" in decoded)) {
1784
- rejectPending(new errors_FsSafeError("helper-failed", "pinned helper returned invalid response"));
1878
+ rejectPending(new errors_FsSafeError("helper-failed", "pinned helper returned invalid response"), currentWorker);
1785
1879
  return;
1786
1880
  }
1787
1881
  const response = decoded;
@@ -1789,13 +1883,13 @@ function handleWorkerLine(line) {
1789
1883
  if (id === undefined) {
1790
1884
  return;
1791
1885
  }
1792
- const pending = worker.pending.get(id);
1886
+ const pending = currentWorker.pending.get(id);
1793
1887
  if (!pending) {
1794
1888
  return;
1795
1889
  }
1796
- worker.pending.delete(id);
1797
- if (worker.pending.size === 0) {
1798
- setWorkerRef(worker, false);
1890
+ currentWorker.pending.delete(id);
1891
+ if (currentWorker.pending.size === 0) {
1892
+ setWorkerRef(currentWorker, false);
1799
1893
  }
1800
1894
  if (response.ok === true) {
1801
1895
  pending.resolve(response.result);
@@ -1811,28 +1905,28 @@ function getWorker() {
1811
1905
  const child = (0,external_node_child_process_.spawn)(resolvePython(), ["-u", "-c", PINNED_PYTHON_WORKER_SOURCE], {
1812
1906
  stdio: ["pipe", "pipe", "pipe"],
1813
1907
  });
1814
- worker = { child, pending: new Map(), stderr: "", stdoutBuffer: "" };
1908
+ const currentWorker = { child, pending: new Map(), stderr: "", stdoutBuffer: "" };
1909
+ worker = currentWorker;
1815
1910
  child.stdout.setEncoding("utf8");
1816
1911
  child.stderr.setEncoding("utf8");
1817
1912
  child.stdout.on("data", (chunk) => {
1818
- const current = worker;
1819
- if (!current) {
1913
+ if (worker !== currentWorker) {
1820
1914
  return;
1821
1915
  }
1822
- current.stdoutBuffer += chunk;
1916
+ currentWorker.stdoutBuffer += chunk;
1823
1917
  for (;;) {
1824
- const newline = current.stdoutBuffer.indexOf("\n");
1918
+ const newline = currentWorker.stdoutBuffer.indexOf("\n");
1825
1919
  if (newline < 0) {
1826
1920
  break;
1827
1921
  }
1828
- const line = current.stdoutBuffer.slice(0, newline);
1829
- current.stdoutBuffer = current.stdoutBuffer.slice(newline + 1);
1830
- handleWorkerLine(line);
1922
+ const line = currentWorker.stdoutBuffer.slice(0, newline);
1923
+ currentWorker.stdoutBuffer = currentWorker.stdoutBuffer.slice(newline + 1);
1924
+ handleWorkerLine(currentWorker, line);
1831
1925
  }
1832
1926
  });
1833
1927
  child.stderr.on("data", (chunk) => {
1834
- if (worker) {
1835
- worker.stderr = `${worker.stderr}${chunk}`.slice(-4096);
1928
+ if (worker === currentWorker) {
1929
+ currentWorker.stderr = `${currentWorker.stderr}${chunk}`.slice(-4096);
1836
1930
  }
1837
1931
  });
1838
1932
  child.once("error", (error) => {
@@ -1841,17 +1935,17 @@ function getWorker() {
1841
1935
  : error instanceof Error
1842
1936
  ? error
1843
1937
  : new Error(String(error));
1844
- rejectPending(mapped);
1938
+ rejectPending(mapped, currentWorker);
1845
1939
  });
1846
1940
  child.once("close", (code, signal) => {
1847
- const stderr = worker?.stderr.trim();
1848
- rejectPending(new errors_FsSafeError("helper-failed", stderr || `pinned helper exited with code ${code ?? "null"} (${signal ?? "?"})`));
1941
+ const stderr = currentWorker.stderr.trim();
1942
+ rejectPending(new errors_FsSafeError("helper-failed", stderr || `pinned helper exited with code ${code ?? "null"} (${signal ?? "?"})`), currentWorker);
1849
1943
  });
1850
1944
  process.once("exit", () => {
1851
1945
  child.kill("SIGTERM");
1852
1946
  });
1853
- setWorkerRef(worker, false);
1854
- return worker;
1947
+ setWorkerRef(currentWorker, false);
1948
+ return currentWorker;
1855
1949
  }
1856
1950
  function setRefable(value, ref) {
1857
1951
  if (!value) {
@@ -1917,9 +2011,7 @@ function validatePinnedOperationPayload(payload) {
1917
2011
  }
1918
2012
  }
1919
2013
  function isPinnedHelperUnavailable(error) {
1920
- return error instanceof Error &&
1921
- "code" in error &&
1922
- error.code === "helper-unavailable";
2014
+ return error instanceof Error && "code" in error && error.code === "helper-unavailable";
1923
2015
  }
1924
2016
  function validatePinnedRelativePath(relativePath) {
1925
2017
  if (relativePath.length === 0 || relativePath === ".") {
@@ -2017,29 +2109,21 @@ function assertWithinMaxBytes(bytes, maxBytes) {
2017
2109
  throw new errors_FsSafeError("too-large", `file exceeds limit of ${maxBytes} bytes (got at least ${bytes})`);
2018
2110
  }
2019
2111
  }
2020
- function pinned_write_createMaxBytesTransform(maxBytes) {
2021
- if (maxBytes === undefined) {
2022
- return undefined;
2023
- }
2112
+ async function writeStreamToHandle(stream, handle, maxBytes) {
2024
2113
  let bytes = 0;
2025
- return new external_node_stream_.Transform({
2026
- transform(chunk, _encoding, callback) {
2027
- bytes += chunk.byteLength;
2028
- if (bytes > maxBytes) {
2029
- callback(new errors_FsSafeError("too-large", `file exceeds limit of ${maxBytes} bytes (got at least ${bytes})`));
2030
- return;
2114
+ for await (const chunk of stream) {
2115
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
2116
+ bytes += buffer.byteLength;
2117
+ assertWithinMaxBytes(bytes, maxBytes);
2118
+ let offset = 0;
2119
+ while (offset < buffer.byteLength) {
2120
+ const { bytesWritten } = await handle.write(buffer, offset, buffer.byteLength - offset);
2121
+ if (bytesWritten <= 0) {
2122
+ throw new errors_FsSafeError("helper-failed", "fallback stream write made no progress");
2031
2123
  }
2032
- callback(null, chunk);
2033
- },
2034
- });
2035
- }
2036
- async function pipelineWithMaxBytes(stream, destination, maxBytes) {
2037
- const limiter = pinned_write_createMaxBytesTransform(maxBytes);
2038
- if (limiter) {
2039
- await (0,external_node_stream_promises_.pipeline)(stream, limiter, destination);
2040
- return;
2124
+ offset += bytesWritten;
2125
+ }
2041
2126
  }
2042
- await (0,external_node_stream_promises_.pipeline)(stream, destination);
2043
2127
  }
2044
2128
  async function inputToBase64(input, maxBytes) {
2045
2129
  if (input.kind === "buffer") {
@@ -2064,7 +2148,7 @@ async function runPinnedWriteHelper(params) {
2064
2148
  relativeParentPath: params.relativeParentPath,
2065
2149
  });
2066
2150
  if (getFsSafePythonConfig().mode === "off") {
2067
- return await runPinnedWriteFallback(params);
2151
+ return await runPinnedWriteFallbackOrThrow(params);
2068
2152
  }
2069
2153
  if (params.input.kind === "stream") {
2070
2154
  try {
@@ -2072,7 +2156,7 @@ async function runPinnedWriteHelper(params) {
2072
2156
  }
2073
2157
  catch (error) {
2074
2158
  if (canFallbackFromPythonError(error)) {
2075
- return await runPinnedWriteFallback(params);
2159
+ return await runPinnedWriteFallbackOrThrow(params, error);
2076
2160
  }
2077
2161
  throw error;
2078
2162
  }
@@ -2085,6 +2169,7 @@ async function runPinnedWriteHelper(params) {
2085
2169
  mode: params.mode || 0o600,
2086
2170
  overwrite: params.overwrite !== false,
2087
2171
  relativeParentPath: params.relativeParentPath,
2172
+ ...(params.rootIdentity ? { rootDev: params.rootIdentity.dev, rootIno: params.rootIdentity.ino } : {}),
2088
2173
  };
2089
2174
  try {
2090
2175
  return await runPinnedPythonOperation({
@@ -2095,7 +2180,7 @@ async function runPinnedWriteHelper(params) {
2095
2180
  }
2096
2181
  catch (error) {
2097
2182
  if (canFallbackFromPythonError(error)) {
2098
- return await runPinnedWriteFallback(params);
2183
+ return await runPinnedWriteFallbackOrThrow(params, error);
2099
2184
  }
2100
2185
  throw error;
2101
2186
  }
@@ -2115,22 +2200,29 @@ async function runPinnedCopyHelper(params) {
2115
2200
  mode: params.mode || 0o600,
2116
2201
  overwrite: params.overwrite !== false,
2117
2202
  relativeParentPath: params.relativeParentPath,
2203
+ ...(params.rootIdentity ? { rootDev: params.rootIdentity.dev, rootIno: params.rootIdentity.ino } : {}),
2118
2204
  sourceDev: params.sourceIdentity.dev,
2119
2205
  sourceIno: params.sourceIdentity.ino,
2120
2206
  sourcePath: params.sourcePath,
2121
2207
  },
2122
2208
  });
2123
2209
  }
2210
+ async function runPinnedWriteFallbackOrThrow(params, cause) {
2211
+ if (process.platform !== "win32") {
2212
+ throw new errors_FsSafeError("helper-unavailable", "Python helper is required for pinned writes on this platform", { cause });
2213
+ }
2214
+ return await runPinnedWriteFallback(params);
2215
+ }
2124
2216
  async function runPinnedWriteFallback(params) {
2125
2217
  const parentPath = params.relativeParentPath
2126
2218
  ? external_node_path_.join(params.rootPath, ...params.relativeParentPath.split("/"))
2127
2219
  : params.rootPath;
2128
- const parentGuard = await directory_guard_createNearestExistingDirectoryGuard(params.rootPath, parentPath);
2129
2220
  if (params.mkdir) {
2130
- await withAsyncDirectoryGuards([parentGuard], async () => {
2131
- await promises_.mkdir(parentPath, { recursive: true });
2132
- });
2221
+ await mkdirPathComponentsWithGuards({ rootReal: params.rootPath, targetPath: parentPath });
2133
2222
  }
2223
+ const parentGuard = params.mkdir
2224
+ ? await directory_guard_createAsyncDirectoryGuard(parentPath)
2225
+ : await directory_guard_createNearestExistingDirectoryGuard(params.rootPath, parentPath);
2134
2226
  const targetPath = external_node_path_.join(parentPath, params.basename);
2135
2227
  if (params.overwrite === false) {
2136
2228
  let handle = await withAsyncDirectoryGuards([parentGuard], async () => await promises_.open(targetPath, external_node_fs_.constants.O_WRONLY | external_node_fs_.constants.O_CREAT | external_node_fs_.constants.O_EXCL, params.mode), {
@@ -2152,7 +2244,7 @@ async function runPinnedWriteFallback(params) {
2152
2244
  }
2153
2245
  }
2154
2246
  else {
2155
- await pipelineWithMaxBytes(params.input.stream, handle.createWriteStream(), params.maxBytes);
2247
+ await writeStreamToHandle(params.input.stream, handle, params.maxBytes);
2156
2248
  }
2157
2249
  const stat = await handle.stat();
2158
2250
  created = false;
@@ -2173,7 +2265,9 @@ async function runPinnedWriteFallback(params) {
2173
2265
  ? external_node_fs_.constants.O_NOFOLLOW
2174
2266
  : 0);
2175
2267
  let handle;
2176
- let handleClosedByStream = false;
2268
+ let tempStat;
2269
+ let targetStat;
2270
+ let renamed = false;
2177
2271
  try {
2178
2272
  handle = await promises_.open(tempPath, tempFlags, params.mode);
2179
2273
  if (params.input.kind === "buffer") {
@@ -2186,29 +2280,36 @@ async function runPinnedWriteFallback(params) {
2186
2280
  }
2187
2281
  }
2188
2282
  else {
2189
- const writable = handle.createWriteStream();
2190
- writable.once("close", () => {
2191
- handleClosedByStream = true;
2192
- });
2193
- await pipelineWithMaxBytes(params.input.stream, writable, params.maxBytes);
2283
+ await writeStreamToHandle(params.input.stream, handle, params.maxBytes);
2194
2284
  }
2195
- if (!handleClosedByStream) {
2196
- await handle.close().catch(() => undefined);
2197
- handle = undefined;
2285
+ tempStat = await handle.stat();
2286
+ const tempPathStat = await promises_.lstat(tempPath);
2287
+ if (tempPathStat.isSymbolicLink() || !file_identity_sameFileIdentity(tempPathStat, tempStat)) {
2288
+ throw new errors_FsSafeError("path-mismatch", "fallback temp path changed during write");
2198
2289
  }
2290
+ const expectedTempStat = tempStat;
2291
+ await handle.close().catch(() => undefined);
2292
+ handle = undefined;
2199
2293
  await withAsyncDirectoryGuards([parentGuard], async () => {
2200
2294
  await promises_.rename(tempPath, targetPath);
2295
+ renamed = true;
2296
+ targetStat = await promises_.lstat(targetPath);
2297
+ if (targetStat.isSymbolicLink() || !file_identity_sameFileIdentity(targetStat, expectedTempStat)) {
2298
+ throw new errors_FsSafeError("path-mismatch", "fallback target changed during write");
2299
+ }
2201
2300
  });
2202
2301
  }
2203
2302
  catch (error) {
2204
- if (handle && !handleClosedByStream) {
2205
- await handle.close().catch(() => undefined);
2303
+ await handle?.close().catch(() => undefined);
2304
+ if (!renamed) {
2305
+ await promises_.rm(tempPath, { force: true }).catch(() => undefined);
2206
2306
  }
2207
- await promises_.rm(tempPath, { force: true }).catch(() => undefined);
2208
2307
  throw error;
2209
2308
  }
2210
- const stat = await promises_.stat(targetPath);
2211
- return { dev: stat.dev, ino: stat.ino };
2309
+ if (!targetStat) {
2310
+ throw new errors_FsSafeError("path-mismatch", "fallback target was not verified");
2311
+ }
2312
+ return { dev: targetStat.dev, ino: targetStat.ino };
2212
2313
  }
2213
2314
 
2214
2315
  // EXTERNAL MODULE: external "node:os"
@@ -2747,7 +2848,7 @@ function relativeInsideRoot(rootPath, targetPath) {
2747
2848
  if (!relative || relative === ".") {
2748
2849
  return "";
2749
2850
  }
2750
- if (relative.startsWith("..") || external_node_path_.isAbsolute(relative)) {
2851
+ if (isPathRelativeEscape(relative)) {
2751
2852
  return "";
2752
2853
  }
2753
2854
  return relative;
@@ -3098,6 +3199,15 @@ function __setFsSafeTestHooksForTest(hooks) {
3098
3199
  test_hooks_fsSafeTestHooks = hooks;
3099
3200
  }
3100
3201
 
3202
+ ;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/json-stringify.js
3203
+ function stringifyJsonDocument(value, replacer, space) {
3204
+ const text = JSON.stringify(value, replacer, space);
3205
+ if (typeof text !== "string") {
3206
+ throw new TypeError("value is not representable as a JSON document");
3207
+ }
3208
+ return text;
3209
+ }
3210
+
3101
3211
  ;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/temp-cleanup.js
3102
3212
 
3103
3213
  const tempCleanupEntries = new Map();
@@ -3185,6 +3295,7 @@ async function serializePathWrite(key, run) {
3185
3295
 
3186
3296
 
3187
3297
 
3298
+
3188
3299
 
3189
3300
 
3190
3301
  function logWarn(message) {
@@ -3425,12 +3536,12 @@ class RootHandle {
3425
3536
  }
3426
3537
  async writeJson(relativePath, data, options = {}) {
3427
3538
  const { replacer, space, trailingNewline = true, ...writeOptions } = options;
3428
- const json = JSON.stringify(data, replacer, space);
3539
+ const json = stringifyJsonDocument(data, replacer, space);
3429
3540
  await this.write(relativePath, trailingNewline ? `${json}\n` : json, writeOptions);
3430
3541
  }
3431
3542
  async createJson(relativePath, data, options = {}) {
3432
3543
  const { replacer, space, trailingNewline = true, ...writeOptions } = options;
3433
- const json = JSON.stringify(data, replacer, space);
3544
+ const json = stringifyJsonDocument(data, replacer, space);
3434
3545
  await this.create(relativePath, trailingNewline ? `${json}\n` : json, writeOptions);
3435
3546
  }
3436
3547
  async copyIn(relativePath, sourcePath, options = {}) {
@@ -4215,6 +4326,9 @@ async function movePathFallback(root, params) {
4215
4326
  if (sourceStat.isFile() && sourceStat.nlink > 1) {
4216
4327
  throw new errors_FsSafeError("hardlink", "hardlinked path not allowed");
4217
4328
  }
4329
+ if (!params.overwrite && sourceStat.isDirectory()) {
4330
+ throw new errors_FsSafeError("invalid-path", "directory moves require overwrite: true");
4331
+ }
4218
4332
  if (!params.overwrite) {
4219
4333
  try {
4220
4334
  await promises_.lstat(target.resolved);
@@ -46023,7 +46137,7 @@ if (installedChunkData !== 0) { // 0 means "already installed".'
46023
46137
  // module factories are used so entry inlining is disabled
46024
46138
  // startup
46025
46139
  // Load entry module and return exports
46026
- var __webpack_exports__ = __webpack_require__(2202);
46140
+ var __webpack_exports__ = __webpack_require__(4403);
46027
46141
  var __webpack_exports__DEFAULT_LINT_RULES = __webpack_exports__.Kd;
46028
46142
  var __webpack_exports__LintValidationError = __webpack_exports__.F5;
46029
46143
  var __webpack_exports__createFormatter = __webpack_exports__.xi;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lousy-agents/lint",
3
- "version": "5.14.8",
3
+ "version": "5.14.10",
4
4
  "description": "Programmatic lint API for validating AI coding assistant configurations — skills, agents, hooks, and instructions",
5
5
  "type": "module",
6
6
  "repository": {