@lousy-agents/agent-shell 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 +2 -2
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import { createRequire as __rspack_createRequire } from "node:module";
3
3
  const __rspack_createRequire_require = __rspack_createRequire(import.meta.url);
4
4
  var __webpack_modules__ = ({
5
- 391(__unused_rspack_module, __unused_rspack___webpack_exports__, __webpack_require__) {
5
+ 145(__unused_rspack_module, __unused_rspack___webpack_exports__, __webpack_require__) {
6
6
 
7
7
  ;// CONCATENATED MODULE: external "node:child_process"
8
8
  const external_node_child_process_namespaceObject = __rspack_createRequire_require("node:child_process");
@@ -275,6 +275,9 @@ function path_isPathInside(root, target) {
275
275
  const firstSegment = relative.split(external_node_path_namespaceObject.posix.sep)[0];
276
276
  return relative === "" || (firstSegment !== ".." && !external_node_path_namespaceObject.isAbsolute(relative));
277
277
  }
278
+ function isPathRelativeEscape(relativePath) {
279
+ return relativePath === ".." || relativePath.startsWith(`..${external_node_path_namespaceObject.sep}`) || external_node_path_namespaceObject.isAbsolute(relativePath);
280
+ }
278
281
  function resolveSafeBaseDir(rootDir) {
279
282
  const resolved = path.resolve(rootDir);
280
283
  return resolved.endsWith(path.sep) ? resolved : `${resolved}${path.sep}`;
@@ -340,7 +343,7 @@ function splitSafeRelativePath(relativePath) {
340
343
  function resolveSafeRelativePath(rootDir, relativePath) {
341
344
  const root = path.resolve(rootDir);
342
345
  const target = path.resolve(root, ...splitSafeRelativePath(relativePath));
343
- if (target !== root && !target.startsWith(root + path.sep)) {
346
+ if (!path_isPathInside(root, target)) {
344
347
  throw new FsSafeError("outside-workspace", "relative path escapes root");
345
348
  }
346
349
  return target;
@@ -423,17 +426,17 @@ function directory_guard_createNearestExistingSyncDirectoryGuard(rootReal, targe
423
426
 
424
427
 
425
428
 
429
+
426
430
  function isSameOrChildPath(candidate, parent) {
427
- return candidate === parent || candidate.startsWith(`${parent}${external_node_path_namespaceObject.sep}`);
428
- }
429
- function isPathEscape(relativePath) {
430
- return relativePath === ".." || relativePath.startsWith(`..${external_node_path_namespaceObject.sep}`) || external_node_path_namespaceObject.isAbsolute(relativePath);
431
+ const parentPrefix = parent.endsWith(external_node_path_namespaceObject.sep) ? parent : `${parent}${external_node_path_namespaceObject.sep}`;
432
+ return candidate === parent || candidate.startsWith(parentPrefix);
431
433
  }
432
434
  async function mkdirPathComponentsWithGuards(params) {
433
435
  const root = external_node_path_namespaceObject.resolve(params.rootReal);
436
+ const rootCanonical = external_node_path_namespaceObject.resolve(await promises_namespaceObject.realpath(root));
434
437
  const target = external_node_path_namespaceObject.resolve(params.targetPath);
435
438
  const relative = external_node_path_namespaceObject.relative(root, target);
436
- if (isPathEscape(relative)) {
439
+ if (isPathRelativeEscape(relative)) {
437
440
  throw new errors_FsSafeError("outside-workspace", "directory is outside workspace root");
438
441
  }
439
442
  let current = root;
@@ -456,7 +459,7 @@ async function mkdirPathComponentsWithGuards(params) {
456
459
  }
457
460
  // Node's recursive mkdir follows symlinks in missing components. Build one
458
461
  // segment at a time and realpath-check each segment before descending.
459
- if (!isSameOrChildPath(external_node_path_namespaceObject.resolve(await promises_namespaceObject.realpath(next)), root)) {
462
+ if (!isSameOrChildPath(external_node_path_namespaceObject.resolve(await promises_namespaceObject.realpath(next)), rootCanonical)) {
460
463
  throw new errors_FsSafeError("outside-workspace", "directory escaped workspace root");
461
464
  }
462
465
  await directory_guard_createAsyncDirectoryGuard(next);
@@ -589,26 +592,20 @@ function canFallbackFromPythonError(error) {
589
592
 
590
593
 
591
594
  const PINNED_PYTHON_WORKER_SOURCE = String.raw `
592
- import base64
593
- import errno
594
- import json
595
- import os
596
- import secrets
597
- import stat
598
- import sys
599
-
595
+ import base64, errno, json, os, secrets, stat, sys
600
596
  DIR_FLAGS = os.O_RDONLY
601
597
  if hasattr(os, "O_DIRECTORY"):
602
598
  DIR_FLAGS |= os.O_DIRECTORY
603
599
  if hasattr(os, "O_NOFOLLOW"):
604
600
  DIR_FLAGS |= os.O_NOFOLLOW
605
601
  READ_FLAGS = os.O_RDONLY
602
+ if hasattr(os, "O_NONBLOCK"):
603
+ READ_FLAGS |= os.O_NONBLOCK
606
604
  if hasattr(os, "O_NOFOLLOW"):
607
605
  READ_FLAGS |= os.O_NOFOLLOW
608
606
  WRITE_FLAGS = os.O_WRONLY | os.O_CREAT | os.O_EXCL
609
607
  if hasattr(os, "O_NOFOLLOW"):
610
608
  WRITE_FLAGS |= os.O_NOFOLLOW
611
-
612
609
  def split_relative(value):
613
610
  if value in ("", "."):
614
611
  return []
@@ -621,10 +618,8 @@ def split_relative(value):
621
618
  if part == "..":
622
619
  raise OSError(errno.EPERM, "path traversal is not allowed")
623
620
  return parts
624
-
625
621
  def open_dir(path_value, dir_fd=None):
626
622
  return os.open(path_value, DIR_FLAGS, dir_fd=dir_fd)
627
-
628
623
  def walk_dir(root_fd, segments, mkdir_enabled=False):
629
624
  current_fd = os.dup(root_fd)
630
625
  try:
@@ -642,14 +637,12 @@ def walk_dir(root_fd, segments, mkdir_enabled=False):
642
637
  except Exception:
643
638
  os.close(current_fd)
644
639
  raise
645
-
646
640
  def parent_and_basename(root_fd, relative):
647
641
  segments = split_relative(relative)
648
642
  if not segments:
649
643
  raise OSError(errno.EPERM, "operation requires a non-root path")
650
644
  parent_fd = walk_dir(root_fd, segments[:-1])
651
645
  return parent_fd, segments[-1]
652
-
653
646
  def encode_stat(st):
654
647
  mode = st.st_mode
655
648
  return {
@@ -665,14 +658,108 @@ def encode_stat(st):
665
658
  "size": st.st_size,
666
659
  "uid": st.st_uid,
667
660
  }
668
-
669
661
  def reject_unsafe_endpoint(st):
670
662
  mode = st.st_mode
671
663
  if stat.S_ISLNK(mode):
672
664
  raise OSError(errno.ELOOP, "symlink endpoint is not allowed")
673
665
  if stat.S_ISREG(mode) and st.st_nlink > 1:
674
666
  raise OSError(errno.EPERM, "hardlinked file endpoint is not allowed")
675
-
667
+ def copy_bytes(source_fd, dest_fd):
668
+ while True:
669
+ chunk = os.read(source_fd, 65536)
670
+ if not chunk:
671
+ break
672
+ view = memoryview(chunk)
673
+ while view:
674
+ written = os.write(dest_fd, view)
675
+ if written <= 0:
676
+ raise OSError(errno.EIO, "short write")
677
+ view = view[written:]
678
+ def write_all(fd, data):
679
+ view = memoryview(data)
680
+ while view:
681
+ written = os.write(fd, view)
682
+ if written <= 0:
683
+ raise OSError(errno.EIO, "short write")
684
+ view = view[written:]
685
+ def link_unsupported(exc):
686
+ unsupported = (errno.EPERM, errno.EOPNOTSUPP, getattr(errno, "ENOTSUP", errno.EOPNOTSUPP))
687
+ return getattr(exc, "errno", None) in unsupported
688
+ def link_no_replace(name, new_name, source_fd, target_fd):
689
+ linked = False
690
+ try:
691
+ os.link(name, new_name, src_dir_fd=source_fd, dst_dir_fd=target_fd, follow_symlinks=False)
692
+ linked = True
693
+ os.unlink(name, dir_fd=source_fd)
694
+ except Exception:
695
+ if linked:
696
+ try: os.unlink(new_name, dir_fd=target_fd)
697
+ except FileNotFoundError: pass
698
+ raise
699
+ os.fsync(source_fd)
700
+ if source_fd != target_fd:
701
+ os.fsync(target_fd)
702
+ def copy_file_no_replace(source_parent_fd, source_name, target_parent_fd, basename, mode, expected=None, unlink_source=False):
703
+ source_fd = os.open(source_name, READ_FLAGS, dir_fd=source_parent_fd)
704
+ dest_fd = None; success = False; dest_stat = None
705
+ try:
706
+ if expected is not None:
707
+ source_stat = os.fstat(source_fd)
708
+ if source_stat.st_dev != expected.st_dev or source_stat.st_ino != expected.st_ino:
709
+ raise RuntimeError("fs-safe-source-mismatch")
710
+ dest_fd = os.open(basename, WRITE_FLAGS, mode, dir_fd=target_parent_fd)
711
+ copy_bytes(source_fd, dest_fd)
712
+ os.fsync(dest_fd)
713
+ dest_stat = os.fstat(dest_fd)
714
+ success = True
715
+ finally:
716
+ os.close(source_fd)
717
+ if dest_fd is not None:
718
+ os.close(dest_fd)
719
+ if dest_fd is not None and not success:
720
+ try: os.unlink(basename, dir_fd=target_parent_fd)
721
+ except FileNotFoundError: pass
722
+ if unlink_source:
723
+ try:
724
+ os.unlink(source_name, dir_fd=source_parent_fd)
725
+ except Exception:
726
+ try: os.unlink(basename, dir_fd=target_parent_fd)
727
+ except FileNotFoundError: pass
728
+ raise
729
+ return dest_stat
730
+ def same_identity(left, right):
731
+ return left.st_dev == right.st_dev and left.st_ino == right.st_ino
732
+ def verify_temp_name(parent_fd, temp_name, expected_stat):
733
+ current_stat = os.lstat(temp_name, dir_fd=parent_fd)
734
+ if stat.S_ISLNK(current_stat.st_mode) or not same_identity(current_stat, expected_stat):
735
+ raise RuntimeError("fs-safe-temp-mismatch")
736
+ def verify_committed_temp(parent_fd, basename, expected_stat):
737
+ final_stat = os.lstat(basename, dir_fd=parent_fd)
738
+ if not stat.S_ISLNK(final_stat.st_mode) and same_identity(final_stat, expected_stat):
739
+ return final_stat
740
+ try: os.unlink(basename, dir_fd=parent_fd)
741
+ except FileNotFoundError: pass
742
+ raise RuntimeError("fs-safe-temp-mismatch")
743
+ def commit_temp_file(parent_fd, temp_name, basename, overwrite, mode, expected_stat):
744
+ verify_temp_name(parent_fd, temp_name, expected_stat)
745
+ if overwrite:
746
+ os.replace(temp_name, basename, src_dir_fd=parent_fd, dst_dir_fd=parent_fd)
747
+ return verify_committed_temp(parent_fd, basename, expected_stat)
748
+ else:
749
+ try:
750
+ os.link(temp_name, basename, src_dir_fd=parent_fd, dst_dir_fd=parent_fd, follow_symlinks=False)
751
+ final_stat = verify_committed_temp(parent_fd, basename, expected_stat)
752
+ os.unlink(temp_name, dir_fd=parent_fd)
753
+ return final_stat
754
+ except OSError as exc:
755
+ if not link_unsupported(exc):
756
+ raise
757
+ return copy_file_no_replace(parent_fd, temp_name, parent_fd, basename, mode, expected_stat, True)
758
+ def assert_expected_root(root_fd, payload):
759
+ if "rootDev" in payload or "rootIno" in payload:
760
+ root_stat = os.fstat(root_fd)
761
+ if root_stat.st_dev != int(payload["rootDev"]) or root_stat.st_ino != int(payload["rootIno"]):
762
+ raise RuntimeError("fs-safe-root-mismatch")
676
763
  def stat_path(root_fd, payload):
677
764
  relative = payload.get("relativePath", "")
678
765
  segments = split_relative(relative)
@@ -686,7 +773,6 @@ def stat_path(root_fd, payload):
686
773
  return encode_stat(st)
687
774
  finally:
688
775
  os.close(parent_fd)
689
-
690
776
  def readdir_path(root_fd, payload):
691
777
  dir_fd = walk_dir(root_fd, split_relative(payload.get("relativePath", "")))
692
778
  try:
@@ -702,12 +788,9 @@ def readdir_path(root_fd, payload):
702
788
  return entries
703
789
  finally:
704
790
  os.close(dir_fd)
705
-
706
791
  def mkdirp_path(root_fd, payload):
707
792
  dir_fd = walk_dir(root_fd, split_relative(payload.get("relativePath", "")), mkdir_enabled=True)
708
- os.close(dir_fd)
709
- return None
710
-
793
+ os.close(dir_fd); return None
711
794
  def remove_tree(parent_fd, basename):
712
795
  st = os.lstat(basename, dir_fd=parent_fd)
713
796
  if stat.S_ISDIR(st.st_mode) and not stat.S_ISLNK(st.st_mode):
@@ -720,7 +803,6 @@ def remove_tree(parent_fd, basename):
720
803
  os.rmdir(basename, dir_fd=parent_fd)
721
804
  else:
722
805
  os.unlink(basename, dir_fd=parent_fd)
723
-
724
806
  def remove_path(root_fd, payload):
725
807
  parent_fd, basename = parent_and_basename(root_fd, payload.get("relativePath", ""))
726
808
  try:
@@ -747,7 +829,18 @@ def rename_path(root_fd, payload):
747
829
  try:
748
830
  from_stat = os.lstat(from_base, dir_fd=from_parent_fd)
749
831
  reject_unsafe_endpoint(from_stat)
750
- if not payload.get("overwrite", True):
832
+ overwrite = payload.get("overwrite", True)
833
+ if not overwrite and stat.S_ISREG(from_stat.st_mode):
834
+ try:
835
+ link_no_replace(from_base, to_base, from_parent_fd, to_parent_fd)
836
+ except OSError as exc:
837
+ if not link_unsupported(exc):
838
+ raise
839
+ copy_file_no_replace(from_parent_fd, from_base, to_parent_fd, to_base, stat.S_IMODE(from_stat.st_mode), from_stat, True)
840
+ return None
841
+ if not overwrite and stat.S_ISDIR(from_stat.st_mode):
842
+ raise RuntimeError("fs-safe-directory-noreplace-unsupported")
843
+ if not overwrite:
751
844
  try:
752
845
  os.lstat(to_base, dir_fd=to_parent_fd)
753
846
  raise FileExistsError(errno.EEXIST, "destination exists", to_base)
@@ -792,19 +885,15 @@ def write_path(root_fd, payload):
792
885
  except FileNotFoundError:
793
886
  pass
794
887
  temp_name, temp_fd = create_temp_file(parent_fd, basename, mode)
795
- view = memoryview(data)
796
- while view:
797
- written = os.write(temp_fd, view)
798
- if written <= 0:
799
- raise OSError(errno.EIO, "short write")
800
- view = view[written:]
888
+ os.fchmod(temp_fd, mode)
889
+ write_all(temp_fd, data)
801
890
  os.fsync(temp_fd)
891
+ temp_stat = os.fstat(temp_fd)
802
892
  os.close(temp_fd)
803
893
  temp_fd = None
804
- os.replace(temp_name, basename, src_dir_fd=parent_fd, dst_dir_fd=parent_fd)
894
+ result_stat = commit_temp_file(parent_fd, temp_name, basename, overwrite, mode, temp_stat)
805
895
  temp_name = None
806
896
  os.fsync(parent_fd)
807
- result_stat = os.stat(basename, dir_fd=parent_fd, follow_symlinks=False)
808
897
  return {"dev": result_stat.st_dev, "ino": result_stat.st_ino}
809
898
  finally:
810
899
  if temp_fd is not None:
@@ -834,13 +923,8 @@ def copy_path(root_fd, payload):
834
923
  if max_bytes >= 0 and source_stat.st_size > max_bytes:
835
924
  raise RuntimeError("fs-safe-too-large:%d:%d" % (max_bytes, source_stat.st_size))
836
925
  parent_fd = walk_dir(root_fd, split_relative(payload.get("relativeParentPath", "")), bool(payload.get("mkdir", True)))
837
- if not overwrite:
838
- try:
839
- os.lstat(basename, dir_fd=parent_fd)
840
- raise FileExistsError(errno.EEXIST, "destination exists", basename)
841
- except FileNotFoundError:
842
- pass
843
926
  temp_name, temp_fd = create_temp_file(parent_fd, basename, mode)
927
+ os.fchmod(temp_fd, mode)
844
928
  written_bytes = 0
845
929
  while True:
846
930
  chunk = os.read(source_fd, 65536)
@@ -856,12 +940,12 @@ def copy_path(root_fd, payload):
856
940
  raise OSError(errno.EIO, "short write")
857
941
  view = view[written:]
858
942
  os.fsync(temp_fd)
943
+ temp_stat = os.fstat(temp_fd)
859
944
  os.close(temp_fd)
860
945
  temp_fd = None
861
- os.replace(temp_name, basename, src_dir_fd=parent_fd, dst_dir_fd=parent_fd)
946
+ result_stat = commit_temp_file(parent_fd, temp_name, basename, overwrite, mode, temp_stat)
862
947
  temp_name = None
863
948
  os.fsync(parent_fd)
864
- result_stat = os.stat(basename, dir_fd=parent_fd, follow_symlinks=False)
865
949
  return {"dev": result_stat.st_dev, "ino": result_stat.st_ino}
866
950
  finally:
867
951
  os.close(source_fd)
@@ -878,6 +962,7 @@ def copy_path(root_fd, payload):
878
962
  def run_operation(operation, root_path, payload):
879
963
  root_fd = open_dir(root_path)
880
964
  try:
965
+ assert_expected_root(root_fd, payload)
881
966
  if operation == "stat":
882
967
  return stat_path(root_fd, payload)
883
968
  if operation == "readdir":
@@ -982,6 +1067,15 @@ function mapWorkerError(response) {
982
1067
  if (message.includes("fs-safe-source-mismatch")) {
983
1068
  return new errors_FsSafeError("path-mismatch", "source path changed during copy");
984
1069
  }
1070
+ if (message.includes("fs-safe-temp-mismatch")) {
1071
+ return new errors_FsSafeError("path-mismatch", "temp path changed during write");
1072
+ }
1073
+ if (message.includes("fs-safe-root-mismatch")) {
1074
+ return new errors_FsSafeError("path-mismatch", "root path changed during operation");
1075
+ }
1076
+ if (message.includes("fs-safe-directory-noreplace-unsupported")) {
1077
+ return new errors_FsSafeError("invalid-path", "directory moves require overwrite: true");
1078
+ }
985
1079
  if (code === "FileNotFoundError" || errno === 2) {
986
1080
  return new errors_FsSafeError("not-found", "file not found");
987
1081
  }
@@ -999,19 +1093,19 @@ function mapWorkerError(response) {
999
1093
  }
1000
1094
  return new errors_FsSafeError("helper-failed", message);
1001
1095
  }
1002
- function rejectPending(error) {
1003
- if (!worker) {
1096
+ function rejectPending(error, targetWorker = worker) {
1097
+ if (!targetWorker || worker !== targetWorker) {
1004
1098
  return;
1005
1099
  }
1006
- setWorkerRef(worker, false);
1007
- for (const pending of worker.pending.values()) {
1100
+ setWorkerRef(targetWorker, false);
1101
+ for (const pending of targetWorker.pending.values()) {
1008
1102
  pending.reject(error);
1009
1103
  }
1010
- worker.pending.clear();
1104
+ targetWorker.pending.clear();
1011
1105
  worker = null;
1012
1106
  }
1013
- function handleWorkerLine(line) {
1014
- if (!worker || !line.trim()) {
1107
+ function handleWorkerLine(currentWorker, line) {
1108
+ if (worker !== currentWorker || !line.trim()) {
1015
1109
  return;
1016
1110
  }
1017
1111
  let decoded;
@@ -1019,11 +1113,11 @@ function handleWorkerLine(line) {
1019
1113
  decoded = JSON.parse(line);
1020
1114
  }
1021
1115
  catch {
1022
- rejectPending(new errors_FsSafeError("helper-failed", `pinned helper returned invalid JSON: ${line}`));
1116
+ rejectPending(new errors_FsSafeError("helper-failed", `pinned helper returned invalid JSON: ${line}`), currentWorker);
1023
1117
  return;
1024
1118
  }
1025
1119
  if (typeof decoded !== "object" || decoded === null || !("id" in decoded)) {
1026
- rejectPending(new errors_FsSafeError("helper-failed", "pinned helper returned invalid response"));
1120
+ rejectPending(new errors_FsSafeError("helper-failed", "pinned helper returned invalid response"), currentWorker);
1027
1121
  return;
1028
1122
  }
1029
1123
  const response = decoded;
@@ -1031,13 +1125,13 @@ function handleWorkerLine(line) {
1031
1125
  if (id === undefined) {
1032
1126
  return;
1033
1127
  }
1034
- const pending = worker.pending.get(id);
1128
+ const pending = currentWorker.pending.get(id);
1035
1129
  if (!pending) {
1036
1130
  return;
1037
1131
  }
1038
- worker.pending.delete(id);
1039
- if (worker.pending.size === 0) {
1040
- setWorkerRef(worker, false);
1132
+ currentWorker.pending.delete(id);
1133
+ if (currentWorker.pending.size === 0) {
1134
+ setWorkerRef(currentWorker, false);
1041
1135
  }
1042
1136
  if (response.ok === true) {
1043
1137
  pending.resolve(response.result);
@@ -1053,28 +1147,28 @@ function getWorker() {
1053
1147
  const child = (0,external_node_child_process_namespaceObject.spawn)(resolvePython(), ["-u", "-c", PINNED_PYTHON_WORKER_SOURCE], {
1054
1148
  stdio: ["pipe", "pipe", "pipe"],
1055
1149
  });
1056
- worker = { child, pending: new Map(), stderr: "", stdoutBuffer: "" };
1150
+ const currentWorker = { child, pending: new Map(), stderr: "", stdoutBuffer: "" };
1151
+ worker = currentWorker;
1057
1152
  child.stdout.setEncoding("utf8");
1058
1153
  child.stderr.setEncoding("utf8");
1059
1154
  child.stdout.on("data", (chunk) => {
1060
- const current = worker;
1061
- if (!current) {
1155
+ if (worker !== currentWorker) {
1062
1156
  return;
1063
1157
  }
1064
- current.stdoutBuffer += chunk;
1158
+ currentWorker.stdoutBuffer += chunk;
1065
1159
  for (;;) {
1066
- const newline = current.stdoutBuffer.indexOf("\n");
1160
+ const newline = currentWorker.stdoutBuffer.indexOf("\n");
1067
1161
  if (newline < 0) {
1068
1162
  break;
1069
1163
  }
1070
- const line = current.stdoutBuffer.slice(0, newline);
1071
- current.stdoutBuffer = current.stdoutBuffer.slice(newline + 1);
1072
- handleWorkerLine(line);
1164
+ const line = currentWorker.stdoutBuffer.slice(0, newline);
1165
+ currentWorker.stdoutBuffer = currentWorker.stdoutBuffer.slice(newline + 1);
1166
+ handleWorkerLine(currentWorker, line);
1073
1167
  }
1074
1168
  });
1075
1169
  child.stderr.on("data", (chunk) => {
1076
- if (worker) {
1077
- worker.stderr = `${worker.stderr}${chunk}`.slice(-4096);
1170
+ if (worker === currentWorker) {
1171
+ currentWorker.stderr = `${currentWorker.stderr}${chunk}`.slice(-4096);
1078
1172
  }
1079
1173
  });
1080
1174
  child.once("error", (error) => {
@@ -1083,17 +1177,17 @@ function getWorker() {
1083
1177
  : error instanceof Error
1084
1178
  ? error
1085
1179
  : new Error(String(error));
1086
- rejectPending(mapped);
1180
+ rejectPending(mapped, currentWorker);
1087
1181
  });
1088
1182
  child.once("close", (code, signal) => {
1089
- const stderr = worker?.stderr.trim();
1090
- rejectPending(new errors_FsSafeError("helper-failed", stderr || `pinned helper exited with code ${code ?? "null"} (${signal ?? "?"})`));
1183
+ const stderr = currentWorker.stderr.trim();
1184
+ rejectPending(new errors_FsSafeError("helper-failed", stderr || `pinned helper exited with code ${code ?? "null"} (${signal ?? "?"})`), currentWorker);
1091
1185
  });
1092
1186
  process.once("exit", () => {
1093
1187
  child.kill("SIGTERM");
1094
1188
  });
1095
- setWorkerRef(worker, false);
1096
- return worker;
1189
+ setWorkerRef(currentWorker, false);
1190
+ return currentWorker;
1097
1191
  }
1098
1192
  function setRefable(value, ref) {
1099
1193
  if (!value) {
@@ -1159,9 +1253,7 @@ function validatePinnedOperationPayload(payload) {
1159
1253
  }
1160
1254
  }
1161
1255
  function isPinnedHelperUnavailable(error) {
1162
- return error instanceof Error &&
1163
- "code" in error &&
1164
- error.code === "helper-unavailable";
1256
+ return error instanceof Error && "code" in error && error.code === "helper-unavailable";
1165
1257
  }
1166
1258
  function validatePinnedRelativePath(relativePath) {
1167
1259
  if (relativePath.length === 0 || relativePath === ".") {
@@ -1259,29 +1351,21 @@ function assertWithinMaxBytes(bytes, maxBytes) {
1259
1351
  throw new errors_FsSafeError("too-large", `file exceeds limit of ${maxBytes} bytes (got at least ${bytes})`);
1260
1352
  }
1261
1353
  }
1262
- function pinned_write_createMaxBytesTransform(maxBytes) {
1263
- if (maxBytes === undefined) {
1264
- return undefined;
1265
- }
1354
+ async function writeStreamToHandle(stream, handle, maxBytes) {
1266
1355
  let bytes = 0;
1267
- return new external_node_stream_namespaceObject.Transform({
1268
- transform(chunk, _encoding, callback) {
1269
- bytes += chunk.byteLength;
1270
- if (bytes > maxBytes) {
1271
- callback(new errors_FsSafeError("too-large", `file exceeds limit of ${maxBytes} bytes (got at least ${bytes})`));
1272
- return;
1356
+ for await (const chunk of stream) {
1357
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
1358
+ bytes += buffer.byteLength;
1359
+ assertWithinMaxBytes(bytes, maxBytes);
1360
+ let offset = 0;
1361
+ while (offset < buffer.byteLength) {
1362
+ const { bytesWritten } = await handle.write(buffer, offset, buffer.byteLength - offset);
1363
+ if (bytesWritten <= 0) {
1364
+ throw new errors_FsSafeError("helper-failed", "fallback stream write made no progress");
1273
1365
  }
1274
- callback(null, chunk);
1275
- },
1276
- });
1277
- }
1278
- async function pipelineWithMaxBytes(stream, destination, maxBytes) {
1279
- const limiter = pinned_write_createMaxBytesTransform(maxBytes);
1280
- if (limiter) {
1281
- await (0,external_node_stream_promises_namespaceObject.pipeline)(stream, limiter, destination);
1282
- return;
1366
+ offset += bytesWritten;
1367
+ }
1283
1368
  }
1284
- await (0,external_node_stream_promises_namespaceObject.pipeline)(stream, destination);
1285
1369
  }
1286
1370
  async function inputToBase64(input, maxBytes) {
1287
1371
  if (input.kind === "buffer") {
@@ -1306,7 +1390,7 @@ async function runPinnedWriteHelper(params) {
1306
1390
  relativeParentPath: params.relativeParentPath,
1307
1391
  });
1308
1392
  if (getFsSafePythonConfig().mode === "off") {
1309
- return await runPinnedWriteFallback(params);
1393
+ return await runPinnedWriteFallbackOrThrow(params);
1310
1394
  }
1311
1395
  if (params.input.kind === "stream") {
1312
1396
  try {
@@ -1314,7 +1398,7 @@ async function runPinnedWriteHelper(params) {
1314
1398
  }
1315
1399
  catch (error) {
1316
1400
  if (canFallbackFromPythonError(error)) {
1317
- return await runPinnedWriteFallback(params);
1401
+ return await runPinnedWriteFallbackOrThrow(params, error);
1318
1402
  }
1319
1403
  throw error;
1320
1404
  }
@@ -1327,6 +1411,7 @@ async function runPinnedWriteHelper(params) {
1327
1411
  mode: params.mode || 0o600,
1328
1412
  overwrite: params.overwrite !== false,
1329
1413
  relativeParentPath: params.relativeParentPath,
1414
+ ...(params.rootIdentity ? { rootDev: params.rootIdentity.dev, rootIno: params.rootIdentity.ino } : {}),
1330
1415
  };
1331
1416
  try {
1332
1417
  return await runPinnedPythonOperation({
@@ -1337,7 +1422,7 @@ async function runPinnedWriteHelper(params) {
1337
1422
  }
1338
1423
  catch (error) {
1339
1424
  if (canFallbackFromPythonError(error)) {
1340
- return await runPinnedWriteFallback(params);
1425
+ return await runPinnedWriteFallbackOrThrow(params, error);
1341
1426
  }
1342
1427
  throw error;
1343
1428
  }
@@ -1357,22 +1442,29 @@ async function runPinnedCopyHelper(params) {
1357
1442
  mode: params.mode || 0o600,
1358
1443
  overwrite: params.overwrite !== false,
1359
1444
  relativeParentPath: params.relativeParentPath,
1445
+ ...(params.rootIdentity ? { rootDev: params.rootIdentity.dev, rootIno: params.rootIdentity.ino } : {}),
1360
1446
  sourceDev: params.sourceIdentity.dev,
1361
1447
  sourceIno: params.sourceIdentity.ino,
1362
1448
  sourcePath: params.sourcePath,
1363
1449
  },
1364
1450
  });
1365
1451
  }
1452
+ async function runPinnedWriteFallbackOrThrow(params, cause) {
1453
+ if (process.platform !== "win32") {
1454
+ throw new errors_FsSafeError("helper-unavailable", "Python helper is required for pinned writes on this platform", { cause });
1455
+ }
1456
+ return await runPinnedWriteFallback(params);
1457
+ }
1366
1458
  async function runPinnedWriteFallback(params) {
1367
1459
  const parentPath = params.relativeParentPath
1368
1460
  ? external_node_path_namespaceObject.join(params.rootPath, ...params.relativeParentPath.split("/"))
1369
1461
  : params.rootPath;
1370
- const parentGuard = await directory_guard_createNearestExistingDirectoryGuard(params.rootPath, parentPath);
1371
1462
  if (params.mkdir) {
1372
- await withAsyncDirectoryGuards([parentGuard], async () => {
1373
- await promises_namespaceObject.mkdir(parentPath, { recursive: true });
1374
- });
1463
+ await mkdirPathComponentsWithGuards({ rootReal: params.rootPath, targetPath: parentPath });
1375
1464
  }
1465
+ const parentGuard = params.mkdir
1466
+ ? await directory_guard_createAsyncDirectoryGuard(parentPath)
1467
+ : await directory_guard_createNearestExistingDirectoryGuard(params.rootPath, parentPath);
1376
1468
  const targetPath = external_node_path_namespaceObject.join(parentPath, params.basename);
1377
1469
  if (params.overwrite === false) {
1378
1470
  let handle = await withAsyncDirectoryGuards([parentGuard], async () => await promises_namespaceObject.open(targetPath, external_node_fs_namespaceObject.constants.O_WRONLY | external_node_fs_namespaceObject.constants.O_CREAT | external_node_fs_namespaceObject.constants.O_EXCL, params.mode), {
@@ -1394,7 +1486,7 @@ async function runPinnedWriteFallback(params) {
1394
1486
  }
1395
1487
  }
1396
1488
  else {
1397
- await pipelineWithMaxBytes(params.input.stream, handle.createWriteStream(), params.maxBytes);
1489
+ await writeStreamToHandle(params.input.stream, handle, params.maxBytes);
1398
1490
  }
1399
1491
  const stat = await handle.stat();
1400
1492
  created = false;
@@ -1415,7 +1507,9 @@ async function runPinnedWriteFallback(params) {
1415
1507
  ? external_node_fs_namespaceObject.constants.O_NOFOLLOW
1416
1508
  : 0);
1417
1509
  let handle;
1418
- let handleClosedByStream = false;
1510
+ let tempStat;
1511
+ let targetStat;
1512
+ let renamed = false;
1419
1513
  try {
1420
1514
  handle = await promises_namespaceObject.open(tempPath, tempFlags, params.mode);
1421
1515
  if (params.input.kind === "buffer") {
@@ -1428,29 +1522,36 @@ async function runPinnedWriteFallback(params) {
1428
1522
  }
1429
1523
  }
1430
1524
  else {
1431
- const writable = handle.createWriteStream();
1432
- writable.once("close", () => {
1433
- handleClosedByStream = true;
1434
- });
1435
- await pipelineWithMaxBytes(params.input.stream, writable, params.maxBytes);
1525
+ await writeStreamToHandle(params.input.stream, handle, params.maxBytes);
1436
1526
  }
1437
- if (!handleClosedByStream) {
1438
- await handle.close().catch(() => undefined);
1439
- handle = undefined;
1527
+ tempStat = await handle.stat();
1528
+ const tempPathStat = await promises_namespaceObject.lstat(tempPath);
1529
+ if (tempPathStat.isSymbolicLink() || !file_identity_sameFileIdentity(tempPathStat, tempStat)) {
1530
+ throw new errors_FsSafeError("path-mismatch", "fallback temp path changed during write");
1440
1531
  }
1532
+ const expectedTempStat = tempStat;
1533
+ await handle.close().catch(() => undefined);
1534
+ handle = undefined;
1441
1535
  await withAsyncDirectoryGuards([parentGuard], async () => {
1442
1536
  await promises_namespaceObject.rename(tempPath, targetPath);
1537
+ renamed = true;
1538
+ targetStat = await promises_namespaceObject.lstat(targetPath);
1539
+ if (targetStat.isSymbolicLink() || !file_identity_sameFileIdentity(targetStat, expectedTempStat)) {
1540
+ throw new errors_FsSafeError("path-mismatch", "fallback target changed during write");
1541
+ }
1443
1542
  });
1444
1543
  }
1445
1544
  catch (error) {
1446
- if (handle && !handleClosedByStream) {
1447
- await handle.close().catch(() => undefined);
1545
+ await handle?.close().catch(() => undefined);
1546
+ if (!renamed) {
1547
+ await promises_namespaceObject.rm(tempPath, { force: true }).catch(() => undefined);
1448
1548
  }
1449
- await promises_namespaceObject.rm(tempPath, { force: true }).catch(() => undefined);
1450
1549
  throw error;
1451
1550
  }
1452
- const stat = await promises_namespaceObject.stat(targetPath);
1453
- return { dev: stat.dev, ino: stat.ino };
1551
+ if (!targetStat) {
1552
+ throw new errors_FsSafeError("path-mismatch", "fallback target was not verified");
1553
+ }
1554
+ return { dev: targetStat.dev, ino: targetStat.ino };
1454
1555
  }
1455
1556
 
1456
1557
  ;// CONCATENATED MODULE: external "node:os"
@@ -1989,7 +2090,7 @@ function relativeInsideRoot(rootPath, targetPath) {
1989
2090
  if (!relative || relative === ".") {
1990
2091
  return "";
1991
2092
  }
1992
- if (relative.startsWith("..") || external_node_path_namespaceObject.isAbsolute(relative)) {
2093
+ if (isPathRelativeEscape(relative)) {
1993
2094
  return "";
1994
2095
  }
1995
2096
  return relative;
@@ -2340,6 +2441,15 @@ function __setFsSafeTestHooksForTest(hooks) {
2340
2441
  test_hooks_fsSafeTestHooks = hooks;
2341
2442
  }
2342
2443
 
2444
+ ;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/json-stringify.js
2445
+ function stringifyJsonDocument(value, replacer, space) {
2446
+ const text = JSON.stringify(value, replacer, space);
2447
+ if (typeof text !== "string") {
2448
+ throw new TypeError("value is not representable as a JSON document");
2449
+ }
2450
+ return text;
2451
+ }
2452
+
2343
2453
  ;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/temp-cleanup.js
2344
2454
 
2345
2455
  const tempCleanupEntries = new Map();
@@ -2427,6 +2537,7 @@ async function serializePathWrite(key, run) {
2427
2537
 
2428
2538
 
2429
2539
 
2540
+
2430
2541
 
2431
2542
 
2432
2543
  function logWarn(message) {
@@ -2667,12 +2778,12 @@ class RootHandle {
2667
2778
  }
2668
2779
  async writeJson(relativePath, data, options = {}) {
2669
2780
  const { replacer, space, trailingNewline = true, ...writeOptions } = options;
2670
- const json = JSON.stringify(data, replacer, space);
2781
+ const json = stringifyJsonDocument(data, replacer, space);
2671
2782
  await this.write(relativePath, trailingNewline ? `${json}\n` : json, writeOptions);
2672
2783
  }
2673
2784
  async createJson(relativePath, data, options = {}) {
2674
2785
  const { replacer, space, trailingNewline = true, ...writeOptions } = options;
2675
- const json = JSON.stringify(data, replacer, space);
2786
+ const json = stringifyJsonDocument(data, replacer, space);
2676
2787
  await this.create(relativePath, trailingNewline ? `${json}\n` : json, writeOptions);
2677
2788
  }
2678
2789
  async copyIn(relativePath, sourcePath, options = {}) {
@@ -3457,6 +3568,9 @@ async function movePathFallback(root, params) {
3457
3568
  if (sourceStat.isFile() && sourceStat.nlink > 1) {
3458
3569
  throw new errors_FsSafeError("hardlink", "hardlinked path not allowed");
3459
3570
  }
3571
+ if (!params.overwrite && sourceStat.isDirectory()) {
3572
+ throw new errors_FsSafeError("invalid-path", "directory moves require overwrite: true");
3573
+ }
3460
3574
  if (!params.overwrite) {
3461
3575
  try {
3462
3576
  await promises_namespaceObject.lstat(target.resolved);
@@ -14782,4 +14896,4 @@ if (installedChunkData !== 0) { // 0 means "already installed".'
14782
14896
  // module factories are used so entry inlining is disabled
14783
14897
  // startup
14784
14898
  // Load entry module and return exports
14785
- var __webpack_exports__ = __webpack_require__(391);
14899
+ var __webpack_exports__ = __webpack_require__(145);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lousy-agents/agent-shell",
3
- "version": "5.14.8",
3
+ "version": "5.14.10",
4
4
  "description": "A flight recorder for npm script execution",
5
5
  "type": "module",
6
6
  "repository": {
@@ -36,7 +36,7 @@
36
36
  "build": "rspack build --config rspack.config.ts && chmod +x dist/index.js"
37
37
  },
38
38
  "dependencies": {
39
- "@openclaw/fs-safe": "0.2.4",
39
+ "@openclaw/fs-safe": "0.2.6",
40
40
  "zod": "4.4.3"
41
41
  },
42
42
  "peerDependencies": {