@lousy-agents/agent-shell 5.18.0 → 5.18.2

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 +1499 -1363
  2. package/package.json +4 -3
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
- 834(__unused_rspack_module, __unused_rspack___webpack_exports__, __webpack_require__) {
5
+ 418(__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");
@@ -91,42 +91,18 @@ function categorizeFsSafeError(code) {
91
91
  class errors_FsSafeError extends Error {
92
92
  code;
93
93
  category;
94
+ details;
94
95
  constructor(code, message, options = {}) {
95
96
  super(message, options);
96
97
  this.name = "FsSafeError";
97
98
  this.code = code;
98
99
  this.category = categorizeFsSafeError(code);
100
+ this.details = options.details;
99
101
  }
100
102
  }
101
103
 
102
104
  ;// CONCATENATED MODULE: external "node:fs"
103
105
  const external_node_fs_namespaceObject = __rspack_createRequire_require("node:fs");
104
- ;// CONCATENATED MODULE: external "node:stream/promises"
105
- const external_node_stream_promises_namespaceObject = __rspack_createRequire_require("node:stream/promises");
106
- ;// CONCATENATED MODULE: external "node:stream"
107
- const external_node_stream_namespaceObject = __rspack_createRequire_require("node:stream");
108
- ;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/bounded-read-stream.js
109
-
110
-
111
- function createMaxBytesTransform(maxBytes) {
112
- let bytes = 0;
113
- return new external_node_stream_namespaceObject.Transform({
114
- transform(chunk, _encoding, callback) {
115
- const buffer = chunk instanceof Buffer ? chunk : Buffer.from(chunk);
116
- bytes += buffer.byteLength;
117
- if (bytes > maxBytes) {
118
- callback(new errors_FsSafeError("too-large", `file exceeds limit of ${maxBytes} bytes (got at least ${bytes})`));
119
- return;
120
- }
121
- callback(null, buffer);
122
- },
123
- });
124
- }
125
- function createBoundedReadStream(opened, maxBytes) {
126
- const stream = opened.handle.createReadStream();
127
- return maxBytes === undefined ? stream : stream.pipe(createMaxBytesTransform(maxBytes));
128
- }
129
-
130
106
  ;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/file-identity.js
131
107
 
132
108
  function isZero(value) {
@@ -151,86 +127,11 @@ function file_identity_sameFileIdentity(left, right, platform = process.platform
151
127
  return platform === "win32" && (isZero(left.dev) || isZero(right.dev));
152
128
  }
153
129
 
154
- ;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/string-coerce.js
155
- function readStringValue(value) {
156
- return typeof value === "string" ? value : undefined;
157
- }
158
- function normalizeNullableString(value) {
159
- if (typeof value !== "string") {
160
- return null;
161
- }
162
- const trimmed = value.trim();
163
- return trimmed ? trimmed : null;
164
- }
165
- function normalizeOptionalString(value) {
166
- return normalizeNullableString(value) ?? undefined;
167
- }
168
- function normalizeStringifiedOptionalString(value) {
169
- if (typeof value === "string") {
170
- return normalizeOptionalString(value);
171
- }
172
- if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
173
- return normalizeOptionalString(String(value));
174
- }
175
- return undefined;
176
- }
177
- function normalizeOptionalLowercaseString(value) {
178
- return normalizeOptionalString(value)?.toLowerCase();
179
- }
180
- function normalizeLowercaseStringOrEmpty(value) {
181
- return normalizeOptionalLowercaseString(value) ?? "";
182
- }
183
- function normalizeFastMode(raw) {
184
- if (typeof raw === "boolean") {
185
- return raw;
186
- }
187
- if (!raw) {
188
- return undefined;
189
- }
190
- const key = normalizeLowercaseStringOrEmpty(raw);
191
- if (["off", "false", "no", "0", "disable", "disabled", "normal"].includes(key)) {
192
- return false;
193
- }
194
- if (["on", "true", "yes", "1", "enable", "enabled", "fast"].includes(key)) {
195
- return true;
196
- }
197
- return undefined;
198
- }
199
- function lowercasePreservingWhitespace(value) {
200
- return value.toLowerCase();
201
- }
202
- function localeLowercasePreservingWhitespace(value) {
203
- return value.toLocaleLowerCase();
204
- }
205
- function resolvePrimaryStringValue(value) {
206
- if (typeof value === "string") {
207
- return normalizeOptionalString(value);
208
- }
209
- if (!value || typeof value !== "object") {
210
- return undefined;
211
- }
212
- return normalizeOptionalString(value.primary);
213
- }
214
- function normalizeOptionalThreadValue(value) {
215
- if (typeof value === "number") {
216
- return Number.isFinite(value) ? Math.trunc(value) : undefined;
217
- }
218
- return normalizeOptionalString(value);
219
- }
220
- function normalizeOptionalStringifiedId(value) {
221
- const normalized = normalizeOptionalThreadValue(value);
222
- return normalized == null ? undefined : String(normalized);
223
- }
224
- function hasNonEmptyString(value) {
225
- return normalizeOptionalString(value) !== undefined;
226
- }
227
-
228
130
  ;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/path.js
229
131
 
230
132
 
231
133
 
232
134
 
233
-
234
135
  const NOT_FOUND_CODES = new Set(["ENOENT", "ENOTDIR"]);
235
136
  const SYMLINK_OPEN_CODES = new Set(["ELOOP", "EINVAL", "ENOTSUP"]);
236
137
  const POSIX_SEPARATOR_CHAR_CODE = 0x2f;
@@ -242,7 +143,9 @@ function normalizeWindowsPathForComparison(input) {
242
143
  normalized = `\\\\${normalized.slice(4)}`;
243
144
  }
244
145
  }
245
- return normalizeLowercaseStringOrEmpty(normalized.replaceAll("/", "\\"));
146
+ // Lowercase only: this value feeds Windows containment math, so surrounding
147
+ // whitespace is part of the path and must not be trimmed away.
148
+ return normalized.replaceAll("/", "\\").toLowerCase();
246
149
  }
247
150
  function isNodeError(value) {
248
151
  return Boolean(value && typeof value === "object" && "code" in value);
@@ -741,6 +644,27 @@ function isSameOrChildPath(candidate, parent) {
741
644
  const parentPrefix = parent.endsWith(external_node_path_namespaceObject.sep) ? parent : `${parent}${external_node_path_namespaceObject.sep}`;
742
645
  return candidate === parent || candidate.startsWith(parentPrefix);
743
646
  }
647
+ async function realpathOrThrowNotFile(target) {
648
+ try {
649
+ return external_node_path_namespaceObject.resolve(await promises_namespaceObject.realpath(target));
650
+ }
651
+ catch (error) {
652
+ if (path_isNotFoundPathError(error)) {
653
+ // A dangling symlink (or a component removed between lstat and
654
+ // realpath) is not a usable directory component.
655
+ throw new errors_FsSafeError("not-file", "directory component must be a directory", {
656
+ cause: error instanceof Error ? error : undefined,
657
+ });
658
+ }
659
+ throw error;
660
+ }
661
+ }
662
+ /**
663
+ * Creates each missing path component from `rootReal` down to `targetPath`,
664
+ * guarding every step. Returns the real (symlink-resolved) path of the final
665
+ * component so callers can guard/use that path directly instead of
666
+ * re-deriving it from the original, possibly-symlinked, lexical path.
667
+ */
744
668
  async function mkdirPathComponentsWithGuards(params) {
745
669
  const root = external_node_path_namespaceObject.resolve(params.rootReal);
746
670
  const rootCanonical = external_node_path_namespaceObject.resolve(await promises_namespaceObject.realpath(root));
@@ -764,18 +688,39 @@ async function mkdirPathComponentsWithGuards(params) {
764
688
  }
765
689
  }
766
690
  const stat = await promises_namespaceObject.lstat(next);
767
- if (stat.isSymbolicLink() || !stat.isDirectory()) {
691
+ if (!stat.isSymbolicLink() && !stat.isDirectory()) {
768
692
  throw new errors_FsSafeError("not-file", "directory component must be a directory");
769
693
  }
770
694
  // Node's recursive mkdir follows symlinks in missing components. Build one
771
695
  // segment at a time and realpath-check each segment before descending.
772
- if (!isSameOrChildPath(external_node_path_namespaceObject.resolve(await promises_namespaceObject.realpath(next)), rootCanonical)) {
696
+ const nextReal = await realpathOrThrowNotFile(next);
697
+ if (!isSameOrChildPath(nextReal, rootCanonical)) {
773
698
  throw new errors_FsSafeError("outside-workspace", "directory escaped workspace root");
774
699
  }
700
+ if (stat.isSymbolicLink()) {
701
+ // An existing path component may legitimately be a symlink to a real
702
+ // directory inside the root (e.g. a skill-bank layout). We already
703
+ // verified above that it resolves inside the root, so treat the
704
+ // resolved real path as the directory for the rest of this walk
705
+ // instead of rejecting it outright. Guard checks from here on operate
706
+ // on the real (non-symlink) path, preserving TOCTOU protection for
707
+ // every subsequent segment. Callers that need the final directory
708
+ // (e.g. to guard it themselves after this function returns) must use
709
+ // the returned resolved path, not their own lexical parent path.
710
+ const targetStat = await promises_namespaceObject.stat(nextReal);
711
+ if (!targetStat.isDirectory()) {
712
+ throw new errors_FsSafeError("not-file", "directory component must be a directory");
713
+ }
714
+ await directory_guard_createAsyncDirectoryGuard(nextReal);
715
+ await assertAsyncDirectoryGuard(parentGuard);
716
+ current = nextReal;
717
+ continue;
718
+ }
775
719
  await directory_guard_createAsyncDirectoryGuard(next);
776
720
  await assertAsyncDirectoryGuard(parentGuard);
777
721
  current = next;
778
722
  }
723
+ return current;
779
724
  }
780
725
 
781
726
  ;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/guarded-mutation.js
@@ -1041,8 +986,20 @@ async function resolveOpenedFileRealPathFromParent(handleStat, ioPath) {
1041
986
  return null;
1042
987
  }
1043
988
 
1044
- ;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/pinned-python-config.js
989
+ ;// CONCATENATED MODULE: external "node:module"
990
+ const external_node_module_namespaceObject = __rspack_createRequire_require("node:module");
991
+ ;// CONCATENATED MODULE: external "node:url"
992
+ const external_node_url_namespaceObject = __rspack_createRequire_require("node:url");
993
+ ;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/native-config.js
1045
994
  let overrideConfig = {};
995
+ let legacyWarningEmitted = false;
996
+ const legacyModeEnvKeys = ["FS_SAFE_PYTHON_MODE", "OPENCLAW_FS_SAFE_PYTHON_MODE"];
997
+ const legacyPathEnvKeys = [
998
+ "FS_SAFE_PYTHON",
999
+ "OPENCLAW_FS_SAFE_PYTHON",
1000
+ "OPENCLAW_PINNED_PYTHON",
1001
+ "OPENCLAW_PINNED_WRITE_PYTHON",
1002
+ ];
1046
1003
  function parseMode(value) {
1047
1004
  if (!value) {
1048
1005
  return undefined;
@@ -1059,680 +1016,495 @@ function parseMode(value) {
1059
1016
  }
1060
1017
  return undefined;
1061
1018
  }
1062
- function configureFsSafePython(config) {
1019
+ function configureFsSafeNative(config) {
1063
1020
  overrideConfig = { ...overrideConfig, ...config };
1064
1021
  }
1065
- function getFsSafePythonConfig() {
1022
+ function warnLegacyPythonConfiguration(source, mappedMode) {
1023
+ if (legacyWarningEmitted) {
1024
+ return;
1025
+ }
1026
+ legacyWarningEmitted = true;
1027
+ process.emitWarning(`${source} is a deprecated 0.4 compatibility bridge; mapped to native mode ` +
1028
+ `"${mappedMode}". Use configureFsSafeNative({ mode: "${mappedMode}" }) or ` +
1029
+ `FS_SAFE_NATIVE_MODE=${mappedMode}. Python interpreter paths are no longer used.`, { code: "FS_SAFE_PYTHON_DEPRECATED", type: "DeprecationWarning" });
1030
+ }
1031
+ function readLegacyPythonMode() {
1032
+ const configuredKeys = [...legacyModeEnvKeys, ...legacyPathEnvKeys].filter((key) => process.env[key] !== undefined);
1033
+ if (configuredKeys.length === 0) {
1034
+ return undefined;
1035
+ }
1036
+ const rawMode = legacyModeEnvKeys.map((key) => process.env[key]).find((value) => value !== undefined);
1037
+ const mappedMode = parseMode(rawMode) ?? "auto";
1038
+ warnLegacyPythonConfiguration(`Legacy ${configuredKeys.join(", ")}`, mappedMode);
1039
+ return mappedMode;
1040
+ }
1041
+ /**
1042
+ * @deprecated Compatibility bridge for 0.4 upgrades. Use configureFsSafeNative.
1043
+ */
1044
+ function configureFsSafePython(config) {
1045
+ const mappedMode = config.mode ?? "auto";
1046
+ warnLegacyPythonConfiguration("configureFsSafePython()", mappedMode);
1047
+ if (config.mode !== undefined) {
1048
+ configureFsSafeNative({ mode: config.mode });
1049
+ }
1050
+ }
1051
+ function getFsSafeNativeConfig() {
1052
+ const legacyMode = readLegacyPythonMode();
1066
1053
  return {
1067
1054
  mode: overrideConfig.mode ??
1068
- parseMode(process.env.FS_SAFE_PYTHON_MODE) ??
1069
- parseMode(process.env.OPENCLAW_FS_SAFE_PYTHON_MODE) ??
1055
+ parseMode(process.env.FS_SAFE_NATIVE_MODE) ??
1056
+ parseMode(process.env.OPENCLAW_FS_SAFE_NATIVE_MODE) ??
1057
+ legacyMode ??
1070
1058
  "auto",
1071
- pythonPath: overrideConfig.pythonPath ??
1072
- process.env.FS_SAFE_PYTHON ??
1073
- process.env.OPENCLAW_FS_SAFE_PYTHON ??
1074
- process.env.OPENCLAW_PINNED_PYTHON ??
1075
- process.env.OPENCLAW_PINNED_WRITE_PYTHON,
1076
- };
1077
- }
1078
- function canFallbackFromPythonError(error) {
1079
- const code = error instanceof Error && "code" in error ? error.code : undefined;
1080
- return (getFsSafePythonConfig().mode !== "require" &&
1081
- (code === "helper-unavailable" || code === "unsupported-platform"));
1082
- }
1083
-
1084
- ;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/pinned-python.js
1085
-
1086
-
1087
-
1088
-
1089
- const PINNED_PYTHON_WORKER_SOURCE = String.raw `
1090
- import base64, errno, json, os, secrets, stat, sys
1091
- DIR_FLAGS = os.O_RDONLY
1092
- if hasattr(os, "O_DIRECTORY"): DIR_FLAGS |= os.O_DIRECTORY
1093
- if hasattr(os, "O_NOFOLLOW"): DIR_FLAGS |= os.O_NOFOLLOW
1094
- READ_FLAGS = os.O_RDONLY
1095
- if hasattr(os, "O_NONBLOCK"): READ_FLAGS |= os.O_NONBLOCK
1096
- if hasattr(os, "O_NOFOLLOW"): READ_FLAGS |= os.O_NOFOLLOW
1097
- WRITE_FLAGS = os.O_WRONLY | os.O_CREAT | os.O_EXCL
1098
- if hasattr(os, "O_NOFOLLOW"):
1099
- WRITE_FLAGS |= os.O_NOFOLLOW
1100
- def split_relative(value):
1101
- if value in ("", "."):
1102
- return []
1103
- if "\x00" in value or value.startswith("/") or value.startswith("//"):
1104
- raise OSError(errno.EPERM, "invalid relative path")
1105
- if value.startswith("..\\"):
1106
- raise OSError(errno.EPERM, "path traversal is not allowed")
1107
- parts = [part for part in value.split("/") if part and part != "."]
1108
- for part in parts:
1109
- if part == "..":
1110
- raise OSError(errno.EPERM, "path traversal is not allowed")
1111
- return parts
1112
- def open_dir(path_value, dir_fd=None):
1113
- return os.open(path_value, DIR_FLAGS, dir_fd=dir_fd)
1114
- def walk_dir(root_fd, segments, mkdir_enabled=False):
1115
- current_fd = os.dup(root_fd)
1116
- try:
1117
- for segment in segments:
1118
- try:
1119
- next_fd = open_dir(segment, dir_fd=current_fd)
1120
- except FileNotFoundError:
1121
- if not mkdir_enabled:
1122
- raise
1123
- os.mkdir(segment, 0o777, dir_fd=current_fd)
1124
- next_fd = open_dir(segment, dir_fd=current_fd)
1125
- os.close(current_fd)
1126
- current_fd = next_fd
1127
- return current_fd
1128
- except Exception:
1129
- os.close(current_fd)
1130
- raise
1131
- def parent_and_basename(root_fd, relative):
1132
- segments = split_relative(relative)
1133
- if not segments:
1134
- raise OSError(errno.EPERM, "operation requires a non-root path")
1135
- parent_fd = walk_dir(root_fd, segments[:-1])
1136
- return parent_fd, segments[-1]
1137
- def encode_stat(st):
1138
- mode = st.st_mode
1139
- return {
1140
- "dev": st.st_dev,
1141
- "gid": st.st_gid,
1142
- "ino": st.st_ino,
1143
- "isDirectory": stat.S_ISDIR(mode),
1144
- "isFile": stat.S_ISREG(mode),
1145
- "isSymbolicLink": stat.S_ISLNK(mode),
1146
- "mode": mode,
1147
- "mtimeMs": st.st_mtime * 1000,
1148
- "nlink": st.st_nlink,
1149
- "size": st.st_size,
1150
- "uid": st.st_uid,
1151
- }
1152
- def reject_unsafe_endpoint(st):
1153
- mode = st.st_mode
1154
- if stat.S_ISLNK(mode):
1155
- raise OSError(errno.ELOOP, "symlink endpoint is not allowed")
1156
- if stat.S_ISREG(mode) and st.st_nlink > 1:
1157
- raise OSError(errno.EPERM, "hardlinked file endpoint is not allowed")
1158
- def copy_bytes(source_fd, dest_fd):
1159
- while True:
1160
- chunk = os.read(source_fd, 65536)
1161
- if not chunk:
1162
- break
1163
- view = memoryview(chunk)
1164
- while view:
1165
- written = os.write(dest_fd, view)
1166
- if written <= 0:
1167
- raise OSError(errno.EIO, "short write")
1168
- view = view[written:]
1169
- def write_all(fd, data):
1170
- view = memoryview(data)
1171
- while view:
1172
- written = os.write(fd, view)
1173
- if written <= 0:
1174
- raise OSError(errno.EIO, "short write")
1175
- view = view[written:]
1176
- def fsync_best_effort(fd):
1177
- try: os.fsync(fd)
1178
- except OSError as error:
1179
- if error.errno != errno.EPERM: raise
1180
- def link_unsupported(exc):
1181
- unsupported = (errno.EPERM, errno.EOPNOTSUPP, getattr(errno, "ENOTSUP", errno.EOPNOTSUPP))
1182
- return getattr(exc, "errno", None) in unsupported
1183
- def link_no_replace(name, new_name, source_fd, target_fd):
1184
- linked = False
1185
- try:
1186
- os.link(name, new_name, src_dir_fd=source_fd, dst_dir_fd=target_fd, follow_symlinks=False)
1187
- linked = True
1188
- os.unlink(name, dir_fd=source_fd)
1189
- except Exception:
1190
- if linked:
1191
- try: os.unlink(new_name, dir_fd=target_fd)
1192
- except FileNotFoundError: pass
1193
- raise
1194
- os.fsync(source_fd)
1195
- if source_fd != target_fd:
1196
- os.fsync(target_fd)
1197
- def copy_file_no_replace(source_parent_fd, source_name, target_parent_fd, basename, mode, expected=None, unlink_source=False):
1198
- source_fd = os.open(source_name, READ_FLAGS, dir_fd=source_parent_fd)
1199
- dest_fd = None; success = False; dest_stat = None
1200
- try:
1201
- if expected is not None:
1202
- source_stat = os.fstat(source_fd)
1203
- if source_stat.st_dev != expected.st_dev or source_stat.st_ino != expected.st_ino:
1204
- raise RuntimeError("fs-safe-source-mismatch")
1205
- dest_fd = os.open(basename, WRITE_FLAGS, mode, dir_fd=target_parent_fd)
1206
- copy_bytes(source_fd, dest_fd)
1207
- os.fsync(dest_fd)
1208
- dest_stat = os.fstat(dest_fd)
1209
- success = True
1210
- finally:
1211
- os.close(source_fd)
1212
- if dest_fd is not None:
1213
- os.close(dest_fd)
1214
- if dest_fd is not None and not success:
1215
- try: os.unlink(basename, dir_fd=target_parent_fd)
1216
- except FileNotFoundError: pass
1217
- if unlink_source:
1218
- try:
1219
- os.unlink(source_name, dir_fd=source_parent_fd)
1220
- except Exception:
1221
- try: os.unlink(basename, dir_fd=target_parent_fd)
1222
- except FileNotFoundError: pass
1223
- raise
1224
- return dest_stat
1225
- def same_identity(left, right):
1226
- return left.st_dev == right.st_dev and left.st_ino == right.st_ino
1227
- def verify_temp_name(parent_fd, temp_name, expected_stat):
1228
- current_stat = os.lstat(temp_name, dir_fd=parent_fd)
1229
- if stat.S_ISLNK(current_stat.st_mode) or not same_identity(current_stat, expected_stat):
1230
- raise RuntimeError("fs-safe-temp-mismatch")
1231
- def verify_committed_temp(parent_fd, basename, expected_stat):
1232
- final_stat = os.lstat(basename, dir_fd=parent_fd)
1233
- if not stat.S_ISLNK(final_stat.st_mode) and same_identity(final_stat, expected_stat):
1234
- return final_stat
1235
- try: os.unlink(basename, dir_fd=parent_fd)
1236
- except FileNotFoundError: pass
1237
- raise RuntimeError("fs-safe-temp-mismatch")
1238
- def commit_temp_file(parent_fd, temp_name, basename, overwrite, mode, expected_stat):
1239
- verify_temp_name(parent_fd, temp_name, expected_stat)
1240
- if overwrite:
1241
- os.replace(temp_name, basename, src_dir_fd=parent_fd, dst_dir_fd=parent_fd)
1242
- return verify_committed_temp(parent_fd, basename, expected_stat)
1243
- else:
1244
- try:
1245
- os.link(temp_name, basename, src_dir_fd=parent_fd, dst_dir_fd=parent_fd, follow_symlinks=False)
1246
- final_stat = verify_committed_temp(parent_fd, basename, expected_stat)
1247
- os.unlink(temp_name, dir_fd=parent_fd)
1248
- return final_stat
1249
- except OSError as exc:
1250
- if not link_unsupported(exc):
1251
- raise
1252
- return copy_file_no_replace(parent_fd, temp_name, parent_fd, basename, mode, expected_stat, True)
1253
- def assert_expected_root(root_fd, payload):
1254
- if "rootDev" in payload or "rootIno" in payload:
1255
- root_stat = os.fstat(root_fd)
1256
- if root_stat.st_dev != int(payload["rootDev"]) or root_stat.st_ino != int(payload["rootIno"]):
1257
- raise RuntimeError("fs-safe-root-mismatch")
1258
- def stat_path(root_fd, payload):
1259
- relative = payload.get("relativePath", "")
1260
- segments = split_relative(relative)
1261
- if not segments:
1262
- return encode_stat(os.fstat(root_fd))
1263
- parent_fd, basename = parent_and_basename(root_fd, relative)
1264
- try:
1265
- st = os.lstat(basename, dir_fd=parent_fd)
1266
- if payload.get("rejectSymlink", True) and stat.S_ISLNK(st.st_mode):
1267
- raise OSError(errno.ELOOP, "symlink endpoint is not allowed")
1268
- return encode_stat(st)
1269
- finally:
1270
- os.close(parent_fd)
1271
- def readdir_path(root_fd, payload):
1272
- dir_fd = walk_dir(root_fd, split_relative(payload.get("relativePath", "")))
1273
- try:
1274
- names = sorted(os.listdir(dir_fd))
1275
- if not payload.get("withFileTypes", False):
1276
- return names
1277
- entries = []
1278
- for name in names:
1279
- st = os.lstat(name, dir_fd=dir_fd)
1280
- entry = encode_stat(st)
1281
- entry["name"] = name
1282
- entries.append(entry)
1283
- return entries
1284
- finally:
1285
- os.close(dir_fd)
1286
- def mkdirp_path(root_fd, payload):
1287
- dir_fd = walk_dir(root_fd, split_relative(payload.get("relativePath", "")), mkdir_enabled=True)
1288
- os.close(dir_fd); return None
1289
- def remove_tree(parent_fd, basename):
1290
- st = os.lstat(basename, dir_fd=parent_fd)
1291
- if stat.S_ISDIR(st.st_mode) and not stat.S_ISLNK(st.st_mode):
1292
- dir_fd = open_dir(basename, dir_fd=parent_fd)
1293
- try:
1294
- for child in os.listdir(dir_fd):
1295
- remove_tree(dir_fd, child)
1296
- finally:
1297
- os.close(dir_fd)
1298
- os.rmdir(basename, dir_fd=parent_fd)
1299
- else:
1300
- os.unlink(basename, dir_fd=parent_fd)
1301
- def remove_path(root_fd, payload):
1302
- parent_fd, basename = parent_and_basename(root_fd, payload.get("relativePath", ""))
1303
- try:
1304
- try:
1305
- st = os.lstat(basename, dir_fd=parent_fd)
1306
- except FileNotFoundError:
1307
- if payload.get("force", True):
1308
- return None
1309
- raise
1310
- if stat.S_ISDIR(st.st_mode) and not stat.S_ISLNK(st.st_mode):
1311
- if payload.get("recursive", False):
1312
- remove_tree(parent_fd, basename)
1313
- else:
1314
- os.rmdir(basename, dir_fd=parent_fd)
1315
- else:
1316
- os.unlink(basename, dir_fd=parent_fd)
1317
- return None
1318
- finally:
1319
- os.close(parent_fd)
1320
-
1321
- def rename_path(root_fd, payload):
1322
- from_parent_fd, from_base = parent_and_basename(root_fd, payload["from"])
1323
- to_parent_fd, to_base = parent_and_basename(root_fd, payload["to"])
1324
- try:
1325
- from_stat = os.lstat(from_base, dir_fd=from_parent_fd)
1326
- reject_unsafe_endpoint(from_stat)
1327
- overwrite = payload.get("overwrite", True)
1328
- if not overwrite and stat.S_ISREG(from_stat.st_mode):
1329
- try:
1330
- link_no_replace(from_base, to_base, from_parent_fd, to_parent_fd)
1331
- except OSError as exc:
1332
- if not link_unsupported(exc):
1333
- raise
1334
- copy_file_no_replace(from_parent_fd, from_base, to_parent_fd, to_base, stat.S_IMODE(from_stat.st_mode), from_stat, True)
1335
- return None
1336
- if not overwrite and stat.S_ISDIR(from_stat.st_mode):
1337
- raise RuntimeError("fs-safe-directory-noreplace-unsupported")
1338
- if not overwrite:
1339
- try:
1340
- os.lstat(to_base, dir_fd=to_parent_fd)
1341
- raise FileExistsError(errno.EEXIST, "destination exists", to_base)
1342
- except FileNotFoundError:
1343
- pass
1344
- os.rename(from_base, to_base, src_dir_fd=from_parent_fd, dst_dir_fd=to_parent_fd)
1345
- os.fsync(from_parent_fd)
1346
- if from_parent_fd != to_parent_fd:
1347
- os.fsync(to_parent_fd)
1348
- return None
1349
- finally:
1350
- os.close(from_parent_fd)
1351
- os.close(to_parent_fd)
1352
-
1353
- def create_temp_file(parent_fd, basename, mode):
1354
- prefix = "." + basename + "."
1355
- for _ in range(128):
1356
- candidate = prefix + secrets.token_hex(6) + ".tmp"
1357
- try:
1358
- fd = os.open(candidate, WRITE_FLAGS, mode, dir_fd=parent_fd)
1359
- return candidate, fd
1360
- except FileExistsError:
1361
- continue
1362
- raise RuntimeError("failed to allocate pinned temp file")
1363
-
1364
- def write_path(root_fd, payload):
1365
- parent_fd = walk_dir(root_fd, split_relative(payload.get("relativeParentPath", "")), bool(payload.get("mkdir", True)))
1366
- temp_fd = None
1367
- temp_name = None
1368
- basename = payload["basename"]
1369
- mode = int(payload.get("mode", 0o600))
1370
- overwrite = bool(payload.get("overwrite", True))
1371
- max_bytes = int(payload.get("maxBytes", -1))
1372
- data = base64.b64decode(payload.get("base64", ""))
1373
- try:
1374
- if max_bytes >= 0 and len(data) > max_bytes:
1375
- raise RuntimeError("fs-safe-too-large:%d:%d" % (max_bytes, len(data)))
1376
- if not overwrite:
1377
- try:
1378
- os.lstat(basename, dir_fd=parent_fd)
1379
- raise FileExistsError(errno.EEXIST, "destination exists", basename)
1380
- except FileNotFoundError:
1381
- pass
1382
- temp_name, temp_fd = create_temp_file(parent_fd, basename, mode)
1383
- os.fchmod(temp_fd, mode)
1384
- write_all(temp_fd, data)
1385
- fsync_best_effort(temp_fd)
1386
- temp_stat = os.fstat(temp_fd)
1387
- os.close(temp_fd)
1388
- temp_fd = None
1389
- result_stat = commit_temp_file(parent_fd, temp_name, basename, overwrite, mode, temp_stat)
1390
- temp_name = None
1391
- fsync_best_effort(parent_fd)
1392
- return {"dev": result_stat.st_dev, "ino": result_stat.st_ino}
1393
- finally:
1394
- if temp_fd is not None:
1395
- os.close(temp_fd)
1396
- if temp_name is not None:
1397
- try:
1398
- os.unlink(temp_name, dir_fd=parent_fd)
1399
- except FileNotFoundError:
1400
- pass
1401
- os.close(parent_fd)
1402
-
1403
- def copy_path(root_fd, payload):
1404
- source_fd = os.open(payload["sourcePath"], READ_FLAGS)
1405
- parent_fd = None
1406
- temp_fd = None
1407
- temp_name = None
1408
- try:
1409
- source_stat = os.fstat(source_fd)
1410
- if not stat.S_ISREG(source_stat.st_mode):
1411
- raise RuntimeError("fs-safe-not-file")
1412
- if source_stat.st_dev != int(payload["sourceDev"]) or source_stat.st_ino != int(payload["sourceIno"]):
1413
- raise RuntimeError("fs-safe-source-mismatch")
1414
- basename = payload["basename"]
1415
- mode = int(payload.get("mode", 0o600))
1416
- overwrite = bool(payload.get("overwrite", True))
1417
- max_bytes = int(payload.get("maxBytes", -1))
1418
- if max_bytes >= 0 and source_stat.st_size > max_bytes:
1419
- raise RuntimeError("fs-safe-too-large:%d:%d" % (max_bytes, source_stat.st_size))
1420
- parent_fd = walk_dir(root_fd, split_relative(payload.get("relativeParentPath", "")), bool(payload.get("mkdir", True)))
1421
- temp_name, temp_fd = create_temp_file(parent_fd, basename, mode)
1422
- os.fchmod(temp_fd, mode)
1423
- written_bytes = 0
1424
- while True:
1425
- chunk = os.read(source_fd, 65536)
1426
- if not chunk:
1427
- break
1428
- written_bytes += len(chunk)
1429
- if max_bytes >= 0 and written_bytes > max_bytes:
1430
- raise RuntimeError("fs-safe-too-large:%d:%d" % (max_bytes, written_bytes))
1431
- view = memoryview(chunk)
1432
- while view:
1433
- written = os.write(temp_fd, view)
1434
- if written <= 0:
1435
- raise OSError(errno.EIO, "short write")
1436
- view = view[written:]
1437
- os.fsync(temp_fd)
1438
- temp_stat = os.fstat(temp_fd)
1439
- os.close(temp_fd)
1440
- temp_fd = None
1441
- result_stat = commit_temp_file(parent_fd, temp_name, basename, overwrite, mode, temp_stat)
1442
- temp_name = None
1443
- os.fsync(parent_fd)
1444
- return {"dev": result_stat.st_dev, "ino": result_stat.st_ino}
1445
- finally:
1446
- os.close(source_fd)
1447
- if temp_fd is not None:
1448
- os.close(temp_fd)
1449
- if temp_name is not None and parent_fd is not None:
1450
- try:
1451
- os.unlink(temp_name, dir_fd=parent_fd)
1452
- except FileNotFoundError:
1453
- pass
1454
- if parent_fd is not None:
1455
- os.close(parent_fd)
1456
-
1457
- def run_operation(operation, root_path, payload):
1458
- root_fd = open_dir(root_path)
1459
- try:
1460
- assert_expected_root(root_fd, payload)
1461
- if operation == "stat":
1462
- return stat_path(root_fd, payload)
1463
- if operation == "readdir":
1464
- return readdir_path(root_fd, payload)
1465
- if operation == "mkdirp":
1466
- return mkdirp_path(root_fd, payload)
1467
- if operation == "remove":
1468
- return remove_path(root_fd, payload)
1469
- if operation == "rename":
1470
- return rename_path(root_fd, payload)
1471
- if operation == "write":
1472
- return write_path(root_fd, payload)
1473
- if operation == "copy":
1474
- return copy_path(root_fd, payload)
1475
- raise RuntimeError("unknown operation: " + operation)
1476
- finally:
1477
- os.close(root_fd)
1478
-
1479
- for line in sys.stdin:
1480
- try:
1481
- request = json.loads(line)
1482
- result = run_operation(request["operation"], request["rootPath"], request.get("payload") or {})
1483
- response = {"id": request["id"], "ok": True, "result": result}
1484
- except Exception as exc:
1485
- response = {
1486
- "id": request.get("id") if isinstance(locals().get("request"), dict) else None,
1487
- "ok": False,
1488
- "code": exc.__class__.__name__,
1489
- "errno": getattr(exc, "errno", None),
1490
- "message": str(exc),
1491
- }
1492
- print(json.dumps(response, separators=(",", ":")), flush=True)
1493
- `;
1494
- let nextRequestId = 1;
1495
- let worker = null;
1496
- function __resetPinnedPythonWorkerForTest() {
1497
- const currentWorker = worker;
1498
- worker = null;
1499
- if (!currentWorker) {
1500
- return;
1501
- }
1502
- currentWorker.pending.clear();
1503
- currentWorker.child.kill("SIGTERM");
1059
+ };
1504
1060
  }
1505
- const PYTHON_CANDIDATE_DEFAULTS = [
1506
- "/usr/bin/python3",
1507
- "/opt/homebrew/bin/python3",
1508
- "/usr/local/bin/python3",
1509
- ];
1510
- function canExecute(binPath) {
1061
+ function __resetFsSafeNativeConfigForTest() {
1062
+ overrideConfig = {};
1063
+ legacyWarningEmitted = false;
1064
+ }
1065
+
1066
+ ;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/native.js
1067
+
1068
+
1069
+
1070
+
1071
+
1072
+
1073
+ const native_require = (0,external_node_module_namespaceObject.createRequire)(import.meta.url);
1074
+ const bundledNativeDirectory = (0,external_node_url_namespaceObject.fileURLToPath)(new URL("../dist/native/", import.meta.url));
1075
+ let native_binding;
1076
+ let native_loadError;
1077
+ let attempted = false;
1078
+ let loadBinding = loadBundledBinding;
1079
+ function isMuslFilename(file) {
1080
+ return file.includes("libc.musl-") || file.includes("ld-musl-");
1081
+ }
1082
+ function isMuslFromReport() {
1511
1083
  try {
1512
- external_node_fs_namespaceObject.accessSync(binPath, external_node_fs_namespaceObject.constants.X_OK);
1513
- return true;
1084
+ if (!process.report || typeof process.report.getReport !== "function")
1085
+ return undefined;
1086
+ const report = process.report.getReport();
1087
+ if (report.header?.glibcVersionRuntime)
1088
+ return false;
1089
+ if (report.sharedObjects?.some(isMuslFilename))
1090
+ return true;
1514
1091
  }
1515
1092
  catch {
1516
- return false;
1093
+ // Continue with filesystem and ELF inspection.
1517
1094
  }
1095
+ return undefined;
1518
1096
  }
1519
- function resolvePython() {
1520
- const configured = getFsSafePythonConfig().pythonPath;
1521
- if (configured) {
1522
- return configured;
1523
- }
1524
- for (const candidate of PYTHON_CANDIDATE_DEFAULTS) {
1525
- if (canExecute(candidate)) {
1526
- return candidate;
1097
+ function isMuslFromFilesystem() {
1098
+ for (const directory of ["/lib", "/usr/lib"]) {
1099
+ try {
1100
+ if ((0,external_node_fs_namespaceObject.readdirSync)(directory).some(isMuslFilename))
1101
+ return true;
1102
+ }
1103
+ catch {
1104
+ // A missing or unreadable conventional library directory is inconclusive.
1527
1105
  }
1528
1106
  }
1529
- return "python3";
1107
+ return undefined;
1530
1108
  }
1531
- function assertPinnedHelperSupported() {
1532
- if (process.platform === "win32") {
1533
- throw new errors_FsSafeError("unsupported-platform", "fd-relative pinned filesystem operations are not available on Windows");
1109
+ function readUInt(buffer, offset, bytes, littleEndian) {
1110
+ if (offset < 0 || offset + bytes > buffer.length)
1111
+ return undefined;
1112
+ if (bytes === 2)
1113
+ return littleEndian ? buffer.readUInt16LE(offset) : buffer.readUInt16BE(offset);
1114
+ if (bytes === 4)
1115
+ return littleEndian ? buffer.readUInt32LE(offset) : buffer.readUInt32BE(offset);
1116
+ const value = littleEndian ? buffer.readBigUInt64LE(offset) : buffer.readBigUInt64BE(offset);
1117
+ return value <= BigInt(Number.MAX_SAFE_INTEGER) ? Number(value) : undefined;
1118
+ }
1119
+ function isMuslFromElfInterpreter() {
1120
+ let fd;
1121
+ try {
1122
+ fd = (0,external_node_fs_namespaceObject.openSync)(process.execPath, "r");
1123
+ const header = Buffer.alloc(64);
1124
+ if ((0,external_node_fs_namespaceObject.readSync)(fd, header, 0, header.length, 0) < 52)
1125
+ return undefined;
1126
+ if (!header.subarray(0, 4).equals(Buffer.from([0x7f, 0x45, 0x4c, 0x46])))
1127
+ return undefined;
1128
+ const elfClass = header[4];
1129
+ const dataEncoding = header[5];
1130
+ if ((elfClass !== 1 && elfClass !== 2) || (dataEncoding !== 1 && dataEncoding !== 2)) {
1131
+ return undefined;
1132
+ }
1133
+ const littleEndian = dataEncoding === 1;
1134
+ const is64Bit = elfClass === 2;
1135
+ const tableOffset = readUInt(header, is64Bit ? 32 : 28, is64Bit ? 8 : 4, littleEndian);
1136
+ const entrySize = readUInt(header, is64Bit ? 54 : 42, 2, littleEndian);
1137
+ const entryCount = readUInt(header, is64Bit ? 56 : 44, 2, littleEndian);
1138
+ if (tableOffset === undefined ||
1139
+ entrySize === undefined ||
1140
+ entryCount === undefined ||
1141
+ entrySize < (is64Bit ? 56 : 32) ||
1142
+ entryCount > 1024) {
1143
+ return undefined;
1144
+ }
1145
+ const programHeader = Buffer.alloc(entrySize);
1146
+ for (let index = 0; index < entryCount; index += 1) {
1147
+ const offset = tableOffset + index * entrySize;
1148
+ if ((0,external_node_fs_namespaceObject.readSync)(fd, programHeader, 0, entrySize, offset) !== entrySize)
1149
+ return undefined;
1150
+ if (readUInt(programHeader, 0, 4, littleEndian) !== 3)
1151
+ continue;
1152
+ const interpreterOffset = readUInt(programHeader, is64Bit ? 8 : 4, is64Bit ? 8 : 4, littleEndian);
1153
+ const interpreterSize = readUInt(programHeader, is64Bit ? 32 : 16, is64Bit ? 8 : 4, littleEndian);
1154
+ if (interpreterOffset === undefined ||
1155
+ interpreterSize === undefined ||
1156
+ interpreterSize < 1 ||
1157
+ interpreterSize > 4096) {
1158
+ return undefined;
1159
+ }
1160
+ const interpreter = Buffer.alloc(interpreterSize);
1161
+ if ((0,external_node_fs_namespaceObject.readSync)(fd, interpreter, 0, interpreterSize, interpreterOffset) !== interpreterSize) {
1162
+ return undefined;
1163
+ }
1164
+ return isMuslFilename(interpreter.toString("utf8"));
1165
+ }
1166
+ }
1167
+ catch {
1168
+ return undefined;
1534
1169
  }
1535
- if (getFsSafePythonConfig().mode === "off") {
1536
- throw new errors_FsSafeError("helper-unavailable", "Python helper is disabled");
1170
+ finally {
1171
+ if (fd !== undefined) {
1172
+ try {
1173
+ (0,external_node_fs_namespaceObject.closeSync)(fd);
1174
+ }
1175
+ catch {
1176
+ // Best-effort detection must never turn close failure into import failure.
1177
+ }
1178
+ }
1537
1179
  }
1180
+ return undefined;
1538
1181
  }
1539
- function isSpawnUnavailable(error) {
1540
- if (!(error instanceof Error)) {
1182
+ function isMusl() {
1183
+ if (process.platform !== "linux")
1541
1184
  return false;
1185
+ for (const detector of [isMuslFromReport, isMuslFromFilesystem, isMuslFromElfInterpreter]) {
1186
+ const result = detector();
1187
+ if (result !== undefined)
1188
+ return result;
1542
1189
  }
1543
- const maybeErrno = error;
1544
- return (typeof maybeErrno.syscall === "string" &&
1545
- maybeErrno.syscall.startsWith("spawn") &&
1546
- ["EACCES", "ENOENT", "ENOEXEC"].includes(maybeErrno.code ?? ""));
1190
+ // Unknown Linux libc: try the glibc binary and let its require fail normally.
1191
+ return false;
1547
1192
  }
1548
- function mapWorkerError(response) {
1549
- const code = typeof response.code === "string" ? response.code : "";
1550
- const errno = typeof response.errno === "number" ? response.errno : undefined;
1551
- const message = typeof response.message === "string" && response.message
1552
- ? response.message
1553
- : "pinned helper failed";
1554
- const tooLarge = message.match(/fs-safe-too-large:(\d+):(\d+)/);
1555
- if (tooLarge) {
1556
- const [, limit, got] = tooLarge;
1557
- return new errors_FsSafeError("too-large", `file exceeds limit of ${limit} bytes (got at least ${got})`);
1558
- }
1559
- if (message.includes("fs-safe-not-file")) {
1560
- return new errors_FsSafeError("not-file", "not a file");
1561
- }
1562
- if (message.includes("fs-safe-source-mismatch")) {
1563
- return new errors_FsSafeError("path-mismatch", "source path changed during copy");
1193
+ function targetFor(platform, arch, musl) {
1194
+ if (platform === "win32" && arch === "x64")
1195
+ return "win32-x64-msvc";
1196
+ if (platform === "darwin" && arch === "x64")
1197
+ return "darwin-x64";
1198
+ if (platform === "darwin" && arch === "arm64")
1199
+ return "darwin-arm64";
1200
+ if (platform === "linux" && (arch === "x64" || arch === "arm64")) {
1201
+ return `linux-${arch}-${musl ? "musl" : "gnu"}`;
1564
1202
  }
1565
- if (message.includes("fs-safe-temp-mismatch")) {
1566
- return new errors_FsSafeError("path-mismatch", "temp path changed during write");
1567
- }
1568
- if (message.includes("fs-safe-root-mismatch")) {
1569
- return new errors_FsSafeError("path-mismatch", "root path changed during operation");
1570
- }
1571
- if (message.includes("fs-safe-directory-noreplace-unsupported")) {
1572
- return new errors_FsSafeError("invalid-path", "directory moves require overwrite: true");
1573
- }
1574
- if (code === "FileNotFoundError" || errno === 2) {
1575
- return new errors_FsSafeError("not-found", "file not found");
1576
- }
1577
- if (code === "FileExistsError" || errno === 17) {
1578
- return new errors_FsSafeError("already-exists", message);
1579
- }
1580
- if (errno === 39) {
1581
- return new errors_FsSafeError("not-empty", "directory is not empty");
1203
+ return undefined;
1204
+ }
1205
+ function bundledTarget() {
1206
+ return targetFor(process.platform, process.arch, isMusl());
1207
+ }
1208
+ function loadBundledBinding() {
1209
+ const target = bundledTarget();
1210
+ if (!target) {
1211
+ throw new Error(`Unsupported OS or architecture: ${process.platform}-${process.arch}`);
1582
1212
  }
1583
- if (errno === 1 || errno === 13 || errno === 21) {
1584
- return new errors_FsSafeError("not-removable", "path is not removable under root");
1213
+ return native_require((0,external_node_path_namespaceObject.join)(bundledNativeDirectory, target, "fs-safe-native.node"));
1214
+ }
1215
+ function __loadBundledNativeForTest() {
1216
+ return loadBundledBinding();
1217
+ }
1218
+ function __nativeLoaderDetectorsForTest() {
1219
+ return {
1220
+ report: isMuslFromReport(),
1221
+ filesystem: isMuslFromFilesystem(),
1222
+ elfInterpreter: isMuslFromElfInterpreter(),
1223
+ };
1224
+ }
1225
+ function __nativeTargetForTest(platform, arch, musl = false) {
1226
+ return targetFor(platform, arch, musl);
1227
+ }
1228
+ function getNativeBinding() {
1229
+ const { mode } = getFsSafeNativeConfig();
1230
+ if (mode === "off")
1231
+ return undefined;
1232
+ if (!attempted) {
1233
+ attempted = true;
1234
+ try {
1235
+ native_binding = loadBinding();
1236
+ }
1237
+ catch (error) {
1238
+ native_loadError = error;
1239
+ }
1585
1240
  }
1586
- if (code === "NotADirectoryError" || code === "OSError" || errno === 20 || errno === 40) {
1587
- return new errors_FsSafeError("path-alias", message);
1241
+ if (native_binding)
1242
+ return native_binding;
1243
+ if (mode === "require") {
1244
+ throw new errors_FsSafeError("helper-unavailable", "native fs-safe helper is unavailable", {
1245
+ cause: native_loadError,
1246
+ });
1588
1247
  }
1589
- return new errors_FsSafeError("helper-failed", message);
1248
+ return undefined;
1590
1249
  }
1591
- function rejectPending(error, targetWorker = worker) {
1592
- if (!targetWorker || worker !== targetWorker) {
1593
- return;
1250
+ function requireNativeBinding() {
1251
+ const native = getNativeBinding();
1252
+ if (!native) {
1253
+ throw new FsSafeError("helper-unavailable", "native fs-safe helper is unavailable", {
1254
+ cause: native_loadError,
1255
+ });
1594
1256
  }
1595
- setWorkerRef(targetWorker, false);
1596
- for (const pending of targetWorker.pending.values()) {
1597
- pending.reject(error);
1257
+ return native;
1258
+ }
1259
+ function __setNativeLoaderForTest(loader) {
1260
+ native_binding = undefined;
1261
+ native_loadError = undefined;
1262
+ attempted = false;
1263
+ loadBinding = loader;
1264
+ }
1265
+ function __resetNativeLoaderForTest() {
1266
+ native_binding = undefined;
1267
+ native_loadError = undefined;
1268
+ attempted = false;
1269
+ loadBinding = loadBundledBinding;
1270
+ }
1271
+
1272
+ ;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/native-operations.js
1273
+
1274
+
1275
+
1276
+
1277
+
1278
+ function nativeOpenFlags(flags) {
1279
+ const closeOnExec = external_node_fs_namespaceObject.constants.O_CLOEXEC;
1280
+ return (flags |
1281
+ (closeOnExec ?? 0) |
1282
+ (typeof external_node_fs_namespaceObject.constants.O_NOFOLLOW === "number" ? external_node_fs_namespaceObject.constants.O_NOFOLLOW : 0));
1283
+ }
1284
+ function writeAll(fd, data) {
1285
+ let offset = 0;
1286
+ while (offset < data.byteLength) {
1287
+ const written = external_node_fs_namespaceObject.writeSync(fd, data, offset, data.byteLength - offset);
1288
+ if (written <= 0) {
1289
+ throw Object.assign(new Error("native file write made no progress"), { code: "EIO" });
1290
+ }
1291
+ offset += written;
1598
1292
  }
1599
- targetWorker.pending.clear();
1600
- worker = null;
1601
1293
  }
1602
- function handleWorkerLine(currentWorker, line) {
1603
- if (worker !== currentWorker || !line.trim()) {
1294
+ function wrapNativeFd(fd, containment) {
1295
+ let open = true;
1296
+ return {
1297
+ fd,
1298
+ containment,
1299
+ async close() {
1300
+ if (open) {
1301
+ open = false;
1302
+ external_node_fs_namespaceObject.closeSync(fd);
1303
+ }
1304
+ },
1305
+ async stat() {
1306
+ return external_node_fs_namespaceObject.fstatSync(fd);
1307
+ },
1308
+ async writeFile(data, encoding) {
1309
+ writeAll(fd, Buffer.isBuffer(data) ? data : Buffer.from(data, encoding ?? "utf8"));
1310
+ },
1311
+ };
1312
+ }
1313
+ function removeNativeCreatedFileIfStillPinned(params) {
1314
+ if (!params.created) {
1604
1315
  return;
1605
1316
  }
1606
- let decoded;
1607
1317
  try {
1608
- decoded = JSON.parse(line);
1318
+ const parentPathStat = external_node_fs_namespaceObject.lstatSync(params.parentPath);
1319
+ const parentFdStat = params.binding.fstatIdentity(params.parentFd);
1320
+ const targetPath = external_node_path_namespaceObject.join(params.parentPath, params.basename);
1321
+ const target = external_node_fs_namespaceObject.lstatSync(targetPath);
1322
+ if (!parentPathStat.isSymbolicLink() &&
1323
+ parentPathStat.dev === parentFdStat.dev &&
1324
+ parentPathStat.ino === parentFdStat.ino &&
1325
+ !target.isSymbolicLink() &&
1326
+ file_identity_sameFileIdentity(target, params.created)) {
1327
+ external_node_fs_namespaceObject.rmSync(targetPath);
1328
+ }
1609
1329
  }
1610
1330
  catch {
1611
- rejectPending(new errors_FsSafeError("helper-failed", `pinned helper returned invalid JSON: ${line}`), currentWorker);
1612
- return;
1331
+ // Failed cleanup leaves a fail-closed artifact instead of deleting an
1332
+ // unverified pathname.
1613
1333
  }
1614
- if (typeof decoded !== "object" || decoded === null || !("id" in decoded)) {
1615
- rejectPending(new errors_FsSafeError("helper-failed", "pinned helper returned invalid response"), currentWorker);
1616
- return;
1617
- }
1618
- const response = decoded;
1619
- const id = typeof response.id === "number" ? response.id : undefined;
1620
- if (id === undefined) {
1621
- return;
1334
+ }
1335
+ async function createNativeExclusiveFile(targetPath, mode) {
1336
+ const binding = getNativeBinding();
1337
+ if (!binding) {
1338
+ return undefined;
1622
1339
  }
1623
- const pending = currentWorker.pending.get(id);
1624
- if (!pending) {
1625
- return;
1340
+ const parentPath = external_node_path_namespaceObject.dirname(targetPath);
1341
+ const basename = external_node_path_namespaceObject.basename(targetPath);
1342
+ const parent = await promises_namespaceObject.open(parentPath, external_node_fs_namespaceObject.constants.O_RDONLY |
1343
+ (typeof external_node_fs_namespaceObject.constants.O_DIRECTORY === "number" ? external_node_fs_namespaceObject.constants.O_DIRECTORY : 0));
1344
+ let fd;
1345
+ let created;
1346
+ try {
1347
+ const opened = binding.openBeneath(parent.fd, basename, nativeOpenFlags(external_node_fs_namespaceObject.constants.O_WRONLY | external_node_fs_namespaceObject.constants.O_CREAT | external_node_fs_namespaceObject.constants.O_EXCL));
1348
+ fd = opened.fd;
1349
+ external_node_fs_namespaceObject.fchmodSync(fd, mode);
1350
+ created = external_node_fs_namespaceObject.fstatSync(fd);
1351
+ return wrapNativeFd(fd, opened.containment);
1626
1352
  }
1627
- currentWorker.pending.delete(id);
1628
- if (currentWorker.pending.size === 0) {
1629
- setWorkerRef(currentWorker, false);
1353
+ catch (error) {
1354
+ if (fd !== undefined) {
1355
+ try {
1356
+ external_node_fs_namespaceObject.closeSync(fd);
1357
+ }
1358
+ catch {
1359
+ // Preserve the original error.
1360
+ }
1361
+ removeNativeCreatedFileIfStillPinned({
1362
+ binding,
1363
+ parentPath,
1364
+ parentFd: parent.fd,
1365
+ basename,
1366
+ created,
1367
+ });
1368
+ }
1369
+ throw error;
1630
1370
  }
1631
- if (response.ok === true) {
1632
- pending.resolve(response.result);
1633
- return;
1371
+ finally {
1372
+ await parent.close().catch(() => undefined);
1634
1373
  }
1635
- pending.reject(mapWorkerError(decoded));
1636
1374
  }
1637
- function getWorker() {
1638
- assertPinnedHelperSupported();
1639
- if (worker) {
1640
- return worker;
1375
+ function syncNativeFileBestEffort(fd) {
1376
+ try {
1377
+ external_node_fs_namespaceObject.fsyncSync(fd);
1641
1378
  }
1642
- const child = (0,external_node_child_process_namespaceObject.spawn)(resolvePython(), ["-u", "-c", PINNED_PYTHON_WORKER_SOURCE], {
1643
- stdio: ["pipe", "pipe", "pipe"],
1644
- });
1645
- const currentWorker = { child, pending: new Map(), stderr: "", stdoutBuffer: "" };
1646
- worker = currentWorker;
1647
- child.stdout.setEncoding("utf8");
1648
- child.stderr.setEncoding("utf8");
1649
- child.stdout.on("data", (chunk) => {
1650
- if (worker !== currentWorker) {
1651
- return;
1652
- }
1653
- currentWorker.stdoutBuffer += chunk;
1654
- for (;;) {
1655
- const newline = currentWorker.stdoutBuffer.indexOf("\n");
1656
- if (newline < 0) {
1657
- break;
1658
- }
1659
- const line = currentWorker.stdoutBuffer.slice(0, newline);
1660
- currentWorker.stdoutBuffer = currentWorker.stdoutBuffer.slice(newline + 1);
1661
- handleWorkerLine(currentWorker, line);
1662
- }
1663
- });
1664
- child.stderr.on("data", (chunk) => {
1665
- if (worker === currentWorker) {
1666
- currentWorker.stderr = `${currentWorker.stderr}${chunk}`.slice(-4096);
1379
+ catch (error) {
1380
+ if (error.code !== "EPERM") {
1381
+ throw error;
1667
1382
  }
1668
- });
1669
- child.once("error", (error) => {
1670
- const mapped = isSpawnUnavailable(error)
1671
- ? new errors_FsSafeError("helper-unavailable", "Python helper is unavailable", { cause: error })
1672
- : error instanceof Error
1673
- ? error
1674
- : new Error(String(error));
1675
- rejectPending(mapped, currentWorker);
1676
- });
1677
- child.once("close", (code, signal) => {
1678
- const stderr = currentWorker.stderr.trim();
1679
- rejectPending(new errors_FsSafeError("helper-failed", stderr || `pinned helper exited with code ${code ?? "null"} (${signal ?? "?"})`), currentWorker);
1680
- });
1681
- process.once("exit", () => {
1682
- child.kill("SIGTERM");
1683
- });
1684
- setWorkerRef(currentWorker, false);
1685
- return currentWorker;
1383
+ }
1686
1384
  }
1687
- function setRefable(value, ref) {
1688
- if (!value) {
1689
- return;
1385
+ function writeNativeFd(fd, data) {
1386
+ writeAll(fd, data);
1387
+ }
1388
+
1389
+ ;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/native-pinned-write.js
1390
+
1391
+
1392
+
1393
+
1394
+
1395
+
1396
+ function assertWithinMaxBytes(bytes, maxBytes) {
1397
+ if (maxBytes !== undefined && bytes > maxBytes) {
1398
+ throw new errors_FsSafeError("too-large", `file exceeds limit of ${maxBytes} bytes (got at least ${bytes})`);
1690
1399
  }
1691
- const method = ref ? "ref" : "unref";
1692
- const refable = value;
1693
- refable[method]?.();
1694
- }
1695
- function setWorkerRef(currentWorker, ref) {
1696
- setRefable(currentWorker.child, ref);
1697
- setRefable(currentWorker.child.stdin, ref);
1698
- setRefable(currentWorker.child.stdout, ref);
1699
- setRefable(currentWorker.child.stderr, ref);
1700
- }
1701
- async function runPinnedPythonOperation(params) {
1702
- const requestId = nextRequestId++;
1703
- const currentWorker = getWorker();
1704
- if (typeof currentWorker.child.stdin?.write !== "function") {
1705
- throw new errors_FsSafeError("helper-unavailable", "Python helper stdin is unavailable");
1706
- }
1707
- setWorkerRef(currentWorker, true);
1708
- return await new Promise((resolve, reject) => {
1709
- currentWorker.pending.set(requestId, {
1710
- reject,
1711
- resolve: (value) => resolve(value),
1712
- });
1713
- const request = JSON.stringify({
1714
- id: requestId,
1715
- operation: params.operation,
1716
- rootPath: params.rootPath,
1717
- payload: params.payload,
1718
- });
1719
- currentWorker.child.stdin.write(`${request}\n`, (error) => {
1720
- if (error) {
1721
- currentWorker.pending.delete(requestId);
1722
- if (currentWorker.pending.size === 0) {
1723
- setWorkerRef(currentWorker, false);
1724
- }
1725
- reject(error);
1726
- }
1727
- });
1728
- });
1729
1400
  }
1730
- function assertPinnedPythonOperationAvailable() {
1731
- const currentWorker = getWorker();
1732
- if (typeof currentWorker.child.stdin?.write !== "function") {
1733
- throw new errors_FsSafeError("helper-unavailable", "Python helper stdin is unavailable");
1401
+ async function inputToBuffer(input, maxBytes) {
1402
+ if (input.kind === "buffer") {
1403
+ const data = typeof input.data === "string"
1404
+ ? Buffer.from(input.data, input.encoding ?? "utf8")
1405
+ : Buffer.from(input.data);
1406
+ assertWithinMaxBytes(data.byteLength, maxBytes);
1407
+ return data;
1408
+ }
1409
+ const chunks = [];
1410
+ let bytes = 0;
1411
+ for await (const chunk of input.stream) {
1412
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
1413
+ bytes += buffer.byteLength;
1414
+ assertWithinMaxBytes(bytes, maxBytes);
1415
+ chunks.push(buffer);
1416
+ }
1417
+ return Buffer.concat(chunks, bytes);
1418
+ }
1419
+ function native_pinned_write_nativeOpenFlags(flags) {
1420
+ const closeOnExec = external_node_fs_namespaceObject.constants.O_CLOEXEC;
1421
+ return (flags |
1422
+ (closeOnExec ?? 0) |
1423
+ (typeof external_node_fs_namespaceObject.constants.O_NOFOLLOW === "number" ? external_node_fs_namespaceObject.constants.O_NOFOLLOW : 0));
1424
+ }
1425
+ function sameNativeIdentity(left, right) {
1426
+ return left.dev === right.dev && left.ino === right.ino;
1427
+ }
1428
+ async function runPinnedWriteNative(binding, params) {
1429
+ const data = await inputToBuffer(params.input, params.maxBytes);
1430
+ const root = await promises_namespaceObject.open(params.rootPath, external_node_fs_namespaceObject.constants.O_RDONLY |
1431
+ (typeof external_node_fs_namespaceObject.constants.O_DIRECTORY === "number" ? external_node_fs_namespaceObject.constants.O_DIRECTORY : 0));
1432
+ let parentFd;
1433
+ let tempFd;
1434
+ let targetFd;
1435
+ let tempIdentity;
1436
+ let parentPath = params.rootPath;
1437
+ const tempName = `.${params.basename}.${(0,external_node_crypto_namespaceObject.randomUUID)()}.native.tmp`;
1438
+ let renamed = false;
1439
+ try {
1440
+ const rootIdentity = binding.fstatIdentity(root.fd);
1441
+ if (params.rootIdentity && !sameNativeIdentity(params.rootIdentity, rootIdentity)) {
1442
+ throw new errors_FsSafeError("path-mismatch", "root path changed during native write");
1443
+ }
1444
+ if (params.mkdir) {
1445
+ binding.mkdirBeneath(root.fd, params.relativeParentPath, 0o777);
1446
+ }
1447
+ const parentFlags = external_node_fs_namespaceObject.constants.O_RDONLY |
1448
+ (typeof external_node_fs_namespaceObject.constants.O_DIRECTORY === "number" ? external_node_fs_namespaceObject.constants.O_DIRECTORY : 0);
1449
+ parentFd = binding.openBeneath(root.fd, params.relativeParentPath, parentFlags).fd;
1450
+ parentPath = await promises_namespaceObject.realpath(params.relativeParentPath
1451
+ ? external_node_path_namespaceObject.join(params.rootPath, ...params.relativeParentPath.split("/"))
1452
+ : params.rootPath);
1453
+ const parentPathStat = await promises_namespaceObject.lstat(parentPath);
1454
+ const parentIdentity = binding.fstatIdentity(parentFd);
1455
+ if (parentPathStat.isSymbolicLink() ||
1456
+ !sameNativeIdentity(parentPathStat, parentIdentity)) {
1457
+ throw new errors_FsSafeError("path-mismatch", "native write parent changed during resolution");
1458
+ }
1459
+ try {
1460
+ await promises_namespaceObject.lstat(external_node_path_namespaceObject.join(parentPath, params.basename));
1461
+ throw Object.assign(new Error("destination already exists"), { code: "EEXIST" });
1462
+ }
1463
+ catch (error) {
1464
+ if (error.code !== "ENOENT") {
1465
+ throw error;
1466
+ }
1467
+ }
1468
+ tempFd = binding.openBeneath(parentFd, tempName, native_pinned_write_nativeOpenFlags(external_node_fs_namespaceObject.constants.O_WRONLY | external_node_fs_namespaceObject.constants.O_CREAT | external_node_fs_namespaceObject.constants.O_EXCL)).fd;
1469
+ external_node_fs_namespaceObject.fchmodSync(tempFd, params.mode || 0o600);
1470
+ writeNativeFd(tempFd, data);
1471
+ syncNativeFileBestEffort(tempFd);
1472
+ tempIdentity = external_node_fs_namespaceObject.fstatSync(tempFd);
1473
+ binding.renameNoReplace(parentFd, tempName, parentFd, params.basename);
1474
+ renamed = true;
1475
+ targetFd = binding.openBeneath(parentFd, params.basename, native_pinned_write_nativeOpenFlags(external_node_fs_namespaceObject.constants.O_RDONLY)).fd;
1476
+ const targetIdentity = binding.fstatIdentity(targetFd);
1477
+ if (!targetIdentity.isFile || !sameNativeIdentity(tempIdentity, targetIdentity)) {
1478
+ throw new errors_FsSafeError("path-mismatch", "native write target changed after rename");
1479
+ }
1480
+ syncNativeFileBestEffort(parentFd);
1481
+ return { dev: targetIdentity.dev, ino: targetIdentity.ino };
1482
+ }
1483
+ finally {
1484
+ if (targetFd !== undefined) {
1485
+ external_node_fs_namespaceObject.closeSync(targetFd);
1486
+ }
1487
+ if (tempFd !== undefined) {
1488
+ external_node_fs_namespaceObject.closeSync(tempFd);
1489
+ }
1490
+ if (!renamed && parentFd !== undefined) {
1491
+ removeNativeCreatedFileIfStillPinned({
1492
+ binding,
1493
+ parentPath,
1494
+ parentFd,
1495
+ basename: tempName,
1496
+ created: tempIdentity,
1497
+ });
1498
+ }
1499
+ if (parentFd !== undefined) {
1500
+ external_node_fs_namespaceObject.closeSync(parentFd);
1501
+ }
1502
+ await root.close().catch(() => undefined);
1734
1503
  }
1735
1504
  }
1505
+
1506
+ ;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/pinned-operation.js
1507
+
1736
1508
  function validatePinnedOperationPayload(payload) {
1737
1509
  if (typeof payload.relativePath === "string") {
1738
1510
  validatePinnedRelativePath(payload.relativePath);
@@ -1747,9 +1519,6 @@ function validatePinnedOperationPayload(payload) {
1747
1519
  validatePinnedRelativePath(payload.to);
1748
1520
  }
1749
1521
  }
1750
- function isPinnedHelperUnavailable(error) {
1751
- return error instanceof Error && "code" in error && error.code === "helper-unavailable";
1752
- }
1753
1522
  function validatePinnedRelativePath(relativePath) {
1754
1523
  if (relativePath.length === 0 || relativePath === ".") {
1755
1524
  return;
@@ -1771,47 +1540,91 @@ function validatePinnedRelativePath(relativePath) {
1771
1540
  }
1772
1541
  }
1773
1542
 
1774
- ;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/pinned-helper.js
1543
+ ;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/bounded-read.js
1775
1544
 
1776
1545
 
1777
- async function runPinnedHelper(operation, rootDir, payload) {
1778
- validatePinnedOperationPayload(payload);
1779
- return await runPinnedPythonOperation({
1780
- operation,
1781
- rootPath: rootDir,
1782
- payload,
1783
- });
1784
- }
1785
- async function helperStat(rootDir, relativePath) {
1786
- return await runPinnedHelper("stat", rootDir, { relativePath });
1546
+ const READ_CHUNK_BYTES = 64 * 1024;
1547
+ function assertMaxBytes(maxBytes) {
1548
+ if (maxBytes === Number.POSITIVE_INFINITY) {
1549
+ return;
1550
+ }
1551
+ if (!Number.isSafeInteger(maxBytes) || maxBytes < 0) {
1552
+ throw new RangeError("maxBytes must be a non-negative safe integer or Infinity");
1553
+ }
1787
1554
  }
1788
- async function helperReaddir(rootDir, relativePath, withFileTypes) {
1789
- return await runPinnedHelper("readdir", rootDir, {
1790
- relativePath,
1791
- withFileTypes,
1792
- });
1555
+ function createScratchBuffer(maxBytes) {
1556
+ const initialReadBytes = Number.isFinite(maxBytes)
1557
+ ? Math.min(READ_CHUNK_BYTES, maxBytes + 1)
1558
+ : READ_CHUNK_BYTES;
1559
+ return Buffer.allocUnsafe(Math.max(1, initialReadBytes));
1793
1560
  }
1794
-
1795
- ;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/pinned-path.js
1796
-
1797
-
1798
-
1799
- function isPinnedPathHelperSpawnError(error) {
1800
- return canFallbackFromPythonError(error);
1561
+ function nextReadLength(total, maxBytes, capacity) {
1562
+ return Number.isFinite(maxBytes)
1563
+ ? Math.min(capacity, maxBytes - total + 1)
1564
+ : capacity;
1801
1565
  }
1802
- async function runPinnedPathHelper(params) {
1803
- try {
1804
- await runPinnedHelper(params.operation, params.rootPath, {
1805
- relativePath: params.relativePath,
1806
- });
1566
+ function appendChunk(params) {
1567
+ const total = params.total + params.bytesRead;
1568
+ if (total > params.maxBytes) {
1569
+ throw new errors_FsSafeError("too-large", `file exceeds limit of ${params.maxBytes} bytes (got at least ${total})`);
1807
1570
  }
1808
- catch (error) {
1809
- if (error instanceof errors_FsSafeError) {
1810
- throw error;
1571
+ params.chunks.push(Buffer.from(params.scratch.subarray(0, params.bytesRead)));
1572
+ return total;
1573
+ }
1574
+ async function readBoundedAsync(maxBytes, readChunk) {
1575
+ assertMaxBytes(maxBytes);
1576
+ const chunks = [];
1577
+ const scratch = createScratchBuffer(maxBytes);
1578
+ let total = 0;
1579
+ while (true) {
1580
+ const length = nextReadLength(total, maxBytes, scratch.length);
1581
+ const bytesRead = await readChunk(scratch, length);
1582
+ if (bytesRead === 0) {
1583
+ return Buffer.concat(chunks, total);
1811
1584
  }
1812
- throw new errors_FsSafeError("helper-failed", "pinned path helper failed", {
1813
- cause: error instanceof Error ? error : undefined,
1585
+ total = appendChunk({ chunks, scratch, bytesRead, total, maxBytes });
1586
+ }
1587
+ }
1588
+ /**
1589
+ * Reads from the handle's current offset without closing it. A bounded read
1590
+ * consumes at most maxBytes + 1 bytes so growth after an earlier stat cannot
1591
+ * force an unbounded allocation.
1592
+ */
1593
+ async function readFileHandleBounded(handle, maxBytes) {
1594
+ return await readBoundedAsync(maxBytes, async (scratch, length) => {
1595
+ return (await handle.read(scratch, 0, length, null)).bytesRead;
1596
+ });
1597
+ }
1598
+ function readDescriptorChunk(fd, scratch, length) {
1599
+ return new Promise((resolve, reject) => {
1600
+ fs.read(fd, scratch, 0, length, null, (error, bytesRead) => {
1601
+ if (error) {
1602
+ reject(error);
1603
+ return;
1604
+ }
1605
+ resolve(bytesRead);
1814
1606
  });
1607
+ });
1608
+ }
1609
+ /** Async bounded read from a numeric descriptor. The caller owns the descriptor. */
1610
+ async function readFileDescriptorBounded(fd, maxBytes) {
1611
+ return await readBoundedAsync(maxBytes, async (scratch, length) => {
1612
+ return await readDescriptorChunk(fd, scratch, length);
1613
+ });
1614
+ }
1615
+ /** Sync bounded read from a numeric descriptor. The caller owns the descriptor. */
1616
+ function readFileDescriptorBoundedSync(fd, maxBytes) {
1617
+ assertMaxBytes(maxBytes);
1618
+ const chunks = [];
1619
+ const scratch = createScratchBuffer(maxBytes);
1620
+ let total = 0;
1621
+ while (true) {
1622
+ const length = nextReadLength(total, maxBytes, scratch.length);
1623
+ const bytesRead = fs.readSync(fd, scratch, 0, length, null);
1624
+ if (bytesRead === 0) {
1625
+ return Buffer.concat(chunks, total);
1626
+ }
1627
+ total = appendChunk({ chunks, scratch, bytesRead, total, maxBytes });
1815
1628
  }
1816
1629
  }
1817
1630
 
@@ -1819,6 +1632,11 @@ async function runPinnedPathHelper(params) {
1819
1632
 
1820
1633
 
1821
1634
 
1635
+
1636
+
1637
+
1638
+
1639
+ const MAX_LOCK_PAYLOAD_BYTES = 1024 * 1024;
1822
1640
  const SIDECAR_LOCK_OWNERSHIP_TOKEN_BYTES = 16;
1823
1641
  const SIDECAR_LOCK_OWNERSHIP_TOKEN_BITS = SIDECAR_LOCK_OWNERSHIP_TOKEN_BYTES * 8;
1824
1642
  const SIDECAR_LOCK_OWNERSHIP_TOKEN_PREFIX = "\t".repeat(8);
@@ -1842,28 +1660,98 @@ function serializeSidecarLockPayload(payload) {
1842
1660
  ownershipToken,
1843
1661
  };
1844
1662
  }
1845
- async function readSidecarLockSnapshot(lockPath) {
1663
+ function relativeSidecarLockPath(lockRoot, lockPath) {
1664
+ const resolved = external_node_path_namespaceObject.resolve(lockPath);
1665
+ const lexicalRelative = external_node_path_namespaceObject.relative(lockRoot.rootDir, resolved);
1666
+ const relative = lexicalRelative !== ".." &&
1667
+ !lexicalRelative.startsWith(`..${external_node_path_namespaceObject.sep}`) &&
1668
+ !external_node_path_namespaceObject.isAbsolute(lexicalRelative)
1669
+ ? lexicalRelative
1670
+ : external_node_path_namespaceObject.relative(lockRoot.rootReal, resolved);
1671
+ if (!relative || relative === ".." || relative.startsWith(`..${external_node_path_namespaceObject.sep}`) || external_node_path_namespaceObject.isAbsolute(relative)) {
1672
+ throw new errors_FsSafeError("outside-workspace", "sidecar lock path is outside lockRoot");
1673
+ }
1674
+ return relative.split(external_node_path_namespaceObject.sep).join(external_node_path_namespaceObject.posix.sep);
1675
+ }
1676
+ function parseSidecarLockPayload(raw, parser) {
1677
+ if (parser) {
1678
+ return parser(raw);
1679
+ }
1846
1680
  try {
1681
+ const parsed = JSON.parse(raw);
1682
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
1683
+ }
1684
+ catch {
1685
+ return null;
1686
+ }
1687
+ }
1688
+ async function readSidecarLockSnapshot(lockPath, options = {}) {
1689
+ try {
1690
+ if (options.lockRoot) {
1691
+ const opened = await options.lockRoot.open(relativeSidecarLockPath(options.lockRoot, lockPath));
1692
+ try {
1693
+ const raw = (await readFileHandleBounded(opened.handle, MAX_LOCK_PAYLOAD_BYTES)).toString("utf8");
1694
+ return {
1695
+ raw,
1696
+ payload: parseSidecarLockPayload(raw, options.parsePayload),
1697
+ stat: opened.stat,
1698
+ };
1699
+ }
1700
+ finally {
1701
+ await opened.handle.close().catch(() => undefined);
1702
+ }
1703
+ }
1847
1704
  const stat = await promises_namespaceObject.lstat(lockPath);
1848
1705
  const raw = await promises_namespaceObject.readFile(lockPath, "utf8");
1849
- try {
1850
- const parsed = JSON.parse(raw);
1851
- const payload = parsed && typeof parsed === "object" && !Array.isArray(parsed)
1852
- ? parsed
1853
- : null;
1854
- return { raw, payload, stat };
1855
- }
1856
- catch {
1857
- return { raw, payload: null, stat };
1858
- }
1706
+ return { raw, payload: parseSidecarLockPayload(raw, options.parsePayload), stat };
1859
1707
  }
1860
1708
  catch (err) {
1861
- if (err.code === "ENOENT") {
1709
+ if (err.code === "ENOENT" ||
1710
+ (err instanceof errors_FsSafeError && err.code === "not-found")) {
1862
1711
  return null;
1863
1712
  }
1864
1713
  throw err;
1865
1714
  }
1866
1715
  }
1716
+ function readSidecarLockSnapshotSync(lockPath, parsePayload) {
1717
+ let fd;
1718
+ try {
1719
+ const before = fsSync.lstatSync(lockPath);
1720
+ if (!before.isFile() || before.isSymbolicLink())
1721
+ return null;
1722
+ const noFollow = process.platform !== "win32" && typeof fsSync.constants.O_NOFOLLOW === "number"
1723
+ ? fsSync.constants.O_NOFOLLOW
1724
+ : 0;
1725
+ fd = fsSync.openSync(lockPath, fsSync.constants.O_RDONLY | noFollow);
1726
+ const opened = fsSync.fstatSync(fd);
1727
+ const raw = fsSync.readFileSync(fd, "utf8");
1728
+ const after = fsSync.lstatSync(lockPath);
1729
+ if (!sameFileIdentity(before, opened) || !sameFileIdentity(opened, after))
1730
+ return null;
1731
+ return {
1732
+ raw,
1733
+ payload: parseSidecarLockPayload(raw, parsePayload),
1734
+ stat: after,
1735
+ ownershipToken: readSidecarLockOwnershipToken(raw),
1736
+ };
1737
+ }
1738
+ catch (error) {
1739
+ if (error.code === "ENOENT")
1740
+ return null;
1741
+ throw error;
1742
+ }
1743
+ finally {
1744
+ if (fd !== undefined)
1745
+ fsSync.closeSync(fd);
1746
+ }
1747
+ }
1748
+ function removeSidecarLockIfUnchangedSync(lockPath, observed) {
1749
+ const current = readSidecarLockSnapshotSync(lockPath);
1750
+ if (!current || !sidecarLockSnapshotMatches(current, observed))
1751
+ return false;
1752
+ fsSync.rmSync(lockPath);
1753
+ return true;
1754
+ }
1867
1755
  function sidecarLockSnapshotMatches(current, observed) {
1868
1756
  if (observed.ownershipToken !== undefined) {
1869
1757
  return (current.stat?.isFile() === true &&
@@ -1881,16 +1769,21 @@ function sidecarLockSnapshotMatches(current, observed) {
1881
1769
  }
1882
1770
  return observed.stat !== undefined && current.stat !== undefined;
1883
1771
  }
1884
- async function removeSidecarLockIfUnchanged(lockPath, observed) {
1885
- const current = await readSidecarLockSnapshot(lockPath);
1772
+ async function removeSidecarLockIfUnchanged(lockPath, observed, options = {}) {
1773
+ const current = await readSidecarLockSnapshot(lockPath, options);
1886
1774
  if (!current || !observed || !sidecarLockSnapshotMatches(current, observed)) {
1887
1775
  return false;
1888
1776
  }
1889
- await promises_namespaceObject.rm(lockPath, { force: true }).catch(() => undefined);
1777
+ if (options.lockRoot) {
1778
+ await options.lockRoot.remove(relativeSidecarLockPath(options.lockRoot, lockPath)).catch(() => undefined);
1779
+ }
1780
+ else {
1781
+ await promises_namespaceObject.rm(lockPath, { force: true }).catch(() => undefined);
1782
+ }
1890
1783
  return true;
1891
1784
  }
1892
- async function sidecarLockSnapshotStillPresent(lockPath, observed) {
1893
- const current = await readSidecarLockSnapshot(lockPath);
1785
+ async function sidecarLockSnapshotStillPresent(lockPath, observed, options = {}) {
1786
+ const current = await readSidecarLockSnapshot(lockPath, options);
1894
1787
  return !!current && !!observed && sidecarLockSnapshotMatches(current, observed);
1895
1788
  }
1896
1789
  async function sidecarReclaimGuardExists(pathname) {
@@ -1926,7 +1819,8 @@ async function removeStaleSidecarLockIfAllowed(params) {
1926
1819
  if (!params.shouldRemoveStaleLock || params.snapshot.raw === undefined) {
1927
1820
  return "not-approved";
1928
1821
  }
1929
- if (!(await sidecarLockSnapshotStillPresent(params.lockPath, params.snapshot))) {
1822
+ const ioOptions = { lockRoot: params.lockRoot, parsePayload: params.parsePayload };
1823
+ if (!(await sidecarLockSnapshotStillPresent(params.lockPath, params.snapshot, ioOptions))) {
1930
1824
  return "changed";
1931
1825
  }
1932
1826
  if (!(await params.shouldRemoveStaleLock({
@@ -1937,11 +1831,16 @@ async function removeStaleSidecarLockIfAllowed(params) {
1937
1831
  }))) {
1938
1832
  return "not-approved";
1939
1833
  }
1940
- if (!(await sidecarLockSnapshotStillPresent(params.lockPath, params.snapshot))) {
1834
+ if (!(await sidecarLockSnapshotStillPresent(params.lockPath, params.snapshot, ioOptions))) {
1941
1835
  return "changed";
1942
1836
  }
1943
1837
  try {
1944
- await promises_namespaceObject.rm(params.lockPath);
1838
+ if (params.lockRoot) {
1839
+ await params.lockRoot.remove(relativeSidecarLockPath(params.lockRoot, params.lockPath));
1840
+ }
1841
+ else {
1842
+ await promises_namespaceObject.rm(params.lockPath);
1843
+ }
1945
1844
  return "removed";
1946
1845
  }
1947
1846
  catch (err) {
@@ -1952,12 +1851,77 @@ async function removeStaleSidecarLockIfAllowed(params) {
1952
1851
  }
1953
1852
  }
1954
1853
 
1854
+ ;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/sidecar-lock-handle.js
1855
+
1856
+ function createSidecarLockHandle(params) {
1857
+ let released = false;
1858
+ const release = async () => {
1859
+ if (released)
1860
+ return;
1861
+ released = true;
1862
+ await params.release();
1863
+ };
1864
+ return {
1865
+ lockPath: params.lockPath,
1866
+ normalizedTargetPath: params.normalizedTargetPath,
1867
+ verifyStillHeld: params.verifyStillHeld,
1868
+ release,
1869
+ [Symbol.asyncDispose]: release,
1870
+ };
1871
+ }
1872
+ function createHeldSidecarLockHandle(params) {
1873
+ return createSidecarLockHandle({
1874
+ lockPath: params.held.lockPath,
1875
+ normalizedTargetPath: params.normalizedTargetPath,
1876
+ verifyStillHeld: async () => await sidecarLockSnapshotStillPresent(params.held.lockPath, params.held.snapshot, {
1877
+ lockRoot: params.held.lockRoot,
1878
+ parsePayload: params.held.parsePayload,
1879
+ }),
1880
+ release: params.release,
1881
+ });
1882
+ }
1883
+
1884
+ ;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/sidecar-lock-policy.js
1885
+
1886
+ function computeSidecarLockDelayMs(retry, attempt) {
1887
+ const minTimeout = retry.minTimeout ?? 50;
1888
+ const maxTimeout = retry.maxTimeout ?? 1000;
1889
+ const factor = retry.factor ?? 1;
1890
+ const base = Math.min(maxTimeout, Math.max(minTimeout, minTimeout * factor ** attempt));
1891
+ const jitter = retry.randomize ? 1 + Math.random() : 1;
1892
+ return Math.min(maxTimeout, Math.round(base * jitter));
1893
+ }
1894
+ function sidecarLockPayloadIsStale(payload, staleMs, nowMs) {
1895
+ const createdAt = payload &&
1896
+ typeof payload === "object" &&
1897
+ "createdAt" in payload &&
1898
+ typeof payload.createdAt === "string"
1899
+ ? payload.createdAt
1900
+ : "";
1901
+ const createdAtMs = Date.parse(createdAt);
1902
+ return Number.isFinite(createdAtMs) && nowMs - createdAtMs > staleMs;
1903
+ }
1904
+ async function defaultSidecarLockShouldReclaim(params) {
1905
+ if (sidecarLockPayloadIsStale(params.payload, params.staleMs, params.nowMs))
1906
+ return true;
1907
+ try {
1908
+ return params.nowMs - (await promises_namespaceObject.stat(params.lockPath)).mtimeMs > params.staleMs;
1909
+ }
1910
+ catch {
1911
+ return true;
1912
+ }
1913
+ }
1914
+
1955
1915
  ;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/sidecar-lock.js
1956
1916
 
1957
1917
 
1958
1918
 
1959
1919
 
1960
1920
 
1921
+
1922
+
1923
+
1924
+
1961
1925
  const GLOBAL_STATE_KEY = Symbol.for("fsSafe.sidecarLockManagers");
1962
1926
  function getGlobalManagers() {
1963
1927
  const globalWithState = globalThis;
@@ -1983,6 +1947,9 @@ function resolveManagerState(key) {
1983
1947
  // Backfill state created by fs-safe versions that predate reclaim guards.
1984
1948
  state.reclaimCleanupRegistered ??= false;
1985
1949
  state.reclaimGuards ??= new Set();
1950
+ for (const held of state.held.values()) {
1951
+ held.refCount ??= 1;
1952
+ }
1986
1953
  }
1987
1954
  return state;
1988
1955
  }
@@ -2038,28 +2005,6 @@ async function resolveNormalizedTargetPath(targetPath) {
2038
2005
  return resolved;
2039
2006
  }
2040
2007
  }
2041
- function computeDelayMs(retry, attempt) {
2042
- const minTimeout = retry.minTimeout ?? 50;
2043
- const maxTimeout = retry.maxTimeout ?? 1000;
2044
- const factor = retry.factor ?? 1;
2045
- const base = Math.min(maxTimeout, Math.max(minTimeout, minTimeout * factor ** attempt));
2046
- const jitter = retry.randomize ? 1 + Math.random() : 1;
2047
- return Math.min(maxTimeout, Math.round(base * jitter));
2048
- }
2049
- async function defaultShouldReclaim(params) {
2050
- const createdAt = typeof params.payload?.createdAt === "string" ? params.payload.createdAt : "";
2051
- const createdAtMs = Date.parse(createdAt);
2052
- if (Number.isFinite(createdAtMs) && params.nowMs - createdAtMs > params.staleMs) {
2053
- return true;
2054
- }
2055
- try {
2056
- const stat = await promises_namespaceObject.stat(params.lockPath);
2057
- return params.nowMs - stat.mtimeMs > params.staleMs;
2058
- }
2059
- catch {
2060
- return true;
2061
- }
2062
- }
2063
2008
  function releaseAllReclaimGuardsSync(state) {
2064
2009
  for (const reclaimGuardPath of state.reclaimGuards) {
2065
2010
  try {
@@ -2075,7 +2020,7 @@ function releaseAllLocksSync(state) {
2075
2020
  for (const [normalizedTargetPath, held] of state.held) {
2076
2021
  void held.handle.close().catch(() => undefined);
2077
2022
  try {
2078
- if (snapshotMatchesSync(held.lockPath, held.snapshot)) {
2023
+ if (!held.lockRoot && snapshotMatchesSync(held.lockPath, held.snapshot)) {
2079
2024
  external_node_fs_namespaceObject.rmSync(held.lockPath, { force: true });
2080
2025
  }
2081
2026
  }
@@ -2086,17 +2031,17 @@ function releaseAllLocksSync(state) {
2086
2031
  }
2087
2032
  releaseAllReclaimGuardsSync(state);
2088
2033
  }
2089
- async function releaseHeldLock(state, normalizedTargetPath, held, opts = {}) {
2034
+ async function releaseHeldLock(state, normalizedTargetPath, held, options = {}) {
2090
2035
  const current = state.held.get(normalizedTargetPath);
2091
2036
  if (current !== held) {
2092
2037
  return false;
2093
2038
  }
2094
- if (opts.force) {
2095
- held.count = 0;
2039
+ if (options.force) {
2040
+ held.refCount = 0;
2096
2041
  }
2097
2042
  else {
2098
- held.count -= 1;
2099
- if (held.count > 0) {
2043
+ held.refCount -= 1;
2044
+ if (held.refCount > 0) {
2100
2045
  return false;
2101
2046
  }
2102
2047
  }
@@ -2105,9 +2050,16 @@ async function releaseHeldLock(state, normalizedTargetPath, held, opts = {}) {
2105
2050
  return true;
2106
2051
  }
2107
2052
  state.held.delete(normalizedTargetPath);
2053
+ if (held.compromiseTimer) {
2054
+ clearInterval(held.compromiseTimer);
2055
+ held.compromiseTimer = undefined;
2056
+ }
2108
2057
  held.releasePromise = (async () => {
2109
2058
  await held.handle.close().catch(() => undefined);
2110
- await removeSidecarLockIfUnchanged(held.lockPath, held.snapshot);
2059
+ await removeSidecarLockIfUnchanged(held.lockPath, held.snapshot, {
2060
+ lockRoot: held.lockRoot,
2061
+ parsePayload: held.parsePayload,
2062
+ });
2111
2063
  })();
2112
2064
  try {
2113
2065
  await held.releasePromise;
@@ -2117,6 +2069,13 @@ async function releaseHeldLock(state, normalizedTargetPath, held, opts = {}) {
2117
2069
  held.releasePromise = undefined;
2118
2070
  }
2119
2071
  }
2072
+ function handleForHeldLock(state, normalizedTargetPath, held) {
2073
+ return createHeldSidecarLockHandle({
2074
+ normalizedTargetPath,
2075
+ held,
2076
+ release: async () => await releaseHeldLock(state, normalizedTargetPath, held),
2077
+ });
2078
+ }
2120
2079
  function createSidecarLockManager(key) {
2121
2080
  const state = resolveManagerState(key);
2122
2081
  function ensureExitCleanupRegistered() {
@@ -2136,15 +2095,12 @@ function createSidecarLockManager(key) {
2136
2095
  const normalizedTargetPath = await resolveNormalizedTargetPath(options.targetPath);
2137
2096
  const lockPath = options.lockPath ?? `${normalizedTargetPath}.lock`;
2138
2097
  const held = state.held.get(normalizedTargetPath);
2139
- if (held && options.allowReentrant) {
2140
- held.count += 1;
2141
- const release = () => releaseHeldLock(state, normalizedTargetPath, held).then(() => undefined);
2142
- return {
2143
- lockPath,
2144
- normalizedTargetPath,
2145
- release,
2146
- [Symbol.asyncDispose]: release,
2147
- };
2098
+ if (held &&
2099
+ options.reentrantOwner !== undefined &&
2100
+ held.reentrantOwner !== undefined &&
2101
+ options.reentrantOwner === held.reentrantOwner) {
2102
+ held.refCount += 1;
2103
+ return handleForHeldLock(state, normalizedTargetPath, held);
2148
2104
  }
2149
2105
  const startedAt = Date.now();
2150
2106
  const retry = options.retry ?? {};
@@ -2167,7 +2123,7 @@ function createSidecarLockManager(key) {
2167
2123
  const remaining = options.timeoutMs === undefined || options.timeoutMs === Number.POSITIVE_INFINITY
2168
2124
  ? Number.POSITIVE_INFINITY
2169
2125
  : Math.max(0, options.timeoutMs - elapsed);
2170
- const delay = Math.min(computeDelayMs(retry, attempt), remaining);
2126
+ const delay = Math.min(computeSidecarLockDelayMs(retry, attempt), remaining);
2171
2127
  attempt += 1;
2172
2128
  await new Promise((resolve) => setTimeout(resolve, delay));
2173
2129
  };
@@ -2179,18 +2135,36 @@ function createSidecarLockManager(key) {
2179
2135
  }
2180
2136
  let handle = null;
2181
2137
  try {
2182
- handle = await promises_namespaceObject.open(lockPath, "wx");
2183
2138
  const payload = await options.payload();
2184
2139
  const { raw, ownershipToken } = serializeSidecarLockPayload(payload);
2185
- await handle.writeFile(raw, "utf8");
2140
+ if (options.lockRoot) {
2141
+ const relativeLockPath = relativeSidecarLockPath(options.lockRoot, lockPath);
2142
+ try {
2143
+ await options.lockRoot.create(relativeLockPath, raw, { mkdir: true, mode: 0o600 });
2144
+ }
2145
+ catch (error) {
2146
+ if (error instanceof errors_FsSafeError && error.code === "already-exists") {
2147
+ throw Object.assign(new Error("sidecar lock exists"), { code: "EEXIST" });
2148
+ }
2149
+ throw error;
2150
+ }
2151
+ handle = (await options.lockRoot.open(relativeLockPath)).handle;
2152
+ }
2153
+ else {
2154
+ handle = (await createNativeExclusiveFile(lockPath, 0o600)) ?? await promises_namespaceObject.open(lockPath, "wx");
2155
+ await handle.writeFile(raw, "utf8");
2156
+ }
2186
2157
  const snapshot = { raw, payload, stat: await handle.stat(), ownershipToken };
2187
2158
  const createdHeld = {
2188
- count: 1,
2159
+ refCount: 1,
2160
+ reentrantOwner: options.reentrantOwner,
2189
2161
  handle,
2190
2162
  lockPath,
2191
2163
  snapshot,
2192
2164
  acquiredAt: Date.now(),
2193
2165
  metadata: options.metadata ?? {},
2166
+ lockRoot: options.lockRoot,
2167
+ parsePayload: options.parsePayload,
2194
2168
  };
2195
2169
  state.held.set(normalizedTargetPath, createdHeld);
2196
2170
  if (ownsReclaimGuard) {
@@ -2203,13 +2177,21 @@ function createSidecarLockManager(key) {
2203
2177
  throw err;
2204
2178
  }
2205
2179
  }
2206
- const release = () => releaseHeldLock(state, normalizedTargetPath, createdHeld).then(() => undefined);
2207
- return {
2208
- lockPath,
2209
- normalizedTargetPath,
2210
- release,
2211
- [Symbol.asyncDispose]: release,
2212
- };
2180
+ const returnedHandle = handleForHeldLock(state, normalizedTargetPath, createdHeld);
2181
+ const interval = options.compromiseCheckIntervalMs;
2182
+ if (options.onCompromised && interval !== undefined && interval > 0) {
2183
+ createdHeld.compromiseTimer = setInterval(() => {
2184
+ void returnedHandle.verifyStillHeld().then((stillHeld) => {
2185
+ if (!stillHeld && createdHeld.compromiseTimer) {
2186
+ clearInterval(createdHeld.compromiseTimer);
2187
+ createdHeld.compromiseTimer = undefined;
2188
+ options.onCompromised?.({ lockPath, normalizedTargetPath });
2189
+ }
2190
+ });
2191
+ }, interval);
2192
+ createdHeld.compromiseTimer.unref();
2193
+ }
2194
+ return returnedHandle;
2213
2195
  }
2214
2196
  catch (err) {
2215
2197
  if (handle) {
@@ -2226,11 +2208,16 @@ function createSidecarLockManager(key) {
2226
2208
  }
2227
2209
  // If payload serialization/write fails, the file may be empty or
2228
2210
  // partial JSON, so remove while our exclusive handle is still open.
2229
- await promises_namespaceObject.rm(lockPath, { force: true }).catch(() => undefined);
2211
+ if (!options.lockRoot) {
2212
+ await promises_namespaceObject.rm(lockPath, { force: true }).catch(() => undefined);
2213
+ }
2230
2214
  await handle.close().catch(() => undefined);
2231
2215
  // Windows can refuse removing an open file; retry after close but
2232
2216
  // only if the path still points at the file identity we created.
2233
- await removeSidecarLockIfUnchanged(lockPath, failedSnapshot);
2217
+ await removeSidecarLockIfUnchanged(lockPath, failedSnapshot, {
2218
+ lockRoot: options.lockRoot,
2219
+ parsePayload: options.parsePayload,
2220
+ });
2234
2221
  }
2235
2222
  if (err.code !== "EEXIST") {
2236
2223
  throw err;
@@ -2241,11 +2228,18 @@ function createSidecarLockManager(key) {
2241
2228
  continue;
2242
2229
  }
2243
2230
  const nowMs = Date.now();
2244
- const snapshot = await readSidecarLockSnapshot(lockPath);
2231
+ const snapshot = await readSidecarLockSnapshot(lockPath, {
2232
+ lockRoot: options.lockRoot,
2233
+ parsePayload: options.parsePayload,
2234
+ });
2245
2235
  if (!snapshot) {
2246
2236
  continue;
2247
2237
  }
2248
- const shouldReclaim = options.shouldReclaim ?? defaultShouldReclaim;
2238
+ if (state.held.has(normalizedTargetPath)) {
2239
+ await waitForRetry();
2240
+ continue;
2241
+ }
2242
+ const shouldReclaim = options.shouldReclaim ?? defaultSidecarLockShouldReclaim;
2249
2243
  if (await shouldReclaim({
2250
2244
  lockPath,
2251
2245
  normalizedTargetPath,
@@ -2254,7 +2248,10 @@ function createSidecarLockManager(key) {
2254
2248
  nowMs,
2255
2249
  heldByThisProcess: state.held.has(normalizedTargetPath),
2256
2250
  })) {
2257
- if (!(await sidecarLockSnapshotStillPresent(lockPath, snapshot))) {
2251
+ if (!(await sidecarLockSnapshotStillPresent(lockPath, snapshot, {
2252
+ lockRoot: options.lockRoot,
2253
+ parsePayload: options.parsePayload,
2254
+ }))) {
2258
2255
  continue;
2259
2256
  }
2260
2257
  const staleRecovery = options.staleRecovery ?? "fail-closed";
@@ -2269,6 +2266,8 @@ function createSidecarLockManager(key) {
2269
2266
  normalizedTargetPath,
2270
2267
  snapshot,
2271
2268
  shouldRemoveStaleLock: options.shouldRemoveStaleLock,
2269
+ lockRoot: options.lockRoot,
2270
+ parsePayload: options.parsePayload,
2272
2271
  });
2273
2272
  if (removal === "removed" || removal === "changed") {
2274
2273
  continue;
@@ -2356,6 +2355,7 @@ function __setFsSafeTestHooksForTest(hooks) {
2356
2355
 
2357
2356
 
2358
2357
 
2358
+
2359
2359
  function byteLength(input, encoding) {
2360
2360
  return typeof input === "string"
2361
2361
  ? Buffer.byteLength(input, encoding ?? "utf8")
@@ -2370,7 +2370,7 @@ function assertSafeBasename(basename) {
2370
2370
  throw new errors_FsSafeError("invalid-path", "invalid target path");
2371
2371
  }
2372
2372
  }
2373
- function assertWithinMaxBytes(bytes, maxBytes) {
2373
+ function pinned_write_assertWithinMaxBytes(bytes, maxBytes) {
2374
2374
  if (maxBytes !== undefined && bytes > maxBytes) {
2375
2375
  throw new errors_FsSafeError("too-large", `file exceeds limit of ${maxBytes} bytes (got at least ${bytes})`);
2376
2376
  }
@@ -2390,7 +2390,7 @@ async function writeStreamToHandle(stream, handle, maxBytes) {
2390
2390
  for await (const chunk of stream) {
2391
2391
  const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
2392
2392
  bytes += buffer.byteLength;
2393
- assertWithinMaxBytes(bytes, maxBytes);
2393
+ pinned_write_assertWithinMaxBytes(bytes, maxBytes);
2394
2394
  let offset = 0;
2395
2395
  while (offset < buffer.byteLength) {
2396
2396
  const { bytesWritten } = await handle.write(buffer, offset, buffer.byteLength - offset);
@@ -2401,74 +2401,21 @@ async function writeStreamToHandle(stream, handle, maxBytes) {
2401
2401
  }
2402
2402
  }
2403
2403
  }
2404
- async function inputToBase64(input, maxBytes) {
2405
- if (input.kind === "buffer") {
2406
- assertWithinMaxBytes(byteLength(input.data, input.encoding), maxBytes);
2407
- return (typeof input.data === "string"
2408
- ? Buffer.from(input.data, input.encoding ?? "utf8")
2409
- : input.data).toString("base64");
2410
- }
2411
- const chunks = [];
2412
- let bytes = 0;
2413
- for await (const chunk of input.stream) {
2414
- const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
2415
- bytes += buffer.byteLength;
2416
- assertWithinMaxBytes(bytes, maxBytes);
2417
- chunks.push(buffer);
2418
- }
2419
- return Buffer.concat(chunks, bytes).toString("base64");
2420
- }
2421
2404
  async function runPinnedWriteHelper(params) {
2422
2405
  assertSafeBasename(params.basename);
2423
2406
  validatePinnedOperationPayload({
2424
2407
  relativeParentPath: params.relativeParentPath,
2425
2408
  });
2426
- // The Python helper deliberately enforces the strict post-rename inode
2427
- // contract. The explicit compatibility policy therefore uses the guarded
2428
- // Node fallback, where content verification can replace that one check.
2409
+ // The explicit compatibility policy uses the guarded Node fallback, where
2410
+ // content verification can replace the strict post-rename inode check.
2429
2411
  if (params.onRenameIdentityMismatch === "verify-content") {
2430
2412
  return await runPinnedWriteFallback(params);
2431
2413
  }
2432
- if (getFsSafePythonConfig().mode === "off") {
2433
- return await runPinnedWriteFallback(params);
2434
- }
2435
- if (params.input.kind === "stream") {
2436
- try {
2437
- assertPinnedPythonOperationAvailable();
2438
- }
2439
- catch (error) {
2440
- if (canFallbackFromPythonError(error)) {
2441
- return await runPinnedWriteFallback(params);
2442
- }
2443
- throw error;
2444
- }
2445
- }
2446
- const input = params.input.kind === "stream"
2447
- ? { kind: "buffer", data: Buffer.from(await inputToBase64(params.input, params.maxBytes), "base64") }
2448
- : params.input;
2449
- const payload = {
2450
- base64: await inputToBase64(input, params.maxBytes),
2451
- basename: params.basename,
2452
- maxBytes: params.maxBytes ?? -1,
2453
- mkdir: params.mkdir,
2454
- mode: params.mode || 0o600,
2455
- overwrite: params.overwrite !== false,
2456
- relativeParentPath: params.relativeParentPath,
2457
- ...(params.rootIdentity ? { rootDev: params.rootIdentity.dev, rootIno: params.rootIdentity.ino } : {}),
2458
- };
2459
- try {
2460
- return await runPinnedPythonOperation({
2461
- operation: "write",
2462
- rootPath: params.rootPath,
2463
- payload,
2464
- });
2465
- }
2466
- catch (error) {
2467
- if (canFallbackFromPythonError(error)) {
2468
- return await runPinnedWriteFallback({ ...params, input });
2469
- }
2470
- throw error;
2414
+ const native = getNativeBinding();
2415
+ if (native && params.overwrite === false) {
2416
+ return await runPinnedWriteNative(native, params);
2471
2417
  }
2418
+ return await runPinnedWriteFallback(params);
2472
2419
  }
2473
2420
  async function runPinnedWriteWithRenamePolicy(params) {
2474
2421
  const { targetPath, renameIdentity, ...writeParams } = params;
@@ -2491,34 +2438,19 @@ async function runPinnedWriteWithRenamePolicy(params) {
2491
2438
  onRenameIdentityMismatch: "verify-content",
2492
2439
  }));
2493
2440
  }
2494
- async function runPinnedCopyHelper(params) {
2495
- assertSafeBasename(params.basename);
2496
- validatePinnedOperationPayload({
2497
- relativeParentPath: params.relativeParentPath,
2498
- });
2499
- return await runPinnedPythonOperation({
2500
- operation: "copy",
2501
- rootPath: params.rootPath,
2502
- payload: {
2503
- basename: params.basename,
2504
- maxBytes: params.maxBytes ?? -1,
2505
- mkdir: params.mkdir,
2506
- mode: params.mode || 0o600,
2507
- overwrite: params.overwrite !== false,
2508
- relativeParentPath: params.relativeParentPath,
2509
- ...(params.rootIdentity ? { rootDev: params.rootIdentity.dev, rootIno: params.rootIdentity.ino } : {}),
2510
- sourceDev: params.sourceIdentity.dev,
2511
- sourceIno: params.sourceIdentity.ino,
2512
- sourcePath: params.sourcePath,
2513
- },
2514
- });
2515
- }
2516
2441
  async function runPinnedWriteFallback(params) {
2517
- const parentPath = params.relativeParentPath
2442
+ let parentPath = params.relativeParentPath
2518
2443
  ? external_node_path_namespaceObject.join(params.rootPath, ...params.relativeParentPath.split("/"))
2519
2444
  : params.rootPath;
2520
2445
  if (params.mkdir) {
2521
- await mkdirPathComponentsWithGuards({ rootReal: params.rootPath, targetPath: parentPath });
2446
+ // mkdirPathComponentsWithGuards may resolve the final component through
2447
+ // an in-root symlink (e.g. a skill-bank layout). Use its returned real
2448
+ // path for the subsequent guard and target path so we don't re-check the
2449
+ // original, possibly-symlinked, lexical path and reject it outright.
2450
+ parentPath = await mkdirPathComponentsWithGuards({
2451
+ rootReal: params.rootPath,
2452
+ targetPath: parentPath,
2453
+ });
2522
2454
  }
2523
2455
  const parentGuard = params.mkdir
2524
2456
  ? await directory_guard_createAsyncDirectoryGuard(parentPath)
@@ -2534,8 +2466,9 @@ async function runPinnedWriteFallback(params) {
2534
2466
  });
2535
2467
  let created = true;
2536
2468
  try {
2469
+ await handle.chmod(params.mode);
2537
2470
  if (params.input.kind === "buffer") {
2538
- assertWithinMaxBytes(byteLength(params.input.data, params.input.encoding), params.maxBytes);
2471
+ pinned_write_assertWithinMaxBytes(byteLength(params.input.data, params.input.encoding), params.maxBytes);
2539
2472
  if (typeof params.input.data === "string") {
2540
2473
  await handle.writeFile(params.input.data, params.input.encoding ?? "utf8");
2541
2474
  }
@@ -2573,8 +2506,9 @@ async function runPinnedWriteFallback(params) {
2573
2506
  let renamed = false;
2574
2507
  try {
2575
2508
  handle = await promises_namespaceObject.open(tempPath, tempFlags, params.mode);
2509
+ await handle.chmod(params.mode);
2576
2510
  if (params.input.kind === "buffer") {
2577
- assertWithinMaxBytes(byteLength(params.input.data, params.input.encoding), params.maxBytes);
2511
+ pinned_write_assertWithinMaxBytes(byteLength(params.input.data, params.input.encoding), params.maxBytes);
2578
2512
  if (typeof params.input.data === "string") {
2579
2513
  await handle.writeFile(params.input.data, params.input.encoding ?? "utf8");
2580
2514
  }
@@ -2657,12 +2591,85 @@ async function runPinnedWriteFallback(params) {
2657
2591
 
2658
2592
  ;// CONCATENATED MODULE: external "node:os"
2659
2593
  const external_node_os_namespaceObject = __rspack_createRequire_require("node:os");
2594
+ ;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/root-path-existing.js
2595
+
2596
+
2597
+
2598
+
2599
+ function isFilesystemRoot(candidate) {
2600
+ return external_node_path_namespaceObject.parse(candidate).root === candidate;
2601
+ }
2602
+ async function root_path_existing_pathExists(targetPath) {
2603
+ try {
2604
+ await promises_namespaceObject.lstat(targetPath);
2605
+ return true;
2606
+ }
2607
+ catch (error) {
2608
+ if (path_isNotFoundPathError(error)) {
2609
+ return false;
2610
+ }
2611
+ throw error;
2612
+ }
2613
+ }
2614
+ async function root_path_existing_resolvePathViaExistingAncestor(targetPath) {
2615
+ const normalized = external_node_path_namespaceObject.resolve(targetPath);
2616
+ let cursor = normalized;
2617
+ const missingSuffix = [];
2618
+ while (!isFilesystemRoot(cursor) && !(await root_path_existing_pathExists(cursor))) {
2619
+ missingSuffix.unshift(external_node_path_namespaceObject.basename(cursor));
2620
+ const parent = external_node_path_namespaceObject.dirname(cursor);
2621
+ if (parent === cursor) {
2622
+ break;
2623
+ }
2624
+ cursor = parent;
2625
+ }
2626
+ if (!(await root_path_existing_pathExists(cursor))) {
2627
+ return normalized;
2628
+ }
2629
+ try {
2630
+ const resolvedAncestor = external_node_path_namespaceObject.resolve(await promises_namespaceObject.realpath(cursor));
2631
+ return missingSuffix.length === 0
2632
+ ? resolvedAncestor
2633
+ : external_node_path_namespaceObject.resolve(resolvedAncestor, ...missingSuffix);
2634
+ }
2635
+ catch {
2636
+ return normalized;
2637
+ }
2638
+ }
2639
+ function root_path_existing_resolvePathViaExistingAncestorSync(targetPath) {
2640
+ const normalized = path.resolve(targetPath);
2641
+ let cursor = normalized;
2642
+ const missingSuffix = [];
2643
+ while (!isFilesystemRoot(cursor) && !fs.existsSync(cursor)) {
2644
+ missingSuffix.unshift(path.basename(cursor));
2645
+ const parent = path.dirname(cursor);
2646
+ if (parent === cursor) {
2647
+ break;
2648
+ }
2649
+ cursor = parent;
2650
+ }
2651
+ if (!fs.existsSync(cursor)) {
2652
+ return normalized;
2653
+ }
2654
+ try {
2655
+ const resolvedAncestor = path.resolve(fs.realpathSync(cursor));
2656
+ return missingSuffix.length === 0
2657
+ ? resolvedAncestor
2658
+ : path.resolve(resolvedAncestor, ...missingSuffix);
2659
+ }
2660
+ catch {
2661
+ return normalized;
2662
+ }
2663
+ }
2664
+
2660
2665
  ;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/root-path.js
2661
2666
 
2662
2667
 
2663
2668
 
2664
2669
 
2665
2670
 
2671
+
2672
+
2666
2673
  const ROOT_PATH_ALIAS_POLICIES = {
2667
2674
  strict: Object.freeze({
2668
2675
  allowFinalSymlinkForUnlink: false,
@@ -2678,7 +2685,7 @@ async function resolveRootPath(params) {
2678
2685
  const absolutePath = external_node_path_namespaceObject.resolve(params.absolutePath);
2679
2686
  const rootCanonicalPath = params.rootCanonicalPath
2680
2687
  ? external_node_path_namespaceObject.resolve(params.rootCanonicalPath)
2681
- : await root_path_resolvePathViaExistingAncestor(rootPath);
2688
+ : await root_path_existing_resolvePathViaExistingAncestor(rootPath);
2682
2689
  const context = createBoundaryResolutionContext({
2683
2690
  resolveParams: params,
2684
2691
  rootPath,
@@ -2740,15 +2747,40 @@ function isPromiseLike(value) {
2740
2747
  typeof value.then === "function");
2741
2748
  }
2742
2749
  function createLexicalTraversalState(params) {
2743
- const relative = external_node_path_namespaceObject.relative(params.rootPath, params.absolutePath);
2750
+ const rawAbsolutePath = params.params.absolutePath;
2751
+ const rawRelativePath = rawPathRelativeToRoot(params.rootPath, rawAbsolutePath);
2752
+ const relative = rawRelativePath ?? external_node_path_namespaceObject.relative(params.rootPath, params.absolutePath);
2744
2753
  return {
2745
- segments: relative.split(external_node_path_namespaceObject.sep).filter(Boolean),
2754
+ segments: splitTraversalSegments(relative),
2746
2755
  allowFinalSymlink: params.params.policy?.allowFinalSymlinkForUnlink === true,
2747
2756
  canonicalCursor: params.rootCanonicalPath,
2748
2757
  lexicalCursor: params.rootPath,
2749
2758
  preserveFinalSymlink: false,
2750
2759
  };
2751
2760
  }
2761
+ function splitTraversalSegments(value) {
2762
+ return value
2763
+ .split(process.platform === "win32" ? /[\\/]+/ : /\/+/)
2764
+ .filter((segment) => Boolean(segment) && segment !== ".");
2765
+ }
2766
+ function rawPathRelativeToRoot(rootPath, candidatePath) {
2767
+ if (!external_node_path_namespaceObject.isAbsolute(candidatePath)) {
2768
+ return undefined;
2769
+ }
2770
+ const root = external_node_path_namespaceObject.resolve(rootPath);
2771
+ const candidate = process.platform === "win32"
2772
+ ? candidatePath.replaceAll("/", external_node_path_namespaceObject.sep)
2773
+ : candidatePath;
2774
+ if (candidate === root) {
2775
+ return "";
2776
+ }
2777
+ const rootWithSep = root.endsWith(external_node_path_namespaceObject.sep) ? root : `${root}${external_node_path_namespaceObject.sep}`;
2778
+ const candidatePrefix = candidate.slice(0, rootWithSep.length);
2779
+ const prefixMatches = process.platform === "win32"
2780
+ ? candidatePrefix.toLowerCase() === rootWithSep.toLowerCase()
2781
+ : candidatePrefix === rootWithSep;
2782
+ return prefixMatches ? candidate.slice(rootWithSep.length) : undefined;
2783
+ }
2752
2784
  function assertLexicalCursorInsideBoundary(params) {
2753
2785
  assertInsideBoundary({
2754
2786
  boundaryLabel: params.params.boundaryLabel,
@@ -2759,13 +2791,15 @@ function assertLexicalCursorInsideBoundary(params) {
2759
2791
  }
2760
2792
  function applyMissingSuffixToCanonicalCursor(params) {
2761
2793
  const missingSuffix = params.state.segments.slice(params.missingFromIndex);
2762
- params.state.canonicalCursor = external_node_path_namespaceObject.resolve(params.state.canonicalCursor, ...missingSuffix);
2763
- assertLexicalCursorInsideBoundary({
2764
- params: params.params,
2765
- rootCanonicalPath: params.rootCanonicalPath,
2766
- candidatePath: params.state.canonicalCursor,
2767
- absolutePath: params.absolutePath,
2768
- });
2794
+ for (const segment of missingSuffix) {
2795
+ advanceCanonicalCursorForSegment({
2796
+ state: params.state,
2797
+ segment,
2798
+ rootCanonicalPath: params.rootCanonicalPath,
2799
+ params: params.params,
2800
+ absolutePath: params.absolutePath,
2801
+ });
2802
+ }
2769
2803
  }
2770
2804
  function advanceCanonicalCursorForSegment(params) {
2771
2805
  params.state.canonicalCursor = external_node_path_namespaceObject.resolve(params.state.canonicalCursor, params.segment);
@@ -2881,11 +2915,20 @@ function resolveAndApplySymlinkHop(params) {
2881
2915
  boundaryLabel: params.boundaryLabel,
2882
2916
  });
2883
2917
  }
2918
+ function applyParentTraversalStep(params) {
2919
+ params.state.lexicalCursor = external_node_path_namespaceObject.resolve(params.state.lexicalCursor, "..");
2920
+ advanceCanonicalCursorForSegment({
2921
+ state: params.state,
2922
+ segment: "..",
2923
+ rootCanonicalPath: params.rootCanonicalPath,
2924
+ params: params.resolveParams,
2925
+ absolutePath: params.absolutePath,
2926
+ });
2927
+ }
2884
2928
  function* iterateLexicalTraversal(state) {
2885
2929
  for (let idx = 0; idx < state.segments.length; idx += 1) {
2886
2930
  const segment = state.segments[idx] ?? "";
2887
2931
  const isLast = idx === state.segments.length - 1;
2888
- state.lexicalCursor = external_node_path_namespaceObject.join(state.lexicalCursor, segment);
2889
2932
  yield { idx, segment, isLast };
2890
2933
  }
2891
2934
  }
@@ -2898,6 +2941,14 @@ async function resolveRootPathLexicalAsync(params) {
2898
2941
  absolutePath: params.absolutePath,
2899
2942
  };
2900
2943
  for (const { idx, segment, isLast } of iterateLexicalTraversal(state)) {
2944
+ if (segment === "..") {
2945
+ applyParentTraversalStep({
2946
+ ...sharedStepParams,
2947
+ resolveParams: params.params,
2948
+ });
2949
+ continue;
2950
+ }
2951
+ state.lexicalCursor = external_node_path_namespaceObject.join(state.lexicalCursor, segment);
2901
2952
  const stat = await readLexicalStat({
2902
2953
  ...sharedStepParams,
2903
2954
  missingFromIndex: idx,
@@ -2925,7 +2976,7 @@ async function resolveRootPathLexicalAsync(params) {
2925
2976
  resolveLinkCanonical: (cursor) => resolveSymlinkHopPath(cursor),
2926
2977
  });
2927
2978
  }
2928
- const kind = await getPathKind(params.absolutePath, state.preserveFinalSymlink);
2979
+ const kind = await getPathKind(state.canonicalCursor, state.preserveFinalSymlink);
2929
2980
  return finalizeLexicalResolution({
2930
2981
  ...params,
2931
2982
  state,
@@ -2937,6 +2988,15 @@ function resolveRootPathLexicalSync(params) {
2937
2988
  for (let idx = 0; idx < state.segments.length; idx += 1) {
2938
2989
  const segment = state.segments[idx] ?? "";
2939
2990
  const isLast = idx === state.segments.length - 1;
2991
+ if (segment === "..") {
2992
+ applyParentTraversalStep({
2993
+ state,
2994
+ rootCanonicalPath: params.rootCanonicalPath,
2995
+ resolveParams: params.params,
2996
+ absolutePath: params.absolutePath,
2997
+ });
2998
+ continue;
2999
+ }
2940
3000
  state.lexicalCursor = path.join(state.lexicalCursor, segment);
2941
3001
  const maybeStat = readLexicalStat({
2942
3002
  state,
@@ -2978,7 +3038,7 @@ function resolveRootPathLexicalSync(params) {
2978
3038
  throw new Error("Unexpected async symlink resolution");
2979
3039
  }
2980
3040
  }
2981
- const kind = getPathKindSync(params.absolutePath, state.preserveFinalSymlink);
3041
+ const kind = getPathKindSync(state.canonicalCursor, state.preserveFinalSymlink);
2982
3042
  return finalizeLexicalResolution({
2983
3043
  ...params,
2984
3044
  state,
@@ -3047,7 +3107,7 @@ async function resolveOutsideLexicalCanonicalPathAsync(params) {
3047
3107
  if (path_isPathInside(params.rootPath, params.absolutePath)) {
3048
3108
  return undefined;
3049
3109
  }
3050
- return await root_path_resolvePathViaExistingAncestor(params.absolutePath);
3110
+ return await root_path_existing_resolvePathViaExistingAncestor(params.absolutePath);
3051
3111
  }
3052
3112
  function resolveOutsideLexicalCanonicalPathSync(params) {
3053
3113
  if (isPathInside(params.rootPath, params.absolutePath)) {
@@ -3094,60 +3154,6 @@ function buildResolvedRootPath(params) {
3094
3154
  kind: params.kind.kind,
3095
3155
  };
3096
3156
  }
3097
- async function root_path_resolvePathViaExistingAncestor(targetPath) {
3098
- const normalized = external_node_path_namespaceObject.resolve(targetPath);
3099
- let cursor = normalized;
3100
- const missingSuffix = [];
3101
- while (!isFilesystemRoot(cursor) && !(await root_path_pathExists(cursor))) {
3102
- missingSuffix.unshift(external_node_path_namespaceObject.basename(cursor));
3103
- const parent = external_node_path_namespaceObject.dirname(cursor);
3104
- if (parent === cursor) {
3105
- break;
3106
- }
3107
- cursor = parent;
3108
- }
3109
- if (!(await root_path_pathExists(cursor))) {
3110
- return normalized;
3111
- }
3112
- try {
3113
- const resolvedAncestor = external_node_path_namespaceObject.resolve(await promises_namespaceObject.realpath(cursor));
3114
- if (missingSuffix.length === 0) {
3115
- return resolvedAncestor;
3116
- }
3117
- return external_node_path_namespaceObject.resolve(resolvedAncestor, ...missingSuffix);
3118
- }
3119
- catch {
3120
- return normalized;
3121
- }
3122
- }
3123
- function resolvePathViaExistingAncestorSync(targetPath) {
3124
- const normalized = path.resolve(targetPath);
3125
- let cursor = normalized;
3126
- const missingSuffix = [];
3127
- while (!isFilesystemRoot(cursor) && !fs.existsSync(cursor)) {
3128
- missingSuffix.unshift(path.basename(cursor));
3129
- const parent = path.dirname(cursor);
3130
- if (parent === cursor) {
3131
- break;
3132
- }
3133
- cursor = parent;
3134
- }
3135
- if (!fs.existsSync(cursor)) {
3136
- return normalized;
3137
- }
3138
- try {
3139
- // Keep sync behavior aligned with async (`fsp.realpath`) to avoid
3140
- // platform-specific canonical alias drift (notably on Windows).
3141
- const resolvedAncestor = path.resolve(fs.realpathSync(cursor));
3142
- if (missingSuffix.length === 0) {
3143
- return resolvedAncestor;
3144
- }
3145
- return path.resolve(resolvedAncestor, ...missingSuffix);
3146
- }
3147
- catch {
3148
- return normalized;
3149
- }
3150
- }
3151
3157
  async function getPathKind(absolutePath, preserveFinalSymlink) {
3152
3158
  try {
3153
3159
  const stat = preserveFinalSymlink
@@ -3215,21 +3221,6 @@ function shortPath(value) {
3215
3221
  }
3216
3222
  return value;
3217
3223
  }
3218
- function isFilesystemRoot(candidate) {
3219
- return external_node_path_namespaceObject.parse(candidate).root === candidate;
3220
- }
3221
- async function root_path_pathExists(targetPath) {
3222
- try {
3223
- await promises_namespaceObject.lstat(targetPath);
3224
- return true;
3225
- }
3226
- catch (error) {
3227
- if (path_isNotFoundPathError(error)) {
3228
- return false;
3229
- }
3230
- throw error;
3231
- }
3232
- }
3233
3224
  async function resolveSymlinkHopPath(symlinkPath) {
3234
3225
  try {
3235
3226
  return external_node_path_namespaceObject.resolve(await promises_namespaceObject.realpath(symlinkPath));
@@ -3240,7 +3231,7 @@ async function resolveSymlinkHopPath(symlinkPath) {
3240
3231
  }
3241
3232
  const linkTarget = await promises_namespaceObject.readlink(symlinkPath);
3242
3233
  const linkAbsolute = external_node_path_namespaceObject.resolve(external_node_path_namespaceObject.dirname(symlinkPath), linkTarget);
3243
- return root_path_resolvePathViaExistingAncestor(linkAbsolute);
3234
+ return root_path_existing_resolvePathViaExistingAncestor(linkAbsolute);
3244
3235
  }
3245
3236
  }
3246
3237
  function resolveSymlinkHopPathSync(symlinkPath) {
@@ -3275,7 +3266,7 @@ async function assertNoPathAliasEscape(params) {
3275
3266
  return;
3276
3267
  }
3277
3268
  await assertNoHardlinkedFinalPath({
3278
- filePath: resolved.absolutePath,
3269
+ filePath: resolved.canonicalPath,
3279
3270
  root: resolved.rootPath,
3280
3271
  boundaryLabel: params.boundaryLabel,
3281
3272
  allowFinalHardlinkForUnlink: params.policy?.allowFinalHardlinkForUnlink,
@@ -3309,8 +3300,80 @@ function path_policy_shortPath(value) {
3309
3300
  return value;
3310
3301
  }
3311
3302
 
3312
- ;// CONCATENATED MODULE: external "node:url"
3313
- const external_node_url_namespaceObject = __rspack_createRequire_require("node:url");
3303
+ ;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/string-coerce.js
3304
+ function readStringValue(value) {
3305
+ return typeof value === "string" ? value : undefined;
3306
+ }
3307
+ function normalizeNullableString(value) {
3308
+ if (typeof value !== "string") {
3309
+ return null;
3310
+ }
3311
+ const trimmed = value.trim();
3312
+ return trimmed ? trimmed : null;
3313
+ }
3314
+ function normalizeOptionalString(value) {
3315
+ return normalizeNullableString(value) ?? undefined;
3316
+ }
3317
+ function normalizeStringifiedOptionalString(value) {
3318
+ if (typeof value === "string") {
3319
+ return normalizeOptionalString(value);
3320
+ }
3321
+ if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
3322
+ return normalizeOptionalString(String(value));
3323
+ }
3324
+ return undefined;
3325
+ }
3326
+ function normalizeOptionalLowercaseString(value) {
3327
+ return normalizeOptionalString(value)?.toLowerCase();
3328
+ }
3329
+ function normalizeLowercaseStringOrEmpty(value) {
3330
+ return normalizeOptionalLowercaseString(value) ?? "";
3331
+ }
3332
+ function normalizeFastMode(raw) {
3333
+ if (typeof raw === "boolean") {
3334
+ return raw;
3335
+ }
3336
+ if (!raw) {
3337
+ return undefined;
3338
+ }
3339
+ const key = normalizeLowercaseStringOrEmpty(raw);
3340
+ if (["off", "false", "no", "0", "disable", "disabled", "normal"].includes(key)) {
3341
+ return false;
3342
+ }
3343
+ if (["on", "true", "yes", "1", "enable", "enabled", "fast"].includes(key)) {
3344
+ return true;
3345
+ }
3346
+ return undefined;
3347
+ }
3348
+ function lowercasePreservingWhitespace(value) {
3349
+ return value.toLowerCase();
3350
+ }
3351
+ function localeLowercasePreservingWhitespace(value) {
3352
+ return value.toLocaleLowerCase();
3353
+ }
3354
+ function resolvePrimaryStringValue(value) {
3355
+ if (typeof value === "string") {
3356
+ return normalizeOptionalString(value);
3357
+ }
3358
+ if (!value || typeof value !== "object") {
3359
+ return undefined;
3360
+ }
3361
+ return normalizeOptionalString(value.primary);
3362
+ }
3363
+ function normalizeOptionalThreadValue(value) {
3364
+ if (typeof value === "number") {
3365
+ return Number.isFinite(value) ? Math.trunc(value) : undefined;
3366
+ }
3367
+ return normalizeOptionalString(value);
3368
+ }
3369
+ function normalizeOptionalStringifiedId(value) {
3370
+ const normalized = normalizeOptionalThreadValue(value);
3371
+ return normalized == null ? undefined : String(normalized);
3372
+ }
3373
+ function hasNonEmptyString(value) {
3374
+ return normalizeOptionalString(value) !== undefined;
3375
+ }
3376
+
3314
3377
  ;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/local-file-access.js
3315
3378
 
3316
3379
 
@@ -3442,6 +3505,28 @@ const WINDOWS_RESERVED_DEVICE_NAMES = new Set([
3442
3505
  "LPT²",
3443
3506
  "LPT³",
3444
3507
  ]);
3508
+ const WINDOWS_SEPARATOR_CHAR_CODE = 0x5c;
3509
+ const WINDOWS_IGNORED_SPACE_CHAR_CODE = 0x20;
3510
+ const WINDOWS_IGNORED_DOT_CHAR_CODE = 0x2e;
3511
+ function trimTrailingWindowsSeparators(value) {
3512
+ let end = value.length;
3513
+ while (end > 0 && value.charCodeAt(end - 1) === WINDOWS_SEPARATOR_CHAR_CODE) {
3514
+ end -= 1;
3515
+ }
3516
+ return end === value.length ? value : value.slice(0, end);
3517
+ }
3518
+ function trimTrailingWindowsIgnoredChars(value) {
3519
+ let end = value.length;
3520
+ while (end > 0) {
3521
+ const charCode = value.charCodeAt(end - 1);
3522
+ if (charCode !== WINDOWS_IGNORED_SPACE_CHAR_CODE &&
3523
+ charCode !== WINDOWS_IGNORED_DOT_CHAR_CODE) {
3524
+ break;
3525
+ }
3526
+ end -= 1;
3527
+ }
3528
+ return end === value.length ? value : value.slice(0, end);
3529
+ }
3445
3530
  function candidateReadPaths(filePath) {
3446
3531
  if (!filePath.startsWith("file://")) {
3447
3532
  return [filePath];
@@ -3470,10 +3555,10 @@ function matchPosixDeviceReadPath(filePath, cwd) {
3470
3555
  return undefined;
3471
3556
  }
3472
3557
  function normalizeWindowsDeviceBaseName(filePath) {
3473
- const normalized = filePath.replace(/\//g, "\\").replace(/[\\]+$/g, "");
3558
+ const normalized = trimTrailingWindowsSeparators(filePath.replace(/\//g, "\\"));
3474
3559
  const lastSegment = normalized.split("\\").filter(Boolean).at(-1) ?? normalized;
3475
3560
  const withoutStream = lastSegment.split(":")[0] ?? lastSegment;
3476
- const withoutTrailingIgnoredChars = withoutStream.replace(/[ .]+$/g, "");
3561
+ const withoutTrailingIgnoredChars = trimTrailingWindowsIgnoredChars(withoutStream);
3477
3562
  return (withoutTrailingIgnoredChars.split(".")[0] ?? withoutTrailingIgnoredChars).toUpperCase();
3478
3563
  }
3479
3564
  function matchWindowsDeviceReadPath(filePath) {
@@ -3490,109 +3575,21 @@ function matchWindowsDeviceReadPath(filePath) {
3490
3575
  function matchUnsafeDeviceReadPath(filePath, options = {}) {
3491
3576
  const platform = options.platform ?? process.platform;
3492
3577
  for (const candidate of candidateReadPaths(filePath)) {
3493
- const match = platform === "win32"
3494
- ? matchWindowsDeviceReadPath(candidate)
3495
- : matchPosixDeviceReadPath(candidate, options.cwd);
3496
- if (match) {
3497
- return match;
3498
- }
3499
- }
3500
- return undefined;
3501
- }
3502
- function isUnsafeDeviceReadPath(filePath, options) {
3503
- return matchUnsafeDeviceReadPath(filePath, options) !== undefined;
3504
- }
3505
- function assertNoUnsafeDeviceReadPath(filePath, options) {
3506
- if (matchUnsafeDeviceReadPath(filePath, options)) {
3507
- throw new errors_FsSafeError("device-path", `file reads from unsafe device paths are not allowed: ${filePath}`);
3508
- }
3509
- }
3510
-
3511
- ;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/bounded-read.js
3512
-
3513
-
3514
- const READ_CHUNK_BYTES = 64 * 1024;
3515
- function assertMaxBytes(maxBytes) {
3516
- if (maxBytes === Number.POSITIVE_INFINITY) {
3517
- return;
3518
- }
3519
- if (!Number.isSafeInteger(maxBytes) || maxBytes < 0) {
3520
- throw new RangeError("maxBytes must be a non-negative safe integer or Infinity");
3521
- }
3522
- }
3523
- function createScratchBuffer(maxBytes) {
3524
- const initialReadBytes = Number.isFinite(maxBytes)
3525
- ? Math.min(READ_CHUNK_BYTES, maxBytes + 1)
3526
- : READ_CHUNK_BYTES;
3527
- return Buffer.allocUnsafe(Math.max(1, initialReadBytes));
3528
- }
3529
- function nextReadLength(total, maxBytes, capacity) {
3530
- return Number.isFinite(maxBytes)
3531
- ? Math.min(capacity, maxBytes - total + 1)
3532
- : capacity;
3533
- }
3534
- function appendChunk(params) {
3535
- const total = params.total + params.bytesRead;
3536
- if (total > params.maxBytes) {
3537
- throw new errors_FsSafeError("too-large", `file exceeds limit of ${params.maxBytes} bytes (got at least ${total})`);
3538
- }
3539
- params.chunks.push(Buffer.from(params.scratch.subarray(0, params.bytesRead)));
3540
- return total;
3541
- }
3542
- async function readBoundedAsync(maxBytes, readChunk) {
3543
- assertMaxBytes(maxBytes);
3544
- const chunks = [];
3545
- const scratch = createScratchBuffer(maxBytes);
3546
- let total = 0;
3547
- while (true) {
3548
- const length = nextReadLength(total, maxBytes, scratch.length);
3549
- const bytesRead = await readChunk(scratch, length);
3550
- if (bytesRead === 0) {
3551
- return Buffer.concat(chunks, total);
3552
- }
3553
- total = appendChunk({ chunks, scratch, bytesRead, total, maxBytes });
3554
- }
3555
- }
3556
- /**
3557
- * Reads from the handle's current offset without closing it. A bounded read
3558
- * consumes at most maxBytes + 1 bytes so growth after an earlier stat cannot
3559
- * force an unbounded allocation.
3560
- */
3561
- async function readFileHandleBounded(handle, maxBytes) {
3562
- return await readBoundedAsync(maxBytes, async (scratch, length) => {
3563
- return (await handle.read(scratch, 0, length, null)).bytesRead;
3564
- });
3565
- }
3566
- function readDescriptorChunk(fd, scratch, length) {
3567
- return new Promise((resolve, reject) => {
3568
- fs.read(fd, scratch, 0, length, null, (error, bytesRead) => {
3569
- if (error) {
3570
- reject(error);
3571
- return;
3572
- }
3573
- resolve(bytesRead);
3574
- });
3575
- });
3576
- }
3577
- /** Async bounded read from a numeric descriptor. The caller owns the descriptor. */
3578
- async function readFileDescriptorBounded(fd, maxBytes) {
3579
- return await readBoundedAsync(maxBytes, async (scratch, length) => {
3580
- return await readDescriptorChunk(fd, scratch, length);
3581
- });
3582
- }
3583
- /** Sync bounded read from a numeric descriptor. The caller owns the descriptor. */
3584
- function readFileDescriptorBoundedSync(fd, maxBytes) {
3585
- assertMaxBytes(maxBytes);
3586
- const chunks = [];
3587
- const scratch = createScratchBuffer(maxBytes);
3588
- let total = 0;
3589
- while (true) {
3590
- const length = nextReadLength(total, maxBytes, scratch.length);
3591
- const bytesRead = fs.readSync(fd, scratch, 0, length, null);
3592
- if (bytesRead === 0) {
3593
- return Buffer.concat(chunks, total);
3578
+ const match = platform === "win32"
3579
+ ? matchWindowsDeviceReadPath(candidate)
3580
+ : matchPosixDeviceReadPath(candidate, options.cwd);
3581
+ if (match) {
3582
+ return match;
3594
3583
  }
3595
- total = appendChunk({ chunks, scratch, bytesRead, total, maxBytes });
3584
+ }
3585
+ return undefined;
3586
+ }
3587
+ function isUnsafeDeviceReadPath(filePath, options) {
3588
+ return matchUnsafeDeviceReadPath(filePath, options) !== undefined;
3589
+ }
3590
+ function assertNoUnsafeDeviceReadPath(filePath, options) {
3591
+ if (matchUnsafeDeviceReadPath(filePath, options)) {
3592
+ throw new errors_FsSafeError("device-path", `file reads from unsafe device paths are not allowed: ${filePath}`);
3596
3593
  }
3597
3594
  }
3598
3595
 
@@ -3608,6 +3605,7 @@ async function read_opened_file_readOpenedFileSafely(params) {
3608
3605
  : await readFileHandleBounded(params.opened.handle, params.maxBytes);
3609
3606
  return {
3610
3607
  buffer,
3608
+ containment: params.opened.containment,
3611
3609
  realPath: params.opened.realPath,
3612
3610
  stat: params.opened.stat,
3613
3611
  };
@@ -3751,6 +3749,7 @@ function resolveOsHomeRelativePath(input, opts) {
3751
3749
 
3752
3750
 
3753
3751
 
3752
+
3754
3753
  const ensureTrailingSep = (value) => value.endsWith(external_node_path_namespaceObject.sep) ? value : value + external_node_path_namespaceObject.sep;
3755
3754
  function assertValidRootRelativePath(relativePath) {
3756
3755
  path_assertNoNulPathInput(relativePath, "relative path contains a NUL byte");
@@ -3795,13 +3794,31 @@ async function resolveRootContext(rootDir) {
3795
3794
  rootWithSep: ensureTrailingSep(rootReal),
3796
3795
  };
3797
3796
  }
3798
- async function resolvePathInRoot(root, relativePath) {
3797
+ async function resolvePathInRoot(root, relativePath, options) {
3799
3798
  assertValidRootRelativePath(relativePath);
3800
3799
  const expanded = await expandRelativePathWithHome(relativePath);
3801
3800
  const resolved = external_node_path_namespaceObject.resolve(root.rootWithSep, expanded);
3802
3801
  if (!path_isPathInside(root.rootWithSep, resolved)) {
3803
3802
  throw new errors_FsSafeError("outside-workspace", "file is outside workspace root");
3804
3803
  }
3804
+ const rawAbsolutePath = external_node_path_namespaceObject.isAbsolute(expanded)
3805
+ ? expanded
3806
+ : `${root.rootWithSep}${expanded}`;
3807
+ try {
3808
+ await resolveRootPath({
3809
+ absolutePath: rawAbsolutePath,
3810
+ rootPath: root.rootReal,
3811
+ rootCanonicalPath: root.rootReal,
3812
+ boundaryLabel: "root",
3813
+ policy: options?.allowFinalSymlink ? ROOT_PATH_ALIAS_POLICIES.unlinkTarget : undefined,
3814
+ });
3815
+ }
3816
+ catch (error) {
3817
+ const code = options?.aliasErrorCode ?? "outside-workspace";
3818
+ throw new errors_FsSafeError(code, code === "path-alias" ? "path alias escape blocked" : "file is outside workspace root", {
3819
+ cause: error instanceof Error ? error : undefined,
3820
+ });
3821
+ }
3805
3822
  return { rootReal: root.rootReal, rootWithSep: root.rootWithSep, resolved };
3806
3823
  }
3807
3824
  async function resolvePathWithinRoot(params) {
@@ -3840,14 +3857,143 @@ function stringifyJsonDocument(value, replacer, space) {
3840
3857
  return text;
3841
3858
  }
3842
3859
 
3860
+ ;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/root-walk.js
3861
+
3862
+
3863
+
3864
+ function validateBudget(name, value) {
3865
+ if (value === undefined)
3866
+ return Number.POSITIVE_INFINITY;
3867
+ if (!Number.isSafeInteger(value) || value < 0) {
3868
+ throw new RangeError(`${name} must be a non-negative safe integer`);
3869
+ }
3870
+ return value;
3871
+ }
3872
+ function entryKind(entry) {
3873
+ if (entry.isSymbolicLink)
3874
+ return "symlink";
3875
+ if (entry.isDirectory)
3876
+ return "directory";
3877
+ if (entry.isFile)
3878
+ return "file";
3879
+ return "other";
3880
+ }
3881
+ function limitEntry(relativePath) {
3882
+ return { relativePath, kind: "truncated", size: 0 };
3883
+ }
3884
+ async function* walkRoot(root, relativePath, options) {
3885
+ const maxDepth = validateBudget("maxDepth", options.maxDepth);
3886
+ const maxEntries = validateBudget("maxEntries", options.maxEntries);
3887
+ const visitedDirectories = new Set();
3888
+ let examined = 0;
3889
+ const onLimit = (atPath) => {
3890
+ if ((options.limitBehavior ?? "truncate") === "throw") {
3891
+ throw new errors_FsSafeError("too-large", `root walk budget exceeded at ${atPath || "."}`);
3892
+ }
3893
+ return limitEntry(atPath);
3894
+ };
3895
+ async function* visit(directory, depth) {
3896
+ options.signal?.throwIfAborted();
3897
+ let entries;
3898
+ try {
3899
+ const resolvedDirectory = await resolveRootPath({
3900
+ absolutePath: external_node_path_namespaceObject.resolve(root.rootReal, directory),
3901
+ rootPath: root.rootReal,
3902
+ rootCanonicalPath: root.rootReal,
3903
+ boundaryLabel: "root walk",
3904
+ });
3905
+ if (!resolvedDirectory.exists || resolvedDirectory.kind !== "directory") {
3906
+ throw new errors_FsSafeError("not-file", `root walk path is not a directory: ${directory || "."}`);
3907
+ }
3908
+ if (visitedDirectories.has(resolvedDirectory.canonicalPath)) {
3909
+ return;
3910
+ }
3911
+ visitedDirectories.add(resolvedDirectory.canonicalPath);
3912
+ const listingDirectory = external_node_path_namespaceObject.relative(root.rootReal, resolvedDirectory.canonicalPath)
3913
+ .split(external_node_path_namespaceObject.sep)
3914
+ .join(external_node_path_namespaceObject.posix.sep);
3915
+ entries = await root.list(listingDirectory, { withFileTypes: true });
3916
+ }
3917
+ catch (error) {
3918
+ if ((options.onDirectoryError ?? "throw") === "throw")
3919
+ throw error;
3920
+ yield { relativePath: directory, kind: "directory-error", size: 0, error };
3921
+ return;
3922
+ }
3923
+ for (const entry of entries) {
3924
+ options.signal?.throwIfAborted();
3925
+ const child = directory
3926
+ ? external_node_path_namespaceObject.posix.join(directory.split(external_node_path_namespaceObject.sep).join(external_node_path_namespaceObject.posix.sep), entry.name)
3927
+ : entry.name;
3928
+ if (examined >= maxEntries) {
3929
+ yield onLimit(child);
3930
+ return;
3931
+ }
3932
+ examined += 1;
3933
+ let kind = entryKind(entry);
3934
+ let size = entry.size;
3935
+ if (kind === "symlink") {
3936
+ if (options.symlinkPolicy === "skip") {
3937
+ continue;
3938
+ }
3939
+ const resolved = await resolveRootPath({
3940
+ absolutePath: external_node_path_namespaceObject.resolve(root.rootReal, child),
3941
+ rootPath: root.rootReal,
3942
+ rootCanonicalPath: root.rootReal,
3943
+ boundaryLabel: "root walk",
3944
+ });
3945
+ if (!resolved.exists) {
3946
+ continue;
3947
+ }
3948
+ kind = resolved.kind === "directory" ? "directory" : resolved.kind === "file" ? "file" : "other";
3949
+ size = entry.size;
3950
+ }
3951
+ const walkEntry = { relativePath: child, kind, size };
3952
+ const filterResult = options.entryFilter?.(walkEntry) ?? "include";
3953
+ if (!["include", "skip", "skip-subtree"].includes(filterResult)) {
3954
+ throw new TypeError(`invalid root walk entryFilter result: ${String(filterResult)}`);
3955
+ }
3956
+ if (filterResult === "include") {
3957
+ yield walkEntry;
3958
+ }
3959
+ if (kind !== "directory") {
3960
+ continue;
3961
+ }
3962
+ if (filterResult === "skip-subtree") {
3963
+ continue;
3964
+ }
3965
+ if (depth >= maxDepth) {
3966
+ yield onLimit(child);
3967
+ return;
3968
+ }
3969
+ yield* visit(child, depth + 1);
3970
+ }
3971
+ }
3972
+ yield* visit(relativePath, 0);
3973
+ }
3974
+
3843
3975
  ;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/temp-cleanup.js
3844
3976
 
3977
+
3845
3978
  const tempCleanupEntries = new Map();
3846
3979
  let cleanupRegistered = false;
3980
+ function pathStillMatchesReceipt(entry) {
3981
+ if (!entry.identity) {
3982
+ return false;
3983
+ }
3984
+ try {
3985
+ return file_identity_sameFileIdentity(external_node_fs_namespaceObject.lstatSync(entry.path), entry.identity);
3986
+ }
3987
+ catch (error) {
3988
+ return error.code === "ENOENT";
3989
+ }
3990
+ }
3847
3991
  function cleanupRegisteredTempPathsSync() {
3848
3992
  for (const entry of tempCleanupEntries.values()) {
3849
3993
  try {
3850
- external_node_fs_namespaceObject.rmSync(entry.path, { force: true, recursive: entry.recursive });
3994
+ if (pathStillMatchesReceipt(entry)) {
3995
+ external_node_fs_namespaceObject.rmSync(entry.path, { force: true, recursive: entry.recursive });
3996
+ }
3851
3997
  }
3852
3998
  catch {
3853
3999
  // Process-exit cleanup is best-effort.
@@ -3860,13 +4006,27 @@ function registerTempPathForExit(tempPath, options) {
3860
4006
  cleanupRegistered = true;
3861
4007
  process.once("exit", cleanupRegisteredTempPathsSync);
3862
4008
  }
3863
- tempCleanupEntries.set(tempPath, {
4009
+ const entry = {
3864
4010
  path: tempPath,
3865
4011
  recursive: options?.recursive === true,
3866
- });
3867
- return () => {
4012
+ identity: options?.identity,
4013
+ };
4014
+ if (!entry.identity) {
4015
+ try {
4016
+ entry.identity = external_node_fs_namespaceObject.lstatSync(tempPath);
4017
+ }
4018
+ catch {
4019
+ // Callers that register before creation set the identity after opening.
4020
+ }
4021
+ }
4022
+ tempCleanupEntries.set(tempPath, entry);
4023
+ const unregister = (() => {
3868
4024
  tempCleanupEntries.delete(tempPath);
4025
+ });
4026
+ unregister.setIdentity = (identity) => {
4027
+ entry.identity = identity;
3869
4028
  };
4029
+ return unregister;
3870
4030
  }
3871
4031
  function __cleanupRegisteredTempPathsForTest() {
3872
4032
  cleanupRegisteredTempPathsSync();
@@ -3877,7 +4037,9 @@ function __cleanupRegisteredTempPathForTest(tempPath) {
3877
4037
  return;
3878
4038
  }
3879
4039
  try {
3880
- fsSync.rmSync(entry.path, { force: true, recursive: entry.recursive });
4040
+ if (pathStillMatchesReceipt(entry)) {
4041
+ fsSync.rmSync(entry.path, { force: true, recursive: entry.recursive });
4042
+ }
3881
4043
  }
3882
4044
  finally {
3883
4045
  tempCleanupEntries.delete(tempPath);
@@ -3928,9 +4090,6 @@ async function serializePathWrite(key, run) {
3928
4090
 
3929
4091
 
3930
4092
 
3931
-
3932
-
3933
-
3934
4093
 
3935
4094
 
3936
4095
 
@@ -3961,6 +4120,7 @@ const DEFAULT_ROOT_MAX_BYTES = 16 * 1024 * 1024;
3961
4120
  function openResult(params) {
3962
4121
  return {
3963
4122
  handle: params.handle,
4123
+ containment: "best-effort",
3964
4124
  realPath: params.realPath,
3965
4125
  stat: params.stat,
3966
4126
  [Symbol.asyncDispose]: () => params.handle.close().catch(() => undefined),
@@ -4079,7 +4239,7 @@ class RootHandle {
4079
4239
  };
4080
4240
  }
4081
4241
  async resolve(relativePath) {
4082
- return (await resolvePathInRoot(this.context, relativePath)).resolved;
4242
+ return (await resolvePathInRoot(this.context, relativePath, { allowFinalSymlink: true })).resolved;
4083
4243
  }
4084
4244
  async open(relativePath, options = {}) {
4085
4245
  return await openFileInRoot(this.context, {
@@ -4218,58 +4378,34 @@ class RootHandle {
4218
4378
  }
4219
4379
  async stat(relativePath) {
4220
4380
  assertValidRootRelativePath(relativePath);
4221
- try {
4222
- return await helperStat(this.rootReal, relativePath);
4223
- }
4224
- catch (error) {
4225
- if (canFallbackFromPythonError(error)) {
4226
- return await statPathFallback(this.context, relativePath);
4227
- }
4228
- throw error;
4229
- }
4381
+ validatePinnedOperationPayload({ relativePath });
4382
+ return await statPathFallback(this.context, relativePath);
4230
4383
  }
4231
4384
  async list(relativePath, options = {}) {
4232
4385
  assertValidRootRelativePath(relativePath);
4233
- try {
4234
- return options.withFileTypes === true
4235
- ? await helperReaddir(this.rootReal, relativePath, true)
4236
- : await helperReaddir(this.rootReal, relativePath, false);
4237
- }
4238
- catch (error) {
4239
- if (canFallbackFromPythonError(error)) {
4240
- return await listPathFallback(this.context, relativePath, options.withFileTypes === true);
4241
- }
4242
- throw error;
4243
- }
4386
+ validatePinnedOperationPayload({ relativePath });
4387
+ return await listPathFallback(this.context, relativePath, options.withFileTypes === true);
4244
4388
  }
4245
4389
  async move(fromRelative, toRelative, options = {}) {
4246
4390
  assertValidRootRelativePath(fromRelative);
4247
4391
  assertValidRootRelativePath(toRelative);
4392
+ validatePinnedOperationPayload({ from: fromRelative, to: toRelative });
4248
4393
  const denyMutations = mergeDenyMutationPolicies(this.defaults.denyMutations, options.denyMutations);
4249
4394
  await assertMoveMutationAllowed(this.context, {
4250
4395
  fromRelative,
4251
4396
  toRelative,
4252
4397
  denyMutations,
4253
4398
  });
4254
- try {
4255
- await runPinnedHelper("rename", this.rootReal, {
4256
- from: fromRelative,
4257
- overwrite: options.overwrite ?? false,
4258
- to: toRelative,
4259
- });
4260
- }
4261
- catch (error) {
4262
- if (canFallbackFromPythonError(error)) {
4263
- await movePathFallback(this.context, {
4264
- fromRelative,
4265
- denyMutations,
4266
- overwrite: options.overwrite ?? false,
4267
- toRelative,
4268
- });
4269
- return;
4270
- }
4271
- throw error;
4272
- }
4399
+ await movePathFallback(this.context, {
4400
+ fromRelative,
4401
+ denyMutations,
4402
+ overwrite: options.overwrite ?? false,
4403
+ toRelative,
4404
+ });
4405
+ }
4406
+ walk(relativePath, options) {
4407
+ assertValidRootRelativePath(relativePath);
4408
+ return walkRoot(this, relativePath, options);
4273
4409
  }
4274
4410
  }
4275
4411
  function readDefaults(defaults) {
@@ -4284,7 +4420,9 @@ async function root_impl_root(rootDir, defaults = {}) {
4284
4420
  return new RootHandle(await resolveRootContext(rootDir), defaults);
4285
4421
  }
4286
4422
  async function openFileInRoot(root, params) {
4287
- const { rootWithSep, resolved } = await resolvePathInRoot(root, params.relativePath);
4423
+ const { rootWithSep, resolved } = await resolvePathInRoot(root, params.relativePath, {
4424
+ allowFinalSymlink: true,
4425
+ });
4288
4426
  let opened;
4289
4427
  try {
4290
4428
  opened = await openVerifiedLocalFile(resolved, {
@@ -4355,6 +4493,15 @@ function buildAtomicWriteTempPath(targetPath) {
4355
4493
  function rootWriteQueueKey(root, relativePath) {
4356
4494
  return `${root.rootReal}\0${relativePath}`;
4357
4495
  }
4496
+ async function prepareRootWriteTarget(rootReal, targetPath) {
4497
+ const parentPath = await mkdirPathComponentsWithGuards({
4498
+ rootReal,
4499
+ targetPath: external_node_path_namespaceObject.dirname(targetPath),
4500
+ });
4501
+ // Continue through the guarded walk's real parent instead of re-entering
4502
+ // the original path through a symlinked component.
4503
+ return external_node_path_namespaceObject.join(parentPath, external_node_path_namespaceObject.basename(targetPath));
4504
+ }
4358
4505
  async function writeTempFileForAtomicReplace(params) {
4359
4506
  const tempHandle = await promises_namespaceObject.open(params.tempPath, OPEN_WRITE_CREATE_FLAGS, params.mode);
4360
4507
  try {
@@ -4385,7 +4532,7 @@ async function verifyAtomicWriteResult(params) {
4385
4532
  }
4386
4533
  }
4387
4534
  async function openWritableFileInRoot(root, params) {
4388
- const { rootReal, rootWithSep, resolved } = await resolvePathInRoot(root, params.relativePath);
4535
+ const { rootReal, rootWithSep, resolved } = await resolvePathInRoot(root, params.relativePath, { aliasErrorCode: "path-alias" });
4389
4536
  await assertMutationNotDenied(resolved, params.denyMutations);
4390
4537
  try {
4391
4538
  await assertNoPathAliasEscape({
@@ -4397,15 +4544,11 @@ async function openWritableFileInRoot(root, params) {
4397
4544
  catch (err) {
4398
4545
  throw new errors_FsSafeError("path-alias", "path alias escape blocked", { cause: err });
4399
4546
  }
4400
- if (params.mkdir !== false) {
4401
- const parentGuard = await directory_guard_createNearestExistingDirectoryGuard(rootReal, external_node_path_namespaceObject.dirname(resolved));
4402
- await withAsyncDirectoryGuards([parentGuard], async () => {
4403
- await promises_namespaceObject.mkdir(external_node_path_namespaceObject.dirname(resolved), { recursive: true });
4404
- });
4405
- }
4406
- let ioPath = resolved;
4547
+ let ioPath = params.mkdir === false
4548
+ ? resolved
4549
+ : await prepareRootWriteTarget(rootReal, resolved);
4407
4550
  try {
4408
- const resolvedRealPath = await promises_namespaceObject.realpath(resolved);
4551
+ const resolvedRealPath = await promises_namespaceObject.realpath(ioPath);
4409
4552
  if (!path_isPathInside(rootWithSep, resolvedRealPath)) {
4410
4553
  throw new errors_FsSafeError("outside-workspace", "file is outside workspace root");
4411
4554
  }
@@ -4490,6 +4633,7 @@ async function openWritableFileInRoot(root, params) {
4490
4633
  }
4491
4634
  return {
4492
4635
  handle,
4636
+ containment: "best-effort",
4493
4637
  createdForWrite,
4494
4638
  realPath,
4495
4639
  stat,
@@ -4545,44 +4689,22 @@ async function appendFileInRoot(root, params) {
4545
4689
  }
4546
4690
  }
4547
4691
  async function removePathInRoot(root, params) {
4692
+ validatePinnedOperationPayload({ relativePath: params.relativePath });
4548
4693
  const resolved = await resolvePinnedRemovePathInRoot(root, params.relativePath, params.denyMutations);
4549
- if (process.platform === "win32") {
4550
- await removePathFallback(resolved);
4551
- return;
4552
- }
4553
4694
  try {
4554
- await runPinnedPathHelper({
4555
- operation: "remove",
4556
- rootPath: resolved.rootReal,
4557
- relativePath: resolved.relativePosix,
4558
- });
4695
+ await removePathFallback(resolved);
4559
4696
  }
4560
4697
  catch (error) {
4561
- if (isPinnedPathHelperSpawnError(error)) {
4562
- await removePathFallback(resolved);
4563
- return;
4564
- }
4565
4698
  throw normalizePinnedPathError(error);
4566
4699
  }
4567
4700
  }
4568
4701
  async function mkdirPathInRoot(root, params) {
4702
+ validatePinnedOperationPayload({ relativePath: params.relativePath });
4569
4703
  const resolved = await resolvePinnedPathInRoot(root, params);
4570
- if (process.platform === "win32") {
4571
- await mkdirPathFallback(resolved);
4572
- return;
4573
- }
4574
4704
  try {
4575
- await runPinnedPathHelper({
4576
- operation: "mkdirp",
4577
- rootPath: resolved.rootReal,
4578
- relativePath: resolved.relativePosix,
4579
- });
4705
+ await mkdirPathFallback(resolved);
4580
4706
  }
4581
4707
  catch (error) {
4582
- if (isPinnedPathHelperSpawnError(error)) {
4583
- await mkdirPathFallback(resolved);
4584
- return;
4585
- }
4586
4708
  throw normalizePinnedPathError(error);
4587
4709
  }
4588
4710
  }
@@ -4648,49 +4770,25 @@ async function copyFileInRoot(root, params) {
4648
4770
  throw new errors_FsSafeError("too-large", `file exceeds limit of ${params.maxBytes} bytes (got ${source.stat.size})`);
4649
4771
  }
4650
4772
  try {
4651
- if (process.platform === "win32") {
4652
- await serializePathWrite(rootWriteQueueKey(root, params.relativePath), async () => {
4653
- await copyFileFallback(root, params, source);
4654
- });
4655
- return;
4656
- }
4657
4773
  const pinned = await resolvePinnedWriteTargetInRoot(root, params.relativePath, params.mode, params.denyMutations);
4658
4774
  await serializePathWrite(pinned.targetPath, async () => {
4659
- let identity;
4775
+ await assertCopySourceCurrent(source);
4776
+ const identity = await runPinnedWriteHelper({
4777
+ rootPath: pinned.rootReal,
4778
+ relativeParentPath: pinned.relativeParentPath,
4779
+ basename: pinned.basename,
4780
+ mkdir: params.mkdir !== false,
4781
+ mode: pinned.mode,
4782
+ overwrite: true,
4783
+ maxBytes: params.maxBytes,
4784
+ input: { kind: "stream", stream: source.handle.createReadStream() },
4785
+ });
4660
4786
  try {
4661
- if (getFsSafePythonConfig().mode === "off") {
4662
- await copyFileFallback(root, params, source);
4663
- return;
4664
- }
4665
- identity = await runPinnedCopyHelper({
4666
- rootPath: pinned.rootReal,
4667
- relativeParentPath: pinned.relativeParentPath,
4668
- basename: pinned.basename,
4669
- mkdir: params.mkdir !== false,
4670
- mode: pinned.mode,
4671
- overwrite: true,
4672
- maxBytes: params.maxBytes,
4673
- sourcePath: source.realPath,
4674
- sourceIdentity: { dev: source.stat.dev, ino: source.stat.ino },
4675
- });
4787
+ await assertCopySourcePathCurrent(source);
4676
4788
  }
4677
4789
  catch (error) {
4678
- if (canFallbackFromPythonError(error)) {
4679
- await copyFileFallback(root, params, source);
4680
- return;
4681
- }
4682
- throw normalizePinnedWriteError(error);
4683
- }
4684
- try {
4685
- await verifyAtomicWriteResult({
4686
- root,
4687
- targetPath: pinned.targetPath,
4688
- expectedIdentity: identity,
4689
- });
4690
- }
4691
- catch (err) {
4692
- emitWriteBoundaryWarning(`post-copy verification failed: ${String(err)}`);
4693
- throw err;
4790
+ await removeCopyTargetIfUnchanged(pinned.targetPath, identity).catch(() => undefined);
4791
+ throw error;
4694
4792
  }
4695
4793
  });
4696
4794
  }
@@ -4698,8 +4796,33 @@ async function copyFileInRoot(root, params) {
4698
4796
  await source.handle.close().catch(() => { });
4699
4797
  }
4700
4798
  }
4799
+ async function assertCopySourceCurrent(source) {
4800
+ const opened = await source.handle.stat();
4801
+ if (!file_identity_sameFileIdentity(opened, source.stat)) {
4802
+ throw new errors_FsSafeError("path-mismatch", "copy source descriptor changed");
4803
+ }
4804
+ await assertCopySourcePathCurrent(source);
4805
+ }
4806
+ async function assertCopySourcePathCurrent(source) {
4807
+ const current = await promises_namespaceObject.lstat(source.realPath);
4808
+ if (current.isSymbolicLink() || !current.isFile() || !file_identity_sameFileIdentity(current, source.stat)) {
4809
+ throw new errors_FsSafeError("path-mismatch", "copy source path changed");
4810
+ }
4811
+ }
4812
+ async function removeCopyTargetIfUnchanged(targetPath, identity) {
4813
+ const parentGuard = await directory_guard_createAsyncDirectoryGuard(external_node_path_namespaceObject.dirname(targetPath));
4814
+ const current = await promises_namespaceObject.lstat(targetPath);
4815
+ if (current.isSymbolicLink() || !file_identity_sameFileIdentity(current, identity)) {
4816
+ return;
4817
+ }
4818
+ await withAsyncDirectoryGuards([parentGuard], async () => {
4819
+ await promises_namespaceObject.rm(targetPath);
4820
+ });
4821
+ }
4701
4822
  async function resolvePinnedWriteTargetInRoot(root, relativePath, requestedMode, denyMutations) {
4702
- const { rootReal, rootWithSep, resolved } = await resolvePathInRoot(root, relativePath);
4823
+ const { rootReal, rootWithSep, resolved } = await resolvePathInRoot(root, relativePath, {
4824
+ aliasErrorCode: "path-alias",
4825
+ });
4703
4826
  await assertMutationNotDenied(resolved, denyMutations);
4704
4827
  try {
4705
4828
  await assertNoPathAliasEscape({
@@ -4801,8 +4924,11 @@ async function resolvePinnedRootPathInRoot(root, params) {
4801
4924
  const rootReal = root.rootReal;
4802
4925
  let resolved;
4803
4926
  try {
4927
+ const expandedPath = await expandRelativePathWithHome(params.relativePath);
4804
4928
  resolved = await resolveRootPath({
4805
- absolutePath: external_node_path_namespaceObject.resolve(rootReal, await expandRelativePathWithHome(params.relativePath)),
4929
+ absolutePath: external_node_path_namespaceObject.isAbsolute(expandedPath)
4930
+ ? expandedPath
4931
+ : `${ensureTrailingSep(rootReal)}${expandedPath}`,
4806
4932
  rootPath: rootReal,
4807
4933
  rootCanonicalPath: rootReal,
4808
4934
  boundaryLabel: "root",
@@ -4873,35 +4999,51 @@ async function listPathFallback(root, relativePath, withFileTypes) {
4873
4999
  }
4874
5000
  }
4875
5001
  async function assertMoveMutationAllowed(root, params) {
4876
- const source = await resolvePathInRoot(root, params.fromRelative);
5002
+ const source = await resolvePathInRoot(root, params.fromRelative, {
5003
+ aliasErrorCode: "path-alias",
5004
+ allowFinalSymlink: true,
5005
+ });
4877
5006
  await assertMutationNotDenied(source.resolved, params.denyMutations, { protectAncestors: true });
4878
- const target = await resolvePathInRoot(root, params.toRelative);
5007
+ const target = await resolvePathInRoot(root, params.toRelative, {
5008
+ aliasErrorCode: "path-alias",
5009
+ allowFinalSymlink: true,
5010
+ });
4879
5011
  await assertMutationNotDenied(target.resolved, params.denyMutations, { protectAncestors: true });
4880
5012
  }
4881
5013
  async function movePathFallback(root, params) {
4882
- const source = await resolvePathInRoot(root, params.fromRelative);
5014
+ const source = await resolvePathInRoot(root, params.fromRelative, {
5015
+ aliasErrorCode: "path-alias",
5016
+ allowFinalSymlink: true,
5017
+ });
4883
5018
  await assertMutationNotDenied(source.resolved, params.denyMutations, { protectAncestors: true });
4884
5019
  await resolvePinnedRootPathInRoot(root, {
4885
5020
  relativePath: params.fromRelative,
4886
5021
  policy: PATH_ALIAS_POLICIES.strict,
4887
5022
  });
4888
- const target = await resolvePathInRoot(root, params.toRelative);
5023
+ const target = await resolvePathInRoot(root, params.toRelative, {
5024
+ aliasErrorCode: "path-alias",
5025
+ allowFinalSymlink: true,
5026
+ });
4889
5027
  await assertMutationNotDenied(target.resolved, params.denyMutations, { protectAncestors: true });
4890
5028
  await resolvePinnedRootPathInRoot(root, {
4891
5029
  relativePath: params.toRelative,
4892
5030
  policy: PATH_ALIAS_POLICIES.unlinkTarget,
4893
5031
  });
4894
- try {
4895
- await assertNoPathAliasEscape({
4896
- absolutePath: target.resolved,
4897
- rootPath: target.rootReal,
4898
- boundaryLabel: "root",
4899
- });
4900
- }
4901
- catch (error) {
4902
- throw new errors_FsSafeError("path-alias", "path alias escape blocked", {
4903
- cause: error instanceof Error ? error : undefined,
4904
- });
5032
+ const targetStat = await promises_namespaceObject.lstat(target.resolved).catch(() => undefined);
5033
+ const replacesFinalSymlink = process.platform !== "win32" && params.overwrite && targetStat?.isSymbolicLink() === true;
5034
+ if (!replacesFinalSymlink) {
5035
+ try {
5036
+ await assertNoPathAliasEscape({
5037
+ absolutePath: target.resolved,
5038
+ rootPath: target.rootReal,
5039
+ boundaryLabel: "root",
5040
+ });
5041
+ }
5042
+ catch (error) {
5043
+ throw new errors_FsSafeError("path-alias", "path alias escape blocked", {
5044
+ cause: error instanceof Error ? error : undefined,
5045
+ });
5046
+ }
4905
5047
  }
4906
5048
  let sourceStat;
4907
5049
  try {
@@ -4988,6 +5130,7 @@ async function writeFileFallback(root, params) {
4988
5130
  encoding: params.encoding,
4989
5131
  mode: mode || 0o600,
4990
5132
  });
5133
+ unregisterTempPath.setIdentity(writtenStat);
4991
5134
  const commitTempPath = tempPath;
4992
5135
  await withAsyncDirectoryGuards([destinationGuard], async () => {
4993
5136
  await promises_namespaceObject.rename(commitTempPath, destinationPath);
@@ -5015,7 +5158,9 @@ async function writeFileFallback(root, params) {
5015
5158
  }
5016
5159
  }
5017
5160
  async function writeMissingFileFallback(root, params) {
5018
- const { rootReal, resolved } = await resolvePathInRoot(root, params.relativePath);
5161
+ const { rootReal, resolved } = await resolvePathInRoot(root, params.relativePath, {
5162
+ aliasErrorCode: "path-alias",
5163
+ });
5019
5164
  await assertMutationNotDenied(resolved, params.denyMutations);
5020
5165
  try {
5021
5166
  await assertNoPathAliasEscape({
@@ -5027,14 +5172,14 @@ async function writeMissingFileFallback(root, params) {
5027
5172
  catch (err) {
5028
5173
  throw new errors_FsSafeError("path-alias", "path alias escape blocked", { cause: err });
5029
5174
  }
5030
- if (params.mkdir !== false) {
5031
- await promises_namespaceObject.mkdir(external_node_path_namespaceObject.dirname(resolved), { recursive: true });
5032
- }
5033
- const parentGuard = await directory_guard_createAsyncDirectoryGuard(external_node_path_namespaceObject.dirname(resolved));
5175
+ const targetPath = params.mkdir === false
5176
+ ? resolved
5177
+ : await prepareRootWriteTarget(rootReal, resolved);
5178
+ const parentGuard = await directory_guard_createAsyncDirectoryGuard(external_node_path_namespaceObject.dirname(targetPath));
5034
5179
  let created = false;
5035
5180
  try {
5036
5181
  const { handle, writtenStat } = await withAsyncDirectoryGuards([parentGuard], async () => {
5037
- const handle = await promises_namespaceObject.open(resolved, OPEN_WRITE_CREATE_FLAGS, params.mode ?? 0o600);
5182
+ const handle = await promises_namespaceObject.open(targetPath, OPEN_WRITE_CREATE_FLAGS, params.mode ?? 0o600);
5038
5183
  created = true;
5039
5184
  try {
5040
5185
  if (typeof params.data === "string") {
@@ -5058,7 +5203,7 @@ async function writeMissingFileFallback(root, params) {
5058
5203
  await handle.close();
5059
5204
  await verifyAtomicWriteResult({
5060
5205
  root,
5061
- targetPath: resolved,
5206
+ targetPath,
5062
5207
  expectedIdentity: writtenStat,
5063
5208
  });
5064
5209
  created = false;
@@ -5073,113 +5218,101 @@ async function writeMissingFileFallback(root, params) {
5073
5218
  }
5074
5219
  finally {
5075
5220
  if (created) {
5076
- await promises_namespaceObject.rm(resolved, { force: true }).catch(() => undefined);
5221
+ await promises_namespaceObject.rm(targetPath, { force: true }).catch(() => undefined);
5077
5222
  }
5078
5223
  }
5079
5224
  }
5080
- async function copyFileFallback(root, params, source) {
5081
- let target = null;
5082
- let sourceClosedByStream = false;
5083
- let targetClosedByUs = false;
5084
- let tempHandle = null;
5085
- let tempPath = null;
5086
- let unregisterTempPath = null;
5087
- let tempClosedByStream = false;
5225
+
5226
+ ;// CONCATENATED MODULE: ./src/lib/symlink-path-guard.ts
5227
+ /**
5228
+ * Path-segment symlink rejection for root-bounded FS ops.
5229
+ */
5230
+
5231
+
5232
+ function isPathWithinRoot(rootPath, candidatePath) {
5233
+ if (candidatePath === rootPath) {
5234
+ return true;
5235
+ }
5236
+ // When root is `/` (or `C:\`), appending sep would yield `//` / `C:\\` and
5237
+ // incorrectly reject every in-root path. Use root as the prefix when it
5238
+ // already ends with the separator.
5239
+ const prefix = rootPath.endsWith(external_node_path_namespaceObject.sep) ? rootPath : `${rootPath}${external_node_path_namespaceObject.sep}`;
5240
+ return candidatePath.startsWith(prefix);
5241
+ }
5242
+ function isEnoentError(error) {
5243
+ return error instanceof Error && "code" in error && error.code === "ENOENT";
5244
+ }
5245
+ function symlinkNotAllowedError(relativePath) {
5246
+ return new Error(`Symlinks are not allowed: path contains symbolic link: ${JSON.stringify(relativePath)}`, {
5247
+ cause: new errors_FsSafeError("symlink", "symlink not allowed")
5248
+ });
5249
+ }
5250
+ async function lstatIfExists(path) {
5088
5251
  try {
5089
- target = await openWritableFileInRoot(root, {
5090
- relativePath: params.relativePath,
5091
- mkdir: params.mkdir,
5092
- mode: params.mode,
5093
- denyMutations: params.denyMutations,
5094
- truncateExisting: false,
5095
- });
5096
- const destinationPath = target.realPath;
5097
- const mode = params.mode ?? (target.stat.mode & 0o777);
5098
- await target.handle.close().catch(() => { });
5099
- targetClosedByUs = true;
5100
- const destinationGuard = await directory_guard_createAsyncDirectoryGuard(external_node_path_namespaceObject.dirname(destinationPath));
5101
- tempPath = buildAtomicWriteTempPath(destinationPath);
5102
- unregisterTempPath = registerTempPathForExit(tempPath);
5103
- tempHandle = await promises_namespaceObject.open(tempPath, OPEN_WRITE_CREATE_FLAGS, mode || 0o600);
5104
- const sourceStream = createBoundedReadStream(source, params.maxBytes);
5105
- const targetStream = tempHandle.createWriteStream();
5106
- sourceStream.once("close", () => {
5107
- sourceClosedByStream = true;
5108
- });
5109
- targetStream.once("close", () => {
5110
- tempClosedByStream = true;
5111
- });
5112
- await (0,external_node_stream_promises_namespaceObject.pipeline)(sourceStream, targetStream);
5113
- const writtenStat = await promises_namespaceObject.stat(tempPath);
5114
- if (!tempClosedByStream) {
5115
- await tempHandle.close().catch(() => { });
5116
- tempClosedByStream = true;
5117
- }
5118
- tempHandle = null;
5119
- const commitTempPath = tempPath;
5120
- await withAsyncDirectoryGuards([destinationGuard], async () => {
5121
- await promises_namespaceObject.rename(commitTempPath, destinationPath);
5122
- });
5123
- tempPath = null;
5124
- unregisterTempPath();
5125
- unregisterTempPath = null;
5126
- try {
5127
- await verifyAtomicWriteResult({
5128
- root,
5129
- targetPath: destinationPath,
5130
- expectedIdentity: writtenStat,
5131
- });
5132
- }
5133
- catch (err) {
5134
- emitWriteBoundaryWarning(`post-copy verification failed: ${String(err)}`);
5135
- throw err;
5252
+ return await (0,promises_namespaceObject.lstat)(path);
5253
+ } catch (error) {
5254
+ if (isEnoentError(error)) {
5255
+ return undefined;
5136
5256
  }
5257
+ throw error;
5137
5258
  }
5138
- catch (err) {
5139
- if (target?.createdForWrite) {
5140
- await promises_namespaceObject.rm(target.realPath, { force: true }).catch(() => { });
5141
- }
5142
- throw err;
5259
+ }
5260
+ async function rejectSymlinkSegments(rootPath, relativeFromRoot, relativePath) {
5261
+ if (!relativeFromRoot) {
5262
+ return;
5143
5263
  }
5144
- finally {
5145
- if (!sourceClosedByStream) {
5146
- await source.handle.close().catch(() => { });
5147
- }
5148
- if (tempHandle && !tempClosedByStream) {
5149
- await tempHandle.close().catch(() => { });
5264
+ let currentPath = rootPath;
5265
+ for (const segment of relativeFromRoot.split(external_node_path_namespaceObject.sep)){
5266
+ if (!segment || segment === ".") {
5267
+ continue;
5150
5268
  }
5151
- if (tempPath) {
5152
- await promises_namespaceObject.rm(tempPath, { force: true }).catch(() => { });
5269
+ currentPath = (0,external_node_path_namespaceObject.join)(currentPath, segment);
5270
+ const stats = await lstatIfExists(currentPath);
5271
+ if (stats === undefined) {
5272
+ return;
5153
5273
  }
5154
- unregisterTempPath?.();
5155
- if (target && !targetClosedByUs) {
5156
- await target.handle.close().catch(() => { });
5274
+ if (stats.isSymbolicLink()) {
5275
+ throw symlinkNotAllowedError(relativePath);
5157
5276
  }
5158
5277
  }
5159
5278
  }
5279
+ /**
5280
+ * fs-safe 0.5 exists/stat/list canonicalize through in-root symlinks; open/read
5281
+ * still honor symlinks:"reject". Enforce reject on every root-bounded op.
5282
+ */ async function assertNoSymlinksWithinRoot(targetDir, relativePath) {
5283
+ if (!relativePath) {
5284
+ return;
5285
+ }
5286
+ const rootPath = await (0,promises_namespaceObject.realpath)(targetDir);
5287
+ const absolutePath = (0,external_node_path_namespaceObject.resolve)(rootPath, relativePath);
5288
+ if (!isPathWithinRoot(rootPath, absolutePath)) {
5289
+ throw new Error(`Resolved path is outside target directory: ${relativePath}`);
5290
+ }
5291
+ await rejectSymlinkSegments(rootPath, (0,external_node_path_namespaceObject.relative)(rootPath, absolutePath), relativePath);
5292
+ }
5160
5293
 
5161
5294
  ;// CONCATENATED MODULE: ./src/lib/safe-fs.ts
5162
5295
 
5296
+
5163
5297
  function mapFsSafeError(error, relativePath) {
5164
- if (error instanceof errors_FsSafeError) {
5165
- switch(error.code){
5166
- case "outside-workspace":
5167
- case "invalid-path":
5168
- throw new Error(`Resolved path is outside target directory: ${relativePath}`, {
5169
- cause: error
5170
- });
5171
- case "path-alias":
5172
- case "symlink":
5173
- throw new Error(`Symlinks are not allowed: path contains symbolic link: ${relativePath}`, {
5174
- cause: error
5175
- });
5176
- case "too-large":
5177
- throw new Error(`File ${relativePath} exceeds size limit: ${error.message}`, {
5178
- cause: error
5179
- });
5180
- default:
5181
- throw error;
5182
- }
5298
+ if (!(error instanceof errors_FsSafeError)) {
5299
+ throw error;
5300
+ }
5301
+ const safePath = JSON.stringify(relativePath);
5302
+ if (error.code === "outside-workspace" || error.code === "invalid-path") {
5303
+ throw new Error(`Resolved path is outside target directory: ${safePath}`, {
5304
+ cause: error
5305
+ });
5306
+ }
5307
+ if (error.code === "path-alias" || error.code === "symlink") {
5308
+ throw new Error(`Symlinks are not allowed: path contains symbolic link: ${safePath}`, {
5309
+ cause: error
5310
+ });
5311
+ }
5312
+ if (error.code === "too-large") {
5313
+ throw new Error(`File ${safePath} exceeds size limit: ${error.message}`, {
5314
+ cause: error
5315
+ });
5183
5316
  }
5184
5317
  throw error;
5185
5318
  }
@@ -5200,6 +5333,7 @@ function toSafeDirEntry(entry) {
5200
5333
  }
5201
5334
  async function readBytesWithinRoot(targetDir, relativePath, maxBytes) {
5202
5335
  try {
5336
+ await assertNoSymlinksWithinRoot(targetDir, relativePath);
5203
5337
  const safeRoot = await createSafeRoot(targetDir, maxBytes);
5204
5338
  return await safeRoot.readBytes(relativePath, {
5205
5339
  maxBytes
@@ -5210,6 +5344,7 @@ async function readBytesWithinRoot(targetDir, relativePath, maxBytes) {
5210
5344
  }
5211
5345
  async function readTextWithinRoot(targetDir, relativePath, maxBytes) {
5212
5346
  try {
5347
+ await assertNoSymlinksWithinRoot(targetDir, relativePath);
5213
5348
  const safeRoot = await createSafeRoot(targetDir, maxBytes);
5214
5349
  return await safeRoot.readText(relativePath, {
5215
5350
  maxBytes
@@ -5220,6 +5355,7 @@ async function readTextWithinRoot(targetDir, relativePath, maxBytes) {
5220
5355
  }
5221
5356
  async function listDirectoryWithinRoot(targetDir, relativePath) {
5222
5357
  try {
5358
+ await assertNoSymlinksWithinRoot(targetDir, relativePath);
5223
5359
  const safeRoot = await createSafeRoot(targetDir);
5224
5360
  const entries = await safeRoot.list(relativePath, {
5225
5361
  withFileTypes: true
@@ -5231,6 +5367,7 @@ async function listDirectoryWithinRoot(targetDir, relativePath) {
5231
5367
  }
5232
5368
  async function statWithinRoot(targetDir, relativePath) {
5233
5369
  try {
5370
+ await assertNoSymlinksWithinRoot(targetDir, relativePath);
5234
5371
  const safeRoot = await createSafeRoot(targetDir);
5235
5372
  return await safeRoot.stat(relativePath);
5236
5373
  } catch (error) {
@@ -5239,6 +5376,7 @@ async function statWithinRoot(targetDir, relativePath) {
5239
5376
  }
5240
5377
  async function pathExistsWithinRoot(targetDir, relativePath) {
5241
5378
  try {
5379
+ await assertNoSymlinksWithinRoot(targetDir, relativePath);
5242
5380
  const safeRoot = await createSafeRoot(targetDir);
5243
5381
  return await safeRoot.exists(relativePath);
5244
5382
  } catch (error) {
@@ -13973,8 +14111,6 @@ function hasProtoKey(value) {
13973
14111
  return hasProtoKeyAtDepth(value, 0);
13974
14112
  }
13975
14113
 
13976
- ;// CONCATENATED MODULE: external "node:module"
13977
- const external_node_module_namespaceObject = __rspack_createRequire_require("node:module");
13978
14114
  ;// CONCATENATED MODULE: ./src/gateways/resolve-sdk.ts
13979
14115
 
13980
14116
 
@@ -16252,4 +16388,4 @@ if (installedChunkData !== 0) { // 0 means "already installed".'
16252
16388
  // module factories are used so entry inlining is disabled
16253
16389
  // startup
16254
16390
  // Load entry module and return exports
16255
- var __webpack_exports__ = __webpack_require__(834);
16391
+ var __webpack_exports__ = __webpack_require__(418);