@lousy-agents/agent-shell 5.19.1 → 5.19.3

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 +1171 -829
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import { createRequire as __rspack_createRequire } from "node:module";
3
3
  const __rspack_createRequire_require = __rspack_createRequire(import.meta.url);
4
4
  var __webpack_modules__ = ({
5
- 418(__unused_rspack_module, __unused_rspack___webpack_exports__, __webpack_require__) {
5
+ 983(__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");
@@ -81,6 +81,9 @@ function defaultExecutor(env) {
81
81
  const OPERATIONAL_CODES = new Set([
82
82
  "helper-failed",
83
83
  "helper-unavailable",
84
+ "not-empty",
85
+ "not-found",
86
+ "not-removable",
84
87
  "permission-unverified",
85
88
  "timeout",
86
89
  "unsupported-platform",
@@ -132,6 +135,7 @@ function file_identity_sameFileIdentity(left, right, platform = process.platform
132
135
 
133
136
 
134
137
 
138
+
135
139
  const NOT_FOUND_CODES = new Set(["ENOENT", "ENOTDIR"]);
136
140
  const SYMLINK_OPEN_CODES = new Set(["ELOOP", "EINVAL", "ENOTSUP"]);
137
141
  const POSIX_SEPARATOR_CHAR_CODE = 0x2f;
@@ -249,6 +253,9 @@ function splitSafeRelativePath(relativePath) {
249
253
  if (segment === "..") {
250
254
  throw new FsSafeError("invalid-path", "relative path must not contain '..'");
251
255
  }
256
+ if (isDriveRelativePath(segment)) {
257
+ throw new FsSafeError("invalid-path", "relative path must not contain a drive letter");
258
+ }
252
259
  }
253
260
  return segments;
254
261
  }
@@ -261,6 +268,74 @@ function resolveSafeRelativePath(rootDir, relativePath) {
261
268
  return target;
262
269
  }
263
270
 
271
+ ;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/root-errors.js
272
+
273
+
274
+ const REMOVE_NOT_EMPTY_CODES = new Set(["ENOTEMPTY", "EEXIST"]);
275
+ function fileNotFoundError(cause) {
276
+ return cause === undefined
277
+ ? new errors_FsSafeError("not-found", "file not found")
278
+ : new errors_FsSafeError("not-found", "file not found", { cause });
279
+ }
280
+ function outsideWorkspaceError() {
281
+ return new errors_FsSafeError("outside-workspace", "file is outside workspace root");
282
+ }
283
+ function root_errors_directoryComponentNotDirectoryError(cause) {
284
+ return cause === undefined
285
+ ? new errors_FsSafeError("not-file", "directory component must be a directory")
286
+ : new errors_FsSafeError("not-file", "directory component must be a directory", { cause });
287
+ }
288
+ function hardlinkedPathNotAllowedError() {
289
+ return new errors_FsSafeError("hardlink", "hardlinked path not allowed");
290
+ }
291
+ function isAlreadyExistsError(error) {
292
+ return hasNodeErrorCode(error, "EEXIST") || /File exists|EEXIST/i.test(String(error));
293
+ }
294
+ function normalizePinnedWriteError(error) {
295
+ if (error instanceof errors_FsSafeError) {
296
+ return error;
297
+ }
298
+ if (path_isNotFoundPathError(error)) {
299
+ return fileNotFoundError(error instanceof Error ? error : undefined);
300
+ }
301
+ return new errors_FsSafeError("invalid-path", "path is not a regular file under root", {
302
+ cause: error instanceof Error ? error : undefined,
303
+ });
304
+ }
305
+ function normalizePinnedPathError(error) {
306
+ if (error instanceof errors_FsSafeError) {
307
+ return error;
308
+ }
309
+ return new errors_FsSafeError("path-alias", "path is not under root", {
310
+ cause: error instanceof Error ? error : undefined,
311
+ });
312
+ }
313
+ function normalizeRemoveGuardError(error) {
314
+ if (error instanceof errors_FsSafeError) {
315
+ return error;
316
+ }
317
+ if (path_isNotFoundPathError(error)) {
318
+ return fileNotFoundError(error instanceof Error ? error : undefined);
319
+ }
320
+ return normalizePinnedPathError(error);
321
+ }
322
+ function normalizeRemovePathError(error) {
323
+ if (error instanceof errors_FsSafeError) {
324
+ return error;
325
+ }
326
+ if (!isNodeError(error) || typeof error.code !== "string") {
327
+ return normalizePinnedPathError(error);
328
+ }
329
+ const cause = error instanceof Error ? error : undefined;
330
+ if (path_isNotFoundPathError(error)) {
331
+ return fileNotFoundError(cause);
332
+ }
333
+ if (REMOVE_NOT_EMPTY_CODES.has(error.code)) {
334
+ return new errors_FsSafeError("not-empty", "directory is not empty", { cause });
335
+ }
336
+ return new errors_FsSafeError("not-removable", "path could not be removed", { cause });
337
+ }
338
+
264
339
  ;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/directory-guard.js
265
340
 
266
341
 
@@ -268,17 +343,18 @@ function resolveSafeRelativePath(rootDir, relativePath) {
268
343
 
269
344
 
270
345
 
346
+
271
347
  async function directory_guard_createAsyncDirectoryGuard(dir) {
272
348
  const stat = await promises_namespaceObject.lstat(dir);
273
349
  if (stat.isSymbolicLink() || !stat.isDirectory()) {
274
- throw new errors_FsSafeError("not-file", "directory component must be a directory");
350
+ throw root_errors_directoryComponentNotDirectoryError();
275
351
  }
276
352
  return { dir, realPath: await promises_namespaceObject.realpath(dir), stat };
277
353
  }
278
354
  async function assertAsyncDirectoryGuard(guard) {
279
355
  const stat = await promises_namespaceObject.lstat(guard.dir);
280
356
  if (stat.isSymbolicLink() || !stat.isDirectory()) {
281
- throw new errors_FsSafeError("not-file", "directory component must be a directory");
357
+ throw root_errors_directoryComponentNotDirectoryError();
282
358
  }
283
359
  if (!file_identity_sameFileIdentity(stat, guard.stat) || (await promises_namespaceObject.realpath(guard.dir)) !== guard.realPath) {
284
360
  throw new errors_FsSafeError("path-mismatch", "directory changed during operation");
@@ -287,14 +363,14 @@ async function assertAsyncDirectoryGuard(guard) {
287
363
  function directory_guard_createSyncDirectoryGuard(dir) {
288
364
  const stat = fsSync.lstatSync(dir);
289
365
  if (stat.isSymbolicLink() || !stat.isDirectory()) {
290
- throw new FsSafeError("not-file", "directory component must be a directory");
366
+ throw directoryComponentNotDirectoryError();
291
367
  }
292
368
  return { dir, realPath: fsSync.realpathSync(dir), stat };
293
369
  }
294
370
  function directory_guard_assertSyncDirectoryGuard(guard) {
295
371
  const stat = fsSync.lstatSync(guard.dir);
296
372
  if (stat.isSymbolicLink() || !stat.isDirectory()) {
297
- throw new FsSafeError("not-file", "directory component must be a directory");
373
+ throw directoryComponentNotDirectoryError();
298
374
  }
299
375
  if (!sameFileIdentity(stat, guard.stat) || fsSync.realpathSync(guard.dir) !== guard.realPath) {
300
376
  throw new FsSafeError("path-mismatch", "directory changed during operation");
@@ -640,6 +716,7 @@ async function ensureDurableDirectory(options) {
640
716
 
641
717
 
642
718
 
719
+
643
720
  function isSameOrChildPath(candidate, parent) {
644
721
  const parentPrefix = parent.endsWith(external_node_path_namespaceObject.sep) ? parent : `${parent}${external_node_path_namespaceObject.sep}`;
645
722
  return candidate === parent || candidate.startsWith(parentPrefix);
@@ -652,9 +729,7 @@ async function realpathOrThrowNotFile(target) {
652
729
  if (path_isNotFoundPathError(error)) {
653
730
  // A dangling symlink (or a component removed between lstat and
654
731
  // 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
- });
732
+ throw root_errors_directoryComponentNotDirectoryError(error instanceof Error ? error : undefined);
658
733
  }
659
734
  throw error;
660
735
  }
@@ -677,8 +752,8 @@ async function mkdirPathComponentsWithGuards(params) {
677
752
  for (const part of relative.split(external_node_path_namespaceObject.sep).filter(Boolean)) {
678
753
  const next = external_node_path_namespaceObject.join(current, part);
679
754
  const parentGuard = await directory_guard_createAsyncDirectoryGuard(current);
680
- await params.beforeComponent?.(next);
681
755
  await assertAsyncDirectoryGuard(parentGuard);
756
+ await params.beforeComponent?.(next);
682
757
  try {
683
758
  await promises_namespaceObject.mkdir(next);
684
759
  }
@@ -689,7 +764,7 @@ async function mkdirPathComponentsWithGuards(params) {
689
764
  }
690
765
  const stat = await promises_namespaceObject.lstat(next);
691
766
  if (!stat.isSymbolicLink() && !stat.isDirectory()) {
692
- throw new errors_FsSafeError("not-file", "directory component must be a directory");
767
+ throw root_errors_directoryComponentNotDirectoryError();
693
768
  }
694
769
  // Node's recursive mkdir follows symlinks in missing components. Build one
695
770
  // segment at a time and realpath-check each segment before descending.
@@ -709,7 +784,7 @@ async function mkdirPathComponentsWithGuards(params) {
709
784
  // the returned resolved path, not their own lexical parent path.
710
785
  const targetStat = await promises_namespaceObject.stat(nextReal);
711
786
  if (!targetStat.isDirectory()) {
712
- throw new errors_FsSafeError("not-file", "directory component must be a directory");
787
+ throw root_errors_directoryComponentNotDirectoryError();
713
788
  }
714
789
  await directory_guard_createAsyncDirectoryGuard(nextReal);
715
790
  await assertAsyncDirectoryGuard(parentGuard);
@@ -801,30 +876,37 @@ function guardedRmSync(params) {
801
876
  }), { verifyAfter: params.verifyAfter });
802
877
  }
803
878
 
804
- ;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/deny-mutations.js
879
+ ;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/root-path-existing.js
805
880
 
806
881
 
807
882
 
808
883
 
809
- async function pathExists(filePath) {
884
+ function isFilesystemRoot(candidate) {
885
+ return external_node_path_namespaceObject.parse(candidate).root === candidate;
886
+ }
887
+ async function pathExists(targetPath) {
810
888
  try {
811
- await promises_namespaceObject.lstat(filePath);
889
+ await promises_namespaceObject.lstat(targetPath);
812
890
  return true;
813
891
  }
814
- catch (err) {
815
- if (!path_isNotFoundPathError(err)) {
816
- throw err;
892
+ catch (error) {
893
+ if (path_isNotFoundPathError(error)) {
894
+ return false;
817
895
  }
818
- return false;
896
+ throw error;
819
897
  }
820
898
  }
821
899
  async function resolvePathViaExistingAncestor(targetPath) {
822
900
  const normalized = external_node_path_namespaceObject.resolve(targetPath);
823
901
  let cursor = normalized;
824
902
  const missingSuffix = [];
825
- while (external_node_path_namespaceObject.dirname(cursor) !== cursor && !(await pathExists(cursor))) {
903
+ while (!isFilesystemRoot(cursor) && !(await pathExists(cursor))) {
826
904
  missingSuffix.unshift(external_node_path_namespaceObject.basename(cursor));
827
- cursor = external_node_path_namespaceObject.dirname(cursor);
905
+ const parent = external_node_path_namespaceObject.dirname(cursor);
906
+ if (parent === cursor) {
907
+ break;
908
+ }
909
+ cursor = parent;
828
910
  }
829
911
  if (!(await pathExists(cursor))) {
830
912
  return normalized;
@@ -839,7 +921,38 @@ async function resolvePathViaExistingAncestor(targetPath) {
839
921
  return normalized;
840
922
  }
841
923
  }
842
- async function comparablePaths(rawPath) {
924
+ function root_path_existing_resolvePathViaExistingAncestorSync(targetPath) {
925
+ const normalized = path.resolve(targetPath);
926
+ let cursor = normalized;
927
+ const missingSuffix = [];
928
+ while (!isFilesystemRoot(cursor) && !fs.existsSync(cursor)) {
929
+ missingSuffix.unshift(path.basename(cursor));
930
+ const parent = path.dirname(cursor);
931
+ if (parent === cursor) {
932
+ break;
933
+ }
934
+ cursor = parent;
935
+ }
936
+ if (!fs.existsSync(cursor)) {
937
+ return normalized;
938
+ }
939
+ try {
940
+ const resolvedAncestor = path.resolve(fs.realpathSync(cursor));
941
+ return missingSuffix.length === 0
942
+ ? resolvedAncestor
943
+ : path.resolve(resolvedAncestor, ...missingSuffix);
944
+ }
945
+ catch {
946
+ return normalized;
947
+ }
948
+ }
949
+
950
+ ;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/deny-mutations.js
951
+
952
+
953
+
954
+
955
+ async function resolveMutationComparablePaths(rawPath) {
843
956
  path_assertNoNulPathInput(rawPath, "path contains a NUL byte");
844
957
  const resolved = external_node_path_namespaceObject.resolve(rawPath);
845
958
  return new Set([resolved, await resolvePathViaExistingAncestor(resolved)]);
@@ -868,9 +981,9 @@ async function assertMutationNotDenied(filePath, policy, options = {}) {
868
981
  if (!hasPolicyEntries(policy)) {
869
982
  return;
870
983
  }
871
- const targetPaths = await comparablePaths(filePath);
984
+ const targetPaths = await resolveMutationComparablePaths(filePath);
872
985
  for (const deniedPath of policyPathEntries(policy.paths)) {
873
- const deniedPaths = await comparablePaths(deniedPath);
986
+ const deniedPaths = await resolveMutationComparablePaths(deniedPath);
874
987
  for (const target of targetPaths) {
875
988
  for (const denied of deniedPaths) {
876
989
  if (isSamePath(denied, target) ||
@@ -881,7 +994,7 @@ async function assertMutationNotDenied(filePath, policy, options = {}) {
881
994
  }
882
995
  }
883
996
  for (const deniedPrefix of policyPathEntries(policy.prefixes)) {
884
- const deniedPaths = await comparablePaths(deniedPrefix);
997
+ const deniedPaths = await resolveMutationComparablePaths(deniedPrefix);
885
998
  for (const target of targetPaths) {
886
999
  for (const denied of deniedPaths) {
887
1000
  if (path_isPathInside(denied, target) ||
@@ -1344,7 +1457,20 @@ async function createNativeExclusiveFile(targetPath, mode) {
1344
1457
  let fd;
1345
1458
  let created;
1346
1459
  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));
1460
+ let opened;
1461
+ try {
1462
+ 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));
1463
+ }
1464
+ catch (error) {
1465
+ // The parent open above and every post-create operation remain untagged.
1466
+ // Only this exclusive-open failure has enough provenance for a caller to
1467
+ // classify a Windows lock-file denial without swallowing setup failures.
1468
+ const openError = error;
1469
+ if (process.platform === "win32" && openError.code === "EPERM") {
1470
+ openError.path = targetPath;
1471
+ }
1472
+ throw error;
1473
+ }
1348
1474
  fd = opened.fd;
1349
1475
  external_node_fs_namespaceObject.fchmodSync(fd, mode);
1350
1476
  created = external_node_fs_namespaceObject.fstatSync(fd);
@@ -1398,23 +1524,22 @@ function assertWithinMaxBytes(bytes, maxBytes) {
1398
1524
  throw new errors_FsSafeError("too-large", `file exceeds limit of ${maxBytes} bytes (got at least ${bytes})`);
1399
1525
  }
1400
1526
  }
1401
- async function inputToBuffer(input, maxBytes) {
1527
+ async function writeNativeInput(fd, input, maxBytes) {
1402
1528
  if (input.kind === "buffer") {
1403
1529
  const data = typeof input.data === "string"
1404
1530
  ? Buffer.from(input.data, input.encoding ?? "utf8")
1405
1531
  : Buffer.from(input.data);
1406
1532
  assertWithinMaxBytes(data.byteLength, maxBytes);
1407
- return data;
1533
+ writeNativeFd(fd, data);
1534
+ return;
1408
1535
  }
1409
- const chunks = [];
1410
1536
  let bytes = 0;
1411
1537
  for await (const chunk of input.stream) {
1412
1538
  const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
1413
1539
  bytes += buffer.byteLength;
1414
1540
  assertWithinMaxBytes(bytes, maxBytes);
1415
- chunks.push(buffer);
1541
+ writeNativeFd(fd, buffer);
1416
1542
  }
1417
- return Buffer.concat(chunks, bytes);
1418
1543
  }
1419
1544
  function native_pinned_write_nativeOpenFlags(flags) {
1420
1545
  const closeOnExec = external_node_fs_namespaceObject.constants.O_CLOEXEC;
@@ -1426,7 +1551,6 @@ function sameNativeIdentity(left, right) {
1426
1551
  return left.dev === right.dev && left.ino === right.ino;
1427
1552
  }
1428
1553
  async function runPinnedWriteNative(binding, params) {
1429
- const data = await inputToBuffer(params.input, params.maxBytes);
1430
1554
  const root = await promises_namespaceObject.open(params.rootPath, external_node_fs_namespaceObject.constants.O_RDONLY |
1431
1555
  (typeof external_node_fs_namespaceObject.constants.O_DIRECTORY === "number" ? external_node_fs_namespaceObject.constants.O_DIRECTORY : 0));
1432
1556
  let parentFd;
@@ -1456,27 +1580,56 @@ async function runPinnedWriteNative(binding, params) {
1456
1580
  !sameNativeIdentity(parentPathStat, parentIdentity)) {
1457
1581
  throw new errors_FsSafeError("path-mismatch", "native write parent changed during resolution");
1458
1582
  }
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;
1583
+ if (params.overwrite === false) {
1584
+ try {
1585
+ await promises_namespaceObject.lstat(external_node_path_namespaceObject.join(parentPath, params.basename));
1586
+ throw Object.assign(new Error("destination already exists"), { code: "EEXIST" });
1587
+ }
1588
+ catch (error) {
1589
+ if (error.code !== "ENOENT") {
1590
+ throw error;
1591
+ }
1466
1592
  }
1467
1593
  }
1468
1594
  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
1595
  tempIdentity = external_node_fs_namespaceObject.fstatSync(tempFd);
1473
- binding.renameNoReplace(parentFd, tempName, parentFd, params.basename);
1596
+ // Creation is requested at 0600 in the binding, but a restrictive umask
1597
+ // can remove owner access. Keep the unpublished inode private and
1598
+ // reopenable until the published name has been identity-fenced.
1599
+ external_node_fs_namespaceObject.fchmodSync(tempFd, 0o600);
1600
+ await writeNativeInput(tempFd, params.input, params.maxBytes);
1601
+ syncNativeFileBestEffort(tempFd);
1602
+ if (params.overwrite === false) {
1603
+ binding.renameNoReplace(parentFd, tempName, parentFd, params.basename);
1604
+ }
1605
+ else {
1606
+ binding.renameReplace(parentFd, tempName, parentFd, params.basename);
1607
+ }
1474
1608
  renamed = true;
1475
1609
  targetFd = binding.openBeneath(parentFd, params.basename, native_pinned_write_nativeOpenFlags(external_node_fs_namespaceObject.constants.O_RDONLY)).fd;
1476
1610
  const targetIdentity = binding.fstatIdentity(targetFd);
1477
1611
  if (!targetIdentity.isFile || !sameNativeIdentity(tempIdentity, targetIdentity)) {
1478
1612
  throw new errors_FsSafeError("path-mismatch", "native write target changed after rename");
1479
1613
  }
1614
+ // Native exclusive creation starts at 0600. Apply the requested mode only
1615
+ // after reopening and fencing the published name, both so mode 000 stays
1616
+ // verifiable and so broader modes are never exposed before that fence.
1617
+ try {
1618
+ external_node_fs_namespaceObject.fchmodSync(targetFd, params.mode);
1619
+ syncNativeFileBestEffort(targetFd);
1620
+ }
1621
+ catch (error) {
1622
+ external_node_fs_namespaceObject.closeSync(targetFd);
1623
+ targetFd = undefined;
1624
+ removeNativeCreatedFileIfStillPinned({
1625
+ binding,
1626
+ parentPath,
1627
+ parentFd,
1628
+ basename: params.basename,
1629
+ created: tempIdentity,
1630
+ });
1631
+ throw error;
1632
+ }
1480
1633
  syncNativeFileBestEffort(parentFd);
1481
1634
  return { dev: targetIdentity.dev, ino: targetIdentity.ino };
1482
1635
  }
@@ -1613,7 +1766,7 @@ async function readFileDescriptorBounded(fd, maxBytes) {
1613
1766
  });
1614
1767
  }
1615
1768
  /** Sync bounded read from a numeric descriptor. The caller owns the descriptor. */
1616
- function readFileDescriptorBoundedSync(fd, maxBytes) {
1769
+ function bounded_read_readFileDescriptorBoundedSync(fd, maxBytes) {
1617
1770
  assertMaxBytes(maxBytes);
1618
1771
  const chunks = [];
1619
1772
  const scratch = createScratchBuffer(maxBytes);
@@ -1628,6 +1781,21 @@ function readFileDescriptorBoundedSync(fd, maxBytes) {
1628
1781
  }
1629
1782
  }
1630
1783
 
1784
+ ;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/test-hooks.js
1785
+ let test_hooks_fsSafeTestHooks;
1786
+ function allowFsSafeTestHooks() {
1787
+ return false || process.env.VITEST === "true";
1788
+ }
1789
+ function getFsSafeTestHooks() {
1790
+ return test_hooks_fsSafeTestHooks;
1791
+ }
1792
+ function __setFsSafeTestHooksForTest(hooks) {
1793
+ if (hooks && !allowFsSafeTestHooks()) {
1794
+ throw new Error("__setFsSafeTestHooksForTest is only available in tests");
1795
+ }
1796
+ test_hooks_fsSafeTestHooks = hooks;
1797
+ }
1798
+
1631
1799
  ;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/sidecar-lock-reclaim.js
1632
1800
 
1633
1801
 
@@ -1636,6 +1804,7 @@ function readFileDescriptorBoundedSync(fd, maxBytes) {
1636
1804
 
1637
1805
 
1638
1806
 
1807
+
1639
1808
  const MAX_LOCK_PAYLOAD_BYTES = 1024 * 1024;
1640
1809
  const SIDECAR_LOCK_OWNERSHIP_TOKEN_BYTES = 16;
1641
1810
  const SIDECAR_LOCK_OWNERSHIP_TOKEN_BITS = SIDECAR_LOCK_OWNERSHIP_TOKEN_BYTES * 8;
@@ -1686,6 +1855,7 @@ function parseSidecarLockPayload(raw, parser) {
1686
1855
  }
1687
1856
  }
1688
1857
  async function readSidecarLockSnapshot(lockPath, options = {}) {
1858
+ let handle;
1689
1859
  try {
1690
1860
  if (options.lockRoot) {
1691
1861
  const opened = await options.lockRoot.open(relativeSidecarLockPath(options.lockRoot, lockPath));
@@ -1701,9 +1871,44 @@ async function readSidecarLockSnapshot(lockPath, options = {}) {
1701
1871
  await opened.handle.close().catch(() => undefined);
1702
1872
  }
1703
1873
  }
1704
- const stat = await promises_namespaceObject.lstat(lockPath);
1705
- const raw = await promises_namespaceObject.readFile(lockPath, "utf8");
1706
- return { raw, payload: parseSidecarLockPayload(raw, options.parsePayload), stat };
1874
+ const before = await promises_namespaceObject.lstat(lockPath);
1875
+ if (!before.isFile() || before.isSymbolicLink()) {
1876
+ if (options.rejectNonFile) {
1877
+ throw new errors_FsSafeError("not-file", `sidecar lock is not a regular file: ${lockPath}`);
1878
+ }
1879
+ return null;
1880
+ }
1881
+ await getFsSafeTestHooks()?.beforeSidecarLockSnapshotOpen?.(lockPath);
1882
+ const noFollow = process.platform !== "win32" && typeof external_node_fs_namespaceObject.constants.O_NOFOLLOW === "number"
1883
+ ? external_node_fs_namespaceObject.constants.O_NOFOLLOW
1884
+ : 0;
1885
+ try {
1886
+ handle = await promises_namespaceObject.open(lockPath, external_node_fs_namespaceObject.constants.O_RDONLY |
1887
+ noFollow |
1888
+ (typeof external_node_fs_namespaceObject.constants.O_NONBLOCK === "number" ? external_node_fs_namespaceObject.constants.O_NONBLOCK : 0));
1889
+ }
1890
+ catch (error) {
1891
+ if (options.rejectNonFile && error.code === "ELOOP") {
1892
+ throw new errors_FsSafeError("not-file", `sidecar lock is not a regular file: ${lockPath}`, {
1893
+ cause: error,
1894
+ });
1895
+ }
1896
+ throw error;
1897
+ }
1898
+ const opened = await handle.stat();
1899
+ if (!opened.isFile()) {
1900
+ if (options.rejectNonFile) {
1901
+ throw new errors_FsSafeError("not-file", `sidecar lock is not a regular file: ${lockPath}`);
1902
+ }
1903
+ return null;
1904
+ }
1905
+ if (!options.allowDescriptorIdentityDrift && !file_identity_sameFileIdentity(before, opened))
1906
+ return null;
1907
+ const raw = (await readFileHandleBounded(handle, MAX_LOCK_PAYLOAD_BYTES)).toString("utf8");
1908
+ const after = await promises_namespaceObject.lstat(lockPath);
1909
+ if (!after.isFile() || !file_identity_sameFileIdentity(before, after))
1910
+ return null;
1911
+ return { raw, payload: parseSidecarLockPayload(raw, options.parsePayload), stat: after };
1707
1912
  }
1708
1913
  catch (err) {
1709
1914
  if (err.code === "ENOENT" ||
@@ -1712,19 +1917,26 @@ async function readSidecarLockSnapshot(lockPath, options = {}) {
1712
1917
  }
1713
1918
  throw err;
1714
1919
  }
1920
+ finally {
1921
+ await handle?.close().catch(() => undefined);
1922
+ }
1715
1923
  }
1716
- function readSidecarLockSnapshotSync(lockPath, parsePayload) {
1924
+ function readSidecarLockSnapshotSync(lockPath, parsePayload, options = {}) {
1717
1925
  let fd;
1718
1926
  try {
1719
1927
  const before = fsSync.lstatSync(lockPath);
1720
- if (!before.isFile() || before.isSymbolicLink())
1928
+ if (!before.isFile() || before.isSymbolicLink()) {
1929
+ if (options.rejectNonFile) {
1930
+ throw new FsSafeError("not-file", `sidecar lock is not a regular file: ${lockPath}`);
1931
+ }
1721
1932
  return null;
1933
+ }
1722
1934
  const noFollow = process.platform !== "win32" && typeof fsSync.constants.O_NOFOLLOW === "number"
1723
1935
  ? fsSync.constants.O_NOFOLLOW
1724
1936
  : 0;
1725
1937
  fd = fsSync.openSync(lockPath, fsSync.constants.O_RDONLY | noFollow);
1726
1938
  const opened = fsSync.fstatSync(fd);
1727
- const raw = fsSync.readFileSync(fd, "utf8");
1939
+ const raw = readFileDescriptorBoundedSync(fd, MAX_LOCK_PAYLOAD_BYTES).toString("utf8");
1728
1940
  const after = fsSync.lstatSync(lockPath);
1729
1941
  if (!sameFileIdentity(before, opened) || !sameFileIdentity(opened, after))
1730
1942
  return null;
@@ -1770,7 +1982,10 @@ function sidecarLockSnapshotMatches(current, observed) {
1770
1982
  return observed.stat !== undefined && current.stat !== undefined;
1771
1983
  }
1772
1984
  async function removeSidecarLockIfUnchanged(lockPath, observed, options = {}) {
1773
- const current = await readSidecarLockSnapshot(lockPath, options);
1985
+ const current = await readSidecarLockSnapshot(lockPath, {
1986
+ ...options,
1987
+ allowDescriptorIdentityDrift: observed?.ownershipToken !== undefined,
1988
+ });
1774
1989
  if (!current || !observed || !sidecarLockSnapshotMatches(current, observed)) {
1775
1990
  return false;
1776
1991
  }
@@ -1783,7 +1998,10 @@ async function removeSidecarLockIfUnchanged(lockPath, observed, options = {}) {
1783
1998
  return true;
1784
1999
  }
1785
2000
  async function sidecarLockSnapshotStillPresent(lockPath, observed, options = {}) {
1786
- const current = await readSidecarLockSnapshot(lockPath, options);
2001
+ const current = await readSidecarLockSnapshot(lockPath, {
2002
+ ...options,
2003
+ allowDescriptorIdentityDrift: observed?.ownershipToken !== undefined,
2004
+ });
1787
2005
  return !!current && !!observed && sidecarLockSnapshotMatches(current, observed);
1788
2006
  }
1789
2007
  async function sidecarReclaimGuardExists(pathname) {
@@ -1851,36 +2069,6 @@ async function removeStaleSidecarLockIfAllowed(params) {
1851
2069
  }
1852
2070
  }
1853
2071
 
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
2072
  ;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/sidecar-lock-policy.js
1885
2073
 
1886
2074
  function computeSidecarLockDelayMs(retry, attempt) {
@@ -1891,7 +2079,23 @@ function computeSidecarLockDelayMs(retry, attempt) {
1891
2079
  const jitter = retry.randomize ? 1 + Math.random() : 1;
1892
2080
  return Math.min(maxTimeout, Math.round(base * jitter));
1893
2081
  }
2082
+ // Windows denies access to a lock file while a just-unlinked directory entry
2083
+ // is still being torn down, so a contended acquire sees EPERM on a name that is
2084
+ // already gone -- both when creating it exclusively and when reading the
2085
+ // holder's snapshot. The next attempt succeeds, so this is contention rather
2086
+ // than a permission failure. The error must name the lock file itself: the
2087
+ // exclusive-create helper opens the parent directory first, and a denial from
2088
+ // that setup step carries no teardown evidence and has to reach the caller.
2089
+ const maxTransientLockDenials = 8;
2090
+ function isTransientLockFileDenial(error, lockPath) {
2091
+ const denial = error;
2092
+ return process.platform === "win32" && denial?.code === "EPERM" && denial.path === lockPath;
2093
+ }
1894
2094
  function sidecarLockPayloadIsStale(payload, staleMs, nowMs) {
2095
+ const createdAtMs = sidecarLockPayloadCreatedAtMs(payload);
2096
+ return createdAtMs !== null && nowMs - createdAtMs > staleMs;
2097
+ }
2098
+ function sidecarLockPayloadCreatedAtMs(payload) {
1895
2099
  const createdAt = payload &&
1896
2100
  typeof payload === "object" &&
1897
2101
  "createdAt" in payload &&
@@ -1899,11 +2103,12 @@ function sidecarLockPayloadIsStale(payload, staleMs, nowMs) {
1899
2103
  ? payload.createdAt
1900
2104
  : "";
1901
2105
  const createdAtMs = Date.parse(createdAt);
1902
- return Number.isFinite(createdAtMs) && nowMs - createdAtMs > staleMs;
2106
+ return Number.isFinite(createdAtMs) ? createdAtMs : null;
1903
2107
  }
1904
2108
  async function defaultSidecarLockShouldReclaim(params) {
1905
- if (sidecarLockPayloadIsStale(params.payload, params.staleMs, params.nowMs))
1906
- return true;
2109
+ const createdAtMs = sidecarLockPayloadCreatedAtMs(params.payload);
2110
+ if (createdAtMs !== null)
2111
+ return params.nowMs - createdAtMs > params.staleMs;
1907
2112
  try {
1908
2113
  return params.nowMs - (await promises_namespaceObject.stat(params.lockPath)).mtimeMs > params.staleMs;
1909
2114
  }
@@ -1912,57 +2117,347 @@ async function defaultSidecarLockShouldReclaim(params) {
1912
2117
  }
1913
2118
  }
1914
2119
 
1915
- ;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/sidecar-lock.js
1916
-
2120
+ ;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/sidecar-lock-acquire.js
1917
2121
 
1918
2122
 
1919
2123
 
1920
2124
 
1921
2125
 
1922
2126
 
1923
-
1924
-
1925
- const GLOBAL_STATE_KEY = Symbol.for("fsSafe.sidecarLockManagers");
1926
- function getGlobalManagers() {
1927
- const globalWithState = globalThis;
1928
- if (!globalWithState[GLOBAL_STATE_KEY]) {
1929
- globalWithState[GLOBAL_STATE_KEY] = new Map();
1930
- }
1931
- return globalWithState[GLOBAL_STATE_KEY];
1932
- }
1933
- function resolveManagerState(key) {
1934
- const managers = getGlobalManagers();
1935
- let state = managers.get(key);
1936
- if (!state) {
1937
- state = {
1938
- cleanupRegistered: false,
1939
- held: new Map(),
1940
- reclaimCleanupRegistered: false,
1941
- reclaimGuards: new Set(),
1942
- };
1943
- managers.set(key, state);
2127
+ async function resolveNormalizedTargetPath(targetPath) {
2128
+ const resolved = external_node_path_namespaceObject.resolve(targetPath);
2129
+ const dir = external_node_path_namespaceObject.dirname(resolved);
2130
+ await promises_namespaceObject.mkdir(dir, { recursive: true });
2131
+ try {
2132
+ return external_node_path_namespaceObject.join(await promises_namespaceObject.realpath(dir), external_node_path_namespaceObject.basename(resolved));
1944
2133
  }
1945
- else {
1946
- // The global manager symbol is shared across package copies and hot reloads.
1947
- // Backfill state created by fs-safe versions that predate reclaim guards.
1948
- state.reclaimCleanupRegistered ??= false;
1949
- state.reclaimGuards ??= new Set();
1950
- for (const held of state.held.values()) {
1951
- held.refCount ??= 1;
1952
- }
2134
+ catch {
2135
+ return resolved;
1953
2136
  }
1954
- return state;
1955
2137
  }
1956
- function snapshotMatchesSync(lockPath, observed) {
1957
- let fd;
1958
- try {
1959
- const beforeStat = external_node_fs_namespaceObject.lstatSync(lockPath);
1960
- if (!beforeStat.isFile()) {
1961
- return false;
2138
+ async function acquireSidecarLock(options, context) {
2139
+ context.ensureExitCleanupRegistered();
2140
+ const normalizedTargetPath = await resolveNormalizedTargetPath(options.targetPath);
2141
+ const lockPath = options.lockPath ?? `${normalizedTargetPath}.lock`;
2142
+ const held = context.held.get(normalizedTargetPath);
2143
+ if (held &&
2144
+ options.reentrantOwner !== undefined &&
2145
+ held.reentrantOwner !== undefined &&
2146
+ options.reentrantOwner === held.reentrantOwner) {
2147
+ held.refCount += 1;
2148
+ return context.handleForHeldLock(normalizedTargetPath, held);
2149
+ }
2150
+ const startedAt = Date.now();
2151
+ const retry = options.retry ?? {};
2152
+ const maxRetries = options.timeoutMs === Number.POSITIVE_INFINITY ? undefined : retry.retries;
2153
+ const reclaimGuardPath = `${lockPath}.reclaim`;
2154
+ let ownsReclaimGuard = false;
2155
+ let attempt = 0;
2156
+ // Bounded so a genuine denial still surfaces as EPERM, not a lock timeout.
2157
+ let transientDenials = 0;
2158
+ const withinDenialBudget = () => ++transientDenials <= (/* inlined export .maxTransientLockDenials */8);
2159
+ const waitForRetry = async () => {
2160
+ const elapsed = Date.now() - startedAt;
2161
+ if ((options.timeoutMs !== undefined &&
2162
+ options.timeoutMs !== Number.POSITIVE_INFINITY &&
2163
+ elapsed >= options.timeoutMs) ||
2164
+ (maxRetries !== undefined && attempt >= maxRetries)) {
2165
+ throw Object.assign(new Error(`file lock timeout for ${normalizedTargetPath}`), {
2166
+ code: "file_lock_timeout",
2167
+ lockPath,
2168
+ normalizedTargetPath,
2169
+ });
1962
2170
  }
1963
- const openFlags = external_node_fs_namespaceObject.constants.O_RDONLY |
1964
- (process.platform !== "win32" && typeof external_node_fs_namespaceObject.constants.O_NOFOLLOW === "number"
1965
- ? external_node_fs_namespaceObject.constants.O_NOFOLLOW
2171
+ const remaining = options.timeoutMs === undefined || options.timeoutMs === Number.POSITIVE_INFINITY
2172
+ ? Number.POSITIVE_INFINITY
2173
+ : Math.max(0, options.timeoutMs - elapsed);
2174
+ const delay = Math.min(computeSidecarLockDelayMs(retry, attempt), remaining);
2175
+ attempt += 1;
2176
+ await new Promise((resolve) => setTimeout(resolve, delay));
2177
+ };
2178
+ // Waiting can fail on the caller's own retry or deadline limits. Classifying
2179
+ // a denial as contention must not cost them the original diagnosis, so hand
2180
+ // the denial back when no further attempt can be scheduled.
2181
+ const retryOrRethrowDenial = async (denial) => {
2182
+ try {
2183
+ await waitForRetry();
2184
+ }
2185
+ catch (waitError) {
2186
+ if (waitError.code === "file_lock_timeout")
2187
+ throw denial;
2188
+ throw waitError;
2189
+ }
2190
+ };
2191
+ try {
2192
+ while (true) {
2193
+ if (!ownsReclaimGuard && (await sidecarReclaimGuardExists(reclaimGuardPath))) {
2194
+ await waitForRetry();
2195
+ continue;
2196
+ }
2197
+ let handle = null;
2198
+ let createdSnapshot = null;
2199
+ let lockFileCreateDenied = false;
2200
+ try {
2201
+ const payload = await options.payload();
2202
+ const { raw, ownershipToken } = serializeSidecarLockPayload(payload);
2203
+ if (options.lockRoot) {
2204
+ const relativeLockPath = relativeSidecarLockPath(options.lockRoot, lockPath);
2205
+ try {
2206
+ await options.lockRoot.create(relativeLockPath, raw, { mkdir: true, mode: 0o600 });
2207
+ }
2208
+ catch (error) {
2209
+ if (error instanceof errors_FsSafeError && error.code === "already-exists") {
2210
+ throw Object.assign(new Error("sidecar lock exists"), { code: "EEXIST" });
2211
+ }
2212
+ throw error;
2213
+ }
2214
+ createdSnapshot = { raw, payload, ownershipToken };
2215
+ handle = (await options.lockRoot.open(relativeLockPath)).handle;
2216
+ }
2217
+ else {
2218
+ try {
2219
+ handle =
2220
+ (await createNativeExclusiveFile(lockPath, 0o600)) ??
2221
+ (await promises_namespaceObject.open(lockPath, "wx"));
2222
+ }
2223
+ catch (createError) {
2224
+ lockFileCreateDenied = isTransientLockFileDenial(createError, lockPath);
2225
+ throw createError;
2226
+ }
2227
+ await handle.writeFile(raw, "utf8");
2228
+ }
2229
+ const snapshot = { raw, payload, stat: await handle.stat(), ownershipToken };
2230
+ const createdHeld = {
2231
+ refCount: 1,
2232
+ reentrantOwner: options.reentrantOwner,
2233
+ handle,
2234
+ lockPath,
2235
+ snapshot,
2236
+ acquiredAt: Date.now(),
2237
+ metadata: options.metadata ?? {},
2238
+ lockRoot: options.lockRoot,
2239
+ parsePayload: options.parsePayload,
2240
+ };
2241
+ context.held.set(normalizedTargetPath, createdHeld);
2242
+ if (ownsReclaimGuard) {
2243
+ try {
2244
+ await releaseSidecarReclaimGuard(context.reclaimGuards, reclaimGuardPath);
2245
+ ownsReclaimGuard = false;
2246
+ }
2247
+ catch (err) {
2248
+ await context.releaseHeldLock(normalizedTargetPath, createdHeld, { force: true });
2249
+ throw err;
2250
+ }
2251
+ }
2252
+ const returnedHandle = context.handleForHeldLock(normalizedTargetPath, createdHeld);
2253
+ const interval = options.compromiseCheckIntervalMs;
2254
+ if (options.onCompromised && interval !== undefined && interval > 0) {
2255
+ createdHeld.compromiseTimer = setInterval(() => {
2256
+ void returnedHandle.verifyStillHeld().then((stillHeld) => {
2257
+ if (!stillHeld && createdHeld.compromiseTimer) {
2258
+ clearInterval(createdHeld.compromiseTimer);
2259
+ createdHeld.compromiseTimer = undefined;
2260
+ options.onCompromised?.({ lockPath, normalizedTargetPath });
2261
+ }
2262
+ });
2263
+ }, interval);
2264
+ createdHeld.compromiseTimer.unref();
2265
+ }
2266
+ return returnedHandle;
2267
+ }
2268
+ catch (err) {
2269
+ if (handle) {
2270
+ const failedSnapshot = { payload: null };
2271
+ try {
2272
+ failedSnapshot.stat = await handle.stat();
2273
+ }
2274
+ catch {
2275
+ // Best-effort cleanup of a failed exclusive create.
2276
+ }
2277
+ const current = context.held.get(normalizedTargetPath);
2278
+ if (current?.handle === handle) {
2279
+ context.held.delete(normalizedTargetPath);
2280
+ }
2281
+ await handle.close().catch(() => undefined);
2282
+ // The file may be empty or partial JSON, so remove by the identity
2283
+ // captured from our exclusive handle rather than by pathname alone.
2284
+ await removeSidecarLockIfUnchanged(lockPath, failedSnapshot, {
2285
+ lockRoot: options.lockRoot,
2286
+ parsePayload: options.parsePayload,
2287
+ });
2288
+ }
2289
+ else if (createdSnapshot) {
2290
+ await removeSidecarLockIfUnchanged(lockPath, createdSnapshot, {
2291
+ lockRoot: options.lockRoot,
2292
+ parsePayload: options.parsePayload,
2293
+ });
2294
+ }
2295
+ if (lockFileCreateDenied && withinDenialBudget()) {
2296
+ await retryOrRethrowDenial(err);
2297
+ continue;
2298
+ }
2299
+ if (err.code !== "EEXIST") {
2300
+ throw err;
2301
+ }
2302
+ if (ownsReclaimGuard) {
2303
+ await releaseSidecarReclaimGuard(context.reclaimGuards, reclaimGuardPath);
2304
+ ownsReclaimGuard = false;
2305
+ continue;
2306
+ }
2307
+ const nowMs = Date.now();
2308
+ let snapshot;
2309
+ try {
2310
+ snapshot = await readSidecarLockSnapshot(lockPath, {
2311
+ lockRoot: options.lockRoot,
2312
+ parsePayload: options.parsePayload,
2313
+ rejectNonFile: true,
2314
+ });
2315
+ }
2316
+ catch (readErr) {
2317
+ if (!isTransientLockFileDenial(readErr, lockPath) || !withinDenialBudget())
2318
+ throw readErr;
2319
+ await retryOrRethrowDenial(readErr);
2320
+ continue;
2321
+ }
2322
+ if (!snapshot) {
2323
+ continue;
2324
+ }
2325
+ if (context.held.has(normalizedTargetPath)) {
2326
+ await waitForRetry();
2327
+ continue;
2328
+ }
2329
+ const shouldReclaim = options.shouldReclaim ?? defaultSidecarLockShouldReclaim;
2330
+ if (await shouldReclaim({
2331
+ lockPath,
2332
+ normalizedTargetPath,
2333
+ payload: snapshot?.payload ?? null,
2334
+ staleMs: options.staleMs,
2335
+ nowMs,
2336
+ heldByThisProcess: context.held.has(normalizedTargetPath),
2337
+ })) {
2338
+ if (!(await sidecarLockSnapshotStillPresent(lockPath, snapshot, {
2339
+ lockRoot: options.lockRoot,
2340
+ parsePayload: options.parsePayload,
2341
+ }))) {
2342
+ continue;
2343
+ }
2344
+ const staleRecovery = options.staleRecovery ?? "fail-closed";
2345
+ if (staleRecovery === "remove-if-unchanged") {
2346
+ if (!(await tryAcquireSidecarReclaimGuard(context.reclaimGuards, reclaimGuardPath))) {
2347
+ await waitForRetry();
2348
+ continue;
2349
+ }
2350
+ ownsReclaimGuard = true;
2351
+ const removal = await removeStaleSidecarLockIfAllowed({
2352
+ lockPath,
2353
+ normalizedTargetPath,
2354
+ snapshot,
2355
+ shouldRemoveStaleLock: options.shouldRemoveStaleLock,
2356
+ lockRoot: options.lockRoot,
2357
+ parsePayload: options.parsePayload,
2358
+ });
2359
+ if (removal === "removed" || removal === "changed") {
2360
+ continue;
2361
+ }
2362
+ await releaseSidecarReclaimGuard(context.reclaimGuards, reclaimGuardPath);
2363
+ ownsReclaimGuard = false;
2364
+ }
2365
+ throw Object.assign(new Error(`file lock stale for ${normalizedTargetPath}`), {
2366
+ code: "file_lock_stale",
2367
+ lockPath,
2368
+ normalizedTargetPath,
2369
+ });
2370
+ }
2371
+ await waitForRetry();
2372
+ }
2373
+ }
2374
+ }
2375
+ finally {
2376
+ if (ownsReclaimGuard) {
2377
+ await releaseSidecarReclaimGuard(context.reclaimGuards, reclaimGuardPath).catch(() => undefined);
2378
+ }
2379
+ }
2380
+ }
2381
+
2382
+ ;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/sidecar-lock-handle.js
2383
+
2384
+ function createSidecarLockHandle(params) {
2385
+ let released = false;
2386
+ const release = async () => {
2387
+ if (released)
2388
+ return;
2389
+ released = true;
2390
+ await params.release();
2391
+ };
2392
+ return {
2393
+ lockPath: params.lockPath,
2394
+ normalizedTargetPath: params.normalizedTargetPath,
2395
+ verifyStillHeld: params.verifyStillHeld,
2396
+ release,
2397
+ [Symbol.asyncDispose]: release,
2398
+ };
2399
+ }
2400
+ function createHeldSidecarLockHandle(params) {
2401
+ return createSidecarLockHandle({
2402
+ lockPath: params.held.lockPath,
2403
+ normalizedTargetPath: params.normalizedTargetPath,
2404
+ verifyStillHeld: async () => await sidecarLockSnapshotStillPresent(params.held.lockPath, params.held.snapshot, {
2405
+ lockRoot: params.held.lockRoot,
2406
+ parsePayload: params.held.parsePayload,
2407
+ }),
2408
+ release: params.release,
2409
+ });
2410
+ }
2411
+
2412
+ ;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/sidecar-lock.js
2413
+
2414
+
2415
+
2416
+
2417
+
2418
+ const GLOBAL_STATE_KEY = Symbol.for("fsSafe.sidecarLockManagers");
2419
+ const GLOBAL_CLEANUP_KEY = Symbol.for("fsSafe.sidecarLockCleanupRegistered");
2420
+ const GLOBAL_CLEANUP_HANDLER_KEY = Symbol.for("fsSafe.sidecarLockCleanupHandler");
2421
+ function getGlobalManagers() {
2422
+ const globalWithState = globalThis;
2423
+ if (!globalWithState[GLOBAL_STATE_KEY]) {
2424
+ globalWithState[GLOBAL_STATE_KEY] = new Map();
2425
+ }
2426
+ return globalWithState[GLOBAL_STATE_KEY];
2427
+ }
2428
+ function resolveManagerState(key) {
2429
+ const managers = getGlobalManagers();
2430
+ let state = managers.get(key);
2431
+ if (!state) {
2432
+ state = {
2433
+ cleanupRegistered: false,
2434
+ held: new Map(),
2435
+ reclaimCleanupRegistered: false,
2436
+ reclaimGuards: new Set(),
2437
+ };
2438
+ managers.set(key, state);
2439
+ }
2440
+ else {
2441
+ // The global manager symbol is shared across package copies and hot reloads.
2442
+ // Backfill state created by fs-safe versions that predate reclaim guards.
2443
+ state.reclaimCleanupRegistered ??= false;
2444
+ state.reclaimGuards ??= new Set();
2445
+ for (const held of state.held.values()) {
2446
+ held.refCount ??= 1;
2447
+ }
2448
+ }
2449
+ return state;
2450
+ }
2451
+ function snapshotMatchesSync(lockPath, observed) {
2452
+ let fd;
2453
+ try {
2454
+ const beforeStat = external_node_fs_namespaceObject.lstatSync(lockPath);
2455
+ if (!beforeStat.isFile()) {
2456
+ return false;
2457
+ }
2458
+ const openFlags = external_node_fs_namespaceObject.constants.O_RDONLY |
2459
+ (process.platform !== "win32" && typeof external_node_fs_namespaceObject.constants.O_NOFOLLOW === "number"
2460
+ ? external_node_fs_namespaceObject.constants.O_NOFOLLOW
1966
2461
  : 0) |
1967
2462
  (typeof external_node_fs_namespaceObject.constants.O_NONBLOCK === "number" ? external_node_fs_namespaceObject.constants.O_NONBLOCK : 0);
1968
2463
  fd = external_node_fs_namespaceObject.openSync(lockPath, openFlags);
@@ -1994,17 +2489,6 @@ function snapshotMatchesSync(lockPath, observed) {
1994
2489
  }
1995
2490
  }
1996
2491
  }
1997
- async function resolveNormalizedTargetPath(targetPath) {
1998
- const resolved = external_node_path_namespaceObject.resolve(targetPath);
1999
- const dir = external_node_path_namespaceObject.dirname(resolved);
2000
- await promises_namespaceObject.mkdir(dir, { recursive: true });
2001
- try {
2002
- return external_node_path_namespaceObject.join(await promises_namespaceObject.realpath(dir), external_node_path_namespaceObject.basename(resolved));
2003
- }
2004
- catch {
2005
- return resolved;
2006
- }
2007
- }
2008
2492
  function releaseAllReclaimGuardsSync(state) {
2009
2493
  for (const reclaimGuardPath of state.reclaimGuards) {
2010
2494
  try {
@@ -2031,6 +2515,19 @@ function releaseAllLocksSync(state) {
2031
2515
  }
2032
2516
  releaseAllReclaimGuardsSync(state);
2033
2517
  }
2518
+ function ensureGlobalExitCleanupRegistered() {
2519
+ const globalWithCleanup = globalThis;
2520
+ if (globalWithCleanup[GLOBAL_CLEANUP_KEY])
2521
+ return;
2522
+ globalWithCleanup[GLOBAL_CLEANUP_KEY] = true;
2523
+ const cleanup = () => {
2524
+ for (const state of getGlobalManagers().values()) {
2525
+ releaseAllLocksSync(state);
2526
+ }
2527
+ };
2528
+ globalWithCleanup[GLOBAL_CLEANUP_HANDLER_KEY] = cleanup;
2529
+ process.on("exit", cleanup);
2530
+ }
2034
2531
  async function releaseHeldLock(state, normalizedTargetPath, held, options = {}) {
2035
2532
  const current = state.held.get(normalizedTargetPath);
2036
2533
  if (current !== held) {
@@ -2079,217 +2576,18 @@ function handleForHeldLock(state, normalizedTargetPath, held) {
2079
2576
  function createSidecarLockManager(key) {
2080
2577
  const state = resolveManagerState(key);
2081
2578
  function ensureExitCleanupRegistered() {
2082
- if (!state.cleanupRegistered) {
2083
- state.cleanupRegistered = true;
2084
- state.reclaimCleanupRegistered = true;
2085
- process.on("exit", () => releaseAllLocksSync(state));
2086
- return;
2087
- }
2088
- if (!state.reclaimCleanupRegistered) {
2089
- state.reclaimCleanupRegistered = true;
2090
- process.on("exit", () => releaseAllReclaimGuardsSync(state));
2091
- }
2579
+ state.cleanupRegistered = true;
2580
+ state.reclaimCleanupRegistered = true;
2581
+ ensureGlobalExitCleanupRegistered();
2092
2582
  }
2093
2583
  async function acquire(options) {
2094
- ensureExitCleanupRegistered();
2095
- const normalizedTargetPath = await resolveNormalizedTargetPath(options.targetPath);
2096
- const lockPath = options.lockPath ?? `${normalizedTargetPath}.lock`;
2097
- const held = state.held.get(normalizedTargetPath);
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);
2104
- }
2105
- const startedAt = Date.now();
2106
- const retry = options.retry ?? {};
2107
- const maxRetries = options.timeoutMs === Number.POSITIVE_INFINITY ? undefined : retry.retries;
2108
- const reclaimGuardPath = `${lockPath}.reclaim`;
2109
- let ownsReclaimGuard = false;
2110
- let attempt = 0;
2111
- const waitForRetry = async () => {
2112
- const elapsed = Date.now() - startedAt;
2113
- if ((options.timeoutMs !== undefined &&
2114
- options.timeoutMs !== Number.POSITIVE_INFINITY &&
2115
- elapsed >= options.timeoutMs) ||
2116
- (maxRetries !== undefined && attempt >= maxRetries)) {
2117
- throw Object.assign(new Error(`file lock timeout for ${normalizedTargetPath}`), {
2118
- code: "file_lock_timeout",
2119
- lockPath,
2120
- normalizedTargetPath,
2121
- });
2122
- }
2123
- const remaining = options.timeoutMs === undefined || options.timeoutMs === Number.POSITIVE_INFINITY
2124
- ? Number.POSITIVE_INFINITY
2125
- : Math.max(0, options.timeoutMs - elapsed);
2126
- const delay = Math.min(computeSidecarLockDelayMs(retry, attempt), remaining);
2127
- attempt += 1;
2128
- await new Promise((resolve) => setTimeout(resolve, delay));
2129
- };
2130
- try {
2131
- while (true) {
2132
- if (!ownsReclaimGuard && (await sidecarReclaimGuardExists(reclaimGuardPath))) {
2133
- await waitForRetry();
2134
- continue;
2135
- }
2136
- let handle = null;
2137
- try {
2138
- const payload = await options.payload();
2139
- const { raw, ownershipToken } = serializeSidecarLockPayload(payload);
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
- }
2157
- const snapshot = { raw, payload, stat: await handle.stat(), ownershipToken };
2158
- const createdHeld = {
2159
- refCount: 1,
2160
- reentrantOwner: options.reentrantOwner,
2161
- handle,
2162
- lockPath,
2163
- snapshot,
2164
- acquiredAt: Date.now(),
2165
- metadata: options.metadata ?? {},
2166
- lockRoot: options.lockRoot,
2167
- parsePayload: options.parsePayload,
2168
- };
2169
- state.held.set(normalizedTargetPath, createdHeld);
2170
- if (ownsReclaimGuard) {
2171
- try {
2172
- await releaseSidecarReclaimGuard(state.reclaimGuards, reclaimGuardPath);
2173
- ownsReclaimGuard = false;
2174
- }
2175
- catch (err) {
2176
- await releaseHeldLock(state, normalizedTargetPath, createdHeld, { force: true });
2177
- throw err;
2178
- }
2179
- }
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;
2195
- }
2196
- catch (err) {
2197
- if (handle) {
2198
- const failedSnapshot = { payload: null };
2199
- try {
2200
- failedSnapshot.stat = await handle.stat();
2201
- }
2202
- catch {
2203
- // Best-effort cleanup of a failed exclusive create.
2204
- }
2205
- const current = state.held.get(normalizedTargetPath);
2206
- if (current?.handle === handle) {
2207
- state.held.delete(normalizedTargetPath);
2208
- }
2209
- // If payload serialization/write fails, the file may be empty or
2210
- // partial JSON, so remove while our exclusive handle is still open.
2211
- if (!options.lockRoot) {
2212
- await promises_namespaceObject.rm(lockPath, { force: true }).catch(() => undefined);
2213
- }
2214
- await handle.close().catch(() => undefined);
2215
- // Windows can refuse removing an open file; retry after close but
2216
- // only if the path still points at the file identity we created.
2217
- await removeSidecarLockIfUnchanged(lockPath, failedSnapshot, {
2218
- lockRoot: options.lockRoot,
2219
- parsePayload: options.parsePayload,
2220
- });
2221
- }
2222
- if (err.code !== "EEXIST") {
2223
- throw err;
2224
- }
2225
- if (ownsReclaimGuard) {
2226
- await releaseSidecarReclaimGuard(state.reclaimGuards, reclaimGuardPath);
2227
- ownsReclaimGuard = false;
2228
- continue;
2229
- }
2230
- const nowMs = Date.now();
2231
- const snapshot = await readSidecarLockSnapshot(lockPath, {
2232
- lockRoot: options.lockRoot,
2233
- parsePayload: options.parsePayload,
2234
- });
2235
- if (!snapshot) {
2236
- continue;
2237
- }
2238
- if (state.held.has(normalizedTargetPath)) {
2239
- await waitForRetry();
2240
- continue;
2241
- }
2242
- const shouldReclaim = options.shouldReclaim ?? defaultSidecarLockShouldReclaim;
2243
- if (await shouldReclaim({
2244
- lockPath,
2245
- normalizedTargetPath,
2246
- payload: snapshot?.payload ?? null,
2247
- staleMs: options.staleMs,
2248
- nowMs,
2249
- heldByThisProcess: state.held.has(normalizedTargetPath),
2250
- })) {
2251
- if (!(await sidecarLockSnapshotStillPresent(lockPath, snapshot, {
2252
- lockRoot: options.lockRoot,
2253
- parsePayload: options.parsePayload,
2254
- }))) {
2255
- continue;
2256
- }
2257
- const staleRecovery = options.staleRecovery ?? "fail-closed";
2258
- if (staleRecovery === "remove-if-unchanged") {
2259
- if (!(await tryAcquireSidecarReclaimGuard(state.reclaimGuards, reclaimGuardPath))) {
2260
- await waitForRetry();
2261
- continue;
2262
- }
2263
- ownsReclaimGuard = true;
2264
- const removal = await removeStaleSidecarLockIfAllowed({
2265
- lockPath,
2266
- normalizedTargetPath,
2267
- snapshot,
2268
- shouldRemoveStaleLock: options.shouldRemoveStaleLock,
2269
- lockRoot: options.lockRoot,
2270
- parsePayload: options.parsePayload,
2271
- });
2272
- if (removal === "removed" || removal === "changed") {
2273
- continue;
2274
- }
2275
- await releaseSidecarReclaimGuard(state.reclaimGuards, reclaimGuardPath);
2276
- ownsReclaimGuard = false;
2277
- }
2278
- throw Object.assign(new Error(`file lock stale for ${normalizedTargetPath}`), {
2279
- code: "file_lock_stale",
2280
- lockPath,
2281
- normalizedTargetPath,
2282
- });
2283
- }
2284
- await waitForRetry();
2285
- }
2286
- }
2287
- }
2288
- finally {
2289
- if (ownsReclaimGuard) {
2290
- await releaseSidecarReclaimGuard(state.reclaimGuards, reclaimGuardPath).catch(() => undefined);
2291
- }
2292
- }
2584
+ return await acquireSidecarLock(options, {
2585
+ held: state.held,
2586
+ reclaimGuards: state.reclaimGuards,
2587
+ ensureExitCleanupRegistered,
2588
+ handleForHeldLock: (normalizedTargetPath, held) => handleForHeldLock(state, normalizedTargetPath, held),
2589
+ releaseHeldLock: async (normalizedTargetPath, held, releaseOptions) => await releaseHeldLock(state, normalizedTargetPath, held, releaseOptions),
2590
+ });
2293
2591
  }
2294
2592
  async function withLock(options, fn) {
2295
2593
  const lock = await acquire(options);
@@ -2325,21 +2623,6 @@ async function withSidecarLock(targetPath, options, fn) {
2325
2623
  return await manager.withLock({ ...acquireOptions, targetPath }, fn);
2326
2624
  }
2327
2625
 
2328
- ;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/test-hooks.js
2329
- let test_hooks_fsSafeTestHooks;
2330
- function allowFsSafeTestHooks() {
2331
- return false || process.env.VITEST === "true";
2332
- }
2333
- function getFsSafeTestHooks() {
2334
- return test_hooks_fsSafeTestHooks;
2335
- }
2336
- function __setFsSafeTestHooksForTest(hooks) {
2337
- if (hooks && !allowFsSafeTestHooks()) {
2338
- throw new Error("__setFsSafeTestHooksForTest is only available in tests");
2339
- }
2340
- test_hooks_fsSafeTestHooks = hooks;
2341
- }
2342
-
2343
2626
  ;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/pinned-write.js
2344
2627
 
2345
2628
 
@@ -2412,7 +2695,7 @@ async function runPinnedWriteHelper(params) {
2412
2695
  return await runPinnedWriteFallback(params);
2413
2696
  }
2414
2697
  const native = getNativeBinding();
2415
- if (native && params.overwrite === false) {
2698
+ if (native) {
2416
2699
  return await runPinnedWriteNative(native, params);
2417
2700
  }
2418
2701
  return await runPinnedWriteFallback(params);
@@ -2450,6 +2733,7 @@ async function runPinnedWriteFallback(params) {
2450
2733
  parentPath = await mkdirPathComponentsWithGuards({
2451
2734
  rootReal: params.rootPath,
2452
2735
  targetPath: parentPath,
2736
+ beforeComponent: async (componentPath) => await getFsSafeTestHooks()?.beforeRootFallbackMutation?.("mkdir", componentPath),
2453
2737
  });
2454
2738
  }
2455
2739
  const parentGuard = params.mkdir
@@ -2589,77 +2873,135 @@ async function runPinnedWriteFallback(params) {
2589
2873
  return { dev: targetStat.dev, ino: targetStat.ino };
2590
2874
  }
2591
2875
 
2592
- ;// CONCATENATED MODULE: external "node:os"
2593
- const external_node_os_namespaceObject = __rspack_createRequire_require("node:os");
2594
- ;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/root-path-existing.js
2876
+ ;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/error-detail.js
2877
+ const UNSAFE_ERROR_DETAIL_CHARACTERS = /[\u0000-\u001f\u007f-\u009f\u2028\u2029]/gu;
2878
+ function formatErrorDetail(value) {
2879
+ return value.replace(UNSAFE_ERROR_DETAIL_CHARACTERS, (character) => `\\u${character.charCodeAt(0).toString(16).padStart(4, "0")}`);
2880
+ }
2595
2881
 
2882
+ ;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/root-path-symlink.js
2596
2883
 
2597
2884
 
2598
2885
 
2599
- function isFilesystemRoot(candidate) {
2600
- return external_node_path_namespaceObject.parse(candidate).root === candidate;
2886
+
2887
+
2888
+
2889
+ function normalizeSymlinkResolutionError(error) {
2890
+ if (isSymlinkOpenError(error)) {
2891
+ throw new errors_FsSafeError("symlink", "symlink path could not be resolved", {
2892
+ cause: error instanceof Error ? error : undefined,
2893
+ });
2894
+ }
2895
+ if (!path_isNotFoundPathError(error))
2896
+ throw error;
2601
2897
  }
2602
- async function root_path_existing_pathExists(targetPath) {
2898
+ async function resolveSymlinkHopPath(symlinkPath) {
2603
2899
  try {
2604
- await promises_namespaceObject.lstat(targetPath);
2605
- return true;
2900
+ return external_node_path_namespaceObject.resolve(await promises_namespaceObject.realpath(symlinkPath));
2606
2901
  }
2607
2902
  catch (error) {
2608
- if (path_isNotFoundPathError(error)) {
2609
- return false;
2610
- }
2611
- throw error;
2903
+ normalizeSymlinkResolutionError(error);
2904
+ const linkTarget = await promises_namespaceObject.readlink(symlinkPath);
2905
+ return resolvePathViaExistingAncestor(external_node_path_namespaceObject.resolve(external_node_path_namespaceObject.dirname(symlinkPath), linkTarget));
2612
2906
  }
2613
2907
  }
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
- }
2908
+ function root_path_symlink_resolveSymlinkHopPathSync(symlinkPath) {
2629
2909
  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);
2910
+ return path.resolve(fs.realpathSync(symlinkPath));
2634
2911
  }
2635
- catch {
2636
- return normalized;
2912
+ catch (error) {
2913
+ normalizeSymlinkResolutionError(error);
2914
+ const linkTarget = fs.readlinkSync(symlinkPath);
2915
+ return resolvePathViaExistingAncestorSync(path.resolve(path.dirname(symlinkPath), linkTarget));
2637
2916
  }
2638
2917
  }
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;
2918
+
2919
+ ;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/safe-path-segment.js
2920
+
2921
+ const SAFE_PATH_SEGMENT_PATTERN = /^[A-Za-z0-9_-][A-Za-z0-9._-]*$/;
2922
+ const SAFE_DOT_PREFIX_PATH_SEGMENT_PATTERN = /^[A-Za-z0-9._-]+$/;
2923
+ // Windows treats "C:name" as relative to the drive's current directory even
2924
+ // though path.win32.isAbsolute() reports false.
2925
+ const DRIVE_RELATIVE_PREFIX = /^[A-Za-z]:(?![\\/])/;
2926
+ const HYPHEN_CHAR_CODE = 0x2d;
2927
+ function safe_path_segment_isDriveRelativePath(value) {
2928
+ return DRIVE_RELATIVE_PREFIX.test(value);
2929
+ }
2930
+ function assertNoDriveRelativePathSegments(value, label) {
2931
+ if (value.split("/").some(safe_path_segment_isDriveRelativePath)) {
2932
+ throw new errors_FsSafeError("invalid-path", `${label} must not contain a drive letter`);
2650
2933
  }
2651
- if (!fs.existsSync(cursor)) {
2652
- return normalized;
2934
+ return value;
2935
+ }
2936
+ function trimHyphenEdges(value) {
2937
+ let start = 0;
2938
+ let end = value.length;
2939
+ while (start < end && value.charCodeAt(start) === HYPHEN_CHAR_CODE) {
2940
+ start += 1;
2653
2941
  }
2654
- try {
2655
- const resolvedAncestor = path.resolve(fs.realpathSync(cursor));
2656
- return missingSuffix.length === 0
2657
- ? resolvedAncestor
2658
- : path.resolve(resolvedAncestor, ...missingSuffix);
2942
+ while (end > start && value.charCodeAt(end - 1) === HYPHEN_CHAR_CODE) {
2943
+ end -= 1;
2659
2944
  }
2660
- catch {
2661
- return normalized;
2945
+ return start === 0 && end === value.length ? value : value.slice(start, end);
2946
+ }
2947
+ function isSafePathSegment(segment, options = {}) {
2948
+ return (segment !== "" &&
2949
+ segment !== "." &&
2950
+ segment !== ".." &&
2951
+ !segment.includes("/") &&
2952
+ !segment.includes("\\") &&
2953
+ !segment.includes("\0") &&
2954
+ (options.allowDotPrefix === true || !segment.startsWith(".")) &&
2955
+ (options.allowDotPrefix === true
2956
+ ? SAFE_DOT_PREFIX_PATH_SEGMENT_PATTERN.test(segment)
2957
+ : SAFE_PATH_SEGMENT_PATTERN.test(segment)));
2958
+ }
2959
+ function assertSafePathSegment(segment, options = {}) {
2960
+ // Validate the exact value callers will later join into paths; trimming here
2961
+ // would let whitespace-padded ids pass and then be used verbatim.
2962
+ if (!isSafePathSegment(segment, options)) {
2963
+ throw new FsSafeError("invalid-path", `${options.label ?? "path segment"} must be a safe path segment`);
2964
+ }
2965
+ return segment;
2966
+ }
2967
+ function sanitizeSafePathSegment(value, fallback, options = {}) {
2968
+ const sanitized = value
2969
+ .trim()
2970
+ .replace(/[\\/]+/g, "-")
2971
+ .replace(/\0/g, "")
2972
+ .replace(/[^A-Za-z0-9._-]+/g, "-");
2973
+ const trimmed = trimHyphenEdges(sanitized);
2974
+ if (isSafePathSegment(trimmed, options)) {
2975
+ return trimmed;
2976
+ }
2977
+ return assertSafePathSegment(fallback, { ...options, label: "fallback path segment" });
2978
+ }
2979
+ function assertSafePathPrefix(prefix, options = {}) {
2980
+ // Prefixes are often derived from safe filenames. Normalize harmless
2981
+ // filename characters first, but still reject real path-control bytes.
2982
+ if (prefix.includes("/") || prefix.includes("\\") || prefix.includes("\0")) {
2983
+ return assertSafePathSegment(prefix, {
2984
+ allowDotPrefix: true,
2985
+ ...options,
2986
+ label: options.label ?? "path prefix",
2987
+ });
2662
2988
  }
2989
+ return assertSafePathSegment(prefix.replace(/[^A-Za-z0-9._-]+/g, "-"), {
2990
+ allowDotPrefix: true,
2991
+ ...options,
2992
+ label: options.label ?? "path prefix",
2993
+ });
2994
+ }
2995
+
2996
+ ;// CONCATENATED MODULE: external "node:os"
2997
+ const external_node_os_namespaceObject = __rspack_createRequire_require("node:os");
2998
+ ;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/short-path.js
2999
+
3000
+
3001
+ function shortPath(value) {
3002
+ const home = external_node_os_namespaceObject.homedir();
3003
+ const shortened = value.startsWith(home) ? `~${value.slice(home.length)}` : value;
3004
+ return formatErrorDetail(shortened);
2663
3005
  }
2664
3006
 
2665
3007
  ;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/root-path.js
@@ -2670,6 +3012,10 @@ function root_path_existing_resolvePathViaExistingAncestorSync(targetPath) {
2670
3012
 
2671
3013
 
2672
3014
 
3015
+
3016
+
3017
+
3018
+
2673
3019
  const ROOT_PATH_ALIAS_POLICIES = {
2674
3020
  strict: Object.freeze({
2675
3021
  allowFinalSymlinkForUnlink: false,
@@ -2681,11 +3027,20 @@ const ROOT_PATH_ALIAS_POLICIES = {
2681
3027
  }),
2682
3028
  };
2683
3029
  async function resolveRootPath(params) {
3030
+ try {
3031
+ return await resolveRootPathInternal(params);
3032
+ }
3033
+ catch (error) {
3034
+ throw sanitizeRootPathError(error);
3035
+ }
3036
+ }
3037
+ async function resolveRootPathInternal(params) {
3038
+ assertValidRootPathInputs(params);
2684
3039
  const rootPath = external_node_path_namespaceObject.resolve(params.rootPath);
2685
3040
  const absolutePath = external_node_path_namespaceObject.resolve(params.absolutePath);
2686
3041
  const rootCanonicalPath = params.rootCanonicalPath
2687
3042
  ? external_node_path_namespaceObject.resolve(params.rootCanonicalPath)
2688
- : await root_path_existing_resolvePathViaExistingAncestor(rootPath);
3043
+ : await resolvePathViaExistingAncestor(rootPath);
2689
3044
  const context = createBoundaryResolutionContext({
2690
3045
  resolveParams: params,
2691
3046
  rootPath,
@@ -2711,6 +3066,15 @@ async function resolveRootPath(params) {
2711
3066
  });
2712
3067
  }
2713
3068
  function resolveRootPathSync(params) {
3069
+ try {
3070
+ return resolveRootPathSyncInternal(params);
3071
+ }
3072
+ catch (error) {
3073
+ throw sanitizeRootPathError(error);
3074
+ }
3075
+ }
3076
+ function resolveRootPathSyncInternal(params) {
3077
+ assertValidRootPathInputs(params);
2714
3078
  const rootPath = path.resolve(params.rootPath);
2715
3079
  const absolutePath = path.resolve(params.absolutePath);
2716
3080
  const rootCanonicalPath = params.rootCanonicalPath
@@ -2740,6 +3104,29 @@ function resolveRootPathSync(params) {
2740
3104
  rootCanonicalPath: context.rootCanonicalPath,
2741
3105
  });
2742
3106
  }
3107
+ function sanitizeRootPathError(error) {
3108
+ if (error instanceof Error) {
3109
+ error.message = formatErrorDetail(error.message);
3110
+ }
3111
+ return error;
3112
+ }
3113
+ function assertValidRootPathInputs(params) {
3114
+ path_assertNoNulPathInput(params.rootPath, "root path contains a NUL byte");
3115
+ path_assertNoNulPathInput(params.absolutePath, "absolute path contains a NUL byte");
3116
+ assertNoEmbeddedDriveRelativeSegment(params.rootPath, "root path");
3117
+ assertNoEmbeddedDriveRelativeSegment(params.absolutePath, "absolute path");
3118
+ if (params.rootCanonicalPath !== undefined) {
3119
+ path_assertNoNulPathInput(params.rootCanonicalPath, "canonical root path contains a NUL byte");
3120
+ assertNoEmbeddedDriveRelativeSegment(params.rootCanonicalPath, "canonical root path");
3121
+ }
3122
+ }
3123
+ function assertNoEmbeddedDriveRelativeSegment(filePath, label) {
3124
+ if (process.platform !== "win32") {
3125
+ return;
3126
+ }
3127
+ const root = external_node_path_namespaceObject.parse(filePath).root;
3128
+ assertNoDriveRelativePathSegments(filePath.slice(root.length).replaceAll("\\", "/"), label);
3129
+ }
2743
3130
  function isPromiseLike(value) {
2744
3131
  return Boolean(value &&
2745
3132
  (typeof value === "object" || typeof value === "function") &&
@@ -2758,6 +3145,15 @@ function createLexicalTraversalState(params) {
2758
3145
  preserveFinalSymlink: false,
2759
3146
  };
2760
3147
  }
3148
+ function createLexicalTraversalContext(params) {
3149
+ return {
3150
+ state: createLexicalTraversalState(params),
3151
+ resolveParams: params.params,
3152
+ rootPath: params.rootPath,
3153
+ rootCanonicalPath: params.rootCanonicalPath,
3154
+ absolutePath: params.absolutePath,
3155
+ };
3156
+ }
2761
3157
  function splitTraversalSegments(value) {
2762
3158
  return value
2763
3159
  .split(process.platform === "win32" ? /[\\/]+/ : /\/+/)
@@ -2781,149 +3177,97 @@ function rawPathRelativeToRoot(rootPath, candidatePath) {
2781
3177
  : candidatePrefix === rootWithSep;
2782
3178
  return prefixMatches ? candidate.slice(rootWithSep.length) : undefined;
2783
3179
  }
2784
- function assertLexicalCursorInsideBoundary(params) {
3180
+ function assertLexicalCursorInsideBoundary(context, candidatePath) {
2785
3181
  assertInsideBoundary({
2786
- boundaryLabel: params.params.boundaryLabel,
2787
- rootCanonicalPath: params.rootCanonicalPath,
2788
- candidatePath: params.candidatePath,
2789
- absolutePath: params.absolutePath,
3182
+ boundaryLabel: context.resolveParams.boundaryLabel,
3183
+ rootCanonicalPath: context.rootCanonicalPath,
3184
+ candidatePath,
3185
+ absolutePath: context.absolutePath,
2790
3186
  });
2791
3187
  }
2792
- function applyMissingSuffixToCanonicalCursor(params) {
2793
- const missingSuffix = params.state.segments.slice(params.missingFromIndex);
3188
+ function applyMissingSuffixToCanonicalCursor(context, missingFromIndex) {
3189
+ const missingSuffix = context.state.segments.slice(missingFromIndex);
2794
3190
  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
- });
3191
+ advanceCanonicalCursorForSegment(context, segment);
2802
3192
  }
2803
3193
  }
2804
- function advanceCanonicalCursorForSegment(params) {
2805
- params.state.canonicalCursor = external_node_path_namespaceObject.resolve(params.state.canonicalCursor, params.segment);
2806
- assertLexicalCursorInsideBoundary({
2807
- params: params.params,
2808
- rootCanonicalPath: params.rootCanonicalPath,
2809
- candidatePath: params.state.canonicalCursor,
2810
- absolutePath: params.absolutePath,
2811
- });
3194
+ function advanceCanonicalCursorForSegment(context, segment) {
3195
+ context.state.canonicalCursor = external_node_path_namespaceObject.resolve(context.state.canonicalCursor, segment);
3196
+ assertLexicalCursorInsideBoundary(context, context.state.canonicalCursor);
2812
3197
  }
2813
- function finalizeLexicalResolution(params) {
2814
- assertLexicalCursorInsideBoundary({
2815
- params: params.params,
2816
- rootCanonicalPath: params.rootCanonicalPath,
2817
- candidatePath: params.state.canonicalCursor,
2818
- absolutePath: params.absolutePath,
2819
- });
3198
+ function finalizeLexicalResolution(context, kind) {
3199
+ assertLexicalCursorInsideBoundary(context, context.state.canonicalCursor);
2820
3200
  return buildResolvedRootPath({
2821
- absolutePath: params.absolutePath,
2822
- canonicalPath: params.state.canonicalCursor,
2823
- rootPath: params.rootPath,
2824
- rootCanonicalPath: params.rootCanonicalPath,
2825
- kind: params.kind,
3201
+ absolutePath: context.absolutePath,
3202
+ canonicalPath: context.state.canonicalCursor,
3203
+ rootPath: context.rootPath,
3204
+ rootCanonicalPath: context.rootCanonicalPath,
3205
+ kind,
2826
3206
  });
2827
3207
  }
2828
- function handleLexicalLstatFailure(params) {
2829
- if (!path_isNotFoundPathError(params.error)) {
3208
+ function handleLexicalLstatFailure(context, error, missingFromIndex) {
3209
+ if (!path_isNotFoundPathError(error)) {
2830
3210
  return false;
2831
3211
  }
2832
- applyMissingSuffixToCanonicalCursor({
2833
- state: params.state,
2834
- missingFromIndex: params.missingFromIndex,
2835
- rootCanonicalPath: params.rootCanonicalPath,
2836
- params: params.resolveParams,
2837
- absolutePath: params.absolutePath,
2838
- });
3212
+ applyMissingSuffixToCanonicalCursor(context, missingFromIndex);
2839
3213
  return true;
2840
3214
  }
2841
- function handleLexicalStatReadFailure(params) {
2842
- if (handleLexicalLstatFailure({
2843
- error: params.error,
2844
- state: params.state,
2845
- missingFromIndex: params.missingFromIndex,
2846
- rootCanonicalPath: params.rootCanonicalPath,
2847
- resolveParams: params.resolveParams,
2848
- absolutePath: params.absolutePath,
2849
- })) {
3215
+ function handleLexicalStatReadFailure(context, error, missingFromIndex) {
3216
+ if (handleLexicalLstatFailure(context, error, missingFromIndex)) {
2850
3217
  return null;
2851
3218
  }
2852
- throw params.error;
3219
+ throw error;
2853
3220
  }
2854
- function handleLexicalStatDisposition(params) {
3221
+ function handleLexicalStatDisposition(context, params) {
2855
3222
  if (!params.isSymbolicLink) {
2856
- advanceCanonicalCursorForSegment({
2857
- state: params.state,
2858
- segment: params.segment,
2859
- rootCanonicalPath: params.rootCanonicalPath,
2860
- params: params.resolveParams,
2861
- absolutePath: params.absolutePath,
2862
- });
3223
+ advanceCanonicalCursorForSegment(context, params.segment);
2863
3224
  return "continue";
2864
3225
  }
2865
- if (params.state.allowFinalSymlink && params.isLast) {
2866
- params.state.preserveFinalSymlink = true;
2867
- advanceCanonicalCursorForSegment({
2868
- state: params.state,
2869
- segment: params.segment,
2870
- rootCanonicalPath: params.rootCanonicalPath,
2871
- params: params.resolveParams,
2872
- absolutePath: params.absolutePath,
2873
- });
3226
+ if (context.resolveParams.rejectSymlinks === true && params.isLast) {
3227
+ throw new errors_FsSafeError("symlink", "symlink path component not allowed");
3228
+ }
3229
+ if (context.state.allowFinalSymlink && params.isLast) {
3230
+ context.state.preserveFinalSymlink = true;
3231
+ advanceCanonicalCursorForSegment(context, params.segment);
2874
3232
  return "break";
2875
3233
  }
2876
3234
  return "resolve-link";
2877
3235
  }
2878
- function applyResolvedSymlinkHop(params) {
2879
- if (!path_isPathInside(params.rootCanonicalPath, params.linkCanonical)) {
3236
+ function applyResolvedSymlinkHop(context, linkCanonical) {
3237
+ if (!path_isPathInside(context.rootCanonicalPath, linkCanonical)) {
2880
3238
  throw symlinkEscapeError({
2881
- boundaryLabel: params.boundaryLabel,
2882
- rootCanonicalPath: params.rootCanonicalPath,
2883
- symlinkPath: params.state.lexicalCursor,
3239
+ boundaryLabel: context.resolveParams.boundaryLabel,
3240
+ rootCanonicalPath: context.rootCanonicalPath,
3241
+ symlinkPath: context.state.lexicalCursor,
2884
3242
  });
2885
3243
  }
2886
- params.state.canonicalCursor = params.linkCanonical;
2887
- params.state.lexicalCursor = params.linkCanonical;
3244
+ context.state.canonicalCursor = linkCanonical;
3245
+ context.state.lexicalCursor = linkCanonical;
2888
3246
  }
2889
- function readLexicalStat(params) {
3247
+ function readLexicalStat(context, params) {
2890
3248
  try {
2891
- const stat = params.read(params.state.lexicalCursor);
3249
+ const stat = params.read(context.state.lexicalCursor);
2892
3250
  if (isPromiseLike(stat)) {
2893
- return Promise.resolve(stat).catch((error) => handleLexicalStatReadFailure({ ...params, error }));
3251
+ return Promise.resolve(stat).catch((error) => handleLexicalStatReadFailure(context, error, params.missingFromIndex));
2894
3252
  }
2895
3253
  return stat;
2896
3254
  }
2897
3255
  catch (error) {
2898
- return handleLexicalStatReadFailure({ ...params, error });
3256
+ return handleLexicalStatReadFailure(context, error, params.missingFromIndex);
2899
3257
  }
2900
3258
  }
2901
- function resolveAndApplySymlinkHop(params) {
2902
- const linkCanonical = params.resolveLinkCanonical(params.state.lexicalCursor);
3259
+ function resolveAndApplySymlinkHop(context, params) {
3260
+ const linkCanonical = params.resolveLinkCanonical(context.state.lexicalCursor);
2903
3261
  if (isPromiseLike(linkCanonical)) {
2904
- return Promise.resolve(linkCanonical).then((value) => applyResolvedSymlinkHop({
2905
- state: params.state,
2906
- linkCanonical: value,
2907
- rootCanonicalPath: params.rootCanonicalPath,
2908
- boundaryLabel: params.boundaryLabel,
2909
- }));
3262
+ return Promise.resolve(linkCanonical).then((value) => {
3263
+ applyResolvedSymlinkHop(context, value);
3264
+ });
2910
3265
  }
2911
- applyResolvedSymlinkHop({
2912
- state: params.state,
2913
- linkCanonical,
2914
- rootCanonicalPath: params.rootCanonicalPath,
2915
- boundaryLabel: params.boundaryLabel,
2916
- });
3266
+ applyResolvedSymlinkHop(context, linkCanonical);
2917
3267
  }
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
- });
3268
+ function applyParentTraversalStep(context) {
3269
+ context.state.lexicalCursor = external_node_path_namespaceObject.resolve(context.state.lexicalCursor, "..");
3270
+ advanceCanonicalCursorForSegment(context, "..");
2927
3271
  }
2928
3272
  function* iterateLexicalTraversal(state) {
2929
3273
  for (let idx = 0; idx < state.segments.length; idx += 1) {
@@ -2933,32 +3277,22 @@ function* iterateLexicalTraversal(state) {
2933
3277
  }
2934
3278
  }
2935
3279
  async function resolveRootPathLexicalAsync(params) {
2936
- const state = createLexicalTraversalState(params);
2937
- const sharedStepParams = {
2938
- state,
2939
- rootCanonicalPath: params.rootCanonicalPath,
2940
- resolveParams: params.params,
2941
- absolutePath: params.absolutePath,
2942
- };
3280
+ const context = createLexicalTraversalContext(params);
3281
+ const { state } = context;
2943
3282
  for (const { idx, segment, isLast } of iterateLexicalTraversal(state)) {
2944
3283
  if (segment === "..") {
2945
- applyParentTraversalStep({
2946
- ...sharedStepParams,
2947
- resolveParams: params.params,
2948
- });
3284
+ applyParentTraversalStep(context);
2949
3285
  continue;
2950
3286
  }
2951
3287
  state.lexicalCursor = external_node_path_namespaceObject.join(state.lexicalCursor, segment);
2952
- const stat = await readLexicalStat({
2953
- ...sharedStepParams,
3288
+ const stat = await readLexicalStat(context, {
2954
3289
  missingFromIndex: idx,
2955
3290
  read: (cursor) => promises_namespaceObject.lstat(cursor),
2956
3291
  });
2957
3292
  if (!stat) {
2958
3293
  break;
2959
3294
  }
2960
- const disposition = handleLexicalStatDisposition({
2961
- ...sharedStepParams,
3295
+ const disposition = handleLexicalStatDisposition(context, {
2962
3296
  isSymbolicLink: stat.isSymbolicLink(),
2963
3297
  segment,
2964
3298
  isLast,
@@ -2969,41 +3303,29 @@ async function resolveRootPathLexicalAsync(params) {
2969
3303
  if (disposition === "break") {
2970
3304
  break;
2971
3305
  }
2972
- await resolveAndApplySymlinkHop({
2973
- state,
2974
- rootCanonicalPath: params.rootCanonicalPath,
2975
- boundaryLabel: params.params.boundaryLabel,
3306
+ await resolveAndApplySymlinkHop(context, {
2976
3307
  resolveLinkCanonical: (cursor) => resolveSymlinkHopPath(cursor),
2977
3308
  });
2978
- }
2979
- const kind = await getPathKind(state.canonicalCursor, state.preserveFinalSymlink);
2980
- return finalizeLexicalResolution({
2981
- ...params,
2982
- state,
2983
- kind,
2984
- });
3309
+ if (context.resolveParams.rejectSymlinks === true) {
3310
+ throw new errors_FsSafeError("symlink", "symlink path component not allowed");
3311
+ }
3312
+ }
3313
+ const kind = await getPathKind(state.canonicalCursor, state.preserveFinalSymlink);
3314
+ return finalizeLexicalResolution(context, kind);
2985
3315
  }
2986
3316
  function resolveRootPathLexicalSync(params) {
2987
- const state = createLexicalTraversalState(params);
3317
+ const context = createLexicalTraversalContext(params);
3318
+ const { state } = context;
2988
3319
  for (let idx = 0; idx < state.segments.length; idx += 1) {
2989
3320
  const segment = state.segments[idx] ?? "";
2990
3321
  const isLast = idx === state.segments.length - 1;
2991
3322
  if (segment === "..") {
2992
- applyParentTraversalStep({
2993
- state,
2994
- rootCanonicalPath: params.rootCanonicalPath,
2995
- resolveParams: params.params,
2996
- absolutePath: params.absolutePath,
2997
- });
3323
+ applyParentTraversalStep(context);
2998
3324
  continue;
2999
3325
  }
3000
3326
  state.lexicalCursor = path.join(state.lexicalCursor, segment);
3001
- const maybeStat = readLexicalStat({
3002
- state,
3327
+ const maybeStat = readLexicalStat(context, {
3003
3328
  missingFromIndex: idx,
3004
- rootCanonicalPath: params.rootCanonicalPath,
3005
- resolveParams: params.params,
3006
- absolutePath: params.absolutePath,
3007
3329
  read: (cursor) => fs.lstatSync(cursor),
3008
3330
  });
3009
3331
  if (isPromiseLike(maybeStat)) {
@@ -3013,14 +3335,10 @@ function resolveRootPathLexicalSync(params) {
3013
3335
  if (!stat) {
3014
3336
  break;
3015
3337
  }
3016
- const disposition = handleLexicalStatDisposition({
3017
- state,
3338
+ const disposition = handleLexicalStatDisposition(context, {
3018
3339
  isSymbolicLink: stat.isSymbolicLink(),
3019
3340
  segment,
3020
3341
  isLast,
3021
- rootCanonicalPath: params.rootCanonicalPath,
3022
- resolveParams: params.params,
3023
- absolutePath: params.absolutePath,
3024
3342
  });
3025
3343
  if (disposition === "continue") {
3026
3344
  continue;
@@ -3028,22 +3346,18 @@ function resolveRootPathLexicalSync(params) {
3028
3346
  if (disposition === "break") {
3029
3347
  break;
3030
3348
  }
3031
- const maybeApplied = resolveAndApplySymlinkHop({
3032
- state,
3033
- rootCanonicalPath: params.rootCanonicalPath,
3034
- boundaryLabel: params.params.boundaryLabel,
3349
+ const maybeApplied = resolveAndApplySymlinkHop(context, {
3035
3350
  resolveLinkCanonical: (cursor) => resolveSymlinkHopPathSync(cursor),
3036
3351
  });
3037
3352
  if (isPromiseLike(maybeApplied)) {
3038
3353
  throw new Error("Unexpected async symlink resolution");
3039
3354
  }
3355
+ if (context.resolveParams.rejectSymlinks === true) {
3356
+ throw new FsSafeError("symlink", "symlink path component not allowed");
3357
+ }
3040
3358
  }
3041
3359
  const kind = getPathKindSync(state.canonicalCursor, state.preserveFinalSymlink);
3042
- return finalizeLexicalResolution({
3043
- ...params,
3044
- state,
3045
- kind,
3046
- });
3360
+ return finalizeLexicalResolution(context, kind);
3047
3361
  }
3048
3362
  function resolveCanonicalOutsideLexicalPath(params) {
3049
3363
  return params.outsideLexicalCanonicalPath ?? params.absolutePath;
@@ -3107,7 +3421,7 @@ async function resolveOutsideLexicalCanonicalPathAsync(params) {
3107
3421
  if (path_isPathInside(params.rootPath, params.absolutePath)) {
3108
3422
  return undefined;
3109
3423
  }
3110
- return await root_path_existing_resolvePathViaExistingAncestor(params.absolutePath);
3424
+ return await resolvePathViaExistingAncestor(params.absolutePath);
3111
3425
  }
3112
3426
  function resolveOutsideLexicalCanonicalPathSync(params) {
3113
3427
  if (isPathInside(params.rootPath, params.absolutePath)) {
@@ -3214,39 +3528,6 @@ function pathEscapeError(params) {
3214
3528
  function symlinkEscapeError(params) {
3215
3529
  return new Error(`Symlink escapes ${params.boundaryLabel} (${shortPath(params.rootCanonicalPath)}): ${shortPath(params.symlinkPath)}`);
3216
3530
  }
3217
- function shortPath(value) {
3218
- const home = external_node_os_namespaceObject.homedir();
3219
- if (value.startsWith(home)) {
3220
- return `~${value.slice(home.length)}`;
3221
- }
3222
- return value;
3223
- }
3224
- async function resolveSymlinkHopPath(symlinkPath) {
3225
- try {
3226
- return external_node_path_namespaceObject.resolve(await promises_namespaceObject.realpath(symlinkPath));
3227
- }
3228
- catch (error) {
3229
- if (!path_isNotFoundPathError(error)) {
3230
- throw error;
3231
- }
3232
- const linkTarget = await promises_namespaceObject.readlink(symlinkPath);
3233
- const linkAbsolute = external_node_path_namespaceObject.resolve(external_node_path_namespaceObject.dirname(symlinkPath), linkTarget);
3234
- return root_path_existing_resolvePathViaExistingAncestor(linkAbsolute);
3235
- }
3236
- }
3237
- function resolveSymlinkHopPathSync(symlinkPath) {
3238
- try {
3239
- return path.resolve(fs.realpathSync(symlinkPath));
3240
- }
3241
- catch (error) {
3242
- if (!isNotFoundPathError(error)) {
3243
- throw error;
3244
- }
3245
- const linkTarget = fs.readlinkSync(symlinkPath);
3246
- const linkAbsolute = path.resolve(path.dirname(symlinkPath), linkTarget);
3247
- return resolvePathViaExistingAncestorSync(linkAbsolute);
3248
- }
3249
- }
3250
3531
 
3251
3532
  ;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/path-policy.js
3252
3533
 
@@ -3290,14 +3571,8 @@ async function assertNoHardlinkedFinalPath(params) {
3290
3571
  return;
3291
3572
  }
3292
3573
  if (stat.nlink > 1) {
3293
- throw new Error(`Hardlinked path is not allowed under ${params.boundaryLabel} (${path_policy_shortPath(params.root)}): ${path_policy_shortPath(params.filePath)}`);
3294
- }
3295
- }
3296
- function path_policy_shortPath(value) {
3297
- if (value.startsWith(external_node_os_namespaceObject.homedir())) {
3298
- return `~${value.slice(external_node_os_namespaceObject.homedir().length)}`;
3574
+ throw new Error(`Hardlinked path is not allowed under ${params.boundaryLabel} (${shortPath(params.root)}): ${shortPath(params.filePath)}`);
3299
3575
  }
3300
- return value;
3301
3576
  }
3302
3577
 
3303
3578
  ;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/string-coerce.js
@@ -3379,6 +3654,10 @@ function hasNonEmptyString(value) {
3379
3654
 
3380
3655
 
3381
3656
  const ENCODED_FILE_URL_SEPARATOR_RE = /%(?:2f|5c)/i;
3657
+ const FILE_URL_PREFIX_RE = /^file:\/\//i;
3658
+ function isFileUrl(input) {
3659
+ return FILE_URL_PREFIX_RE.test(input);
3660
+ }
3382
3661
  function isLocalFileUrlHost(hostname) {
3383
3662
  const normalized = normalizeLowercaseStringOrEmpty(hostname);
3384
3663
  return normalized === "" || normalized === "localhost";
@@ -3409,7 +3688,7 @@ function assertNoWindowsNetworkPath(filePath, label = "Path") {
3409
3688
  throw new Error(`${label} cannot use Windows network paths: ${filePath}`);
3410
3689
  }
3411
3690
  }
3412
- function safeFileURLToPath(fileUrl) {
3691
+ function safeFileURLToPath(fileUrl, platform = process.platform) {
3413
3692
  let parsed;
3414
3693
  try {
3415
3694
  parsed = new external_node_url_namespaceObject.URL(fileUrl);
@@ -3426,13 +3705,15 @@ function safeFileURLToPath(fileUrl) {
3426
3705
  if (hasEncodedFileUrlSeparator(parsed.pathname)) {
3427
3706
  throw new Error(`file:// URLs cannot encode path separators: ${fileUrl}`);
3428
3707
  }
3429
- const filePath = (0,external_node_url_namespaceObject.fileURLToPath)(parsed);
3430
- assertNoWindowsNetworkPath(filePath, "Local file URL");
3708
+ const filePath = (0,external_node_url_namespaceObject.fileURLToPath)(parsed, { windows: platform === "win32" });
3709
+ if (isWindowsNetworkPath(filePath, platform)) {
3710
+ throw new Error(`Local file URL cannot use Windows network paths: ${filePath}`);
3711
+ }
3431
3712
  return filePath;
3432
3713
  }
3433
- function trySafeFileURLToPath(fileUrl) {
3714
+ function trySafeFileURLToPath(fileUrl, platform = process.platform) {
3434
3715
  try {
3435
- return safeFileURLToPath(fileUrl);
3716
+ return safeFileURLToPath(fileUrl, platform);
3436
3717
  }
3437
3718
  catch {
3438
3719
  return undefined;
@@ -3442,7 +3723,7 @@ function basenameFromMediaSource(source) {
3442
3723
  if (!source) {
3443
3724
  return undefined;
3444
3725
  }
3445
- if (source.startsWith("file://")) {
3726
+ if (isFileUrl(source)) {
3446
3727
  const filePath = trySafeFileURLToPath(source);
3447
3728
  return filePath ? path.basename(filePath) || undefined : undefined;
3448
3729
  }
@@ -3527,11 +3808,11 @@ function trimTrailingWindowsIgnoredChars(value) {
3527
3808
  }
3528
3809
  return end === value.length ? value : value.slice(0, end);
3529
3810
  }
3530
- function candidateReadPaths(filePath) {
3531
- if (!filePath.startsWith("file://")) {
3811
+ function candidateReadPaths(filePath, platform) {
3812
+ if (!isFileUrl(filePath)) {
3532
3813
  return [filePath];
3533
3814
  }
3534
- const parsed = trySafeFileURLToPath(filePath);
3815
+ const parsed = trySafeFileURLToPath(filePath, platform);
3535
3816
  return parsed === undefined ? [filePath] : [filePath, parsed];
3536
3817
  }
3537
3818
  function normalizePosixPath(filePath, cwd) {
@@ -3574,7 +3855,7 @@ function matchWindowsDeviceReadPath(filePath) {
3574
3855
  }
3575
3856
  function matchUnsafeDeviceReadPath(filePath, options = {}) {
3576
3857
  const platform = options.platform ?? process.platform;
3577
- for (const candidate of candidateReadPaths(filePath)) {
3858
+ for (const candidate of candidateReadPaths(filePath, platform)) {
3578
3859
  const match = platform === "win32"
3579
3860
  ? matchWindowsDeviceReadPath(candidate)
3580
3861
  : matchPosixDeviceReadPath(candidate, options.cwd);
@@ -3611,23 +3892,6 @@ async function read_opened_file_readOpenedFileSafely(params) {
3611
3892
  };
3612
3893
  }
3613
3894
 
3614
- ;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/path-stat.js
3615
- function pathStatFromStats(stat) {
3616
- return {
3617
- dev: Number(stat.dev),
3618
- gid: Number(stat.gid),
3619
- ino: Number(stat.ino),
3620
- isDirectory: stat.isDirectory(),
3621
- isFile: stat.isFile(),
3622
- isSymbolicLink: stat.isSymbolicLink(),
3623
- mode: stat.mode,
3624
- mtimeMs: stat.mtimeMs,
3625
- nlink: stat.nlink,
3626
- size: stat.size,
3627
- uid: stat.uid,
3628
- };
3629
- }
3630
-
3631
3895
  ;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/home-dir.js
3632
3896
 
3633
3897
 
@@ -3750,10 +4014,19 @@ function resolveOsHomeRelativePath(input, opts) {
3750
4014
 
3751
4015
 
3752
4016
 
4017
+
4018
+
4019
+
3753
4020
  const ensureTrailingSep = (value) => value.endsWith(external_node_path_namespaceObject.sep) ? value : value + external_node_path_namespaceObject.sep;
3754
4021
  function assertValidRootRelativePath(relativePath) {
3755
4022
  path_assertNoNulPathInput(relativePath, "relative path contains a NUL byte");
3756
4023
  }
4024
+ function assertValidRootDestinationPath(relativePath) {
4025
+ assertValidRootRelativePath(relativePath);
4026
+ if (safe_path_segment_isDriveRelativePath(relativePath)) {
4027
+ throw new errors_FsSafeError("invalid-path", "relative path must not start with a drive letter");
4028
+ }
4029
+ }
3757
4030
  let cachedHomePath;
3758
4031
  async function expandRelativePathWithHome(relativePath) {
3759
4032
  const rawHome = process.env.HOME || process.env.USERPROFILE || external_node_os_namespaceObject.homedir();
@@ -3772,12 +4045,14 @@ async function expandRelativePathWithHome(relativePath) {
3772
4045
  async function resolveRootContext(rootDir) {
3773
4046
  path_assertNoNulPathInput(rootDir, "root dir contains a NUL byte");
3774
4047
  let rootReal;
4048
+ let rootIdentity;
3775
4049
  try {
3776
4050
  rootReal = await promises_namespaceObject.realpath(rootDir);
3777
4051
  const rootStat = await promises_namespaceObject.stat(rootReal);
3778
4052
  if (!rootStat.isDirectory()) {
3779
4053
  throw new errors_FsSafeError("invalid-path", "root dir is not a directory");
3780
4054
  }
4055
+ rootIdentity = { dev: rootStat.dev, ino: rootStat.ino };
3781
4056
  }
3782
4057
  catch (err) {
3783
4058
  if (err instanceof errors_FsSafeError) {
@@ -3790,16 +4065,37 @@ async function resolveRootContext(rootDir) {
3790
4065
  }
3791
4066
  return {
3792
4067
  rootDir: external_node_path_namespaceObject.resolve(rootDir),
4068
+ rootIdentity,
3793
4069
  rootReal,
3794
4070
  rootWithSep: ensureTrailingSep(rootReal),
3795
4071
  };
3796
4072
  }
4073
+ async function assertRootIdentityCurrent(root) {
4074
+ let current;
4075
+ try {
4076
+ current = await promises_namespaceObject.lstat(root.rootReal);
4077
+ }
4078
+ catch (error) {
4079
+ throw new errors_FsSafeError("path-mismatch", "root path changed during operation", {
4080
+ cause: error instanceof Error ? error : undefined,
4081
+ });
4082
+ }
4083
+ if (current.isSymbolicLink() ||
4084
+ !current.isDirectory() ||
4085
+ !file_identity_sameFileIdentity(current, root.rootIdentity)) {
4086
+ throw new errors_FsSafeError("path-mismatch", "root path changed during operation");
4087
+ }
4088
+ }
3797
4089
  async function resolvePathInRoot(root, relativePath, options) {
3798
4090
  assertValidRootRelativePath(relativePath);
4091
+ await assertRootIdentityCurrent(root);
3799
4092
  const expanded = await expandRelativePathWithHome(relativePath);
3800
4093
  const resolved = external_node_path_namespaceObject.resolve(root.rootWithSep, expanded);
3801
4094
  if (!path_isPathInside(root.rootWithSep, resolved)) {
3802
- throw new errors_FsSafeError("outside-workspace", "file is outside workspace root");
4095
+ throw outsideWorkspaceError();
4096
+ }
4097
+ if (options?.rejectUnsafeDeviceReads === true) {
4098
+ assertNoUnsafeDeviceReadPath(resolved);
3803
4099
  }
3804
4100
  const rawAbsolutePath = external_node_path_namespaceObject.isAbsolute(expanded)
3805
4101
  ? expanded
@@ -3811,9 +4107,18 @@ async function resolvePathInRoot(root, relativePath, options) {
3811
4107
  rootCanonicalPath: root.rootReal,
3812
4108
  boundaryLabel: "root",
3813
4109
  policy: options?.allowFinalSymlink ? ROOT_PATH_ALIAS_POLICIES.unlinkTarget : undefined,
4110
+ rejectSymlinks: options?.rejectSymlinks,
3814
4111
  });
3815
4112
  }
3816
4113
  catch (error) {
4114
+ if (error instanceof errors_FsSafeError && error.code === "symlink") {
4115
+ throw error;
4116
+ }
4117
+ if (hasNodeErrorCode(error, "ENAMETOOLONG")) {
4118
+ throw new errors_FsSafeError("invalid-path", "relative path is too long", {
4119
+ cause: error instanceof Error ? error : undefined,
4120
+ });
4121
+ }
3817
4122
  const code = options?.aliasErrorCode ?? "outside-workspace";
3818
4123
  throw new errors_FsSafeError(code, code === "path-alias" ? "path alias escape blocked" : "file is outside workspace root", {
3819
4124
  cause: error instanceof Error ? error : undefined,
@@ -3825,29 +4130,6 @@ async function resolvePathWithinRoot(params) {
3825
4130
  return await resolvePathInRoot(await resolveRootContext(params.rootDir), params.relativePath);
3826
4131
  }
3827
4132
 
3828
- ;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/root-errors.js
3829
-
3830
-
3831
- function isAlreadyExistsError(error) {
3832
- return hasNodeErrorCode(error, "EEXIST") || /File exists|EEXIST/i.test(String(error));
3833
- }
3834
- function normalizePinnedWriteError(error) {
3835
- if (error instanceof errors_FsSafeError) {
3836
- return error;
3837
- }
3838
- return new errors_FsSafeError("invalid-path", "path is not a regular file under root", {
3839
- cause: error instanceof Error ? error : undefined,
3840
- });
3841
- }
3842
- function normalizePinnedPathError(error) {
3843
- if (error instanceof errors_FsSafeError) {
3844
- return error;
3845
- }
3846
- return new errors_FsSafeError("path-alias", "path is not under root", {
3847
- cause: error instanceof Error ? error : undefined,
3848
- });
3849
- }
3850
-
3851
4133
  ;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/json-stringify.js
3852
4134
  function stringifyJsonDocument(value, replacer, space) {
3853
4135
  const text = JSON.stringify(value, replacer, space);
@@ -3882,6 +4164,17 @@ function limitEntry(relativePath) {
3882
4164
  return { relativePath, kind: "truncated", size: 0 };
3883
4165
  }
3884
4166
  async function* walkRoot(root, relativePath, options) {
4167
+ if (!["skip", "follow-within-root"].includes(options.symlinkPolicy)) {
4168
+ throw new TypeError(`invalid root walk symlink policy: ${String(options.symlinkPolicy)}`);
4169
+ }
4170
+ if (options.limitBehavior !== undefined &&
4171
+ !["truncate", "throw"].includes(options.limitBehavior)) {
4172
+ throw new TypeError(`invalid root walk limit behavior: ${String(options.limitBehavior)}`);
4173
+ }
4174
+ if (options.onDirectoryError !== undefined &&
4175
+ !["throw", "skip-and-report"].includes(options.onDirectoryError)) {
4176
+ throw new TypeError(`invalid root walk directory error behavior: ${String(options.onDirectoryError)}`);
4177
+ }
3885
4178
  const maxDepth = validateBudget("maxDepth", options.maxDepth);
3886
4179
  const maxEntries = validateBudget("maxEntries", options.maxEntries);
3887
4180
  const visitedDirectories = new Set();
@@ -3909,17 +4202,21 @@ async function* walkRoot(root, relativePath, options) {
3909
4202
  return;
3910
4203
  }
3911
4204
  visitedDirectories.add(resolvedDirectory.canonicalPath);
4205
+ options.signal?.throwIfAborted();
3912
4206
  const listingDirectory = external_node_path_namespaceObject.relative(root.rootReal, resolvedDirectory.canonicalPath)
3913
4207
  .split(external_node_path_namespaceObject.sep)
3914
4208
  .join(external_node_path_namespaceObject.posix.sep);
3915
4209
  entries = await root.list(listingDirectory, { withFileTypes: true });
3916
4210
  }
3917
4211
  catch (error) {
4212
+ // Cancellation is never a recoverable directory read failure.
4213
+ options.signal?.throwIfAborted();
3918
4214
  if ((options.onDirectoryError ?? "throw") === "throw")
3919
4215
  throw error;
3920
4216
  yield { relativePath: directory, kind: "directory-error", size: 0, error };
3921
4217
  return;
3922
4218
  }
4219
+ options.signal?.throwIfAborted();
3923
4220
  for (const entry of entries) {
3924
4221
  options.signal?.throwIfAborted();
3925
4222
  const child = directory
@@ -4100,11 +4397,11 @@ function logWarn(message) {
4100
4397
  }
4101
4398
  }
4102
4399
  const SUPPORTS_NOFOLLOW = process.platform !== "win32" && "O_NOFOLLOW" in external_node_fs_namespaceObject.constants;
4103
- const NONBLOCK_OPEN_FLAG = "O_NONBLOCK" in external_node_fs_namespaceObject.constants ? external_node_fs_namespaceObject.constants.O_NONBLOCK : 0;
4104
- const OPEN_READ_FLAGS = external_node_fs_namespaceObject.constants.O_RDONLY | (SUPPORTS_NOFOLLOW ? external_node_fs_namespaceObject.constants.O_NOFOLLOW : 0);
4105
- const OPEN_READ_NONBLOCK_FLAGS = OPEN_READ_FLAGS | NONBLOCK_OPEN_FLAG;
4106
- const OPEN_READ_FOLLOW_FLAGS = external_node_fs_namespaceObject.constants.O_RDONLY;
4107
- const OPEN_READ_FOLLOW_NONBLOCK_FLAGS = OPEN_READ_FOLLOW_FLAGS | NONBLOCK_OPEN_FLAG;
4400
+ const NONBLOCK_OPEN_FLAG = process.platform !== "win32" && "O_NONBLOCK" in external_node_fs_namespaceObject.constants ? external_node_fs_namespaceObject.constants.O_NONBLOCK : 0;
4401
+ const OPEN_READ_FLAGS = external_node_fs_namespaceObject.constants.O_RDONLY |
4402
+ (SUPPORTS_NOFOLLOW ? external_node_fs_namespaceObject.constants.O_NOFOLLOW : 0) |
4403
+ NONBLOCK_OPEN_FLAG;
4404
+ const OPEN_READ_FOLLOW_FLAGS = external_node_fs_namespaceObject.constants.O_RDONLY | NONBLOCK_OPEN_FLAG;
4108
4405
  const OPEN_WRITE_EXISTING_FLAGS = external_node_fs_namespaceObject.constants.O_WRONLY | (SUPPORTS_NOFOLLOW ? external_node_fs_namespaceObject.constants.O_NOFOLLOW : 0);
4109
4406
  const OPEN_WRITE_CREATE_FLAGS = external_node_fs_namespaceObject.constants.O_WRONLY |
4110
4407
  external_node_fs_namespaceObject.constants.O_CREAT |
@@ -4117,6 +4414,21 @@ const OPEN_APPEND_CREATE_FLAGS = external_node_fs_namespaceObject.constants.O_RD
4117
4414
  external_node_fs_namespaceObject.constants.O_EXCL |
4118
4415
  (SUPPORTS_NOFOLLOW ? external_node_fs_namespaceObject.constants.O_NOFOLLOW : 0);
4119
4416
  const DEFAULT_ROOT_MAX_BYTES = 16 * 1024 * 1024;
4417
+ function pathStatFromStats(stat) {
4418
+ return {
4419
+ dev: Number(stat.dev),
4420
+ gid: Number(stat.gid),
4421
+ ino: Number(stat.ino),
4422
+ isDirectory: stat.isDirectory(),
4423
+ isFile: stat.isFile(),
4424
+ isSymbolicLink: stat.isSymbolicLink(),
4425
+ mode: stat.mode,
4426
+ mtimeMs: stat.mtimeMs,
4427
+ nlink: stat.nlink,
4428
+ size: stat.size,
4429
+ uid: stat.uid,
4430
+ };
4431
+ }
4120
4432
  function openResult(params) {
4121
4433
  return {
4122
4434
  handle: params.handle,
@@ -4129,11 +4441,15 @@ function openResult(params) {
4129
4441
  async function openVerifiedLocalFile(filePath, options) {
4130
4442
  assertNoUnsafeDeviceReadPath(filePath);
4131
4443
  const fsSafeTestHooks = getFsSafeTestHooks();
4444
+ let preOpenStat;
4132
4445
  // Reject directories before opening so we never surface EISDIR to callers (e.g. tool
4133
4446
  // results that get sent to messaging channels). See openclaw/openclaw#31186.
4134
4447
  try {
4135
- const preStat = await promises_namespaceObject.lstat(filePath);
4136
- if (preStat.isDirectory()) {
4448
+ preOpenStat = await promises_namespaceObject.lstat(filePath);
4449
+ if (preOpenStat.isSymbolicLink() && options?.symlinks !== "follow-within-root") {
4450
+ throw new errors_FsSafeError("symlink", "symlink not allowed");
4451
+ }
4452
+ if (!preOpenStat.isFile() && !preOpenStat.isSymbolicLink()) {
4137
4453
  throw new errors_FsSafeError("not-file", "not a file");
4138
4454
  }
4139
4455
  await fsSafeTestHooks?.afterPreOpenLstat?.(filePath);
@@ -4147,12 +4463,8 @@ async function openVerifiedLocalFile(filePath, options) {
4147
4463
  let handle;
4148
4464
  try {
4149
4465
  const openFlags = options?.symlinks === "follow-within-root"
4150
- ? options?.nonBlockingRead
4151
- ? OPEN_READ_FOLLOW_NONBLOCK_FLAGS
4152
- : OPEN_READ_FOLLOW_FLAGS
4153
- : options?.nonBlockingRead
4154
- ? OPEN_READ_NONBLOCK_FLAGS
4155
- : OPEN_READ_FLAGS;
4466
+ ? OPEN_READ_FOLLOW_FLAGS
4467
+ : OPEN_READ_FLAGS;
4156
4468
  await fsSafeTestHooks?.beforeOpen?.(filePath, openFlags);
4157
4469
  handle = await promises_namespaceObject.open(filePath, openFlags);
4158
4470
  try {
@@ -4165,7 +4477,7 @@ async function openVerifiedLocalFile(filePath, options) {
4165
4477
  }
4166
4478
  catch (err) {
4167
4479
  if (path_isNotFoundPathError(err)) {
4168
- throw new errors_FsSafeError("not-found", "file not found");
4480
+ throw fileNotFoundError();
4169
4481
  }
4170
4482
  if (isSymlinkOpenError(err)) {
4171
4483
  throw new errors_FsSafeError("symlink", "symlink open blocked", { cause: err });
@@ -4181,8 +4493,13 @@ async function openVerifiedLocalFile(filePath, options) {
4181
4493
  if (!stat.isFile()) {
4182
4494
  throw new errors_FsSafeError("not-file", "not a file");
4183
4495
  }
4496
+ if (preOpenStat &&
4497
+ !preOpenStat.isSymbolicLink() &&
4498
+ !file_identity_sameFileIdentity(stat, preOpenStat)) {
4499
+ throw new errors_FsSafeError("path-mismatch", "path changed before open");
4500
+ }
4184
4501
  if (options?.hardlinks === "reject" && stat.nlink > 1) {
4185
- throw new errors_FsSafeError("hardlink", "hardlinked path not allowed");
4502
+ throw hardlinkedPathNotAllowedError();
4186
4503
  }
4187
4504
  if (options?.symlinks === "follow-within-root") {
4188
4505
  const pathStat = await promises_namespaceObject.stat(filePath);
@@ -4202,7 +4519,7 @@ async function openVerifiedLocalFile(filePath, options) {
4202
4519
  const realPath = await resolveOpenedFileRealPathForHandle(handle, filePath);
4203
4520
  const realStat = await promises_namespaceObject.stat(realPath);
4204
4521
  if (options?.hardlinks === "reject" && realStat.nlink > 1) {
4205
- throw new errors_FsSafeError("hardlink", "hardlinked path not allowed");
4522
+ throw hardlinkedPathNotAllowedError();
4206
4523
  }
4207
4524
  if (!file_identity_sameFileIdentity(stat, realStat)) {
4208
4525
  throw new errors_FsSafeError("path-mismatch", "path mismatch");
@@ -4215,17 +4532,19 @@ async function openVerifiedLocalFile(filePath, options) {
4215
4532
  throw err;
4216
4533
  }
4217
4534
  if (path_isNotFoundPathError(err)) {
4218
- throw new errors_FsSafeError("not-found", "file not found");
4535
+ throw fileNotFoundError();
4219
4536
  }
4220
4537
  throw err;
4221
4538
  }
4222
4539
  }
4223
4540
  class RootHandle {
4541
+ rootIdentity;
4224
4542
  rootDir;
4225
4543
  rootReal;
4226
4544
  rootWithSep;
4227
4545
  defaults;
4228
4546
  constructor(context, defaults = {}) {
4547
+ this.rootIdentity = context.rootIdentity;
4229
4548
  this.rootDir = context.rootDir;
4230
4549
  this.rootReal = context.rootReal;
4231
4550
  this.rootWithSep = context.rootWithSep;
@@ -4234,11 +4553,19 @@ class RootHandle {
4234
4553
  get context() {
4235
4554
  return {
4236
4555
  rootDir: this.rootDir,
4556
+ rootIdentity: this.rootIdentity,
4237
4557
  rootReal: this.rootReal,
4238
4558
  rootWithSep: this.rootWithSep,
4239
4559
  };
4240
4560
  }
4561
+ mutationOptions(options) {
4562
+ return {
4563
+ ...options,
4564
+ denyMutations: mergeDenyMutationPolicies(this.defaults.denyMutations, options.denyMutations),
4565
+ };
4566
+ }
4241
4567
  async resolve(relativePath) {
4568
+ assertValidRootDestinationPath(relativePath);
4242
4569
  return (await resolvePathInRoot(this.context, relativePath, { allowFinalSymlink: true })).resolved;
4243
4570
  }
4244
4571
  async open(relativePath, options = {}) {
@@ -4278,67 +4605,67 @@ class RootHandle {
4278
4605
  };
4279
4606
  }
4280
4607
  async openWritable(relativePath, options = {}) {
4608
+ assertValidRootDestinationPath(relativePath);
4281
4609
  const writeMode = options.writeMode ?? "replace";
4282
4610
  return await openWritableFileInRoot(this.context, {
4283
4611
  relativePath,
4284
4612
  mkdir: this.defaults.mkdir,
4285
4613
  mode: this.defaults.mode,
4286
- ...options,
4287
- denyMutations: mergeDenyMutationPolicies(this.defaults.denyMutations, options.denyMutations),
4614
+ ...this.mutationOptions(options),
4288
4615
  append: writeMode === "append",
4289
4616
  truncateExisting: writeMode === "replace",
4290
4617
  });
4291
4618
  }
4292
4619
  async append(relativePath, data, options = {}) {
4620
+ assertValidRootDestinationPath(relativePath);
4293
4621
  await appendFileInRoot(this.context, {
4294
4622
  relativePath,
4295
4623
  data,
4296
4624
  mkdir: this.defaults.mkdir,
4297
4625
  mode: this.defaults.mode,
4298
- ...options,
4299
- denyMutations: mergeDenyMutationPolicies(this.defaults.denyMutations, options.denyMutations),
4626
+ ...this.mutationOptions(options),
4300
4627
  });
4301
4628
  }
4302
4629
  async remove(relativePath, options = {}) {
4303
4630
  assertValidRootRelativePath(relativePath);
4304
4631
  await removePathInRoot(this.context, {
4305
4632
  relativePath,
4306
- denyMutations: mergeDenyMutationPolicies(this.defaults.denyMutations, options.denyMutations),
4633
+ ...this.mutationOptions(options),
4307
4634
  });
4308
4635
  }
4309
4636
  async mkdir(relativePath, options = {}) {
4310
- assertValidRootRelativePath(relativePath);
4637
+ assertValidRootDestinationPath(relativePath);
4311
4638
  await mkdirPathInRoot(this.context, {
4312
4639
  relativePath,
4313
- denyMutations: mergeDenyMutationPolicies(this.defaults.denyMutations, options.denyMutations),
4640
+ ...this.mutationOptions(options),
4314
4641
  });
4315
4642
  }
4316
4643
  async ensureRoot(options = {}) {
4317
4644
  await mkdirPathInRoot(this.context, {
4318
4645
  relativePath: "",
4319
4646
  allowRoot: true,
4320
- denyMutations: mergeDenyMutationPolicies(this.defaults.denyMutations, options.denyMutations),
4647
+ ...this.mutationOptions(options),
4321
4648
  });
4322
4649
  }
4323
4650
  async write(relativePath, data, options = {}) {
4651
+ assertValidRootDestinationPath(relativePath);
4324
4652
  await writeFileInRoot(this.context, {
4325
4653
  relativePath,
4326
4654
  data,
4327
4655
  mkdir: this.defaults.mkdir,
4328
4656
  mode: this.defaults.mode,
4329
4657
  renameIdentity: this.defaults.renameIdentity,
4330
- ...options,
4331
- denyMutations: mergeDenyMutationPolicies(this.defaults.denyMutations, options.denyMutations),
4658
+ ...this.mutationOptions(options),
4332
4659
  });
4333
4660
  }
4334
4661
  async create(relativePath, data, options = {}) {
4662
+ assertValidRootDestinationPath(relativePath);
4335
4663
  await writeFileInRoot(this.context, {
4336
4664
  relativePath,
4337
4665
  data,
4338
4666
  mkdir: this.defaults.mkdir,
4339
4667
  mode: this.defaults.mode,
4340
- ...options,
4341
- denyMutations: mergeDenyMutationPolicies(this.defaults.denyMutations, options.denyMutations),
4668
+ ...this.mutationOptions(options),
4342
4669
  overwrite: false,
4343
4670
  });
4344
4671
  }
@@ -4353,15 +4680,14 @@ class RootHandle {
4353
4680
  await this.create(relativePath, trailingNewline ? `${json}\n` : json, writeOptions);
4354
4681
  }
4355
4682
  async copyIn(relativePath, sourcePath, options = {}) {
4356
- assertValidRootRelativePath(relativePath);
4683
+ assertValidRootDestinationPath(relativePath);
4357
4684
  await copyFileInRoot(this.context, {
4358
4685
  sourcePath,
4359
4686
  relativePath,
4360
4687
  maxBytes: this.defaults.maxBytes,
4361
4688
  mkdir: this.defaults.mkdir,
4362
4689
  mode: this.defaults.mode,
4363
- ...options,
4364
- denyMutations: mergeDenyMutationPolicies(this.defaults.denyMutations, options.denyMutations),
4690
+ ...this.mutationOptions(options),
4365
4691
  });
4366
4692
  }
4367
4693
  async exists(relativePath) {
@@ -4388,9 +4714,9 @@ class RootHandle {
4388
4714
  }
4389
4715
  async move(fromRelative, toRelative, options = {}) {
4390
4716
  assertValidRootRelativePath(fromRelative);
4391
- assertValidRootRelativePath(toRelative);
4717
+ assertValidRootDestinationPath(toRelative);
4392
4718
  validatePinnedOperationPayload({ from: fromRelative, to: toRelative });
4393
- const denyMutations = mergeDenyMutationPolicies(this.defaults.denyMutations, options.denyMutations);
4719
+ const { denyMutations } = this.mutationOptions(options);
4394
4720
  await assertMoveMutationAllowed(this.context, {
4395
4721
  fromRelative,
4396
4722
  toRelative,
@@ -4422,6 +4748,8 @@ async function root_impl_root(rootDir, defaults = {}) {
4422
4748
  async function openFileInRoot(root, params) {
4423
4749
  const { rootWithSep, resolved } = await resolvePathInRoot(root, params.relativePath, {
4424
4750
  allowFinalSymlink: true,
4751
+ rejectUnsafeDeviceReads: true,
4752
+ rejectSymlinks: params.symlinks !== "follow-within-root",
4425
4753
  });
4426
4754
  let opened;
4427
4755
  try {
@@ -4438,11 +4766,11 @@ async function openFileInRoot(root, params) {
4438
4766
  }
4439
4767
  if (params.hardlinks !== "allow" && opened.stat.nlink > 1) {
4440
4768
  await opened.handle.close().catch(() => { });
4441
- throw new errors_FsSafeError("hardlink", "hardlinked path not allowed");
4769
+ throw hardlinkedPathNotAllowedError();
4442
4770
  }
4443
4771
  if (!path_isPathInside(rootWithSep, opened.realPath)) {
4444
4772
  await opened.handle.close().catch(() => { });
4445
- throw new errors_FsSafeError("outside-workspace", "file is outside workspace root");
4773
+ throw outsideWorkspaceError();
4446
4774
  }
4447
4775
  return opened;
4448
4776
  }
@@ -4524,33 +4852,47 @@ async function verifyAtomicWriteResult(params) {
4524
4852
  throw new errors_FsSafeError("path-mismatch", "path changed during write");
4525
4853
  }
4526
4854
  if (!path_isPathInside(params.root.rootWithSep, opened.realPath)) {
4527
- throw new errors_FsSafeError("outside-workspace", "file is outside workspace root");
4855
+ throw outsideWorkspaceError();
4528
4856
  }
4529
4857
  }
4530
4858
  finally {
4531
4859
  await opened.handle.close().catch(() => { });
4532
4860
  }
4533
4861
  }
4534
- async function openWritableFileInRoot(root, params) {
4535
- const { rootReal, rootWithSep, resolved } = await resolvePathInRoot(root, params.relativePath, { aliasErrorCode: "path-alias" });
4536
- await assertMutationNotDenied(resolved, params.denyMutations);
4537
- try {
4538
- await assertNoPathAliasEscape({
4539
- absolutePath: resolved,
4540
- rootPath: rootReal,
4541
- boundaryLabel: "root",
4542
- });
4543
- }
4544
- catch (err) {
4545
- throw new errors_FsSafeError("path-alias", "path alias escape blocked", { cause: err });
4862
+ async function resolveGuardedWritePathInRoot(root, params) {
4863
+ const resolvedPath = await resolvePathInRoot(root, params.relativePath, {
4864
+ aliasErrorCode: "path-alias",
4865
+ allowFinalSymlink: params.allowFinalSymlink,
4866
+ });
4867
+ await assertMutationNotDenied(resolvedPath.resolved, params.denyMutations, params.protectDeniedAncestors ? { protectAncestors: true } : undefined);
4868
+ if (await (params.shouldAssertNoPathAlias?.(resolvedPath) ?? true)) {
4869
+ try {
4870
+ await assertNoPathAliasEscape({
4871
+ absolutePath: resolvedPath.resolved,
4872
+ rootPath: resolvedPath.rootReal,
4873
+ boundaryLabel: "root",
4874
+ });
4875
+ }
4876
+ catch (error) {
4877
+ throw new errors_FsSafeError("path-alias", "path alias escape blocked", {
4878
+ cause: error instanceof Error ? error : undefined,
4879
+ });
4880
+ }
4546
4881
  }
4882
+ return resolvedPath;
4883
+ }
4884
+ async function openWritableFileInRoot(root, params) {
4885
+ const { rootReal, rootWithSep, resolved } = await resolveGuardedWritePathInRoot(root, {
4886
+ relativePath: params.relativePath,
4887
+ denyMutations: params.denyMutations,
4888
+ });
4547
4889
  let ioPath = params.mkdir === false
4548
4890
  ? resolved
4549
4891
  : await prepareRootWriteTarget(rootReal, resolved);
4550
4892
  try {
4551
4893
  const resolvedRealPath = await promises_namespaceObject.realpath(ioPath);
4552
4894
  if (!path_isPathInside(rootWithSep, resolvedRealPath)) {
4553
- throw new errors_FsSafeError("outside-workspace", "file is outside workspace root");
4895
+ throw outsideWorkspaceError();
4554
4896
  }
4555
4897
  ioPath = resolvedRealPath;
4556
4898
  }
@@ -4581,7 +4923,7 @@ async function openWritableFileInRoot(root, params) {
4581
4923
  }
4582
4924
  catch (err) {
4583
4925
  if (path_isNotFoundPathError(err)) {
4584
- throw new errors_FsSafeError("not-found", "file not found");
4926
+ throw fileNotFoundError();
4585
4927
  }
4586
4928
  if (isSymlinkOpenError(err)) {
4587
4929
  throw new errors_FsSafeError("symlink", "symlink open blocked", { cause: err });
@@ -4592,13 +4934,17 @@ async function openWritableFileInRoot(root, params) {
4592
4934
  throw err;
4593
4935
  }
4594
4936
  let realPathForCleanup = null;
4937
+ let createdIdentity = null;
4595
4938
  try {
4596
4939
  const stat = await handle.stat();
4940
+ if (createdForWrite) {
4941
+ createdIdentity = stat;
4942
+ }
4597
4943
  if (!stat.isFile()) {
4598
- throw new errors_FsSafeError("invalid-path", "path is not a regular file under root");
4944
+ throw new errors_FsSafeError("not-file", "path is not a regular file under root");
4599
4945
  }
4600
4946
  if (stat.nlink > 1) {
4601
- throw new errors_FsSafeError("hardlink", "hardlinked path not allowed");
4947
+ throw hardlinkedPathNotAllowedError();
4602
4948
  }
4603
4949
  try {
4604
4950
  const lstat = await promises_namespaceObject.lstat(ioPath);
@@ -4621,10 +4967,10 @@ async function openWritableFileInRoot(root, params) {
4621
4967
  throw new errors_FsSafeError("path-mismatch", "path mismatch");
4622
4968
  }
4623
4969
  if (realStat.nlink > 1) {
4624
- throw new errors_FsSafeError("hardlink", "hardlinked path not allowed");
4970
+ throw hardlinkedPathNotAllowedError();
4625
4971
  }
4626
4972
  if (!path_isPathInside(rootWithSep, realPath)) {
4627
- throw new errors_FsSafeError("outside-workspace", "file is outside workspace root");
4973
+ throw outsideWorkspaceError();
4628
4974
  }
4629
4975
  // Truncate only after boundary and identity checks complete. This avoids
4630
4976
  // irreversible side effects if a symlink target changes before validation.
@@ -4644,8 +4990,8 @@ async function openWritableFileInRoot(root, params) {
4644
4990
  const cleanupCreatedPath = createdForWrite && err instanceof errors_FsSafeError;
4645
4991
  const cleanupPath = realPathForCleanup ?? ioPath;
4646
4992
  await handle.close().catch(() => { });
4647
- if (cleanupCreatedPath) {
4648
- await promises_namespaceObject.rm(cleanupPath, { force: true }).catch(() => { });
4993
+ if (cleanupCreatedPath && createdIdentity) {
4994
+ await removePathIfIdentityUnchanged(cleanupPath, createdIdentity).catch(() => { });
4649
4995
  }
4650
4996
  throw err;
4651
4997
  }
@@ -4690,7 +5036,11 @@ async function appendFileInRoot(root, params) {
4690
5036
  }
4691
5037
  async function removePathInRoot(root, params) {
4692
5038
  validatePinnedOperationPayload({ relativePath: params.relativePath });
4693
- const resolved = await resolvePinnedRemovePathInRoot(root, params.relativePath, params.denyMutations);
5039
+ const resolved = await resolvePinnedPathInRoot(root, {
5040
+ relativePath: params.relativePath,
5041
+ denyMutations: params.denyMutations,
5042
+ remove: true,
5043
+ });
4694
5044
  try {
4695
5045
  await removePathFallback(resolved);
4696
5046
  }
@@ -4709,15 +5059,16 @@ async function mkdirPathInRoot(root, params) {
4709
5059
  }
4710
5060
  }
4711
5061
  async function writeFileInRoot(root, params) {
4712
- if (process.platform === "win32") {
4713
- await serializePathWrite(rootWriteQueueKey(root, params.relativePath), async () => {
5062
+ await serializePathWrite(rootWriteQueueKey(root, params.relativePath), async () => {
5063
+ if (process.platform === "win32" &&
5064
+ (params.renameIdentity === "verify-content-with-lock" || !getNativeBinding())) {
4714
5065
  await writeFileFallback(root, params);
5066
+ return;
5067
+ }
5068
+ const pinned = await resolvePinnedWriteTargetInRoot(root, params.relativePath, params.mode, params.denyMutations);
5069
+ await serializePathWrite(pinned.targetPath, async () => {
5070
+ await commitPinnedWriteInRoot(root, pinned, params);
4715
5071
  });
4716
- return;
4717
- }
4718
- const pinned = await resolvePinnedWriteTargetInRoot(root, params.relativePath, params.mode, params.denyMutations);
4719
- await serializePathWrite(pinned.targetPath, async () => {
4720
- await commitPinnedWriteInRoot(root, pinned, params);
4721
5072
  });
4722
5073
  }
4723
5074
  async function commitPinnedWriteInRoot(root, pinned, params) {
@@ -4733,6 +5084,7 @@ async function commitPinnedWriteInRoot(root, pinned, params) {
4733
5084
  mode: params.mode ?? pinned.mode,
4734
5085
  overwrite: params.overwrite,
4735
5086
  input: { kind: "buffer", data: params.data, encoding: params.encoding },
5087
+ rootIdentity: root.rootIdentity,
4736
5088
  });
4737
5089
  }
4738
5090
  catch (error) {
@@ -4770,26 +5122,35 @@ async function copyFileInRoot(root, params) {
4770
5122
  throw new errors_FsSafeError("too-large", `file exceeds limit of ${params.maxBytes} bytes (got ${source.stat.size})`);
4771
5123
  }
4772
5124
  try {
4773
- const pinned = await resolvePinnedWriteTargetInRoot(root, params.relativePath, params.mode, params.denyMutations);
4774
- await serializePathWrite(pinned.targetPath, async () => {
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() },
5125
+ await serializePathWrite(rootWriteQueueKey(root, params.relativePath), async () => {
5126
+ const pinned = await resolvePinnedWriteTargetInRoot(root, params.relativePath, params.mode, params.denyMutations);
5127
+ await serializePathWrite(pinned.targetPath, async () => {
5128
+ await assertCopySourceCurrent(source);
5129
+ let identity;
5130
+ try {
5131
+ identity = await runPinnedWriteHelper({
5132
+ rootPath: pinned.rootReal,
5133
+ relativeParentPath: pinned.relativeParentPath,
5134
+ basename: pinned.basename,
5135
+ mkdir: params.mkdir !== false,
5136
+ mode: pinned.mode,
5137
+ overwrite: true,
5138
+ maxBytes: params.maxBytes,
5139
+ input: { kind: "stream", stream: source.handle.createReadStream() },
5140
+ rootIdentity: root.rootIdentity,
5141
+ });
5142
+ }
5143
+ catch (error) {
5144
+ throw normalizePinnedWriteError(error);
5145
+ }
5146
+ try {
5147
+ await assertCopySourcePathCurrent(source);
5148
+ }
5149
+ catch (error) {
5150
+ await removePathIfIdentityUnchanged(pinned.targetPath, identity).catch(() => undefined);
5151
+ throw error;
5152
+ }
4785
5153
  });
4786
- try {
4787
- await assertCopySourcePathCurrent(source);
4788
- }
4789
- catch (error) {
4790
- await removeCopyTargetIfUnchanged(pinned.targetPath, identity).catch(() => undefined);
4791
- throw error;
4792
- }
4793
5154
  });
4794
5155
  }
4795
5156
  finally {
@@ -4809,7 +5170,7 @@ async function assertCopySourcePathCurrent(source) {
4809
5170
  throw new errors_FsSafeError("path-mismatch", "copy source path changed");
4810
5171
  }
4811
5172
  }
4812
- async function removeCopyTargetIfUnchanged(targetPath, identity) {
5173
+ async function removePathIfIdentityUnchanged(targetPath, identity) {
4813
5174
  const parentGuard = await directory_guard_createAsyncDirectoryGuard(external_node_path_namespaceObject.dirname(targetPath));
4814
5175
  const current = await promises_namespaceObject.lstat(targetPath);
4815
5176
  if (current.isSymbolicLink() || !file_identity_sameFileIdentity(current, identity)) {
@@ -4820,25 +5181,15 @@ async function removeCopyTargetIfUnchanged(targetPath, identity) {
4820
5181
  });
4821
5182
  }
4822
5183
  async function resolvePinnedWriteTargetInRoot(root, relativePath, requestedMode, denyMutations) {
4823
- const { rootReal, rootWithSep, resolved } = await resolvePathInRoot(root, relativePath, {
4824
- aliasErrorCode: "path-alias",
5184
+ const { rootReal, rootWithSep, resolved } = await resolveGuardedWritePathInRoot(root, {
5185
+ relativePath,
5186
+ denyMutations,
4825
5187
  });
4826
- await assertMutationNotDenied(resolved, denyMutations);
4827
- try {
4828
- await assertNoPathAliasEscape({
4829
- absolutePath: resolved,
4830
- rootPath: rootReal,
4831
- boundaryLabel: "root",
4832
- });
4833
- }
4834
- catch (err) {
4835
- throw new errors_FsSafeError("path-alias", "path alias escape blocked", { cause: err });
4836
- }
4837
5188
  // resolvePathInRoot already enforces isPathInside, so any actual escape
4838
5189
  // is rejected upstream.
4839
5190
  const relativeResolved = external_node_path_namespaceObject.relative(rootReal, resolved);
4840
5191
  if (external_node_path_namespaceObject.isAbsolute(relativeResolved)) {
4841
- throw new errors_FsSafeError("outside-workspace", "file is outside workspace root");
5192
+ throw outsideWorkspaceError();
4842
5193
  }
4843
5194
  const relativePosix = relativeResolved
4844
5195
  ? relativeResolved.split(external_node_path_namespaceObject.sep).join(external_node_path_namespaceObject.posix.sep)
@@ -4853,11 +5204,12 @@ async function resolvePinnedWriteTargetInRoot(root, relativePath, requestedMode,
4853
5204
  relativePath,
4854
5205
  hardlinks: "reject",
4855
5206
  nonBlockingRead: true,
5207
+ symlinks: "follow-within-root",
4856
5208
  });
4857
5209
  try {
4858
5210
  mode = requestedMode ?? (opened.stat.mode & 0o777);
4859
5211
  if (!path_isPathInside(rootWithSep, opened.realPath)) {
4860
- throw new errors_FsSafeError("outside-workspace", "file is outside workspace root");
5212
+ throw outsideWorkspaceError();
4861
5213
  }
4862
5214
  }
4863
5215
  finally {
@@ -4881,17 +5233,9 @@ async function resolvePinnedPathInRoot(root, params) {
4881
5233
  return await resolvePinnedOperationPathInRoot(root, {
4882
5234
  allowRoot: params.allowRoot,
4883
5235
  denyMutations: params.denyMutations,
4884
- protectDenyMutationAncestors: false,
5236
+ protectDenyMutationAncestors: params.remove === true,
4885
5237
  relativePath: params.relativePath,
4886
- policy: PATH_ALIAS_POLICIES.strict,
4887
- });
4888
- }
4889
- async function resolvePinnedRemovePathInRoot(root, relativePath, denyMutations) {
4890
- return await resolvePinnedOperationPathInRoot(root, {
4891
- denyMutations,
4892
- protectDenyMutationAncestors: true,
4893
- relativePath,
4894
- policy: PATH_ALIAS_POLICIES.unlinkTarget,
5238
+ policy: params.remove ? PATH_ALIAS_POLICIES.unlinkTarget : PATH_ALIAS_POLICIES.strict,
4895
5239
  });
4896
5240
  }
4897
5241
  async function resolvePinnedOperationPathInRoot(root, params) {
@@ -4909,11 +5253,11 @@ async function resolvePinnedOperationPathInRoot(root, params) {
4909
5253
  relativeResolved === "." ||
4910
5254
  firstSegment === ".." ||
4911
5255
  external_node_path_namespaceObject.isAbsolute(relativeResolved)) {
4912
- throw new errors_FsSafeError("outside-workspace", "file is outside workspace root");
5256
+ throw outsideWorkspaceError();
4913
5257
  }
4914
5258
  const relativePosix = relativeResolved.split(external_node_path_namespaceObject.sep).join(external_node_path_namespaceObject.posix.sep);
4915
5259
  if (!path_isPathInside(resolved.rootWithSep, resolved.canonicalPath)) {
4916
- throw new errors_FsSafeError("outside-workspace", "file is outside workspace root");
5260
+ throw outsideWorkspaceError();
4917
5261
  }
4918
5262
  await assertMutationNotDenied(resolved.canonicalPath, params.denyMutations, {
4919
5263
  protectAncestors: params.protectDenyMutationAncestors,
@@ -4921,6 +5265,7 @@ async function resolvePinnedOperationPathInRoot(root, params) {
4921
5265
  return { rootReal: resolved.rootReal, resolved: resolved.canonicalPath, relativePosix };
4922
5266
  }
4923
5267
  async function resolvePinnedRootPathInRoot(root, params) {
5268
+ await assertRootIdentityCurrent(root);
4924
5269
  const rootReal = root.rootReal;
4925
5270
  let resolved;
4926
5271
  try {
@@ -4945,11 +5290,25 @@ async function resolvePinnedRootPathInRoot(root, params) {
4945
5290
  canonicalPath: resolved.canonicalPath,
4946
5291
  };
4947
5292
  }
5293
+ async function prepareRemoveGuard(targetPath) {
5294
+ try {
5295
+ const guard = await directory_guard_createAsyncDirectoryGuard(external_node_path_namespaceObject.dirname(targetPath));
5296
+ await getFsSafeTestHooks()?.beforeRootFallbackMutation?.("remove", targetPath);
5297
+ await assertAsyncDirectoryGuard(guard);
5298
+ return guard;
5299
+ }
5300
+ catch (error) {
5301
+ throw normalizeRemoveGuardError(error);
5302
+ }
5303
+ }
4948
5304
  async function removePathFallback(resolved) {
4949
- const guard = await directory_guard_createAsyncDirectoryGuard(external_node_path_namespaceObject.dirname(resolved.resolved));
4950
- await getFsSafeTestHooks()?.beforeRootFallbackMutation?.("remove", resolved.resolved);
4951
- await assertAsyncDirectoryGuard(guard);
4952
- await ((await promises_namespaceObject.lstat(resolved.resolved)).isDirectory() ? promises_namespaceObject.rmdir(resolved.resolved) : promises_namespaceObject.rm(resolved.resolved));
5305
+ const guard = await prepareRemoveGuard(resolved.resolved);
5306
+ try {
5307
+ await ((await promises_namespaceObject.lstat(resolved.resolved)).isDirectory() ? promises_namespaceObject.rmdir(resolved.resolved) : promises_namespaceObject.rm(resolved.resolved));
5308
+ }
5309
+ catch (error) {
5310
+ throw normalizeRemovePathError(error);
5311
+ }
4953
5312
  await assertAsyncDirectoryGuard(guard).catch(() => undefined);
4954
5313
  }
4955
5314
  async function mkdirPathFallback(resolved) {
@@ -4961,13 +5320,13 @@ async function mkdirPathFallback(resolved) {
4961
5320
  async function statPathFallback(root, relativePath) {
4962
5321
  const resolved = await resolvePinnedPathInRoot(root, { relativePath, allowRoot: true });
4963
5322
  try {
4964
- return pathStatFromStats(await promises_namespaceObject.lstat(resolved.resolved));
5323
+ const stat = pathStatFromStats(await promises_namespaceObject.lstat(resolved.resolved));
5324
+ await assertRootIdentityCurrent(root);
5325
+ return stat;
4965
5326
  }
4966
5327
  catch (error) {
4967
5328
  if (path_isNotFoundPathError(error)) {
4968
- throw new errors_FsSafeError("not-found", "file not found", {
4969
- cause: error instanceof Error ? error : undefined,
4970
- });
5329
+ throw fileNotFoundError(error instanceof Error ? error : undefined);
4971
5330
  }
4972
5331
  throw error;
4973
5332
  }
@@ -4978,6 +5337,7 @@ async function listPathFallback(root, relativePath, withFileTypes) {
4978
5337
  const names = await promises_namespaceObject.readdir(resolved.resolved);
4979
5338
  const sortedNames = names.toSorted();
4980
5339
  if (!withFileTypes) {
5340
+ await assertRootIdentityCurrent(root);
4981
5341
  return sortedNames;
4982
5342
  }
4983
5343
  const entries = [];
@@ -4987,6 +5347,7 @@ async function listPathFallback(root, relativePath, withFileTypes) {
4987
5347
  ...pathStatFromStats(await promises_namespaceObject.lstat(external_node_path_namespaceObject.join(resolved.resolved, name))),
4988
5348
  });
4989
5349
  }
5350
+ await assertRootIdentityCurrent(root);
4990
5351
  return entries;
4991
5352
  }
4992
5353
  catch (error) {
@@ -4999,6 +5360,8 @@ async function listPathFallback(root, relativePath, withFileTypes) {
4999
5360
  }
5000
5361
  }
5001
5362
  async function assertMoveMutationAllowed(root, params) {
5363
+ // Keep this preflight separate from the pinned resolutions in movePathFallback:
5364
+ // mutation denials must take precedence over source alias or identity failures.
5002
5365
  const source = await resolvePathInRoot(root, params.fromRelative, {
5003
5366
  aliasErrorCode: "path-alias",
5004
5367
  allowFinalSymlink: true,
@@ -5020,40 +5383,29 @@ async function movePathFallback(root, params) {
5020
5383
  relativePath: params.fromRelative,
5021
5384
  policy: PATH_ALIAS_POLICIES.strict,
5022
5385
  });
5023
- const target = await resolvePathInRoot(root, params.toRelative, {
5024
- aliasErrorCode: "path-alias",
5025
- allowFinalSymlink: true,
5026
- });
5027
- await assertMutationNotDenied(target.resolved, params.denyMutations, { protectAncestors: true });
5028
- await resolvePinnedRootPathInRoot(root, {
5386
+ const target = await resolveGuardedWritePathInRoot(root, {
5029
5387
  relativePath: params.toRelative,
5030
- policy: PATH_ALIAS_POLICIES.unlinkTarget,
5031
- });
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,
5388
+ denyMutations: params.denyMutations,
5389
+ allowFinalSymlink: true,
5390
+ protectDeniedAncestors: true,
5391
+ shouldAssertNoPathAlias: async (resolvedTarget) => {
5392
+ await resolvePinnedRootPathInRoot(root, {
5393
+ relativePath: params.toRelative,
5394
+ policy: PATH_ALIAS_POLICIES.unlinkTarget,
5045
5395
  });
5046
- }
5047
- }
5396
+ const targetStat = await promises_namespaceObject.lstat(resolvedTarget.resolved).catch(() => undefined);
5397
+ return !(process.platform !== "win32" &&
5398
+ params.overwrite &&
5399
+ targetStat?.isSymbolicLink() === true);
5400
+ },
5401
+ });
5048
5402
  let sourceStat;
5049
5403
  try {
5050
5404
  sourceStat = await promises_namespaceObject.lstat(source.resolved);
5051
5405
  }
5052
5406
  catch (error) {
5053
5407
  if (path_isNotFoundPathError(error)) {
5054
- throw new errors_FsSafeError("not-found", "file not found", {
5055
- cause: error instanceof Error ? error : undefined,
5056
- });
5408
+ throw fileNotFoundError(error instanceof Error ? error : undefined);
5057
5409
  }
5058
5410
  throw error;
5059
5411
  }
@@ -5061,7 +5413,7 @@ async function movePathFallback(root, params) {
5061
5413
  throw new errors_FsSafeError("symlink", "symlink not allowed");
5062
5414
  }
5063
5415
  if (sourceStat.isFile() && sourceStat.nlink > 1) {
5064
- throw new errors_FsSafeError("hardlink", "hardlinked path not allowed");
5416
+ throw hardlinkedPathNotAllowedError();
5065
5417
  }
5066
5418
  if (!params.overwrite && sourceStat.isDirectory()) {
5067
5419
  throw new errors_FsSafeError("invalid-path", "directory moves require overwrite: true");
@@ -5090,9 +5442,7 @@ async function movePathFallback(root, params) {
5090
5442
  }
5091
5443
  catch (error) {
5092
5444
  if (path_isNotFoundPathError(error)) {
5093
- throw new errors_FsSafeError("not-found", "file not found", {
5094
- cause: error instanceof Error ? error : undefined,
5095
- });
5445
+ throw fileNotFoundError(error instanceof Error ? error : undefined);
5096
5446
  }
5097
5447
  if (hasNodeErrorCode(error, "EEXIST")) {
5098
5448
  throw new errors_FsSafeError("already-exists", "destination exists", {
@@ -5158,30 +5508,22 @@ async function writeFileFallback(root, params) {
5158
5508
  }
5159
5509
  }
5160
5510
  async function writeMissingFileFallback(root, params) {
5161
- const { rootReal, resolved } = await resolvePathInRoot(root, params.relativePath, {
5162
- aliasErrorCode: "path-alias",
5511
+ const { rootReal, resolved } = await resolveGuardedWritePathInRoot(root, {
5512
+ relativePath: params.relativePath,
5513
+ denyMutations: params.denyMutations,
5163
5514
  });
5164
- await assertMutationNotDenied(resolved, params.denyMutations);
5165
- try {
5166
- await assertNoPathAliasEscape({
5167
- absolutePath: resolved,
5168
- rootPath: rootReal,
5169
- boundaryLabel: "root",
5170
- });
5171
- }
5172
- catch (err) {
5173
- throw new errors_FsSafeError("path-alias", "path alias escape blocked", { cause: err });
5174
- }
5175
5515
  const targetPath = params.mkdir === false
5176
5516
  ? resolved
5177
5517
  : await prepareRootWriteTarget(rootReal, resolved);
5178
5518
  const parentGuard = await directory_guard_createAsyncDirectoryGuard(external_node_path_namespaceObject.dirname(targetPath));
5179
5519
  let created = false;
5520
+ let createdIdentity;
5180
5521
  try {
5181
5522
  const { handle, writtenStat } = await withAsyncDirectoryGuards([parentGuard], async () => {
5182
5523
  const handle = await promises_namespaceObject.open(targetPath, OPEN_WRITE_CREATE_FLAGS, params.mode ?? 0o600);
5183
5524
  created = true;
5184
5525
  try {
5526
+ createdIdentity = await handle.stat();
5185
5527
  if (typeof params.data === "string") {
5186
5528
  await handle.writeFile(params.data, params.encoding ?? "utf8");
5187
5529
  }
@@ -5217,8 +5559,8 @@ async function writeMissingFileFallback(root, params) {
5217
5559
  throw err;
5218
5560
  }
5219
5561
  finally {
5220
- if (created) {
5221
- await promises_namespaceObject.rm(targetPath, { force: true }).catch(() => undefined);
5562
+ if (created && createdIdentity) {
5563
+ await removePathIfIdentityUnchanged(targetPath, createdIdentity).catch(() => undefined);
5222
5564
  }
5223
5565
  }
5224
5566
  }
@@ -16388,4 +16730,4 @@ if (installedChunkData !== 0) { // 0 means "already installed".'
16388
16730
  // module factories are used so entry inlining is disabled
16389
16731
  // startup
16390
16732
  // Load entry module and return exports
16391
- var __webpack_exports__ = __webpack_require__(418);
16733
+ var __webpack_exports__ = __webpack_require__(983);