@lousy-agents/agent-shell 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.
- package/dist/index.js +106 -87
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -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
|
-
|
|
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 (
|
|
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)),
|
|
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,12 @@ 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
|
-
|
|
676
667
|
def copy_bytes(source_fd, dest_fd):
|
|
677
668
|
while True:
|
|
678
669
|
chunk = os.read(source_fd, 65536)
|
|
@@ -684,7 +675,6 @@ def copy_bytes(source_fd, dest_fd):
|
|
|
684
675
|
if written <= 0:
|
|
685
676
|
raise OSError(errno.EIO, "short write")
|
|
686
677
|
view = view[written:]
|
|
687
|
-
|
|
688
678
|
def write_all(fd, data):
|
|
689
679
|
view = memoryview(data)
|
|
690
680
|
while view:
|
|
@@ -692,11 +682,9 @@ def write_all(fd, data):
|
|
|
692
682
|
if written <= 0:
|
|
693
683
|
raise OSError(errno.EIO, "short write")
|
|
694
684
|
view = view[written:]
|
|
695
|
-
|
|
696
685
|
def link_unsupported(exc):
|
|
697
686
|
unsupported = (errno.EPERM, errno.EOPNOTSUPP, getattr(errno, "ENOTSUP", errno.EOPNOTSUPP))
|
|
698
687
|
return getattr(exc, "errno", None) in unsupported
|
|
699
|
-
|
|
700
688
|
def link_no_replace(name, new_name, source_fd, target_fd):
|
|
701
689
|
linked = False
|
|
702
690
|
try:
|
|
@@ -711,11 +699,9 @@ def link_no_replace(name, new_name, source_fd, target_fd):
|
|
|
711
699
|
os.fsync(source_fd)
|
|
712
700
|
if source_fd != target_fd:
|
|
713
701
|
os.fsync(target_fd)
|
|
714
|
-
|
|
715
702
|
def copy_file_no_replace(source_parent_fd, source_name, target_parent_fd, basename, mode, expected=None, unlink_source=False):
|
|
716
703
|
source_fd = os.open(source_name, READ_FLAGS, dir_fd=source_parent_fd)
|
|
717
|
-
dest_fd = None
|
|
718
|
-
success = False
|
|
704
|
+
dest_fd = None; success = False; dest_stat = None
|
|
719
705
|
try:
|
|
720
706
|
if expected is not None:
|
|
721
707
|
source_stat = os.fstat(source_fd)
|
|
@@ -724,6 +710,7 @@ def copy_file_no_replace(source_parent_fd, source_name, target_parent_fd, basena
|
|
|
724
710
|
dest_fd = os.open(basename, WRITE_FLAGS, mode, dir_fd=target_parent_fd)
|
|
725
711
|
copy_bytes(source_fd, dest_fd)
|
|
726
712
|
os.fsync(dest_fd)
|
|
713
|
+
dest_stat = os.fstat(dest_fd)
|
|
727
714
|
success = True
|
|
728
715
|
finally:
|
|
729
716
|
os.close(source_fd)
|
|
@@ -739,19 +726,40 @@ def copy_file_no_replace(source_parent_fd, source_name, target_parent_fd, basena
|
|
|
739
726
|
try: os.unlink(basename, dir_fd=target_parent_fd)
|
|
740
727
|
except FileNotFoundError: pass
|
|
741
728
|
raise
|
|
742
|
-
|
|
743
|
-
def
|
|
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)
|
|
744
745
|
if overwrite:
|
|
745
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)
|
|
746
748
|
else:
|
|
747
749
|
try:
|
|
748
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)
|
|
749
752
|
os.unlink(temp_name, dir_fd=parent_fd)
|
|
753
|
+
return final_stat
|
|
750
754
|
except OSError as exc:
|
|
751
755
|
if not link_unsupported(exc):
|
|
752
756
|
raise
|
|
753
|
-
copy_file_no_replace(parent_fd, temp_name, parent_fd, basename, mode,
|
|
754
|
-
|
|
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")
|
|
755
763
|
def stat_path(root_fd, payload):
|
|
756
764
|
relative = payload.get("relativePath", "")
|
|
757
765
|
segments = split_relative(relative)
|
|
@@ -765,7 +773,6 @@ def stat_path(root_fd, payload):
|
|
|
765
773
|
return encode_stat(st)
|
|
766
774
|
finally:
|
|
767
775
|
os.close(parent_fd)
|
|
768
|
-
|
|
769
776
|
def readdir_path(root_fd, payload):
|
|
770
777
|
dir_fd = walk_dir(root_fd, split_relative(payload.get("relativePath", "")))
|
|
771
778
|
try:
|
|
@@ -781,12 +788,9 @@ def readdir_path(root_fd, payload):
|
|
|
781
788
|
return entries
|
|
782
789
|
finally:
|
|
783
790
|
os.close(dir_fd)
|
|
784
|
-
|
|
785
791
|
def mkdirp_path(root_fd, payload):
|
|
786
792
|
dir_fd = walk_dir(root_fd, split_relative(payload.get("relativePath", "")), mkdir_enabled=True)
|
|
787
|
-
os.close(dir_fd)
|
|
788
|
-
return None
|
|
789
|
-
|
|
793
|
+
os.close(dir_fd); return None
|
|
790
794
|
def remove_tree(parent_fd, basename):
|
|
791
795
|
st = os.lstat(basename, dir_fd=parent_fd)
|
|
792
796
|
if stat.S_ISDIR(st.st_mode) and not stat.S_ISLNK(st.st_mode):
|
|
@@ -799,7 +803,6 @@ def remove_tree(parent_fd, basename):
|
|
|
799
803
|
os.rmdir(basename, dir_fd=parent_fd)
|
|
800
804
|
else:
|
|
801
805
|
os.unlink(basename, dir_fd=parent_fd)
|
|
802
|
-
|
|
803
806
|
def remove_path(root_fd, payload):
|
|
804
807
|
parent_fd, basename = parent_and_basename(root_fd, payload.get("relativePath", ""))
|
|
805
808
|
try:
|
|
@@ -882,14 +885,15 @@ def write_path(root_fd, payload):
|
|
|
882
885
|
except FileNotFoundError:
|
|
883
886
|
pass
|
|
884
887
|
temp_name, temp_fd = create_temp_file(parent_fd, basename, mode)
|
|
888
|
+
os.fchmod(temp_fd, mode)
|
|
885
889
|
write_all(temp_fd, data)
|
|
886
890
|
os.fsync(temp_fd)
|
|
891
|
+
temp_stat = os.fstat(temp_fd)
|
|
887
892
|
os.close(temp_fd)
|
|
888
893
|
temp_fd = None
|
|
889
|
-
commit_temp_file(parent_fd, temp_name, basename, overwrite, mode)
|
|
894
|
+
result_stat = commit_temp_file(parent_fd, temp_name, basename, overwrite, mode, temp_stat)
|
|
890
895
|
temp_name = None
|
|
891
896
|
os.fsync(parent_fd)
|
|
892
|
-
result_stat = os.stat(basename, dir_fd=parent_fd, follow_symlinks=False)
|
|
893
897
|
return {"dev": result_stat.st_dev, "ino": result_stat.st_ino}
|
|
894
898
|
finally:
|
|
895
899
|
if temp_fd is not None:
|
|
@@ -920,6 +924,7 @@ def copy_path(root_fd, payload):
|
|
|
920
924
|
raise RuntimeError("fs-safe-too-large:%d:%d" % (max_bytes, source_stat.st_size))
|
|
921
925
|
parent_fd = walk_dir(root_fd, split_relative(payload.get("relativeParentPath", "")), bool(payload.get("mkdir", True)))
|
|
922
926
|
temp_name, temp_fd = create_temp_file(parent_fd, basename, mode)
|
|
927
|
+
os.fchmod(temp_fd, mode)
|
|
923
928
|
written_bytes = 0
|
|
924
929
|
while True:
|
|
925
930
|
chunk = os.read(source_fd, 65536)
|
|
@@ -935,12 +940,12 @@ def copy_path(root_fd, payload):
|
|
|
935
940
|
raise OSError(errno.EIO, "short write")
|
|
936
941
|
view = view[written:]
|
|
937
942
|
os.fsync(temp_fd)
|
|
943
|
+
temp_stat = os.fstat(temp_fd)
|
|
938
944
|
os.close(temp_fd)
|
|
939
945
|
temp_fd = None
|
|
940
|
-
commit_temp_file(parent_fd, temp_name, basename, overwrite, mode)
|
|
946
|
+
result_stat = commit_temp_file(parent_fd, temp_name, basename, overwrite, mode, temp_stat)
|
|
941
947
|
temp_name = None
|
|
942
948
|
os.fsync(parent_fd)
|
|
943
|
-
result_stat = os.stat(basename, dir_fd=parent_fd, follow_symlinks=False)
|
|
944
949
|
return {"dev": result_stat.st_dev, "ino": result_stat.st_ino}
|
|
945
950
|
finally:
|
|
946
951
|
os.close(source_fd)
|
|
@@ -957,6 +962,7 @@ def copy_path(root_fd, payload):
|
|
|
957
962
|
def run_operation(operation, root_path, payload):
|
|
958
963
|
root_fd = open_dir(root_path)
|
|
959
964
|
try:
|
|
965
|
+
assert_expected_root(root_fd, payload)
|
|
960
966
|
if operation == "stat":
|
|
961
967
|
return stat_path(root_fd, payload)
|
|
962
968
|
if operation == "readdir":
|
|
@@ -1061,6 +1067,12 @@ function mapWorkerError(response) {
|
|
|
1061
1067
|
if (message.includes("fs-safe-source-mismatch")) {
|
|
1062
1068
|
return new errors_FsSafeError("path-mismatch", "source path changed during copy");
|
|
1063
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
|
+
}
|
|
1064
1076
|
if (message.includes("fs-safe-directory-noreplace-unsupported")) {
|
|
1065
1077
|
return new errors_FsSafeError("invalid-path", "directory moves require overwrite: true");
|
|
1066
1078
|
}
|
|
@@ -1241,9 +1253,7 @@ function validatePinnedOperationPayload(payload) {
|
|
|
1241
1253
|
}
|
|
1242
1254
|
}
|
|
1243
1255
|
function isPinnedHelperUnavailable(error) {
|
|
1244
|
-
return error instanceof Error &&
|
|
1245
|
-
"code" in error &&
|
|
1246
|
-
error.code === "helper-unavailable";
|
|
1256
|
+
return error instanceof Error && "code" in error && error.code === "helper-unavailable";
|
|
1247
1257
|
}
|
|
1248
1258
|
function validatePinnedRelativePath(relativePath) {
|
|
1249
1259
|
if (relativePath.length === 0 || relativePath === ".") {
|
|
@@ -1341,29 +1351,21 @@ function assertWithinMaxBytes(bytes, maxBytes) {
|
|
|
1341
1351
|
throw new errors_FsSafeError("too-large", `file exceeds limit of ${maxBytes} bytes (got at least ${bytes})`);
|
|
1342
1352
|
}
|
|
1343
1353
|
}
|
|
1344
|
-
function
|
|
1345
|
-
if (maxBytes === undefined) {
|
|
1346
|
-
return undefined;
|
|
1347
|
-
}
|
|
1354
|
+
async function writeStreamToHandle(stream, handle, maxBytes) {
|
|
1348
1355
|
let bytes = 0;
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
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");
|
|
1355
1365
|
}
|
|
1356
|
-
|
|
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;
|
|
1366
|
+
offset += bytesWritten;
|
|
1367
|
+
}
|
|
1365
1368
|
}
|
|
1366
|
-
await (0,external_node_stream_promises_namespaceObject.pipeline)(stream, destination);
|
|
1367
1369
|
}
|
|
1368
1370
|
async function inputToBase64(input, maxBytes) {
|
|
1369
1371
|
if (input.kind === "buffer") {
|
|
@@ -1388,7 +1390,7 @@ async function runPinnedWriteHelper(params) {
|
|
|
1388
1390
|
relativeParentPath: params.relativeParentPath,
|
|
1389
1391
|
});
|
|
1390
1392
|
if (getFsSafePythonConfig().mode === "off") {
|
|
1391
|
-
return await
|
|
1393
|
+
return await runPinnedWriteFallbackOrThrow(params);
|
|
1392
1394
|
}
|
|
1393
1395
|
if (params.input.kind === "stream") {
|
|
1394
1396
|
try {
|
|
@@ -1396,7 +1398,7 @@ async function runPinnedWriteHelper(params) {
|
|
|
1396
1398
|
}
|
|
1397
1399
|
catch (error) {
|
|
1398
1400
|
if (canFallbackFromPythonError(error)) {
|
|
1399
|
-
return await
|
|
1401
|
+
return await runPinnedWriteFallbackOrThrow(params, error);
|
|
1400
1402
|
}
|
|
1401
1403
|
throw error;
|
|
1402
1404
|
}
|
|
@@ -1409,6 +1411,7 @@ async function runPinnedWriteHelper(params) {
|
|
|
1409
1411
|
mode: params.mode || 0o600,
|
|
1410
1412
|
overwrite: params.overwrite !== false,
|
|
1411
1413
|
relativeParentPath: params.relativeParentPath,
|
|
1414
|
+
...(params.rootIdentity ? { rootDev: params.rootIdentity.dev, rootIno: params.rootIdentity.ino } : {}),
|
|
1412
1415
|
};
|
|
1413
1416
|
try {
|
|
1414
1417
|
return await runPinnedPythonOperation({
|
|
@@ -1419,7 +1422,7 @@ async function runPinnedWriteHelper(params) {
|
|
|
1419
1422
|
}
|
|
1420
1423
|
catch (error) {
|
|
1421
1424
|
if (canFallbackFromPythonError(error)) {
|
|
1422
|
-
return await
|
|
1425
|
+
return await runPinnedWriteFallbackOrThrow(params, error);
|
|
1423
1426
|
}
|
|
1424
1427
|
throw error;
|
|
1425
1428
|
}
|
|
@@ -1439,22 +1442,29 @@ async function runPinnedCopyHelper(params) {
|
|
|
1439
1442
|
mode: params.mode || 0o600,
|
|
1440
1443
|
overwrite: params.overwrite !== false,
|
|
1441
1444
|
relativeParentPath: params.relativeParentPath,
|
|
1445
|
+
...(params.rootIdentity ? { rootDev: params.rootIdentity.dev, rootIno: params.rootIdentity.ino } : {}),
|
|
1442
1446
|
sourceDev: params.sourceIdentity.dev,
|
|
1443
1447
|
sourceIno: params.sourceIdentity.ino,
|
|
1444
1448
|
sourcePath: params.sourcePath,
|
|
1445
1449
|
},
|
|
1446
1450
|
});
|
|
1447
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
|
+
}
|
|
1448
1458
|
async function runPinnedWriteFallback(params) {
|
|
1449
1459
|
const parentPath = params.relativeParentPath
|
|
1450
1460
|
? external_node_path_namespaceObject.join(params.rootPath, ...params.relativeParentPath.split("/"))
|
|
1451
1461
|
: params.rootPath;
|
|
1452
|
-
const parentGuard = await directory_guard_createNearestExistingDirectoryGuard(params.rootPath, parentPath);
|
|
1453
1462
|
if (params.mkdir) {
|
|
1454
|
-
await
|
|
1455
|
-
await promises_namespaceObject.mkdir(parentPath, { recursive: true });
|
|
1456
|
-
});
|
|
1463
|
+
await mkdirPathComponentsWithGuards({ rootReal: params.rootPath, targetPath: parentPath });
|
|
1457
1464
|
}
|
|
1465
|
+
const parentGuard = params.mkdir
|
|
1466
|
+
? await directory_guard_createAsyncDirectoryGuard(parentPath)
|
|
1467
|
+
: await directory_guard_createNearestExistingDirectoryGuard(params.rootPath, parentPath);
|
|
1458
1468
|
const targetPath = external_node_path_namespaceObject.join(parentPath, params.basename);
|
|
1459
1469
|
if (params.overwrite === false) {
|
|
1460
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), {
|
|
@@ -1476,7 +1486,7 @@ async function runPinnedWriteFallback(params) {
|
|
|
1476
1486
|
}
|
|
1477
1487
|
}
|
|
1478
1488
|
else {
|
|
1479
|
-
await
|
|
1489
|
+
await writeStreamToHandle(params.input.stream, handle, params.maxBytes);
|
|
1480
1490
|
}
|
|
1481
1491
|
const stat = await handle.stat();
|
|
1482
1492
|
created = false;
|
|
@@ -1497,7 +1507,9 @@ async function runPinnedWriteFallback(params) {
|
|
|
1497
1507
|
? external_node_fs_namespaceObject.constants.O_NOFOLLOW
|
|
1498
1508
|
: 0);
|
|
1499
1509
|
let handle;
|
|
1500
|
-
let
|
|
1510
|
+
let tempStat;
|
|
1511
|
+
let targetStat;
|
|
1512
|
+
let renamed = false;
|
|
1501
1513
|
try {
|
|
1502
1514
|
handle = await promises_namespaceObject.open(tempPath, tempFlags, params.mode);
|
|
1503
1515
|
if (params.input.kind === "buffer") {
|
|
@@ -1510,29 +1522,36 @@ async function runPinnedWriteFallback(params) {
|
|
|
1510
1522
|
}
|
|
1511
1523
|
}
|
|
1512
1524
|
else {
|
|
1513
|
-
|
|
1514
|
-
writable.once("close", () => {
|
|
1515
|
-
handleClosedByStream = true;
|
|
1516
|
-
});
|
|
1517
|
-
await pipelineWithMaxBytes(params.input.stream, writable, params.maxBytes);
|
|
1525
|
+
await writeStreamToHandle(params.input.stream, handle, params.maxBytes);
|
|
1518
1526
|
}
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
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");
|
|
1522
1531
|
}
|
|
1532
|
+
const expectedTempStat = tempStat;
|
|
1533
|
+
await handle.close().catch(() => undefined);
|
|
1534
|
+
handle = undefined;
|
|
1523
1535
|
await withAsyncDirectoryGuards([parentGuard], async () => {
|
|
1524
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
|
+
}
|
|
1525
1542
|
});
|
|
1526
1543
|
}
|
|
1527
1544
|
catch (error) {
|
|
1528
|
-
|
|
1529
|
-
|
|
1545
|
+
await handle?.close().catch(() => undefined);
|
|
1546
|
+
if (!renamed) {
|
|
1547
|
+
await promises_namespaceObject.rm(tempPath, { force: true }).catch(() => undefined);
|
|
1530
1548
|
}
|
|
1531
|
-
await promises_namespaceObject.rm(tempPath, { force: true }).catch(() => undefined);
|
|
1532
1549
|
throw error;
|
|
1533
1550
|
}
|
|
1534
|
-
|
|
1535
|
-
|
|
1551
|
+
if (!targetStat) {
|
|
1552
|
+
throw new errors_FsSafeError("path-mismatch", "fallback target was not verified");
|
|
1553
|
+
}
|
|
1554
|
+
return { dev: targetStat.dev, ino: targetStat.ino };
|
|
1536
1555
|
}
|
|
1537
1556
|
|
|
1538
1557
|
;// CONCATENATED MODULE: external "node:os"
|
|
@@ -2071,7 +2090,7 @@ function relativeInsideRoot(rootPath, targetPath) {
|
|
|
2071
2090
|
if (!relative || relative === ".") {
|
|
2072
2091
|
return "";
|
|
2073
2092
|
}
|
|
2074
|
-
if (
|
|
2093
|
+
if (isPathRelativeEscape(relative)) {
|
|
2075
2094
|
return "";
|
|
2076
2095
|
}
|
|
2077
2096
|
return relative;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lousy-agents/agent-shell",
|
|
3
|
-
"version": "5.14.
|
|
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.
|
|
39
|
+
"@openclaw/fs-safe": "0.2.6",
|
|
40
40
|
"zod": "4.4.3"
|
|
41
41
|
},
|
|
42
42
|
"peerDependencies": {
|