@lousy-agents/lint 5.14.9 → 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 +106 -87
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -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}`;
@@ -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,12 @@ 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
-
1434
1425
  def copy_bytes(source_fd, dest_fd):
1435
1426
  while True:
1436
1427
  chunk = os.read(source_fd, 65536)
@@ -1442,7 +1433,6 @@ def copy_bytes(source_fd, dest_fd):
1442
1433
  if written <= 0:
1443
1434
  raise OSError(errno.EIO, "short write")
1444
1435
  view = view[written:]
1445
-
1446
1436
  def write_all(fd, data):
1447
1437
  view = memoryview(data)
1448
1438
  while view:
@@ -1450,11 +1440,9 @@ def write_all(fd, data):
1450
1440
  if written <= 0:
1451
1441
  raise OSError(errno.EIO, "short write")
1452
1442
  view = view[written:]
1453
-
1454
1443
  def link_unsupported(exc):
1455
1444
  unsupported = (errno.EPERM, errno.EOPNOTSUPP, getattr(errno, "ENOTSUP", errno.EOPNOTSUPP))
1456
1445
  return getattr(exc, "errno", None) in unsupported
1457
-
1458
1446
  def link_no_replace(name, new_name, source_fd, target_fd):
1459
1447
  linked = False
1460
1448
  try:
@@ -1469,11 +1457,9 @@ def link_no_replace(name, new_name, source_fd, target_fd):
1469
1457
  os.fsync(source_fd)
1470
1458
  if source_fd != target_fd:
1471
1459
  os.fsync(target_fd)
1472
-
1473
1460
  def copy_file_no_replace(source_parent_fd, source_name, target_parent_fd, basename, mode, expected=None, unlink_source=False):
1474
1461
  source_fd = os.open(source_name, READ_FLAGS, dir_fd=source_parent_fd)
1475
- dest_fd = None
1476
- success = False
1462
+ dest_fd = None; success = False; dest_stat = None
1477
1463
  try:
1478
1464
  if expected is not None:
1479
1465
  source_stat = os.fstat(source_fd)
@@ -1482,6 +1468,7 @@ def copy_file_no_replace(source_parent_fd, source_name, target_parent_fd, basena
1482
1468
  dest_fd = os.open(basename, WRITE_FLAGS, mode, dir_fd=target_parent_fd)
1483
1469
  copy_bytes(source_fd, dest_fd)
1484
1470
  os.fsync(dest_fd)
1471
+ dest_stat = os.fstat(dest_fd)
1485
1472
  success = True
1486
1473
  finally:
1487
1474
  os.close(source_fd)
@@ -1497,19 +1484,40 @@ def copy_file_no_replace(source_parent_fd, source_name, target_parent_fd, basena
1497
1484
  try: os.unlink(basename, dir_fd=target_parent_fd)
1498
1485
  except FileNotFoundError: pass
1499
1486
  raise
1500
-
1501
- def commit_temp_file(parent_fd, temp_name, basename, overwrite, mode):
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)
1502
1503
  if overwrite:
1503
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)
1504
1506
  else:
1505
1507
  try:
1506
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)
1507
1510
  os.unlink(temp_name, dir_fd=parent_fd)
1511
+ return final_stat
1508
1512
  except OSError as exc:
1509
1513
  if not link_unsupported(exc):
1510
1514
  raise
1511
- copy_file_no_replace(parent_fd, temp_name, parent_fd, basename, mode, unlink_source=True)
1512
-
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")
1513
1521
  def stat_path(root_fd, payload):
1514
1522
  relative = payload.get("relativePath", "")
1515
1523
  segments = split_relative(relative)
@@ -1523,7 +1531,6 @@ def stat_path(root_fd, payload):
1523
1531
  return encode_stat(st)
1524
1532
  finally:
1525
1533
  os.close(parent_fd)
1526
-
1527
1534
  def readdir_path(root_fd, payload):
1528
1535
  dir_fd = walk_dir(root_fd, split_relative(payload.get("relativePath", "")))
1529
1536
  try:
@@ -1539,12 +1546,9 @@ def readdir_path(root_fd, payload):
1539
1546
  return entries
1540
1547
  finally:
1541
1548
  os.close(dir_fd)
1542
-
1543
1549
  def mkdirp_path(root_fd, payload):
1544
1550
  dir_fd = walk_dir(root_fd, split_relative(payload.get("relativePath", "")), mkdir_enabled=True)
1545
- os.close(dir_fd)
1546
- return None
1547
-
1551
+ os.close(dir_fd); return None
1548
1552
  def remove_tree(parent_fd, basename):
1549
1553
  st = os.lstat(basename, dir_fd=parent_fd)
1550
1554
  if stat.S_ISDIR(st.st_mode) and not stat.S_ISLNK(st.st_mode):
@@ -1557,7 +1561,6 @@ def remove_tree(parent_fd, basename):
1557
1561
  os.rmdir(basename, dir_fd=parent_fd)
1558
1562
  else:
1559
1563
  os.unlink(basename, dir_fd=parent_fd)
1560
-
1561
1564
  def remove_path(root_fd, payload):
1562
1565
  parent_fd, basename = parent_and_basename(root_fd, payload.get("relativePath", ""))
1563
1566
  try:
@@ -1640,14 +1643,15 @@ def write_path(root_fd, payload):
1640
1643
  except FileNotFoundError:
1641
1644
  pass
1642
1645
  temp_name, temp_fd = create_temp_file(parent_fd, basename, mode)
1646
+ os.fchmod(temp_fd, mode)
1643
1647
  write_all(temp_fd, data)
1644
1648
  os.fsync(temp_fd)
1649
+ temp_stat = os.fstat(temp_fd)
1645
1650
  os.close(temp_fd)
1646
1651
  temp_fd = None
1647
- commit_temp_file(parent_fd, temp_name, basename, overwrite, mode)
1652
+ result_stat = commit_temp_file(parent_fd, temp_name, basename, overwrite, mode, temp_stat)
1648
1653
  temp_name = None
1649
1654
  os.fsync(parent_fd)
1650
- result_stat = os.stat(basename, dir_fd=parent_fd, follow_symlinks=False)
1651
1655
  return {"dev": result_stat.st_dev, "ino": result_stat.st_ino}
1652
1656
  finally:
1653
1657
  if temp_fd is not None:
@@ -1678,6 +1682,7 @@ def copy_path(root_fd, payload):
1678
1682
  raise RuntimeError("fs-safe-too-large:%d:%d" % (max_bytes, source_stat.st_size))
1679
1683
  parent_fd = walk_dir(root_fd, split_relative(payload.get("relativeParentPath", "")), bool(payload.get("mkdir", True)))
1680
1684
  temp_name, temp_fd = create_temp_file(parent_fd, basename, mode)
1685
+ os.fchmod(temp_fd, mode)
1681
1686
  written_bytes = 0
1682
1687
  while True:
1683
1688
  chunk = os.read(source_fd, 65536)
@@ -1693,12 +1698,12 @@ def copy_path(root_fd, payload):
1693
1698
  raise OSError(errno.EIO, "short write")
1694
1699
  view = view[written:]
1695
1700
  os.fsync(temp_fd)
1701
+ temp_stat = os.fstat(temp_fd)
1696
1702
  os.close(temp_fd)
1697
1703
  temp_fd = None
1698
- commit_temp_file(parent_fd, temp_name, basename, overwrite, mode)
1704
+ result_stat = commit_temp_file(parent_fd, temp_name, basename, overwrite, mode, temp_stat)
1699
1705
  temp_name = None
1700
1706
  os.fsync(parent_fd)
1701
- result_stat = os.stat(basename, dir_fd=parent_fd, follow_symlinks=False)
1702
1707
  return {"dev": result_stat.st_dev, "ino": result_stat.st_ino}
1703
1708
  finally:
1704
1709
  os.close(source_fd)
@@ -1715,6 +1720,7 @@ def copy_path(root_fd, payload):
1715
1720
  def run_operation(operation, root_path, payload):
1716
1721
  root_fd = open_dir(root_path)
1717
1722
  try:
1723
+ assert_expected_root(root_fd, payload)
1718
1724
  if operation == "stat":
1719
1725
  return stat_path(root_fd, payload)
1720
1726
  if operation == "readdir":
@@ -1819,6 +1825,12 @@ function mapWorkerError(response) {
1819
1825
  if (message.includes("fs-safe-source-mismatch")) {
1820
1826
  return new errors_FsSafeError("path-mismatch", "source path changed during copy");
1821
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
+ }
1822
1834
  if (message.includes("fs-safe-directory-noreplace-unsupported")) {
1823
1835
  return new errors_FsSafeError("invalid-path", "directory moves require overwrite: true");
1824
1836
  }
@@ -1999,9 +2011,7 @@ function validatePinnedOperationPayload(payload) {
1999
2011
  }
2000
2012
  }
2001
2013
  function isPinnedHelperUnavailable(error) {
2002
- return error instanceof Error &&
2003
- "code" in error &&
2004
- error.code === "helper-unavailable";
2014
+ return error instanceof Error && "code" in error && error.code === "helper-unavailable";
2005
2015
  }
2006
2016
  function validatePinnedRelativePath(relativePath) {
2007
2017
  if (relativePath.length === 0 || relativePath === ".") {
@@ -2099,29 +2109,21 @@ function assertWithinMaxBytes(bytes, maxBytes) {
2099
2109
  throw new errors_FsSafeError("too-large", `file exceeds limit of ${maxBytes} bytes (got at least ${bytes})`);
2100
2110
  }
2101
2111
  }
2102
- function pinned_write_createMaxBytesTransform(maxBytes) {
2103
- if (maxBytes === undefined) {
2104
- return undefined;
2105
- }
2112
+ async function writeStreamToHandle(stream, handle, maxBytes) {
2106
2113
  let bytes = 0;
2107
- return new external_node_stream_.Transform({
2108
- transform(chunk, _encoding, callback) {
2109
- bytes += chunk.byteLength;
2110
- if (bytes > maxBytes) {
2111
- callback(new errors_FsSafeError("too-large", `file exceeds limit of ${maxBytes} bytes (got at least ${bytes})`));
2112
- 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");
2113
2123
  }
2114
- callback(null, chunk);
2115
- },
2116
- });
2117
- }
2118
- async function pipelineWithMaxBytes(stream, destination, maxBytes) {
2119
- const limiter = pinned_write_createMaxBytesTransform(maxBytes);
2120
- if (limiter) {
2121
- await (0,external_node_stream_promises_.pipeline)(stream, limiter, destination);
2122
- return;
2124
+ offset += bytesWritten;
2125
+ }
2123
2126
  }
2124
- await (0,external_node_stream_promises_.pipeline)(stream, destination);
2125
2127
  }
2126
2128
  async function inputToBase64(input, maxBytes) {
2127
2129
  if (input.kind === "buffer") {
@@ -2146,7 +2148,7 @@ async function runPinnedWriteHelper(params) {
2146
2148
  relativeParentPath: params.relativeParentPath,
2147
2149
  });
2148
2150
  if (getFsSafePythonConfig().mode === "off") {
2149
- return await runPinnedWriteFallback(params);
2151
+ return await runPinnedWriteFallbackOrThrow(params);
2150
2152
  }
2151
2153
  if (params.input.kind === "stream") {
2152
2154
  try {
@@ -2154,7 +2156,7 @@ async function runPinnedWriteHelper(params) {
2154
2156
  }
2155
2157
  catch (error) {
2156
2158
  if (canFallbackFromPythonError(error)) {
2157
- return await runPinnedWriteFallback(params);
2159
+ return await runPinnedWriteFallbackOrThrow(params, error);
2158
2160
  }
2159
2161
  throw error;
2160
2162
  }
@@ -2167,6 +2169,7 @@ async function runPinnedWriteHelper(params) {
2167
2169
  mode: params.mode || 0o600,
2168
2170
  overwrite: params.overwrite !== false,
2169
2171
  relativeParentPath: params.relativeParentPath,
2172
+ ...(params.rootIdentity ? { rootDev: params.rootIdentity.dev, rootIno: params.rootIdentity.ino } : {}),
2170
2173
  };
2171
2174
  try {
2172
2175
  return await runPinnedPythonOperation({
@@ -2177,7 +2180,7 @@ async function runPinnedWriteHelper(params) {
2177
2180
  }
2178
2181
  catch (error) {
2179
2182
  if (canFallbackFromPythonError(error)) {
2180
- return await runPinnedWriteFallback(params);
2183
+ return await runPinnedWriteFallbackOrThrow(params, error);
2181
2184
  }
2182
2185
  throw error;
2183
2186
  }
@@ -2197,22 +2200,29 @@ async function runPinnedCopyHelper(params) {
2197
2200
  mode: params.mode || 0o600,
2198
2201
  overwrite: params.overwrite !== false,
2199
2202
  relativeParentPath: params.relativeParentPath,
2203
+ ...(params.rootIdentity ? { rootDev: params.rootIdentity.dev, rootIno: params.rootIdentity.ino } : {}),
2200
2204
  sourceDev: params.sourceIdentity.dev,
2201
2205
  sourceIno: params.sourceIdentity.ino,
2202
2206
  sourcePath: params.sourcePath,
2203
2207
  },
2204
2208
  });
2205
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
+ }
2206
2216
  async function runPinnedWriteFallback(params) {
2207
2217
  const parentPath = params.relativeParentPath
2208
2218
  ? external_node_path_.join(params.rootPath, ...params.relativeParentPath.split("/"))
2209
2219
  : params.rootPath;
2210
- const parentGuard = await directory_guard_createNearestExistingDirectoryGuard(params.rootPath, parentPath);
2211
2220
  if (params.mkdir) {
2212
- await withAsyncDirectoryGuards([parentGuard], async () => {
2213
- await promises_.mkdir(parentPath, { recursive: true });
2214
- });
2221
+ await mkdirPathComponentsWithGuards({ rootReal: params.rootPath, targetPath: parentPath });
2215
2222
  }
2223
+ const parentGuard = params.mkdir
2224
+ ? await directory_guard_createAsyncDirectoryGuard(parentPath)
2225
+ : await directory_guard_createNearestExistingDirectoryGuard(params.rootPath, parentPath);
2216
2226
  const targetPath = external_node_path_.join(parentPath, params.basename);
2217
2227
  if (params.overwrite === false) {
2218
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), {
@@ -2234,7 +2244,7 @@ async function runPinnedWriteFallback(params) {
2234
2244
  }
2235
2245
  }
2236
2246
  else {
2237
- await pipelineWithMaxBytes(params.input.stream, handle.createWriteStream(), params.maxBytes);
2247
+ await writeStreamToHandle(params.input.stream, handle, params.maxBytes);
2238
2248
  }
2239
2249
  const stat = await handle.stat();
2240
2250
  created = false;
@@ -2255,7 +2265,9 @@ async function runPinnedWriteFallback(params) {
2255
2265
  ? external_node_fs_.constants.O_NOFOLLOW
2256
2266
  : 0);
2257
2267
  let handle;
2258
- let handleClosedByStream = false;
2268
+ let tempStat;
2269
+ let targetStat;
2270
+ let renamed = false;
2259
2271
  try {
2260
2272
  handle = await promises_.open(tempPath, tempFlags, params.mode);
2261
2273
  if (params.input.kind === "buffer") {
@@ -2268,29 +2280,36 @@ async function runPinnedWriteFallback(params) {
2268
2280
  }
2269
2281
  }
2270
2282
  else {
2271
- const writable = handle.createWriteStream();
2272
- writable.once("close", () => {
2273
- handleClosedByStream = true;
2274
- });
2275
- await pipelineWithMaxBytes(params.input.stream, writable, params.maxBytes);
2283
+ await writeStreamToHandle(params.input.stream, handle, params.maxBytes);
2276
2284
  }
2277
- if (!handleClosedByStream) {
2278
- await handle.close().catch(() => undefined);
2279
- 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");
2280
2289
  }
2290
+ const expectedTempStat = tempStat;
2291
+ await handle.close().catch(() => undefined);
2292
+ handle = undefined;
2281
2293
  await withAsyncDirectoryGuards([parentGuard], async () => {
2282
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
+ }
2283
2300
  });
2284
2301
  }
2285
2302
  catch (error) {
2286
- if (handle && !handleClosedByStream) {
2287
- await handle.close().catch(() => undefined);
2303
+ await handle?.close().catch(() => undefined);
2304
+ if (!renamed) {
2305
+ await promises_.rm(tempPath, { force: true }).catch(() => undefined);
2288
2306
  }
2289
- await promises_.rm(tempPath, { force: true }).catch(() => undefined);
2290
2307
  throw error;
2291
2308
  }
2292
- const stat = await promises_.stat(targetPath);
2293
- 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 };
2294
2313
  }
2295
2314
 
2296
2315
  // EXTERNAL MODULE: external "node:os"
@@ -2829,7 +2848,7 @@ function relativeInsideRoot(rootPath, targetPath) {
2829
2848
  if (!relative || relative === ".") {
2830
2849
  return "";
2831
2850
  }
2832
- if (relative.startsWith("..") || external_node_path_.isAbsolute(relative)) {
2851
+ if (isPathRelativeEscape(relative)) {
2833
2852
  return "";
2834
2853
  }
2835
2854
  return relative;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lousy-agents/lint",
3
- "version": "5.14.9",
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": {