@lousy-agents/cli 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
@@ -3210,6 +3210,9 @@ function path_isPathInside(root, target) {
3210
3210
  const firstSegment = relative.split(external_node_path_.posix.sep)[0];
3211
3211
  return relative === "" || (firstSegment !== ".." && !external_node_path_.isAbsolute(relative));
3212
3212
  }
3213
+ function isPathRelativeEscape(relativePath) {
3214
+ return relativePath === ".." || relativePath.startsWith(`..${external_node_path_.sep}`) || external_node_path_.isAbsolute(relativePath);
3215
+ }
3213
3216
  function resolveSafeBaseDir(rootDir) {
3214
3217
  const resolved = path.resolve(rootDir);
3215
3218
  return resolved.endsWith(path.sep) ? resolved : `${resolved}${path.sep}`;
@@ -3358,17 +3361,17 @@ function directory_guard_createNearestExistingSyncDirectoryGuard(rootReal, targe
3358
3361
 
3359
3362
 
3360
3363
 
3364
+
3361
3365
  function isSameOrChildPath(candidate, parent) {
3362
- return candidate === parent || candidate.startsWith(`${parent}${external_node_path_.sep}`);
3363
- }
3364
- function isPathEscape(relativePath) {
3365
- return relativePath === ".." || relativePath.startsWith(`..${external_node_path_.sep}`) || external_node_path_.isAbsolute(relativePath);
3366
+ const parentPrefix = parent.endsWith(external_node_path_.sep) ? parent : `${parent}${external_node_path_.sep}`;
3367
+ return candidate === parent || candidate.startsWith(parentPrefix);
3366
3368
  }
3367
3369
  async function mkdirPathComponentsWithGuards(params) {
3368
3370
  const root = external_node_path_.resolve(params.rootReal);
3371
+ const rootCanonical = external_node_path_.resolve(await promises_.realpath(root));
3369
3372
  const target = external_node_path_.resolve(params.targetPath);
3370
3373
  const relative = external_node_path_.relative(root, target);
3371
- if (isPathEscape(relative)) {
3374
+ if (isPathRelativeEscape(relative)) {
3372
3375
  throw new errors_FsSafeError("outside-workspace", "directory is outside workspace root");
3373
3376
  }
3374
3377
  let current = root;
@@ -3391,7 +3394,7 @@ async function mkdirPathComponentsWithGuards(params) {
3391
3394
  }
3392
3395
  // Node's recursive mkdir follows symlinks in missing components. Build one
3393
3396
  // segment at a time and realpath-check each segment before descending.
3394
- if (!isSameOrChildPath(external_node_path_.resolve(await promises_.realpath(next)), root)) {
3397
+ if (!isSameOrChildPath(external_node_path_.resolve(await promises_.realpath(next)), rootCanonical)) {
3395
3398
  throw new errors_FsSafeError("outside-workspace", "directory escaped workspace root");
3396
3399
  }
3397
3400
  await directory_guard_createAsyncDirectoryGuard(next);
@@ -3526,26 +3529,20 @@ var external_node_child_process_ = __webpack_require__(1421);
3526
3529
 
3527
3530
 
3528
3531
  const PINNED_PYTHON_WORKER_SOURCE = String.raw `
3529
- import base64
3530
- import errno
3531
- import json
3532
- import os
3533
- import secrets
3534
- import stat
3535
- import sys
3536
-
3532
+ import base64, errno, json, os, secrets, stat, sys
3537
3533
  DIR_FLAGS = os.O_RDONLY
3538
3534
  if hasattr(os, "O_DIRECTORY"):
3539
3535
  DIR_FLAGS |= os.O_DIRECTORY
3540
3536
  if hasattr(os, "O_NOFOLLOW"):
3541
3537
  DIR_FLAGS |= os.O_NOFOLLOW
3542
3538
  READ_FLAGS = os.O_RDONLY
3539
+ if hasattr(os, "O_NONBLOCK"):
3540
+ READ_FLAGS |= os.O_NONBLOCK
3543
3541
  if hasattr(os, "O_NOFOLLOW"):
3544
3542
  READ_FLAGS |= os.O_NOFOLLOW
3545
3543
  WRITE_FLAGS = os.O_WRONLY | os.O_CREAT | os.O_EXCL
3546
3544
  if hasattr(os, "O_NOFOLLOW"):
3547
3545
  WRITE_FLAGS |= os.O_NOFOLLOW
3548
-
3549
3546
  def split_relative(value):
3550
3547
  if value in ("", "."):
3551
3548
  return []
@@ -3558,10 +3555,8 @@ def split_relative(value):
3558
3555
  if part == "..":
3559
3556
  raise OSError(errno.EPERM, "path traversal is not allowed")
3560
3557
  return parts
3561
-
3562
3558
  def open_dir(path_value, dir_fd=None):
3563
3559
  return os.open(path_value, DIR_FLAGS, dir_fd=dir_fd)
3564
-
3565
3560
  def walk_dir(root_fd, segments, mkdir_enabled=False):
3566
3561
  current_fd = os.dup(root_fd)
3567
3562
  try:
@@ -3579,14 +3574,12 @@ def walk_dir(root_fd, segments, mkdir_enabled=False):
3579
3574
  except Exception:
3580
3575
  os.close(current_fd)
3581
3576
  raise
3582
-
3583
3577
  def parent_and_basename(root_fd, relative):
3584
3578
  segments = split_relative(relative)
3585
3579
  if not segments:
3586
3580
  raise OSError(errno.EPERM, "operation requires a non-root path")
3587
3581
  parent_fd = walk_dir(root_fd, segments[:-1])
3588
3582
  return parent_fd, segments[-1]
3589
-
3590
3583
  def encode_stat(st):
3591
3584
  mode = st.st_mode
3592
3585
  return {
@@ -3602,14 +3595,12 @@ def encode_stat(st):
3602
3595
  "size": st.st_size,
3603
3596
  "uid": st.st_uid,
3604
3597
  }
3605
-
3606
3598
  def reject_unsafe_endpoint(st):
3607
3599
  mode = st.st_mode
3608
3600
  if stat.S_ISLNK(mode):
3609
3601
  raise OSError(errno.ELOOP, "symlink endpoint is not allowed")
3610
3602
  if stat.S_ISREG(mode) and st.st_nlink > 1:
3611
3603
  raise OSError(errno.EPERM, "hardlinked file endpoint is not allowed")
3612
-
3613
3604
  def copy_bytes(source_fd, dest_fd):
3614
3605
  while True:
3615
3606
  chunk = os.read(source_fd, 65536)
@@ -3621,7 +3612,6 @@ def copy_bytes(source_fd, dest_fd):
3621
3612
  if written <= 0:
3622
3613
  raise OSError(errno.EIO, "short write")
3623
3614
  view = view[written:]
3624
-
3625
3615
  def write_all(fd, data):
3626
3616
  view = memoryview(data)
3627
3617
  while view:
@@ -3629,11 +3619,9 @@ def write_all(fd, data):
3629
3619
  if written <= 0:
3630
3620
  raise OSError(errno.EIO, "short write")
3631
3621
  view = view[written:]
3632
-
3633
3622
  def link_unsupported(exc):
3634
3623
  unsupported = (errno.EPERM, errno.EOPNOTSUPP, getattr(errno, "ENOTSUP", errno.EOPNOTSUPP))
3635
3624
  return getattr(exc, "errno", None) in unsupported
3636
-
3637
3625
  def link_no_replace(name, new_name, source_fd, target_fd):
3638
3626
  linked = False
3639
3627
  try:
@@ -3648,11 +3636,9 @@ def link_no_replace(name, new_name, source_fd, target_fd):
3648
3636
  os.fsync(source_fd)
3649
3637
  if source_fd != target_fd:
3650
3638
  os.fsync(target_fd)
3651
-
3652
3639
  def copy_file_no_replace(source_parent_fd, source_name, target_parent_fd, basename, mode, expected=None, unlink_source=False):
3653
3640
  source_fd = os.open(source_name, READ_FLAGS, dir_fd=source_parent_fd)
3654
- dest_fd = None
3655
- success = False
3641
+ dest_fd = None; success = False; dest_stat = None
3656
3642
  try:
3657
3643
  if expected is not None:
3658
3644
  source_stat = os.fstat(source_fd)
@@ -3661,6 +3647,7 @@ def copy_file_no_replace(source_parent_fd, source_name, target_parent_fd, basena
3661
3647
  dest_fd = os.open(basename, WRITE_FLAGS, mode, dir_fd=target_parent_fd)
3662
3648
  copy_bytes(source_fd, dest_fd)
3663
3649
  os.fsync(dest_fd)
3650
+ dest_stat = os.fstat(dest_fd)
3664
3651
  success = True
3665
3652
  finally:
3666
3653
  os.close(source_fd)
@@ -3676,19 +3663,40 @@ def copy_file_no_replace(source_parent_fd, source_name, target_parent_fd, basena
3676
3663
  try: os.unlink(basename, dir_fd=target_parent_fd)
3677
3664
  except FileNotFoundError: pass
3678
3665
  raise
3679
-
3680
- def commit_temp_file(parent_fd, temp_name, basename, overwrite, mode):
3666
+ return dest_stat
3667
+ def same_identity(left, right):
3668
+ return left.st_dev == right.st_dev and left.st_ino == right.st_ino
3669
+ def verify_temp_name(parent_fd, temp_name, expected_stat):
3670
+ current_stat = os.lstat(temp_name, dir_fd=parent_fd)
3671
+ if stat.S_ISLNK(current_stat.st_mode) or not same_identity(current_stat, expected_stat):
3672
+ raise RuntimeError("fs-safe-temp-mismatch")
3673
+ def verify_committed_temp(parent_fd, basename, expected_stat):
3674
+ final_stat = os.lstat(basename, dir_fd=parent_fd)
3675
+ if not stat.S_ISLNK(final_stat.st_mode) and same_identity(final_stat, expected_stat):
3676
+ return final_stat
3677
+ try: os.unlink(basename, dir_fd=parent_fd)
3678
+ except FileNotFoundError: pass
3679
+ raise RuntimeError("fs-safe-temp-mismatch")
3680
+ def commit_temp_file(parent_fd, temp_name, basename, overwrite, mode, expected_stat):
3681
+ verify_temp_name(parent_fd, temp_name, expected_stat)
3681
3682
  if overwrite:
3682
3683
  os.replace(temp_name, basename, src_dir_fd=parent_fd, dst_dir_fd=parent_fd)
3684
+ return verify_committed_temp(parent_fd, basename, expected_stat)
3683
3685
  else:
3684
3686
  try:
3685
3687
  os.link(temp_name, basename, src_dir_fd=parent_fd, dst_dir_fd=parent_fd, follow_symlinks=False)
3688
+ final_stat = verify_committed_temp(parent_fd, basename, expected_stat)
3686
3689
  os.unlink(temp_name, dir_fd=parent_fd)
3690
+ return final_stat
3687
3691
  except OSError as exc:
3688
3692
  if not link_unsupported(exc):
3689
3693
  raise
3690
- copy_file_no_replace(parent_fd, temp_name, parent_fd, basename, mode, unlink_source=True)
3691
-
3694
+ return copy_file_no_replace(parent_fd, temp_name, parent_fd, basename, mode, expected_stat, True)
3695
+ def assert_expected_root(root_fd, payload):
3696
+ if "rootDev" in payload or "rootIno" in payload:
3697
+ root_stat = os.fstat(root_fd)
3698
+ if root_stat.st_dev != int(payload["rootDev"]) or root_stat.st_ino != int(payload["rootIno"]):
3699
+ raise RuntimeError("fs-safe-root-mismatch")
3692
3700
  def stat_path(root_fd, payload):
3693
3701
  relative = payload.get("relativePath", "")
3694
3702
  segments = split_relative(relative)
@@ -3702,7 +3710,6 @@ def stat_path(root_fd, payload):
3702
3710
  return encode_stat(st)
3703
3711
  finally:
3704
3712
  os.close(parent_fd)
3705
-
3706
3713
  def readdir_path(root_fd, payload):
3707
3714
  dir_fd = walk_dir(root_fd, split_relative(payload.get("relativePath", "")))
3708
3715
  try:
@@ -3718,12 +3725,9 @@ def readdir_path(root_fd, payload):
3718
3725
  return entries
3719
3726
  finally:
3720
3727
  os.close(dir_fd)
3721
-
3722
3728
  def mkdirp_path(root_fd, payload):
3723
3729
  dir_fd = walk_dir(root_fd, split_relative(payload.get("relativePath", "")), mkdir_enabled=True)
3724
- os.close(dir_fd)
3725
- return None
3726
-
3730
+ os.close(dir_fd); return None
3727
3731
  def remove_tree(parent_fd, basename):
3728
3732
  st = os.lstat(basename, dir_fd=parent_fd)
3729
3733
  if stat.S_ISDIR(st.st_mode) and not stat.S_ISLNK(st.st_mode):
@@ -3736,7 +3740,6 @@ def remove_tree(parent_fd, basename):
3736
3740
  os.rmdir(basename, dir_fd=parent_fd)
3737
3741
  else:
3738
3742
  os.unlink(basename, dir_fd=parent_fd)
3739
-
3740
3743
  def remove_path(root_fd, payload):
3741
3744
  parent_fd, basename = parent_and_basename(root_fd, payload.get("relativePath", ""))
3742
3745
  try:
@@ -3819,14 +3822,15 @@ def write_path(root_fd, payload):
3819
3822
  except FileNotFoundError:
3820
3823
  pass
3821
3824
  temp_name, temp_fd = create_temp_file(parent_fd, basename, mode)
3825
+ os.fchmod(temp_fd, mode)
3822
3826
  write_all(temp_fd, data)
3823
3827
  os.fsync(temp_fd)
3828
+ temp_stat = os.fstat(temp_fd)
3824
3829
  os.close(temp_fd)
3825
3830
  temp_fd = None
3826
- commit_temp_file(parent_fd, temp_name, basename, overwrite, mode)
3831
+ result_stat = commit_temp_file(parent_fd, temp_name, basename, overwrite, mode, temp_stat)
3827
3832
  temp_name = None
3828
3833
  os.fsync(parent_fd)
3829
- result_stat = os.stat(basename, dir_fd=parent_fd, follow_symlinks=False)
3830
3834
  return {"dev": result_stat.st_dev, "ino": result_stat.st_ino}
3831
3835
  finally:
3832
3836
  if temp_fd is not None:
@@ -3857,6 +3861,7 @@ def copy_path(root_fd, payload):
3857
3861
  raise RuntimeError("fs-safe-too-large:%d:%d" % (max_bytes, source_stat.st_size))
3858
3862
  parent_fd = walk_dir(root_fd, split_relative(payload.get("relativeParentPath", "")), bool(payload.get("mkdir", True)))
3859
3863
  temp_name, temp_fd = create_temp_file(parent_fd, basename, mode)
3864
+ os.fchmod(temp_fd, mode)
3860
3865
  written_bytes = 0
3861
3866
  while True:
3862
3867
  chunk = os.read(source_fd, 65536)
@@ -3872,12 +3877,12 @@ def copy_path(root_fd, payload):
3872
3877
  raise OSError(errno.EIO, "short write")
3873
3878
  view = view[written:]
3874
3879
  os.fsync(temp_fd)
3880
+ temp_stat = os.fstat(temp_fd)
3875
3881
  os.close(temp_fd)
3876
3882
  temp_fd = None
3877
- commit_temp_file(parent_fd, temp_name, basename, overwrite, mode)
3883
+ result_stat = commit_temp_file(parent_fd, temp_name, basename, overwrite, mode, temp_stat)
3878
3884
  temp_name = None
3879
3885
  os.fsync(parent_fd)
3880
- result_stat = os.stat(basename, dir_fd=parent_fd, follow_symlinks=False)
3881
3886
  return {"dev": result_stat.st_dev, "ino": result_stat.st_ino}
3882
3887
  finally:
3883
3888
  os.close(source_fd)
@@ -3894,6 +3899,7 @@ def copy_path(root_fd, payload):
3894
3899
  def run_operation(operation, root_path, payload):
3895
3900
  root_fd = open_dir(root_path)
3896
3901
  try:
3902
+ assert_expected_root(root_fd, payload)
3897
3903
  if operation == "stat":
3898
3904
  return stat_path(root_fd, payload)
3899
3905
  if operation == "readdir":
@@ -3998,6 +4004,12 @@ function mapWorkerError(response) {
3998
4004
  if (message.includes("fs-safe-source-mismatch")) {
3999
4005
  return new errors_FsSafeError("path-mismatch", "source path changed during copy");
4000
4006
  }
4007
+ if (message.includes("fs-safe-temp-mismatch")) {
4008
+ return new errors_FsSafeError("path-mismatch", "temp path changed during write");
4009
+ }
4010
+ if (message.includes("fs-safe-root-mismatch")) {
4011
+ return new errors_FsSafeError("path-mismatch", "root path changed during operation");
4012
+ }
4001
4013
  if (message.includes("fs-safe-directory-noreplace-unsupported")) {
4002
4014
  return new errors_FsSafeError("invalid-path", "directory moves require overwrite: true");
4003
4015
  }
@@ -4178,9 +4190,7 @@ function validatePinnedOperationPayload(payload) {
4178
4190
  }
4179
4191
  }
4180
4192
  function isPinnedHelperUnavailable(error) {
4181
- return error instanceof Error &&
4182
- "code" in error &&
4183
- error.code === "helper-unavailable";
4193
+ return error instanceof Error && "code" in error && error.code === "helper-unavailable";
4184
4194
  }
4185
4195
  function validatePinnedRelativePath(relativePath) {
4186
4196
  if (relativePath.length === 0 || relativePath === ".") {
@@ -4278,29 +4288,21 @@ function assertWithinMaxBytes(bytes, maxBytes) {
4278
4288
  throw new errors_FsSafeError("too-large", `file exceeds limit of ${maxBytes} bytes (got at least ${bytes})`);
4279
4289
  }
4280
4290
  }
4281
- function pinned_write_createMaxBytesTransform(maxBytes) {
4282
- if (maxBytes === undefined) {
4283
- return undefined;
4284
- }
4291
+ async function writeStreamToHandle(stream, handle, maxBytes) {
4285
4292
  let bytes = 0;
4286
- return new external_node_stream_.Transform({
4287
- transform(chunk, _encoding, callback) {
4288
- bytes += chunk.byteLength;
4289
- if (bytes > maxBytes) {
4290
- callback(new errors_FsSafeError("too-large", `file exceeds limit of ${maxBytes} bytes (got at least ${bytes})`));
4291
- return;
4293
+ for await (const chunk of stream) {
4294
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
4295
+ bytes += buffer.byteLength;
4296
+ assertWithinMaxBytes(bytes, maxBytes);
4297
+ let offset = 0;
4298
+ while (offset < buffer.byteLength) {
4299
+ const { bytesWritten } = await handle.write(buffer, offset, buffer.byteLength - offset);
4300
+ if (bytesWritten <= 0) {
4301
+ throw new errors_FsSafeError("helper-failed", "fallback stream write made no progress");
4292
4302
  }
4293
- callback(null, chunk);
4294
- },
4295
- });
4296
- }
4297
- async function pipelineWithMaxBytes(stream, destination, maxBytes) {
4298
- const limiter = pinned_write_createMaxBytesTransform(maxBytes);
4299
- if (limiter) {
4300
- await (0,external_node_stream_promises_.pipeline)(stream, limiter, destination);
4301
- return;
4303
+ offset += bytesWritten;
4304
+ }
4302
4305
  }
4303
- await (0,external_node_stream_promises_.pipeline)(stream, destination);
4304
4306
  }
4305
4307
  async function inputToBase64(input, maxBytes) {
4306
4308
  if (input.kind === "buffer") {
@@ -4325,7 +4327,7 @@ async function runPinnedWriteHelper(params) {
4325
4327
  relativeParentPath: params.relativeParentPath,
4326
4328
  });
4327
4329
  if (getFsSafePythonConfig().mode === "off") {
4328
- return await runPinnedWriteFallback(params);
4330
+ return await runPinnedWriteFallbackOrThrow(params);
4329
4331
  }
4330
4332
  if (params.input.kind === "stream") {
4331
4333
  try {
@@ -4333,7 +4335,7 @@ async function runPinnedWriteHelper(params) {
4333
4335
  }
4334
4336
  catch (error) {
4335
4337
  if (canFallbackFromPythonError(error)) {
4336
- return await runPinnedWriteFallback(params);
4338
+ return await runPinnedWriteFallbackOrThrow(params, error);
4337
4339
  }
4338
4340
  throw error;
4339
4341
  }
@@ -4346,6 +4348,7 @@ async function runPinnedWriteHelper(params) {
4346
4348
  mode: params.mode || 0o600,
4347
4349
  overwrite: params.overwrite !== false,
4348
4350
  relativeParentPath: params.relativeParentPath,
4351
+ ...(params.rootIdentity ? { rootDev: params.rootIdentity.dev, rootIno: params.rootIdentity.ino } : {}),
4349
4352
  };
4350
4353
  try {
4351
4354
  return await runPinnedPythonOperation({
@@ -4356,7 +4359,7 @@ async function runPinnedWriteHelper(params) {
4356
4359
  }
4357
4360
  catch (error) {
4358
4361
  if (canFallbackFromPythonError(error)) {
4359
- return await runPinnedWriteFallback(params);
4362
+ return await runPinnedWriteFallbackOrThrow(params, error);
4360
4363
  }
4361
4364
  throw error;
4362
4365
  }
@@ -4376,22 +4379,29 @@ async function runPinnedCopyHelper(params) {
4376
4379
  mode: params.mode || 0o600,
4377
4380
  overwrite: params.overwrite !== false,
4378
4381
  relativeParentPath: params.relativeParentPath,
4382
+ ...(params.rootIdentity ? { rootDev: params.rootIdentity.dev, rootIno: params.rootIdentity.ino } : {}),
4379
4383
  sourceDev: params.sourceIdentity.dev,
4380
4384
  sourceIno: params.sourceIdentity.ino,
4381
4385
  sourcePath: params.sourcePath,
4382
4386
  },
4383
4387
  });
4384
4388
  }
4389
+ async function runPinnedWriteFallbackOrThrow(params, cause) {
4390
+ if (process.platform !== "win32") {
4391
+ throw new errors_FsSafeError("helper-unavailable", "Python helper is required for pinned writes on this platform", { cause });
4392
+ }
4393
+ return await runPinnedWriteFallback(params);
4394
+ }
4385
4395
  async function runPinnedWriteFallback(params) {
4386
4396
  const parentPath = params.relativeParentPath
4387
4397
  ? external_node_path_.join(params.rootPath, ...params.relativeParentPath.split("/"))
4388
4398
  : params.rootPath;
4389
- const parentGuard = await directory_guard_createNearestExistingDirectoryGuard(params.rootPath, parentPath);
4390
4399
  if (params.mkdir) {
4391
- await withAsyncDirectoryGuards([parentGuard], async () => {
4392
- await promises_.mkdir(parentPath, { recursive: true });
4393
- });
4400
+ await mkdirPathComponentsWithGuards({ rootReal: params.rootPath, targetPath: parentPath });
4394
4401
  }
4402
+ const parentGuard = params.mkdir
4403
+ ? await directory_guard_createAsyncDirectoryGuard(parentPath)
4404
+ : await directory_guard_createNearestExistingDirectoryGuard(params.rootPath, parentPath);
4395
4405
  const targetPath = external_node_path_.join(parentPath, params.basename);
4396
4406
  if (params.overwrite === false) {
4397
4407
  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), {
@@ -4413,7 +4423,7 @@ async function runPinnedWriteFallback(params) {
4413
4423
  }
4414
4424
  }
4415
4425
  else {
4416
- await pipelineWithMaxBytes(params.input.stream, handle.createWriteStream(), params.maxBytes);
4426
+ await writeStreamToHandle(params.input.stream, handle, params.maxBytes);
4417
4427
  }
4418
4428
  const stat = await handle.stat();
4419
4429
  created = false;
@@ -4434,7 +4444,9 @@ async function runPinnedWriteFallback(params) {
4434
4444
  ? external_node_fs_.constants.O_NOFOLLOW
4435
4445
  : 0);
4436
4446
  let handle;
4437
- let handleClosedByStream = false;
4447
+ let tempStat;
4448
+ let targetStat;
4449
+ let renamed = false;
4438
4450
  try {
4439
4451
  handle = await promises_.open(tempPath, tempFlags, params.mode);
4440
4452
  if (params.input.kind === "buffer") {
@@ -4447,29 +4459,36 @@ async function runPinnedWriteFallback(params) {
4447
4459
  }
4448
4460
  }
4449
4461
  else {
4450
- const writable = handle.createWriteStream();
4451
- writable.once("close", () => {
4452
- handleClosedByStream = true;
4453
- });
4454
- await pipelineWithMaxBytes(params.input.stream, writable, params.maxBytes);
4462
+ await writeStreamToHandle(params.input.stream, handle, params.maxBytes);
4455
4463
  }
4456
- if (!handleClosedByStream) {
4457
- await handle.close().catch(() => undefined);
4458
- handle = undefined;
4464
+ tempStat = await handle.stat();
4465
+ const tempPathStat = await promises_.lstat(tempPath);
4466
+ if (tempPathStat.isSymbolicLink() || !file_identity_sameFileIdentity(tempPathStat, tempStat)) {
4467
+ throw new errors_FsSafeError("path-mismatch", "fallback temp path changed during write");
4459
4468
  }
4469
+ const expectedTempStat = tempStat;
4470
+ await handle.close().catch(() => undefined);
4471
+ handle = undefined;
4460
4472
  await withAsyncDirectoryGuards([parentGuard], async () => {
4461
4473
  await promises_.rename(tempPath, targetPath);
4474
+ renamed = true;
4475
+ targetStat = await promises_.lstat(targetPath);
4476
+ if (targetStat.isSymbolicLink() || !file_identity_sameFileIdentity(targetStat, expectedTempStat)) {
4477
+ throw new errors_FsSafeError("path-mismatch", "fallback target changed during write");
4478
+ }
4462
4479
  });
4463
4480
  }
4464
4481
  catch (error) {
4465
- if (handle && !handleClosedByStream) {
4466
- await handle.close().catch(() => undefined);
4482
+ await handle?.close().catch(() => undefined);
4483
+ if (!renamed) {
4484
+ await promises_.rm(tempPath, { force: true }).catch(() => undefined);
4467
4485
  }
4468
- await promises_.rm(tempPath, { force: true }).catch(() => undefined);
4469
4486
  throw error;
4470
4487
  }
4471
- const stat = await promises_.stat(targetPath);
4472
- return { dev: stat.dev, ino: stat.ino };
4488
+ if (!targetStat) {
4489
+ throw new errors_FsSafeError("path-mismatch", "fallback target was not verified");
4490
+ }
4491
+ return { dev: targetStat.dev, ino: targetStat.ino };
4473
4492
  }
4474
4493
 
4475
4494
  // EXTERNAL MODULE: external "node:os"
@@ -5008,7 +5027,7 @@ function relativeInsideRoot(rootPath, targetPath) {
5008
5027
  if (!relative || relative === ".") {
5009
5028
  return "";
5010
5029
  }
5011
- if (relative.startsWith("..") || external_node_path_.isAbsolute(relative)) {
5030
+ if (isPathRelativeEscape(relative)) {
5012
5031
  return "";
5013
5032
  }
5014
5033
  return relative;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lousy-agents/cli",
3
- "version": "5.14.9",
3
+ "version": "5.14.10",
4
4
  "description": "CLI scaffolding tool that sets up projects with structure, instructions, and feedback loops for AI coding assistants",
5
5
  "type": "module",
6
6
  "repository": {