@lousy-agents/agent-shell 5.14.9 → 5.15.0

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 +373 -198
  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
- 145(__unused_rspack_module, __unused_rspack___webpack_exports__, __webpack_require__) {
5
+ 341(__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}`;
@@ -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);
@@ -543,6 +546,191 @@ function guardedRmSync(params) {
543
546
  }), { verifyAfter: params.verifyAfter });
544
547
  }
545
548
 
549
+ ;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/deny-mutations.js
550
+
551
+
552
+
553
+
554
+ async function pathExists(filePath) {
555
+ try {
556
+ await promises_namespaceObject.lstat(filePath);
557
+ return true;
558
+ }
559
+ catch (err) {
560
+ if (!path_isNotFoundPathError(err)) {
561
+ throw err;
562
+ }
563
+ return false;
564
+ }
565
+ }
566
+ async function resolvePathViaExistingAncestor(targetPath) {
567
+ const normalized = external_node_path_namespaceObject.resolve(targetPath);
568
+ let cursor = normalized;
569
+ const missingSuffix = [];
570
+ while (external_node_path_namespaceObject.dirname(cursor) !== cursor && !(await pathExists(cursor))) {
571
+ missingSuffix.unshift(external_node_path_namespaceObject.basename(cursor));
572
+ cursor = external_node_path_namespaceObject.dirname(cursor);
573
+ }
574
+ if (!(await pathExists(cursor))) {
575
+ return normalized;
576
+ }
577
+ try {
578
+ const resolvedAncestor = external_node_path_namespaceObject.resolve(await promises_namespaceObject.realpath(cursor));
579
+ return missingSuffix.length === 0
580
+ ? resolvedAncestor
581
+ : external_node_path_namespaceObject.resolve(resolvedAncestor, ...missingSuffix);
582
+ }
583
+ catch {
584
+ return normalized;
585
+ }
586
+ }
587
+ async function comparablePaths(rawPath) {
588
+ path_assertNoNulPathInput(rawPath, "path contains a NUL byte");
589
+ const resolved = external_node_path_namespaceObject.resolve(rawPath);
590
+ return new Set([resolved, await resolvePathViaExistingAncestor(resolved)]);
591
+ }
592
+ function isSamePath(left, right) {
593
+ return path_isPathInside(left, right) && path_isPathInside(right, left);
594
+ }
595
+ function hasPolicyEntries(policy) {
596
+ return Boolean(policy?.paths?.length || policy?.prefixes?.length);
597
+ }
598
+ function policyPathEntries(entries) {
599
+ const paths = [];
600
+ for (const entry of entries ?? []) {
601
+ if (entry.length === 0) {
602
+ throw new errors_FsSafeError("invalid-path", "deny mutation paths must be non-empty");
603
+ }
604
+ path_assertNoNulPathInput(entry, "deny mutation path contains a NUL byte");
605
+ if (!external_node_path_namespaceObject.isAbsolute(entry)) {
606
+ throw new errors_FsSafeError("invalid-path", "deny mutation paths must be absolute");
607
+ }
608
+ paths.push(entry);
609
+ }
610
+ return paths;
611
+ }
612
+ async function assertMutationNotDenied(filePath, policy, options = {}) {
613
+ if (!hasPolicyEntries(policy)) {
614
+ return;
615
+ }
616
+ const targetPaths = await comparablePaths(filePath);
617
+ for (const deniedPath of policyPathEntries(policy.paths)) {
618
+ const deniedPaths = await comparablePaths(deniedPath);
619
+ for (const target of targetPaths) {
620
+ for (const denied of deniedPaths) {
621
+ if (isSamePath(denied, target) ||
622
+ (options.protectAncestors === true && path_isPathInside(target, denied))) {
623
+ throw new errors_FsSafeError("denied-path", "path is denied by denyMutations policy");
624
+ }
625
+ }
626
+ }
627
+ }
628
+ for (const deniedPrefix of policyPathEntries(policy.prefixes)) {
629
+ const deniedPaths = await comparablePaths(deniedPrefix);
630
+ for (const target of targetPaths) {
631
+ for (const denied of deniedPaths) {
632
+ if (path_isPathInside(denied, target) ||
633
+ (options.protectAncestors === true && path_isPathInside(target, denied))) {
634
+ throw new errors_FsSafeError("denied-path", "path is denied by denyMutations policy");
635
+ }
636
+ }
637
+ }
638
+ }
639
+ }
640
+ function mergeDenyMutationPolicies(defaultPolicy, callPolicy) {
641
+ if (!defaultPolicy) {
642
+ return callPolicy;
643
+ }
644
+ if (!callPolicy) {
645
+ return defaultPolicy;
646
+ }
647
+ return {
648
+ paths: [...(defaultPolicy.paths ?? []), ...(callPolicy.paths ?? [])],
649
+ prefixes: [...(defaultPolicy.prefixes ?? []), ...(callPolicy.prefixes ?? [])],
650
+ };
651
+ }
652
+
653
+ ;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/opened-realpath.js
654
+
655
+
656
+
657
+
658
+
659
+ async function resolveOpenedFileRealPathForHandle(handle, ioPath) {
660
+ const handleStat = await handle.stat();
661
+ const fdCandidates = process.platform === "linux"
662
+ ? [`/proc/self/fd/${handle.fd}`, `/dev/fd/${handle.fd}`]
663
+ : process.platform === "win32"
664
+ ? []
665
+ : [`/dev/fd/${handle.fd}`];
666
+ for (const fdPath of fdCandidates) {
667
+ try {
668
+ const fdRealPath = await promises_namespaceObject.realpath(fdPath);
669
+ const fdRealStat = await promises_namespaceObject.stat(fdRealPath);
670
+ if (file_identity_sameFileIdentity(handleStat, fdRealStat)) {
671
+ return fdRealPath;
672
+ }
673
+ }
674
+ catch {
675
+ // try next fd path
676
+ }
677
+ }
678
+ try {
679
+ const ioRealPath = await promises_namespaceObject.realpath(ioPath);
680
+ const ioRealStat = await promises_namespaceObject.stat(ioRealPath);
681
+ if (file_identity_sameFileIdentity(handleStat, ioRealStat)) {
682
+ return ioRealPath;
683
+ }
684
+ }
685
+ catch (err) {
686
+ if (!path_isNotFoundPathError(err)) {
687
+ throw err;
688
+ }
689
+ }
690
+ const parentResolved = await resolveOpenedFileRealPathFromParent(handleStat, ioPath);
691
+ if (parentResolved) {
692
+ return parentResolved;
693
+ }
694
+ throw new errors_FsSafeError("path-mismatch", "unable to resolve opened file path");
695
+ }
696
+ async function resolveOpenedFileRealPathFromParent(handleStat, ioPath) {
697
+ let parentReal;
698
+ try {
699
+ parentReal = await promises_namespaceObject.realpath(external_node_path_namespaceObject.dirname(ioPath));
700
+ }
701
+ catch (err) {
702
+ if (path_isNotFoundPathError(err)) {
703
+ return null;
704
+ }
705
+ throw err;
706
+ }
707
+ let entries;
708
+ try {
709
+ entries = await promises_namespaceObject.readdir(parentReal);
710
+ }
711
+ catch (err) {
712
+ if (path_isNotFoundPathError(err)) {
713
+ return null;
714
+ }
715
+ throw err;
716
+ }
717
+ for (const entry of entries.toSorted()) {
718
+ const candidatePath = external_node_path_namespaceObject.join(parentReal, entry);
719
+ try {
720
+ const candidateStat = await promises_namespaceObject.lstat(candidatePath);
721
+ if (candidateStat.isFile() && file_identity_sameFileIdentity(handleStat, candidateStat)) {
722
+ return await promises_namespaceObject.realpath(candidatePath);
723
+ }
724
+ }
725
+ catch (err) {
726
+ if (!path_isNotFoundPathError(err)) {
727
+ throw err;
728
+ }
729
+ }
730
+ }
731
+ return null;
732
+ }
733
+
546
734
  ;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/pinned-python-config.js
547
735
  let overrideConfig = {};
548
736
  function parseMode(value) {
@@ -589,26 +777,20 @@ function canFallbackFromPythonError(error) {
589
777
 
590
778
 
591
779
  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
-
780
+ import base64, errno, json, os, secrets, stat, sys
600
781
  DIR_FLAGS = os.O_RDONLY
601
782
  if hasattr(os, "O_DIRECTORY"):
602
783
  DIR_FLAGS |= os.O_DIRECTORY
603
784
  if hasattr(os, "O_NOFOLLOW"):
604
785
  DIR_FLAGS |= os.O_NOFOLLOW
605
786
  READ_FLAGS = os.O_RDONLY
787
+ if hasattr(os, "O_NONBLOCK"):
788
+ READ_FLAGS |= os.O_NONBLOCK
606
789
  if hasattr(os, "O_NOFOLLOW"):
607
790
  READ_FLAGS |= os.O_NOFOLLOW
608
791
  WRITE_FLAGS = os.O_WRONLY | os.O_CREAT | os.O_EXCL
609
792
  if hasattr(os, "O_NOFOLLOW"):
610
793
  WRITE_FLAGS |= os.O_NOFOLLOW
611
-
612
794
  def split_relative(value):
613
795
  if value in ("", "."):
614
796
  return []
@@ -621,10 +803,8 @@ def split_relative(value):
621
803
  if part == "..":
622
804
  raise OSError(errno.EPERM, "path traversal is not allowed")
623
805
  return parts
624
-
625
806
  def open_dir(path_value, dir_fd=None):
626
807
  return os.open(path_value, DIR_FLAGS, dir_fd=dir_fd)
627
-
628
808
  def walk_dir(root_fd, segments, mkdir_enabled=False):
629
809
  current_fd = os.dup(root_fd)
630
810
  try:
@@ -642,14 +822,12 @@ def walk_dir(root_fd, segments, mkdir_enabled=False):
642
822
  except Exception:
643
823
  os.close(current_fd)
644
824
  raise
645
-
646
825
  def parent_and_basename(root_fd, relative):
647
826
  segments = split_relative(relative)
648
827
  if not segments:
649
828
  raise OSError(errno.EPERM, "operation requires a non-root path")
650
829
  parent_fd = walk_dir(root_fd, segments[:-1])
651
830
  return parent_fd, segments[-1]
652
-
653
831
  def encode_stat(st):
654
832
  mode = st.st_mode
655
833
  return {
@@ -665,14 +843,12 @@ def encode_stat(st):
665
843
  "size": st.st_size,
666
844
  "uid": st.st_uid,
667
845
  }
668
-
669
846
  def reject_unsafe_endpoint(st):
670
847
  mode = st.st_mode
671
848
  if stat.S_ISLNK(mode):
672
849
  raise OSError(errno.ELOOP, "symlink endpoint is not allowed")
673
850
  if stat.S_ISREG(mode) and st.st_nlink > 1:
674
851
  raise OSError(errno.EPERM, "hardlinked file endpoint is not allowed")
675
-
676
852
  def copy_bytes(source_fd, dest_fd):
677
853
  while True:
678
854
  chunk = os.read(source_fd, 65536)
@@ -684,7 +860,6 @@ def copy_bytes(source_fd, dest_fd):
684
860
  if written <= 0:
685
861
  raise OSError(errno.EIO, "short write")
686
862
  view = view[written:]
687
-
688
863
  def write_all(fd, data):
689
864
  view = memoryview(data)
690
865
  while view:
@@ -692,11 +867,9 @@ def write_all(fd, data):
692
867
  if written <= 0:
693
868
  raise OSError(errno.EIO, "short write")
694
869
  view = view[written:]
695
-
696
870
  def link_unsupported(exc):
697
871
  unsupported = (errno.EPERM, errno.EOPNOTSUPP, getattr(errno, "ENOTSUP", errno.EOPNOTSUPP))
698
872
  return getattr(exc, "errno", None) in unsupported
699
-
700
873
  def link_no_replace(name, new_name, source_fd, target_fd):
701
874
  linked = False
702
875
  try:
@@ -711,11 +884,9 @@ def link_no_replace(name, new_name, source_fd, target_fd):
711
884
  os.fsync(source_fd)
712
885
  if source_fd != target_fd:
713
886
  os.fsync(target_fd)
714
-
715
887
  def copy_file_no_replace(source_parent_fd, source_name, target_parent_fd, basename, mode, expected=None, unlink_source=False):
716
888
  source_fd = os.open(source_name, READ_FLAGS, dir_fd=source_parent_fd)
717
- dest_fd = None
718
- success = False
889
+ dest_fd = None; success = False; dest_stat = None
719
890
  try:
720
891
  if expected is not None:
721
892
  source_stat = os.fstat(source_fd)
@@ -724,6 +895,7 @@ def copy_file_no_replace(source_parent_fd, source_name, target_parent_fd, basena
724
895
  dest_fd = os.open(basename, WRITE_FLAGS, mode, dir_fd=target_parent_fd)
725
896
  copy_bytes(source_fd, dest_fd)
726
897
  os.fsync(dest_fd)
898
+ dest_stat = os.fstat(dest_fd)
727
899
  success = True
728
900
  finally:
729
901
  os.close(source_fd)
@@ -739,19 +911,40 @@ def copy_file_no_replace(source_parent_fd, source_name, target_parent_fd, basena
739
911
  try: os.unlink(basename, dir_fd=target_parent_fd)
740
912
  except FileNotFoundError: pass
741
913
  raise
742
-
743
- def commit_temp_file(parent_fd, temp_name, basename, overwrite, mode):
914
+ return dest_stat
915
+ def same_identity(left, right):
916
+ return left.st_dev == right.st_dev and left.st_ino == right.st_ino
917
+ def verify_temp_name(parent_fd, temp_name, expected_stat):
918
+ current_stat = os.lstat(temp_name, dir_fd=parent_fd)
919
+ if stat.S_ISLNK(current_stat.st_mode) or not same_identity(current_stat, expected_stat):
920
+ raise RuntimeError("fs-safe-temp-mismatch")
921
+ def verify_committed_temp(parent_fd, basename, expected_stat):
922
+ final_stat = os.lstat(basename, dir_fd=parent_fd)
923
+ if not stat.S_ISLNK(final_stat.st_mode) and same_identity(final_stat, expected_stat):
924
+ return final_stat
925
+ try: os.unlink(basename, dir_fd=parent_fd)
926
+ except FileNotFoundError: pass
927
+ raise RuntimeError("fs-safe-temp-mismatch")
928
+ def commit_temp_file(parent_fd, temp_name, basename, overwrite, mode, expected_stat):
929
+ verify_temp_name(parent_fd, temp_name, expected_stat)
744
930
  if overwrite:
745
931
  os.replace(temp_name, basename, src_dir_fd=parent_fd, dst_dir_fd=parent_fd)
932
+ return verify_committed_temp(parent_fd, basename, expected_stat)
746
933
  else:
747
934
  try:
748
935
  os.link(temp_name, basename, src_dir_fd=parent_fd, dst_dir_fd=parent_fd, follow_symlinks=False)
936
+ final_stat = verify_committed_temp(parent_fd, basename, expected_stat)
749
937
  os.unlink(temp_name, dir_fd=parent_fd)
938
+ return final_stat
750
939
  except OSError as exc:
751
940
  if not link_unsupported(exc):
752
941
  raise
753
- copy_file_no_replace(parent_fd, temp_name, parent_fd, basename, mode, unlink_source=True)
754
-
942
+ return copy_file_no_replace(parent_fd, temp_name, parent_fd, basename, mode, expected_stat, True)
943
+ def assert_expected_root(root_fd, payload):
944
+ if "rootDev" in payload or "rootIno" in payload:
945
+ root_stat = os.fstat(root_fd)
946
+ if root_stat.st_dev != int(payload["rootDev"]) or root_stat.st_ino != int(payload["rootIno"]):
947
+ raise RuntimeError("fs-safe-root-mismatch")
755
948
  def stat_path(root_fd, payload):
756
949
  relative = payload.get("relativePath", "")
757
950
  segments = split_relative(relative)
@@ -765,7 +958,6 @@ def stat_path(root_fd, payload):
765
958
  return encode_stat(st)
766
959
  finally:
767
960
  os.close(parent_fd)
768
-
769
961
  def readdir_path(root_fd, payload):
770
962
  dir_fd = walk_dir(root_fd, split_relative(payload.get("relativePath", "")))
771
963
  try:
@@ -781,12 +973,9 @@ def readdir_path(root_fd, payload):
781
973
  return entries
782
974
  finally:
783
975
  os.close(dir_fd)
784
-
785
976
  def mkdirp_path(root_fd, payload):
786
977
  dir_fd = walk_dir(root_fd, split_relative(payload.get("relativePath", "")), mkdir_enabled=True)
787
- os.close(dir_fd)
788
- return None
789
-
978
+ os.close(dir_fd); return None
790
979
  def remove_tree(parent_fd, basename):
791
980
  st = os.lstat(basename, dir_fd=parent_fd)
792
981
  if stat.S_ISDIR(st.st_mode) and not stat.S_ISLNK(st.st_mode):
@@ -799,7 +988,6 @@ def remove_tree(parent_fd, basename):
799
988
  os.rmdir(basename, dir_fd=parent_fd)
800
989
  else:
801
990
  os.unlink(basename, dir_fd=parent_fd)
802
-
803
991
  def remove_path(root_fd, payload):
804
992
  parent_fd, basename = parent_and_basename(root_fd, payload.get("relativePath", ""))
805
993
  try:
@@ -882,14 +1070,15 @@ def write_path(root_fd, payload):
882
1070
  except FileNotFoundError:
883
1071
  pass
884
1072
  temp_name, temp_fd = create_temp_file(parent_fd, basename, mode)
1073
+ os.fchmod(temp_fd, mode)
885
1074
  write_all(temp_fd, data)
886
1075
  os.fsync(temp_fd)
1076
+ temp_stat = os.fstat(temp_fd)
887
1077
  os.close(temp_fd)
888
1078
  temp_fd = None
889
- commit_temp_file(parent_fd, temp_name, basename, overwrite, mode)
1079
+ result_stat = commit_temp_file(parent_fd, temp_name, basename, overwrite, mode, temp_stat)
890
1080
  temp_name = None
891
1081
  os.fsync(parent_fd)
892
- result_stat = os.stat(basename, dir_fd=parent_fd, follow_symlinks=False)
893
1082
  return {"dev": result_stat.st_dev, "ino": result_stat.st_ino}
894
1083
  finally:
895
1084
  if temp_fd is not None:
@@ -920,6 +1109,7 @@ def copy_path(root_fd, payload):
920
1109
  raise RuntimeError("fs-safe-too-large:%d:%d" % (max_bytes, source_stat.st_size))
921
1110
  parent_fd = walk_dir(root_fd, split_relative(payload.get("relativeParentPath", "")), bool(payload.get("mkdir", True)))
922
1111
  temp_name, temp_fd = create_temp_file(parent_fd, basename, mode)
1112
+ os.fchmod(temp_fd, mode)
923
1113
  written_bytes = 0
924
1114
  while True:
925
1115
  chunk = os.read(source_fd, 65536)
@@ -935,12 +1125,12 @@ def copy_path(root_fd, payload):
935
1125
  raise OSError(errno.EIO, "short write")
936
1126
  view = view[written:]
937
1127
  os.fsync(temp_fd)
1128
+ temp_stat = os.fstat(temp_fd)
938
1129
  os.close(temp_fd)
939
1130
  temp_fd = None
940
- commit_temp_file(parent_fd, temp_name, basename, overwrite, mode)
1131
+ result_stat = commit_temp_file(parent_fd, temp_name, basename, overwrite, mode, temp_stat)
941
1132
  temp_name = None
942
1133
  os.fsync(parent_fd)
943
- result_stat = os.stat(basename, dir_fd=parent_fd, follow_symlinks=False)
944
1134
  return {"dev": result_stat.st_dev, "ino": result_stat.st_ino}
945
1135
  finally:
946
1136
  os.close(source_fd)
@@ -957,6 +1147,7 @@ def copy_path(root_fd, payload):
957
1147
  def run_operation(operation, root_path, payload):
958
1148
  root_fd = open_dir(root_path)
959
1149
  try:
1150
+ assert_expected_root(root_fd, payload)
960
1151
  if operation == "stat":
961
1152
  return stat_path(root_fd, payload)
962
1153
  if operation == "readdir":
@@ -1061,6 +1252,12 @@ function mapWorkerError(response) {
1061
1252
  if (message.includes("fs-safe-source-mismatch")) {
1062
1253
  return new errors_FsSafeError("path-mismatch", "source path changed during copy");
1063
1254
  }
1255
+ if (message.includes("fs-safe-temp-mismatch")) {
1256
+ return new errors_FsSafeError("path-mismatch", "temp path changed during write");
1257
+ }
1258
+ if (message.includes("fs-safe-root-mismatch")) {
1259
+ return new errors_FsSafeError("path-mismatch", "root path changed during operation");
1260
+ }
1064
1261
  if (message.includes("fs-safe-directory-noreplace-unsupported")) {
1065
1262
  return new errors_FsSafeError("invalid-path", "directory moves require overwrite: true");
1066
1263
  }
@@ -1241,9 +1438,7 @@ function validatePinnedOperationPayload(payload) {
1241
1438
  }
1242
1439
  }
1243
1440
  function isPinnedHelperUnavailable(error) {
1244
- return error instanceof Error &&
1245
- "code" in error &&
1246
- error.code === "helper-unavailable";
1441
+ return error instanceof Error && "code" in error && error.code === "helper-unavailable";
1247
1442
  }
1248
1443
  function validatePinnedRelativePath(relativePath) {
1249
1444
  if (relativePath.length === 0 || relativePath === ".") {
@@ -1341,29 +1536,21 @@ function assertWithinMaxBytes(bytes, maxBytes) {
1341
1536
  throw new errors_FsSafeError("too-large", `file exceeds limit of ${maxBytes} bytes (got at least ${bytes})`);
1342
1537
  }
1343
1538
  }
1344
- function pinned_write_createMaxBytesTransform(maxBytes) {
1345
- if (maxBytes === undefined) {
1346
- return undefined;
1347
- }
1539
+ async function writeStreamToHandle(stream, handle, maxBytes) {
1348
1540
  let bytes = 0;
1349
- return new external_node_stream_namespaceObject.Transform({
1350
- transform(chunk, _encoding, callback) {
1351
- bytes += chunk.byteLength;
1352
- if (bytes > maxBytes) {
1353
- callback(new errors_FsSafeError("too-large", `file exceeds limit of ${maxBytes} bytes (got at least ${bytes})`));
1354
- return;
1541
+ for await (const chunk of stream) {
1542
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
1543
+ bytes += buffer.byteLength;
1544
+ assertWithinMaxBytes(bytes, maxBytes);
1545
+ let offset = 0;
1546
+ while (offset < buffer.byteLength) {
1547
+ const { bytesWritten } = await handle.write(buffer, offset, buffer.byteLength - offset);
1548
+ if (bytesWritten <= 0) {
1549
+ throw new errors_FsSafeError("helper-failed", "fallback stream write made no progress");
1355
1550
  }
1356
- callback(null, chunk);
1357
- },
1358
- });
1359
- }
1360
- async function pipelineWithMaxBytes(stream, destination, maxBytes) {
1361
- const limiter = pinned_write_createMaxBytesTransform(maxBytes);
1362
- if (limiter) {
1363
- await (0,external_node_stream_promises_namespaceObject.pipeline)(stream, limiter, destination);
1364
- return;
1551
+ offset += bytesWritten;
1552
+ }
1365
1553
  }
1366
- await (0,external_node_stream_promises_namespaceObject.pipeline)(stream, destination);
1367
1554
  }
1368
1555
  async function inputToBase64(input, maxBytes) {
1369
1556
  if (input.kind === "buffer") {
@@ -1409,6 +1596,7 @@ async function runPinnedWriteHelper(params) {
1409
1596
  mode: params.mode || 0o600,
1410
1597
  overwrite: params.overwrite !== false,
1411
1598
  relativeParentPath: params.relativeParentPath,
1599
+ ...(params.rootIdentity ? { rootDev: params.rootIdentity.dev, rootIno: params.rootIdentity.ino } : {}),
1412
1600
  };
1413
1601
  try {
1414
1602
  return await runPinnedPythonOperation({
@@ -1439,6 +1627,7 @@ async function runPinnedCopyHelper(params) {
1439
1627
  mode: params.mode || 0o600,
1440
1628
  overwrite: params.overwrite !== false,
1441
1629
  relativeParentPath: params.relativeParentPath,
1630
+ ...(params.rootIdentity ? { rootDev: params.rootIdentity.dev, rootIno: params.rootIdentity.ino } : {}),
1442
1631
  sourceDev: params.sourceIdentity.dev,
1443
1632
  sourceIno: params.sourceIdentity.ino,
1444
1633
  sourcePath: params.sourcePath,
@@ -1449,12 +1638,12 @@ async function runPinnedWriteFallback(params) {
1449
1638
  const parentPath = params.relativeParentPath
1450
1639
  ? external_node_path_namespaceObject.join(params.rootPath, ...params.relativeParentPath.split("/"))
1451
1640
  : params.rootPath;
1452
- const parentGuard = await directory_guard_createNearestExistingDirectoryGuard(params.rootPath, parentPath);
1453
1641
  if (params.mkdir) {
1454
- await withAsyncDirectoryGuards([parentGuard], async () => {
1455
- await promises_namespaceObject.mkdir(parentPath, { recursive: true });
1456
- });
1642
+ await mkdirPathComponentsWithGuards({ rootReal: params.rootPath, targetPath: parentPath });
1457
1643
  }
1644
+ const parentGuard = params.mkdir
1645
+ ? await directory_guard_createAsyncDirectoryGuard(parentPath)
1646
+ : await directory_guard_createNearestExistingDirectoryGuard(params.rootPath, parentPath);
1458
1647
  const targetPath = external_node_path_namespaceObject.join(parentPath, params.basename);
1459
1648
  if (params.overwrite === false) {
1460
1649
  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), {
@@ -1476,7 +1665,7 @@ async function runPinnedWriteFallback(params) {
1476
1665
  }
1477
1666
  }
1478
1667
  else {
1479
- await pipelineWithMaxBytes(params.input.stream, handle.createWriteStream(), params.maxBytes);
1668
+ await writeStreamToHandle(params.input.stream, handle, params.maxBytes);
1480
1669
  }
1481
1670
  const stat = await handle.stat();
1482
1671
  created = false;
@@ -1497,7 +1686,9 @@ async function runPinnedWriteFallback(params) {
1497
1686
  ? external_node_fs_namespaceObject.constants.O_NOFOLLOW
1498
1687
  : 0);
1499
1688
  let handle;
1500
- let handleClosedByStream = false;
1689
+ let tempStat;
1690
+ let targetStat;
1691
+ let renamed = false;
1501
1692
  try {
1502
1693
  handle = await promises_namespaceObject.open(tempPath, tempFlags, params.mode);
1503
1694
  if (params.input.kind === "buffer") {
@@ -1510,29 +1701,36 @@ async function runPinnedWriteFallback(params) {
1510
1701
  }
1511
1702
  }
1512
1703
  else {
1513
- const writable = handle.createWriteStream();
1514
- writable.once("close", () => {
1515
- handleClosedByStream = true;
1516
- });
1517
- await pipelineWithMaxBytes(params.input.stream, writable, params.maxBytes);
1704
+ await writeStreamToHandle(params.input.stream, handle, params.maxBytes);
1518
1705
  }
1519
- if (!handleClosedByStream) {
1520
- await handle.close().catch(() => undefined);
1521
- handle = undefined;
1706
+ tempStat = await handle.stat();
1707
+ const tempPathStat = await promises_namespaceObject.lstat(tempPath);
1708
+ if (tempPathStat.isSymbolicLink() || !file_identity_sameFileIdentity(tempPathStat, tempStat)) {
1709
+ throw new errors_FsSafeError("path-mismatch", "fallback temp path changed during write");
1522
1710
  }
1711
+ const expectedTempStat = tempStat;
1712
+ await handle.close().catch(() => undefined);
1713
+ handle = undefined;
1523
1714
  await withAsyncDirectoryGuards([parentGuard], async () => {
1524
1715
  await promises_namespaceObject.rename(tempPath, targetPath);
1716
+ renamed = true;
1717
+ targetStat = await promises_namespaceObject.lstat(targetPath);
1718
+ if (targetStat.isSymbolicLink() || !file_identity_sameFileIdentity(targetStat, expectedTempStat)) {
1719
+ throw new errors_FsSafeError("path-mismatch", "fallback target changed during write");
1720
+ }
1525
1721
  });
1526
1722
  }
1527
1723
  catch (error) {
1528
- if (handle && !handleClosedByStream) {
1529
- await handle.close().catch(() => undefined);
1724
+ await handle?.close().catch(() => undefined);
1725
+ if (!renamed) {
1726
+ await promises_namespaceObject.rm(tempPath, { force: true }).catch(() => undefined);
1530
1727
  }
1531
- await promises_namespaceObject.rm(tempPath, { force: true }).catch(() => undefined);
1532
1728
  throw error;
1533
1729
  }
1534
- const stat = await promises_namespaceObject.stat(targetPath);
1535
- return { dev: stat.dev, ino: stat.ino };
1730
+ if (!targetStat) {
1731
+ throw new errors_FsSafeError("path-mismatch", "fallback target was not verified");
1732
+ }
1733
+ return { dev: targetStat.dev, ino: targetStat.ino };
1536
1734
  }
1537
1735
 
1538
1736
  ;// CONCATENATED MODULE: external "node:os"
@@ -1558,7 +1756,7 @@ async function resolveRootPath(params) {
1558
1756
  const absolutePath = external_node_path_namespaceObject.resolve(params.absolutePath);
1559
1757
  const rootCanonicalPath = params.rootCanonicalPath
1560
1758
  ? external_node_path_namespaceObject.resolve(params.rootCanonicalPath)
1561
- : await resolvePathViaExistingAncestor(rootPath);
1759
+ : await root_path_resolvePathViaExistingAncestor(rootPath);
1562
1760
  const context = createBoundaryResolutionContext({
1563
1761
  resolveParams: params,
1564
1762
  rootPath,
@@ -1927,7 +2125,7 @@ async function resolveOutsideLexicalCanonicalPathAsync(params) {
1927
2125
  if (path_isPathInside(params.rootPath, params.absolutePath)) {
1928
2126
  return undefined;
1929
2127
  }
1930
- return await resolvePathViaExistingAncestor(params.absolutePath);
2128
+ return await root_path_resolvePathViaExistingAncestor(params.absolutePath);
1931
2129
  }
1932
2130
  function resolveOutsideLexicalCanonicalPathSync(params) {
1933
2131
  if (isPathInside(params.rootPath, params.absolutePath)) {
@@ -1974,11 +2172,11 @@ function buildResolvedRootPath(params) {
1974
2172
  kind: params.kind.kind,
1975
2173
  };
1976
2174
  }
1977
- async function resolvePathViaExistingAncestor(targetPath) {
2175
+ async function root_path_resolvePathViaExistingAncestor(targetPath) {
1978
2176
  const normalized = external_node_path_namespaceObject.resolve(targetPath);
1979
2177
  let cursor = normalized;
1980
2178
  const missingSuffix = [];
1981
- while (!isFilesystemRoot(cursor) && !(await pathExists(cursor))) {
2179
+ while (!isFilesystemRoot(cursor) && !(await root_path_pathExists(cursor))) {
1982
2180
  missingSuffix.unshift(external_node_path_namespaceObject.basename(cursor));
1983
2181
  const parent = external_node_path_namespaceObject.dirname(cursor);
1984
2182
  if (parent === cursor) {
@@ -1986,7 +2184,7 @@ async function resolvePathViaExistingAncestor(targetPath) {
1986
2184
  }
1987
2185
  cursor = parent;
1988
2186
  }
1989
- if (!(await pathExists(cursor))) {
2187
+ if (!(await root_path_pathExists(cursor))) {
1990
2188
  return normalized;
1991
2189
  }
1992
2190
  try {
@@ -2071,7 +2269,7 @@ function relativeInsideRoot(rootPath, targetPath) {
2071
2269
  if (!relative || relative === ".") {
2072
2270
  return "";
2073
2271
  }
2074
- if (relative.startsWith("..") || external_node_path_namespaceObject.isAbsolute(relative)) {
2272
+ if (isPathRelativeEscape(relative)) {
2075
2273
  return "";
2076
2274
  }
2077
2275
  return relative;
@@ -2098,7 +2296,7 @@ function shortPath(value) {
2098
2296
  function isFilesystemRoot(candidate) {
2099
2297
  return external_node_path_namespaceObject.parse(candidate).root === candidate;
2100
2298
  }
2101
- async function pathExists(targetPath) {
2299
+ async function root_path_pathExists(targetPath) {
2102
2300
  try {
2103
2301
  await promises_namespaceObject.lstat(targetPath);
2104
2302
  return true;
@@ -2120,7 +2318,7 @@ async function resolveSymlinkHopPath(symlinkPath) {
2120
2318
  }
2121
2319
  const linkTarget = await promises_namespaceObject.readlink(symlinkPath);
2122
2320
  const linkAbsolute = external_node_path_namespaceObject.resolve(external_node_path_namespaceObject.dirname(symlinkPath), linkTarget);
2123
- return resolvePathViaExistingAncestor(linkAbsolute);
2321
+ return root_path_resolvePathViaExistingAncestor(linkAbsolute);
2124
2322
  }
2125
2323
  }
2126
2324
  function resolveSymlinkHopPathSync(symlinkPath) {
@@ -2189,6 +2387,23 @@ function path_policy_shortPath(value) {
2189
2387
  return value;
2190
2388
  }
2191
2389
 
2390
+ ;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/read-opened-file.js
2391
+
2392
+ async function read_opened_file_readOpenedFileSafely(params) {
2393
+ if (params.maxBytes !== undefined && params.opened.stat.size > params.maxBytes) {
2394
+ throw new errors_FsSafeError("too-large", `file exceeds limit of ${params.maxBytes} bytes (got ${params.opened.stat.size})`);
2395
+ }
2396
+ const buffer = await params.opened.handle.readFile();
2397
+ if (params.maxBytes !== undefined && buffer.byteLength > params.maxBytes) {
2398
+ throw new errors_FsSafeError("too-large", `file exceeds limit of ${params.maxBytes} bytes (got ${buffer.byteLength})`);
2399
+ }
2400
+ return {
2401
+ buffer,
2402
+ realPath: params.opened.realPath,
2403
+ stat: params.opened.stat,
2404
+ };
2405
+ }
2406
+
2192
2407
  ;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/path-stat.js
2193
2408
  function pathStatFromStats(stat) {
2194
2409
  return {
@@ -2516,6 +2731,10 @@ async function serializePathWrite(key, run) {
2516
2731
 
2517
2732
 
2518
2733
 
2734
+
2735
+
2736
+
2737
+
2519
2738
 
2520
2739
 
2521
2740
 
@@ -2714,6 +2933,7 @@ class RootHandle {
2714
2933
  mkdir: this.defaults.mkdir,
2715
2934
  mode: this.defaults.mode,
2716
2935
  ...options,
2936
+ denyMutations: mergeDenyMutationPolicies(this.defaults.denyMutations, options.denyMutations),
2717
2937
  append: writeMode === "append",
2718
2938
  truncateExisting: writeMode === "replace",
2719
2939
  });
@@ -2725,18 +2945,29 @@ class RootHandle {
2725
2945
  mkdir: this.defaults.mkdir,
2726
2946
  mode: this.defaults.mode,
2727
2947
  ...options,
2948
+ denyMutations: mergeDenyMutationPolicies(this.defaults.denyMutations, options.denyMutations),
2728
2949
  });
2729
2950
  }
2730
- async remove(relativePath) {
2951
+ async remove(relativePath, options = {}) {
2731
2952
  assertValidRootRelativePath(relativePath);
2732
- await removePathInRoot(this.context, relativePath);
2953
+ await removePathInRoot(this.context, {
2954
+ relativePath,
2955
+ denyMutations: mergeDenyMutationPolicies(this.defaults.denyMutations, options.denyMutations),
2956
+ });
2733
2957
  }
2734
- async mkdir(relativePath) {
2958
+ async mkdir(relativePath, options = {}) {
2735
2959
  assertValidRootRelativePath(relativePath);
2736
- await mkdirPathInRoot(this.context, { relativePath });
2960
+ await mkdirPathInRoot(this.context, {
2961
+ relativePath,
2962
+ denyMutations: mergeDenyMutationPolicies(this.defaults.denyMutations, options.denyMutations),
2963
+ });
2737
2964
  }
2738
- async ensureRoot() {
2739
- await mkdirPathInRoot(this.context, { relativePath: "", allowRoot: true });
2965
+ async ensureRoot(options = {}) {
2966
+ await mkdirPathInRoot(this.context, {
2967
+ relativePath: "",
2968
+ allowRoot: true,
2969
+ denyMutations: mergeDenyMutationPolicies(this.defaults.denyMutations, options.denyMutations),
2970
+ });
2740
2971
  }
2741
2972
  async write(relativePath, data, options = {}) {
2742
2973
  await writeFileInRoot(this.context, {
@@ -2745,6 +2976,7 @@ class RootHandle {
2745
2976
  mkdir: this.defaults.mkdir,
2746
2977
  mode: this.defaults.mode,
2747
2978
  ...options,
2979
+ denyMutations: mergeDenyMutationPolicies(this.defaults.denyMutations, options.denyMutations),
2748
2980
  });
2749
2981
  }
2750
2982
  async create(relativePath, data, options = {}) {
@@ -2754,6 +2986,7 @@ class RootHandle {
2754
2986
  mkdir: this.defaults.mkdir,
2755
2987
  mode: this.defaults.mode,
2756
2988
  ...options,
2989
+ denyMutations: mergeDenyMutationPolicies(this.defaults.denyMutations, options.denyMutations),
2757
2990
  overwrite: false,
2758
2991
  });
2759
2992
  }
@@ -2776,6 +3009,7 @@ class RootHandle {
2776
3009
  mkdir: this.defaults.mkdir,
2777
3010
  mode: this.defaults.mode,
2778
3011
  ...options,
3012
+ denyMutations: mergeDenyMutationPolicies(this.defaults.denyMutations, options.denyMutations),
2779
3013
  });
2780
3014
  }
2781
3015
  async exists(relativePath) {
@@ -2819,6 +3053,12 @@ class RootHandle {
2819
3053
  async move(fromRelative, toRelative, options = {}) {
2820
3054
  assertValidRootRelativePath(fromRelative);
2821
3055
  assertValidRootRelativePath(toRelative);
3056
+ const denyMutations = mergeDenyMutationPolicies(this.defaults.denyMutations, options.denyMutations);
3057
+ await assertMoveMutationAllowed(this.context, {
3058
+ fromRelative,
3059
+ toRelative,
3060
+ denyMutations,
3061
+ });
2822
3062
  try {
2823
3063
  await runPinnedHelper("rename", this.rootReal, {
2824
3064
  from: fromRelative,
@@ -2830,6 +3070,7 @@ class RootHandle {
2830
3070
  if (canFallbackFromPythonError(error)) {
2831
3071
  await movePathFallback(this.context, {
2832
3072
  fromRelative,
3073
+ denyMutations,
2833
3074
  overwrite: options.overwrite ?? false,
2834
3075
  toRelative,
2835
3076
  });
@@ -2878,7 +3119,7 @@ async function openFileInRoot(root, params) {
2878
3119
  async function readFileInRoot(root, params) {
2879
3120
  const opened = await openFileInRoot(root, params);
2880
3121
  try {
2881
- return await readOpenedFileSafely({ opened, maxBytes: params.maxBytes });
3122
+ return await read_opened_file_readOpenedFileSafely({ opened, maxBytes: params.maxBytes });
2882
3123
  }
2883
3124
  finally {
2884
3125
  await opened.handle.close().catch(() => { });
@@ -2911,20 +3152,6 @@ async function openLocalFileSafely(params) {
2911
3152
  assertNoNulPathInput(params.filePath, "file path contains a NUL byte");
2912
3153
  return await openVerifiedLocalFile(params.filePath);
2913
3154
  }
2914
- async function readOpenedFileSafely(params) {
2915
- if (params.maxBytes !== undefined && params.opened.stat.size > params.maxBytes) {
2916
- throw new errors_FsSafeError("too-large", `file exceeds limit of ${params.maxBytes} bytes (got ${params.opened.stat.size})`);
2917
- }
2918
- const buffer = await params.opened.handle.readFile();
2919
- if (params.maxBytes !== undefined && buffer.byteLength > params.maxBytes) {
2920
- throw new errors_FsSafeError("too-large", `file exceeds limit of ${params.maxBytes} bytes (got ${buffer.byteLength})`);
2921
- }
2922
- return {
2923
- buffer,
2924
- realPath: params.opened.realPath,
2925
- stat: params.opened.stat,
2926
- };
2927
- }
2928
3155
  function emitWriteBoundaryWarning(reason) {
2929
3156
  logWarn(`security: fs-safe write boundary warning (${reason})`);
2930
3157
  }
@@ -2965,82 +3192,9 @@ async function verifyAtomicWriteResult(params) {
2965
3192
  await opened.handle.close().catch(() => { });
2966
3193
  }
2967
3194
  }
2968
- async function resolveOpenedFileRealPathForHandle(handle, ioPath) {
2969
- const handleStat = await handle.stat();
2970
- const fdCandidates = process.platform === "linux"
2971
- ? [`/proc/self/fd/${handle.fd}`, `/dev/fd/${handle.fd}`]
2972
- : process.platform === "win32"
2973
- ? []
2974
- : [`/dev/fd/${handle.fd}`];
2975
- for (const fdPath of fdCandidates) {
2976
- try {
2977
- const fdRealPath = await promises_namespaceObject.realpath(fdPath);
2978
- const fdRealStat = await promises_namespaceObject.stat(fdRealPath);
2979
- if (file_identity_sameFileIdentity(handleStat, fdRealStat)) {
2980
- return fdRealPath;
2981
- }
2982
- }
2983
- catch {
2984
- // try next fd path
2985
- }
2986
- }
2987
- try {
2988
- const ioRealPath = await promises_namespaceObject.realpath(ioPath);
2989
- const ioRealStat = await promises_namespaceObject.stat(ioRealPath);
2990
- if (file_identity_sameFileIdentity(handleStat, ioRealStat)) {
2991
- return ioRealPath;
2992
- }
2993
- }
2994
- catch (err) {
2995
- if (!path_isNotFoundPathError(err)) {
2996
- throw err;
2997
- }
2998
- }
2999
- const parentResolved = await resolveOpenedFileRealPathFromParent(handleStat, ioPath);
3000
- if (parentResolved) {
3001
- return parentResolved;
3002
- }
3003
- throw new errors_FsSafeError("path-mismatch", "unable to resolve opened file path");
3004
- }
3005
- async function resolveOpenedFileRealPathFromParent(handleStat, ioPath) {
3006
- let parentReal;
3007
- try {
3008
- parentReal = await promises_namespaceObject.realpath(external_node_path_namespaceObject.dirname(ioPath));
3009
- }
3010
- catch (err) {
3011
- if (path_isNotFoundPathError(err)) {
3012
- return null;
3013
- }
3014
- throw err;
3015
- }
3016
- let entries;
3017
- try {
3018
- entries = await promises_namespaceObject.readdir(parentReal);
3019
- }
3020
- catch (err) {
3021
- if (path_isNotFoundPathError(err)) {
3022
- return null;
3023
- }
3024
- throw err;
3025
- }
3026
- for (const entry of entries.toSorted()) {
3027
- const candidatePath = external_node_path_namespaceObject.join(parentReal, entry);
3028
- try {
3029
- const candidateStat = await promises_namespaceObject.lstat(candidatePath);
3030
- if (candidateStat.isFile() && file_identity_sameFileIdentity(handleStat, candidateStat)) {
3031
- return await promises_namespaceObject.realpath(candidatePath);
3032
- }
3033
- }
3034
- catch (err) {
3035
- if (!path_isNotFoundPathError(err)) {
3036
- throw err;
3037
- }
3038
- }
3039
- }
3040
- return null;
3041
- }
3042
3195
  async function openWritableFileInRoot(root, params) {
3043
3196
  const { rootReal, rootWithSep, resolved } = await resolvePathInRoot(root, params.relativePath);
3197
+ await assertMutationNotDenied(resolved, params.denyMutations);
3044
3198
  try {
3045
3199
  await assertNoPathAliasEscape({
3046
3200
  absolutePath: resolved,
@@ -3167,6 +3321,7 @@ async function appendFileInRoot(root, params) {
3167
3321
  relativePath: params.relativePath,
3168
3322
  mkdir: params.mkdir,
3169
3323
  mode: params.mode,
3324
+ denyMutations: params.denyMutations,
3170
3325
  truncateExisting: false,
3171
3326
  append: true,
3172
3327
  });
@@ -3194,8 +3349,8 @@ async function appendFileInRoot(root, params) {
3194
3349
  await target.handle.close().catch(() => { });
3195
3350
  }
3196
3351
  }
3197
- async function removePathInRoot(root, relativePath) {
3198
- const resolved = await resolvePinnedRemovePathInRoot(root, relativePath);
3352
+ async function removePathInRoot(root, params) {
3353
+ const resolved = await resolvePinnedRemovePathInRoot(root, params.relativePath, params.denyMutations);
3199
3354
  if (process.platform === "win32") {
3200
3355
  await removePathFallback(resolved);
3201
3356
  return;
@@ -3243,7 +3398,7 @@ async function writeFileInRoot(root, params) {
3243
3398
  });
3244
3399
  return;
3245
3400
  }
3246
- const pinned = await resolvePinnedWriteTargetInRoot(root, params.relativePath, params.mode);
3401
+ const pinned = await resolvePinnedWriteTargetInRoot(root, params.relativePath, params.mode, params.denyMutations);
3247
3402
  await serializePathWrite(pinned.targetPath, async () => {
3248
3403
  let identity;
3249
3404
  try {
@@ -3299,7 +3454,7 @@ async function copyFileInRoot(root, params) {
3299
3454
  });
3300
3455
  return;
3301
3456
  }
3302
- const pinned = await resolvePinnedWriteTargetInRoot(root, params.relativePath, params.mode);
3457
+ const pinned = await resolvePinnedWriteTargetInRoot(root, params.relativePath, params.mode, params.denyMutations);
3303
3458
  await serializePathWrite(pinned.targetPath, async () => {
3304
3459
  let identity;
3305
3460
  try {
@@ -3343,8 +3498,9 @@ async function copyFileInRoot(root, params) {
3343
3498
  await source.handle.close().catch(() => { });
3344
3499
  }
3345
3500
  }
3346
- async function resolvePinnedWriteTargetInRoot(root, relativePath, requestedMode) {
3501
+ async function resolvePinnedWriteTargetInRoot(root, relativePath, requestedMode, denyMutations) {
3347
3502
  const { rootReal, rootWithSep, resolved } = await resolvePathInRoot(root, relativePath);
3503
+ await assertMutationNotDenied(resolved, denyMutations);
3348
3504
  try {
3349
3505
  await assertNoPathAliasEscape({
3350
3506
  absolutePath: resolved,
@@ -3401,12 +3557,16 @@ async function resolvePinnedWriteTargetInRoot(root, relativePath, requestedMode)
3401
3557
  async function resolvePinnedPathInRoot(root, params) {
3402
3558
  return await resolvePinnedOperationPathInRoot(root, {
3403
3559
  allowRoot: params.allowRoot,
3560
+ denyMutations: params.denyMutations,
3561
+ protectDenyMutationAncestors: false,
3404
3562
  relativePath: params.relativePath,
3405
3563
  policy: PATH_ALIAS_POLICIES.strict,
3406
3564
  });
3407
3565
  }
3408
- async function resolvePinnedRemovePathInRoot(root, relativePath) {
3566
+ async function resolvePinnedRemovePathInRoot(root, relativePath, denyMutations) {
3409
3567
  return await resolvePinnedOperationPathInRoot(root, {
3568
+ denyMutations,
3569
+ protectDenyMutationAncestors: true,
3410
3570
  relativePath,
3411
3571
  policy: PATH_ALIAS_POLICIES.unlinkTarget,
3412
3572
  });
@@ -3418,6 +3578,7 @@ async function resolvePinnedOperationPathInRoot(root, params) {
3418
3578
  });
3419
3579
  const relativeResolved = external_node_path_namespaceObject.relative(resolved.rootReal, resolved.canonicalPath);
3420
3580
  if ((relativeResolved === "" || relativeResolved === ".") && params.allowRoot === true) {
3581
+ await assertMutationNotDenied(resolved.canonicalPath, params.denyMutations);
3421
3582
  return { rootReal: resolved.rootReal, resolved: resolved.canonicalPath, relativePosix: "" };
3422
3583
  }
3423
3584
  const firstSegment = relativeResolved.split(external_node_path_namespaceObject.sep)[0];
@@ -3431,6 +3592,9 @@ async function resolvePinnedOperationPathInRoot(root, params) {
3431
3592
  if (!path_isPathInside(resolved.rootWithSep, resolved.canonicalPath)) {
3432
3593
  throw new errors_FsSafeError("outside-workspace", "file is outside workspace root");
3433
3594
  }
3595
+ await assertMutationNotDenied(resolved.canonicalPath, params.denyMutations, {
3596
+ protectAncestors: params.protectDenyMutationAncestors,
3597
+ });
3434
3598
  return { rootReal: resolved.rootReal, resolved: resolved.canonicalPath, relativePosix };
3435
3599
  }
3436
3600
  async function resolvePinnedRootPathInRoot(root, params) {
@@ -3508,13 +3672,21 @@ async function listPathFallback(root, relativePath, withFileTypes) {
3508
3672
  throw error;
3509
3673
  }
3510
3674
  }
3675
+ async function assertMoveMutationAllowed(root, params) {
3676
+ const source = await resolvePathInRoot(root, params.fromRelative);
3677
+ await assertMutationNotDenied(source.resolved, params.denyMutations, { protectAncestors: true });
3678
+ const target = await resolvePathInRoot(root, params.toRelative);
3679
+ await assertMutationNotDenied(target.resolved, params.denyMutations, { protectAncestors: true });
3680
+ }
3511
3681
  async function movePathFallback(root, params) {
3512
3682
  const source = await resolvePathInRoot(root, params.fromRelative);
3683
+ await assertMutationNotDenied(source.resolved, params.denyMutations, { protectAncestors: true });
3513
3684
  await resolvePinnedRootPathInRoot(root, {
3514
3685
  relativePath: params.fromRelative,
3515
3686
  policy: PATH_ALIAS_POLICIES.strict,
3516
3687
  });
3517
3688
  const target = await resolvePathInRoot(root, params.toRelative);
3689
+ await assertMutationNotDenied(target.resolved, params.denyMutations, { protectAncestors: true });
3518
3690
  await resolvePinnedRootPathInRoot(root, {
3519
3691
  relativePath: params.toRelative,
3520
3692
  policy: PATH_ALIAS_POLICIES.unlinkTarget,
@@ -3598,6 +3770,7 @@ async function writeFileFallback(root, params) {
3598
3770
  relativePath: params.relativePath,
3599
3771
  mkdir: params.mkdir,
3600
3772
  mode: params.mode,
3773
+ denyMutations: params.denyMutations,
3601
3774
  truncateExisting: false,
3602
3775
  });
3603
3776
  const destinationPath = target.realPath;
@@ -3643,6 +3816,7 @@ async function writeFileFallback(root, params) {
3643
3816
  }
3644
3817
  async function writeMissingFileFallback(root, params) {
3645
3818
  const { rootReal, resolved } = await resolvePathInRoot(root, params.relativePath);
3819
+ await assertMutationNotDenied(resolved, params.denyMutations);
3646
3820
  try {
3647
3821
  await assertNoPathAliasEscape({
3648
3822
  absolutePath: resolved,
@@ -3716,6 +3890,7 @@ async function copyFileFallback(root, params, source) {
3716
3890
  relativePath: params.relativePath,
3717
3891
  mkdir: params.mkdir,
3718
3892
  mode: params.mode,
3893
+ denyMutations: params.denyMutations,
3719
3894
  truncateExisting: false,
3720
3895
  });
3721
3896
  const destinationPath = target.realPath;
@@ -12347,7 +12522,7 @@ async function emitScriptEndEvent(options, deps) {
12347
12522
  const capturedEnv = captureEnv(options.env);
12348
12523
  const tags = captureTags(options.env);
12349
12524
  const event = {
12350
- v: SCHEMA_VERSION,
12525
+ v: (/* inlined export .SCHEMA_VERSION */1),
12351
12526
  session_id: sessionId,
12352
12527
  event: "script_end",
12353
12528
  command: options.command,
@@ -12376,7 +12551,7 @@ async function emitShimErrorEvent(options, deps) {
12376
12551
  const capturedEnv = captureEnv(options.env);
12377
12552
  const tags = captureTags(options.env);
12378
12553
  const event = {
12379
- v: SCHEMA_VERSION,
12554
+ v: (/* inlined export .SCHEMA_VERSION */1),
12380
12555
  session_id: sessionId,
12381
12556
  event: "shim_error",
12382
12557
  command: options.command,
@@ -12397,7 +12572,7 @@ async function emitPolicyDecisionEvent(options, deps) {
12397
12572
  const capturedEnv = captureEnv(options.env);
12398
12573
  const tags = captureTags(options.env);
12399
12574
  const event = {
12400
- v: SCHEMA_VERSION,
12575
+ v: (/* inlined export .SCHEMA_VERSION */1),
12401
12576
  session_id: sessionId,
12402
12577
  event: "policy_decision",
12403
12578
  command: options.command,
@@ -12420,7 +12595,7 @@ async function emitToolUseEvent(options, deps) {
12420
12595
  const capturedEnv = captureEnv(options.env);
12421
12596
  const tags = captureTags(options.env);
12422
12597
  const event = {
12423
- v: SCHEMA_VERSION,
12598
+ v: (/* inlined export .SCHEMA_VERSION */1),
12424
12599
  session_id: sessionId,
12425
12600
  event: "tool_use",
12426
12601
  tool_name: options.tool_name,
@@ -14877,4 +15052,4 @@ if (installedChunkData !== 0) { // 0 means "already installed".'
14877
15052
  // module factories are used so entry inlining is disabled
14878
15053
  // startup
14879
15054
  // Load entry module and return exports
14880
- var __webpack_exports__ = __webpack_require__(145);
15055
+ var __webpack_exports__ = __webpack_require__(341);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lousy-agents/agent-shell",
3
- "version": "5.14.9",
3
+ "version": "5.15.0",
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.5",
39
+ "@openclaw/fs-safe": "0.3.0",
40
40
  "zod": "4.4.3"
41
41
  },
42
42
  "peerDependencies": {