@openclaw/openshell-sandbox 2026.10.1-beta.2 → 2026.10.5-beta.1

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 (126) hide show
  1. package/dist/index.js +69 -111
  2. package/node_modules/@openclaw/fs-safe/CHANGELOG.md +91 -0
  3. package/node_modules/@openclaw/fs-safe/README.md +1 -1
  4. package/node_modules/@openclaw/fs-safe/dist/advanced.d.ts +1 -0
  5. package/node_modules/@openclaw/fs-safe/dist/advanced.js +1 -0
  6. package/node_modules/@openclaw/fs-safe/dist/archive-durability.js +1 -1
  7. package/node_modules/@openclaw/fs-safe/dist/archive-entry.js +7 -3
  8. package/node_modules/@openclaw/fs-safe/dist/archive-merge.js +4 -4
  9. package/node_modules/@openclaw/fs-safe/dist/archive-parser.wasm +0 -0
  10. package/node_modules/@openclaw/fs-safe/dist/archive-read.js +5 -4
  11. package/node_modules/@openclaw/fs-safe/dist/archive-staging.d.ts +1 -1
  12. package/node_modules/@openclaw/fs-safe/dist/archive-staging.js +22 -38
  13. package/node_modules/@openclaw/fs-safe/dist/copy-path-admission.d.ts +17 -0
  14. package/node_modules/@openclaw/fs-safe/dist/copy-path-admission.js +117 -0
  15. package/node_modules/@openclaw/fs-safe/dist/copy-root-file-sync.d.ts +38 -0
  16. package/node_modules/@openclaw/fs-safe/dist/copy-root-file-sync.js +195 -0
  17. package/node_modules/@openclaw/fs-safe/dist/create.d.ts +1 -0
  18. package/node_modules/@openclaw/fs-safe/dist/create.js +4 -1
  19. package/node_modules/@openclaw/fs-safe/dist/entry-publication.js +3 -1
  20. package/node_modules/@openclaw/fs-safe/dist/errors.d.ts +2 -0
  21. package/node_modules/@openclaw/fs-safe/dist/errors.js +5 -0
  22. package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root-acquire.d.ts +3 -0
  23. package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root-acquire.js +57 -11
  24. package/node_modules/@openclaw/fs-safe/dist/guarded-mutation.js +5 -5
  25. package/node_modules/@openclaw/fs-safe/dist/guest-native-python.js +52 -5
  26. package/node_modules/@openclaw/fs-safe/dist/guest.js +1 -1
  27. package/node_modules/@openclaw/fs-safe/dist/index.d.ts +1 -1
  28. package/node_modules/@openclaw/fs-safe/dist/index.js +1 -1
  29. package/node_modules/@openclaw/fs-safe/dist/native-binding.d.ts +4 -0
  30. package/node_modules/@openclaw/fs-safe/dist/native-move-fallback.d.ts +3 -0
  31. package/node_modules/@openclaw/fs-safe/dist/native-move-fallback.js +54 -0
  32. package/node_modules/@openclaw/fs-safe/dist/native-noreplace.d.ts +8 -0
  33. package/node_modules/@openclaw/fs-safe/dist/native-noreplace.js +55 -0
  34. package/node_modules/@openclaw/fs-safe/dist/native-operations.d.ts +0 -1
  35. package/node_modules/@openclaw/fs-safe/dist/native-operations.js +22 -26
  36. package/node_modules/@openclaw/fs-safe/dist/native-pinned-write-windows.js +5 -1
  37. package/node_modules/@openclaw/fs-safe/dist/native-pinned-write.d.ts +1 -1
  38. package/node_modules/@openclaw/fs-safe/dist/native-pinned-write.js +12 -2
  39. package/node_modules/@openclaw/fs-safe/dist/native-rename-outcome.js +3 -1
  40. package/node_modules/@openclaw/fs-safe/dist/native-staged-file.d.ts +1 -1
  41. package/node_modules/@openclaw/fs-safe/dist/native-staged-file.js +52 -6
  42. package/node_modules/@openclaw/fs-safe/dist/permission-exec.js +2 -1
  43. package/node_modules/@openclaw/fs-safe/dist/pinned-write-input.d.ts +2 -0
  44. package/node_modules/@openclaw/fs-safe/dist/pinned-write-input.js +17 -0
  45. package/node_modules/@openclaw/fs-safe/dist/pinned-write-types.d.ts +6 -1
  46. package/node_modules/@openclaw/fs-safe/dist/pinned-write.js +1 -1
  47. package/node_modules/@openclaw/fs-safe/dist/private-temp-workspace.js +2 -2
  48. package/node_modules/@openclaw/fs-safe/dist/publish-file-failure.d.ts +5 -0
  49. package/node_modules/@openclaw/fs-safe/dist/publish-file-failure.js +1 -0
  50. package/node_modules/@openclaw/fs-safe/dist/publish-file.d.ts +2 -0
  51. package/node_modules/@openclaw/fs-safe/dist/publish-file.js +35 -2
  52. package/node_modules/@openclaw/fs-safe/dist/replace-directory.js +6 -5
  53. package/node_modules/@openclaw/fs-safe/dist/replace-file-copy-fallback.js +1 -4
  54. package/node_modules/@openclaw/fs-safe/dist/replace-file-descriptor.js +6 -0
  55. package/node_modules/@openclaw/fs-safe/dist/root-create-input.d.ts +15 -1
  56. package/node_modules/@openclaw/fs-safe/dist/root-create-input.js +4 -0
  57. package/node_modules/@openclaw/fs-safe/dist/root-directory-entry.js +1 -1
  58. package/node_modules/@openclaw/fs-safe/dist/root-directory-list.d.ts +0 -1
  59. package/node_modules/@openclaw/fs-safe/dist/root-directory-list.js +6 -12
  60. package/node_modules/@openclaw/fs-safe/dist/root-errors.js +25 -4
  61. package/node_modules/@openclaw/fs-safe/dist/root-impl.js +18 -18
  62. package/node_modules/@openclaw/fs-safe/dist/root-move-noreplace.d.ts +4 -2
  63. package/node_modules/@openclaw/fs-safe/dist/root-move-noreplace.js +13 -17
  64. package/node_modules/@openclaw/fs-safe/dist/root-paths.js +1 -1
  65. package/node_modules/@openclaw/fs-safe/dist/root-walk.js +4 -4
  66. package/node_modules/@openclaw/fs-safe/dist/root-write-verification.d.ts +10 -1
  67. package/node_modules/@openclaw/fs-safe/dist/root-write-verification.js +20 -0
  68. package/node_modules/@openclaw/fs-safe/dist/secure-temp-dir.js +19 -7
  69. package/node_modules/@openclaw/fs-safe/dist/sibling-staged-file.js +18 -3
  70. package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-acquire.js +25 -3
  71. package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-admission.d.ts +2 -0
  72. package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-retained.d.ts +14 -0
  73. package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-retained.js +187 -0
  74. package/node_modules/@openclaw/fs-safe/dist/sidecar-lock.js +15 -1
  75. package/node_modules/@openclaw/fs-safe/dist/staged-file-settlement.js +4 -1
  76. package/node_modules/@openclaw/fs-safe/dist/temp-directory-diagnostics.d.ts +10 -0
  77. package/node_modules/@openclaw/fs-safe/dist/temp-directory-diagnostics.js +29 -0
  78. package/node_modules/@openclaw/fs-safe/dist/temp-target.js +1 -1
  79. package/node_modules/@openclaw/fs-safe/dist/temp-workspace-admission.js +6 -6
  80. package/node_modules/@openclaw/fs-safe/dist/temp-workspace-child-admission.d.ts +4 -4
  81. package/node_modules/@openclaw/fs-safe/dist/temp-workspace-child-admission.js +17 -16
  82. package/node_modules/@openclaw/fs-safe/dist/temp-workspace-descriptor.js +20 -28
  83. package/node_modules/@openclaw/fs-safe/dist/temp-workspace-owner.d.ts +1 -0
  84. package/node_modules/@openclaw/fs-safe/dist/temp-workspace-owner.js +27 -4
  85. package/node_modules/@openclaw/fs-safe/dist/walk.js +2 -13
  86. package/node_modules/@openclaw/fs-safe/dist/watch-native.d.ts +2 -2
  87. package/node_modules/@openclaw/fs-safe/dist/watch-native.js +3 -1
  88. package/node_modules/@openclaw/fs-safe/dist/watch-rescan.js +3 -9
  89. package/node_modules/@openclaw/fs-safe/dist/watch.js +2 -3
  90. package/node_modules/@openclaw/fs-safe/docs/advanced.md +1 -0
  91. package/node_modules/@openclaw/fs-safe/docs/archive.md +5 -1
  92. package/node_modules/@openclaw/fs-safe/docs/atomic.md +11 -3
  93. package/node_modules/@openclaw/fs-safe/docs/contributing.md +1 -1
  94. package/node_modules/@openclaw/fs-safe/docs/copy.md +109 -1
  95. package/node_modules/@openclaw/fs-safe/docs/durability.md +50 -5
  96. package/node_modules/@openclaw/fs-safe/docs/errors.md +37 -0
  97. package/node_modules/@openclaw/fs-safe/docs/guest.md +21 -4
  98. package/node_modules/@openclaw/fs-safe/docs/native-helper.md +5 -3
  99. package/node_modules/@openclaw/fs-safe/docs/native.md +39 -6
  100. package/node_modules/@openclaw/fs-safe/docs/path-scope.md +4 -0
  101. package/node_modules/@openclaw/fs-safe/docs/public-api.md +3 -1
  102. package/node_modules/@openclaw/fs-safe/docs/sidecar-lock.md +46 -8
  103. package/node_modules/@openclaw/fs-safe/docs/staged-file.md +10 -3
  104. package/node_modules/@openclaw/fs-safe/docs/temp.md +12 -0
  105. package/node_modules/@openclaw/fs-safe/docs/watch.md +6 -3
  106. package/node_modules/@openclaw/fs-safe/docs/writing.md +40 -11
  107. package/node_modules/@openclaw/fs-safe/package.json +16 -16
  108. package/node_modules/@openclaw/fs-safe-darwin-arm64/fs-safe-native.node +0 -0
  109. package/node_modules/@openclaw/fs-safe-darwin-arm64/package.json +1 -1
  110. package/node_modules/@openclaw/fs-safe-darwin-x64/fs-safe-native.node +0 -0
  111. package/node_modules/@openclaw/fs-safe-darwin-x64/package.json +1 -1
  112. package/node_modules/@openclaw/fs-safe-linux-arm64-gnu/fs-safe-native.node +0 -0
  113. package/node_modules/@openclaw/fs-safe-linux-arm64-gnu/package.json +1 -1
  114. package/node_modules/@openclaw/fs-safe-linux-arm64-musl/fs-safe-native.node +0 -0
  115. package/node_modules/@openclaw/fs-safe-linux-arm64-musl/package.json +1 -1
  116. package/node_modules/@openclaw/fs-safe-linux-x64-gnu/fs-safe-native.node +0 -0
  117. package/node_modules/@openclaw/fs-safe-linux-x64-gnu/package.json +1 -1
  118. package/node_modules/@openclaw/fs-safe-linux-x64-musl/fs-safe-native.node +0 -0
  119. package/node_modules/@openclaw/fs-safe-linux-x64-musl/package.json +1 -1
  120. package/node_modules/@openclaw/fs-safe-win32-x64-msvc/fs-safe-native.node +0 -0
  121. package/node_modules/@openclaw/fs-safe-win32-x64-msvc/package.json +1 -1
  122. package/package.json +5 -5
  123. package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root-arbitration.d.ts +0 -17
  124. package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root-arbitration.js +0 -66
  125. package/node_modules/@openclaw/fs-safe/dist/root-move-preflight.d.ts +0 -7
  126. package/node_modules/@openclaw/fs-safe/dist/root-move-preflight.js +0 -16
package/dist/index.js CHANGED
@@ -189,29 +189,24 @@ var OpenShellFsBridge = class {
189
189
  return entries;
190
190
  }
191
191
  async writeFile(params) {
192
- const target = this.resolveTarget(params);
193
- const hostPath = target.hostPath;
194
- this.ensureWritable(target, "write files");
195
- const buffer = Buffer.isBuffer(params.data) ? params.data : Buffer.from(params.data, params.encoding ?? "utf8");
196
- await (await root(target.mountHostRoot)).write(relativeToRoot(target, hostPath), buffer, {
197
- mkdir: params.mkdir,
198
- mutationSymlinks: "reject"
199
- });
200
- await this.backend.syncLocalPathToRemote(hostPath, target.containerPath);
192
+ await this.writeLocalFile(params, "write");
201
193
  }
202
194
  async createFileExclusive(params) {
195
+ return this.writeLocalFile(params, "create");
196
+ }
197
+ async writeLocalFile(params, method) {
203
198
  const target = this.resolveTarget(params);
204
199
  const hostPath = target.hostPath;
205
- this.ensureWritable(target, "create files");
200
+ this.ensureWritable(target, method === "create" ? "create files" : "write files");
206
201
  const buffer = Buffer.isBuffer(params.data) ? params.data : Buffer.from(params.data, params.encoding ?? "utf8");
207
202
  const root$2 = await root(target.mountHostRoot);
208
203
  try {
209
- await root$2.create(relativeToRoot(target, hostPath), buffer, {
210
- mkdir: params.mkdir !== false,
204
+ await root$2[method](relativeToRoot(target, hostPath), buffer, {
205
+ mkdir: method === "create" ? params.mkdir !== false : params.mkdir,
211
206
  mutationSymlinks: "reject"
212
207
  });
213
208
  } catch (error) {
214
- if (error instanceof FsSafeError && error.code === "already-exists") return "exists";
209
+ if (method === "create" && error instanceof FsSafeError && error.code === "already-exists") return "exists";
215
210
  throw error;
216
211
  }
217
212
  await this.backend.syncLocalPathToRemote(hostPath, target.containerPath);
@@ -366,25 +361,25 @@ var OpenShellFsBridge = class {
366
361
  }
367
362
  const cwd = params.cwd ? path.resolve(params.cwd) : workspaceRoot;
368
363
  const hostPath = path.isAbsolute(input) ? path.resolve(input) : path.resolve(cwd, input);
369
- for (const mount of readOnlyMounts) if (isPathInside(mount.hostPath, hostPath)) {
364
+ for (const mount of [
365
+ ...readOnlyMounts,
366
+ {
367
+ hostPath: workspaceRoot,
368
+ containerPath: workspaceContainerRoot
369
+ },
370
+ ...hasAgentMount ? [{
371
+ hostPath: agentRoot,
372
+ containerPath: agentContainerRoot
373
+ }] : []
374
+ ]) if (isPathInside(mount.hostPath, hostPath)) {
370
375
  const relative = path.relative(mount.hostPath, hostPath).split(path.sep).join(path.posix.sep);
371
- return expectResolvedContainerTarget(resolveContainerTarget(path.posix.join(mount.containerPath, relative)), input);
372
- }
373
- if (isPathInside(workspaceRoot, hostPath)) {
374
- const relative = path.relative(workspaceRoot, hostPath).split(path.sep).join(path.posix.sep);
375
- return expectResolvedContainerTarget(resolveContainerTarget(path.posix.join(workspaceContainerRoot, relative)), input);
376
- }
377
- if (hasAgentMount && isPathInside(agentRoot, hostPath)) {
378
- const relative = path.relative(agentRoot, hostPath).split(path.sep).join(path.posix.sep);
379
- return expectResolvedContainerTarget(resolveContainerTarget(path.posix.join(agentContainerRoot, relative)), input);
376
+ const target = resolveContainerTarget(path.posix.join(mount.containerPath, relative));
377
+ if (!target) throw new Error(`Sandbox path escapes allowed mounts; cannot access: ${input}`);
378
+ return target;
380
379
  }
381
380
  throw new Error(`Path escapes sandbox root (${workspaceRoot}): ${params.filePath}`);
382
381
  }
383
382
  };
384
- function expectResolvedContainerTarget(target, input) {
385
- if (!target) throw new Error(`Sandbox path escapes allowed mounts; cannot access: ${input}`);
386
- return target;
387
- }
388
383
  async function removeLocalRootPath(params) {
389
384
  const root$4 = await root(params.target.mountHostRoot);
390
385
  const relativePath = relativeToRoot(params.target, params.target.hostPath);
@@ -529,16 +524,6 @@ async function stageDirectoryContents(params) {
529
524
  const openShellWorkspaceOperations = new KeyedAsyncQueue();
530
525
  let openShellDetachedCreateSupport;
531
526
  const MATERIALIZED_SKILLS_REMOTE_PARTS = [".openclaw", "sandbox-skills"];
532
- function buildOpenShellDirectoryUploadArgs(params) {
533
- return [
534
- "sandbox",
535
- "upload",
536
- "--no-git-ignore",
537
- params.sandboxName,
538
- params.localPath,
539
- `${normalizeRemotePath(params.remotePath)}/`
540
- ];
541
- }
542
527
  const REMOTE_MANAGED_ROOTS_EMPTY_SCRIPT = "for root in \"$@\"; do if [ -d \"$root\" ] && [ -n \"$(ls -A \"$root\")\" ]; then printf \"1\\n\"; exit 0; fi; done; printf \"0\\n\"";
543
528
  const OPEN_SHELL_GUEST_MUTATION_PYTHON = `
544
529
  import os, sys
@@ -973,19 +958,27 @@ var OpenShellSandboxBackendImpl = class {
973
958
  target.root,
974
959
  path.posix.dirname(target.relativePath) === "." ? "" : path.posix.dirname(target.relativePath)
975
960
  ] });
961
+ await this.uploadFileToRemote(localPath, remotePath);
962
+ }
963
+ async uploadFileToRemote(localPath, remotePath) {
964
+ await this.runCli([
965
+ "sandbox",
966
+ "upload",
967
+ "--no-git-ignore",
968
+ this.params.execContext.sandboxName,
969
+ localPath,
970
+ remotePath
971
+ ], "openshell sandbox upload failed");
972
+ }
973
+ async runCli(args, failureMessage, timeoutMs) {
976
974
  const result = await runOpenShellCli({
977
975
  context: this.params.execContext,
978
- args: [
979
- "sandbox",
980
- "upload",
981
- "--no-git-ignore",
982
- this.params.execContext.sandboxName,
983
- localPath,
984
- remotePath
985
- ],
986
- cwd: this.params.createParams.workspaceDir
976
+ args,
977
+ cwd: this.params.createParams.workspaceDir,
978
+ ...timeoutMs === void 0 ? {} : { timeoutMs }
987
979
  });
988
- if (result.code !== 0) throw new Error(result.stderr.trim() || "openshell sandbox upload failed");
980
+ if (failureMessage && result.code !== 0) throw new Error(result.stderr.trim() || failureMessage);
981
+ return result;
989
982
  }
990
983
  async runPinnedRemotePathMutation(params) {
991
984
  return await this.runRemoteShellScript({
@@ -1034,15 +1027,11 @@ var OpenShellSandboxBackendImpl = class {
1034
1027
  }
1035
1028
  }
1036
1029
  async ensureSandboxExistsInner() {
1037
- const getResult = await runOpenShellCli({
1038
- context: this.params.execContext,
1039
- args: [
1040
- "sandbox",
1041
- "get",
1042
- this.params.execContext.sandboxName
1043
- ],
1044
- cwd: this.params.createParams.workspaceDir
1045
- });
1030
+ const getResult = await this.runCli([
1031
+ "sandbox",
1032
+ "get",
1033
+ this.params.execContext.sandboxName
1034
+ ]);
1046
1035
  if (getResult.code === 0) {
1047
1036
  if (this.params.legacyRuntimeAdopted) {
1048
1037
  const phase = await this.resolveLegacyRuntimePhase();
@@ -1073,13 +1062,7 @@ var OpenShellSandboxBackendImpl = class {
1073
1062
  "infinity"
1074
1063
  ] : ["--", "true"]
1075
1064
  ];
1076
- const createResult = await runOpenShellCli({
1077
- context: this.params.execContext,
1078
- args: createArgs,
1079
- cwd: this.params.createParams.workspaceDir,
1080
- timeoutMs: Math.max(this.params.execContext.config.timeoutMs, 3e5)
1081
- });
1082
- if (createResult.code !== 0) throw new Error(createResult.stderr.trim() || "openshell sandbox create failed");
1065
+ await this.runCli(createArgs, "openshell sandbox create failed", Math.max(this.params.execContext.config.timeoutMs, 3e5));
1083
1066
  this.remoteSeedPending = true;
1084
1067
  }
1085
1068
  async supportsDetachedSandboxCreation() {
@@ -1093,16 +1076,11 @@ var OpenShellSandboxBackendImpl = class {
1093
1076
  let support = openShellDetachedCreateSupport?.key === cliIdentity ? openShellDetachedCreateSupport.promise : void 0;
1094
1077
  if (!support) {
1095
1078
  support = (async () => {
1096
- const result = await runOpenShellCli({
1097
- context: this.params.execContext,
1098
- args: [
1099
- "sandbox",
1100
- "create",
1101
- "--help"
1102
- ],
1103
- cwd: this.params.createParams.workspaceDir
1104
- });
1105
- if (result.code !== 0) throw new Error(result.stderr.trim() || "openshell sandbox create capability check failed");
1079
+ const result = await this.runCli([
1080
+ "sandbox",
1081
+ "create",
1082
+ "--help"
1083
+ ], "openshell sandbox create capability check failed");
1106
1084
  return /^\s*--detach(?:\s|$)/mu.test(result.stdout);
1107
1085
  })();
1108
1086
  openShellDetachedCreateSupport = {
@@ -1120,20 +1098,16 @@ var OpenShellSandboxBackendImpl = class {
1120
1098
  async resolveLegacyRuntimePhase() {
1121
1099
  const pageSize = 100;
1122
1100
  for (let offset = 0;; offset += pageSize) {
1123
- const listResult = await runOpenShellCli({
1124
- context: this.params.execContext,
1125
- args: [
1126
- "sandbox",
1127
- "list",
1128
- "--limit",
1129
- String(pageSize),
1130
- "--offset",
1131
- String(offset),
1132
- "--output",
1133
- "json"
1134
- ],
1135
- cwd: this.params.createParams.workspaceDir
1136
- });
1101
+ const listResult = await this.runCli([
1102
+ "sandbox",
1103
+ "list",
1104
+ "--limit",
1105
+ String(pageSize),
1106
+ "--offset",
1107
+ String(offset),
1108
+ "--output",
1109
+ "json"
1110
+ ]);
1137
1111
  if (listResult.code !== 0) throw this.buildLegacyRuntimeUnavailableError(listResult.stderr.trim());
1138
1112
  const page = parseOpenShellSandboxPhasePage(listResult.stdout, this.params.execContext.sandboxName);
1139
1113
  if (!page) throw this.buildLegacyRuntimeUnavailableError("OpenShell returned malformed sandbox lifecycle data.");
@@ -1203,18 +1177,13 @@ var OpenShellSandboxBackendImpl = class {
1203
1177
  rootDir: resolveOpenShellTmpRoot(),
1204
1178
  prefix: "openclaw-openshell-sync-"
1205
1179
  }, async ({ dir: tmpDir }) => {
1206
- const result = await runOpenShellCli({
1207
- context: this.params.execContext,
1208
- args: [
1209
- "sandbox",
1210
- "download",
1211
- this.params.execContext.sandboxName,
1212
- root.remote,
1213
- tmpDir
1214
- ],
1215
- cwd: this.params.createParams.workspaceDir
1216
- });
1217
- if (result.code !== 0) throw new Error(result.stderr.trim() || "openshell sandbox download failed");
1180
+ await this.runCli([
1181
+ "sandbox",
1182
+ "download",
1183
+ this.params.execContext.sandboxName,
1184
+ root.remote,
1185
+ tmpDir
1186
+ ], "openshell sandbox download failed");
1218
1187
  const preservedShadows = [];
1219
1188
  const failures = [];
1220
1189
  try {
@@ -1276,18 +1245,7 @@ var OpenShellSandboxBackendImpl = class {
1276
1245
  targetDir: stagedRoot,
1277
1246
  excludeDirs: DEFAULT_OPEN_SHELL_MIRROR_EXCLUDE_DIRS
1278
1247
  });
1279
- for (const entry of (await fsPromises.readdir(stagedRoot)).toSorted()) {
1280
- const result = await runOpenShellCli({
1281
- context: this.params.execContext,
1282
- args: buildOpenShellDirectoryUploadArgs({
1283
- sandboxName: this.params.execContext.sandboxName,
1284
- localPath: path.join(stagedRoot, entry),
1285
- remotePath
1286
- }),
1287
- cwd: this.params.createParams.workspaceDir
1288
- });
1289
- if (result.code !== 0) throw new Error(result.stderr.trim() || "openshell sandbox upload failed");
1290
- }
1248
+ for (const entry of (await fsPromises.readdir(stagedRoot)).toSorted()) await this.uploadFileToRemote(path.join(stagedRoot, entry), `${normalizeRemotePath(remotePath)}/`);
1291
1249
  });
1292
1250
  }
1293
1251
  async remoteManagedRootsEmpty() {
@@ -2,6 +2,97 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.25.0 - 2026-10-08
6
+
7
+ ### Highlights
8
+
9
+ - **Faster synchronous plugin captures:** 16% faster than pre-change main on a measured 6,000-file tree; the new `createRootFileCopyBatchSync` API on `advanced` also reuses directory admissions across copies. ([#878](https://github.com/openclaw/fs-safe/pull/878), [#849](https://github.com/openclaw/fs-safe/pull/849))
10
+ - **Guest workspace publication:** works on gVisor and other Linux filesystems that reject `RENAME_NOREPLACE`. ([#876](https://github.com/openclaw/fs-safe/pull/876))
11
+ - **Root-backed sidecar locks:** native asynchronous release and exit cleanup no longer strand locks after their Root moves. ([#879](https://github.com/openclaw/fs-safe/pull/879))
12
+ - **Guest cross-device moves:** preserve the admitted file permissions. ([#869](https://github.com/openclaw/fs-safe/pull/869))
13
+
14
+ ### Features
15
+
16
+ - **Guarded copy batches:** `createRootFileCopyBatchSync` on `advanced` reuses directory admissions across synchronous copies on canonical POSIX paths while rechecking ancestor and file identities for every copy; single-file copies also reuse admission within the operation. ([#849](https://github.com/openclaw/fs-safe/pull/849))
17
+
18
+ ### Fixes
19
+
20
+ - **Root-backed file locks:** native asynchronous release and exit cleanup follow the created lock's retained identity after its Root moves, preserving replacement files and reporting ownership mismatches. ([#879](https://github.com/openclaw/fs-safe/pull/879)) Thanks @axiom-ncis.
21
+ - **Temp directory diagnostics:** name rejected roots and ancestors, explain ownership, mode, or symlink failures, and provide safe recovery guidance, including for root-run appliances. Thanks @joequant. ([#877](https://github.com/openclaw/fs-safe/pull/877))
22
+ - **Guest directory publication:** publish staged workspaces when Linux filesystems reject `RENAME_NOREPLACE`, with checked file link/unlink fallback and directory rename that can only replace a concurrently created empty directory. ([#876](https://github.com/openclaw/fs-safe/pull/876)) Thanks @valkyriweb.
23
+ - **Guest cross-device moves:** preserve the admitted source file's permissions instead of leaving the destination with a broader default mode. ([#869](https://github.com/openclaw/fs-safe/pull/869))
24
+ - **Move receipts:** publication receipts capture the source identity after synchronous authorization callbacks return, so a source replaced during authorization is reported accurately. ([#868](https://github.com/openclaw/fs-safe/pull/868))
25
+ - **Atomic copy fallback:** report a failed restoration as a failure even when the underlying error carries a restored receipt. ([#851](https://github.com/openclaw/fs-safe/pull/851))
26
+ - **Archive read limits:** keep the validated entry-read byte limit fixed when callers mutate the options during a read. ([#852](https://github.com/openclaw/fs-safe/pull/852))
27
+ - **Watch on macOS:** reconcile notifications for changes to the watched Root itself immediately instead of waiting for the periodic scan. ([#858](https://github.com/openclaw/fs-safe/pull/858))
28
+ - **`pathScope` tilde names:** keep literal `~` directory names inside scoped file and existing-path lookups instead of expanding them. ([#856](https://github.com/openclaw/fs-safe/pull/856))
29
+
30
+ ### Performance
31
+
32
+ - **Guarded file copies:** reuse checked canonical POSIX roots during initial path admission, reducing repeated path-resolution work in synchronous plugin source captures without changing per-file identity or race checks.
33
+ - **Root walks:** reuse normalized directory prefixes, cutting a 50k-file walk by 5.6% and its sampled allocation by about 9% while keeping every filesystem and identity check. ([#870](https://github.com/openclaw/fs-safe/pull/870))
34
+ - **Atomic writes:** non-durable async POSIX writes skip a redundant mode change when the staged file already has the requested permissions. ([#871](https://github.com/openclaw/fs-safe/pull/871))
35
+ - **Guarded mutations:** fewer promise allocations around synchronous guard checks, with directory identity checks unchanged. ([#857](https://github.com/openclaw/fs-safe/pull/857))
36
+ - **Archive publication:** fewer promise allocations in archive guard checks, without removing identity or containment checks. ([#864](https://github.com/openclaw/fs-safe/pull/864))
37
+ - **Watch:** Linux and Windows watch passes skip macOS stream-path preparation. ([#860](https://github.com/openclaw/fs-safe/pull/860))
38
+ - **Permission diagnostics:** bound escaping work for large error messages without changing the displayed excerpts. ([#859](https://github.com/openclaw/fs-safe/pull/859))
39
+
40
+ ## 0.24.2 - 2026-10-07
41
+
42
+ ### Fixes
43
+
44
+ - **Linux exclusive publication:** `publishFileExclusive` with `rename-noreplace` uses the identity-checked link/unlink move fallback in native `auto` when `RENAME_NOREPLACE` is unsupported, consumes the source on success, and reports `fallback: "link-unlink"` plus precise partial-failure receipts; native `require` stays fail-closed. Link/unlink is not crash-atomic. ([#843](https://github.com/openclaw/fs-safe/pull/843), [openclaw#166246](https://github.com/openclaw/openclaw/issues/166246)) Thanks @MertBasar0.
45
+ - **Native clone descriptors:** native file copies return read-write descriptors, so Linux clones can be read immediately and macOS clones can be written while preserving read-only source modes. ([#844](https://github.com/openclaw/fs-safe/pull/844))
46
+
47
+ ## 0.24.1 - 2026-10-06
48
+
49
+ ### Features
50
+
51
+ - **Pinned source copies:** `copyRootFileSync` accepts `expectedSourceIdentity` to reject a replaced source before destination creation and returns the verified `sourceIdentity` for every copy method. This refines the `copyRootFileSync` API introduced in 0.24.0. ([#841](https://github.com/openclaw/fs-safe/pull/841))
52
+
53
+ ## 0.24.0 - 2026-10-06
54
+
55
+ ### Highlights
56
+
57
+ - **No-replace moves on more Linux filesystems:** when filesystems reject `renameat2` with `RENAME_NOREPLACE`, including reported QNAP ZFS shares and some overlay mounts, `auto` falls back safely: directories use plain rename that can at most replace an empty directory, and files use identity-checked link/unlink that cannot overwrite a target. Native `require` fails closed. ([#838](https://github.com/openclaw/fs-safe/pull/838), fixes [openclaw#165617](https://github.com/openclaw/openclaw/issues/165617)) Thanks @StarGazer1995, original reporter @bbs2i58, and @MertBasar0.
58
+ - **Synchronous guarded copies:** new `copyRootFileSync` on `advanced` supports APFS cloning, reflinks, `copy_file_range`, and guarded byte fallback, returning an owned destination descriptor for synchronous capture hashes. ([#839](https://github.com/openclaw/fs-safe/pull/839))
59
+ - **Public capability classification:** new `isNoReplaceUnsupported(error)` identifies unavailable no-replace rename support without matching internal native error codes. ([#838](https://github.com/openclaw/fs-safe/pull/838))
60
+
61
+ ### Features
62
+
63
+ - **Synchronous root-to-root copy:** `copyRootFileSync` exclusively creates an absent destination under its admitted root, bounds the copy with `maxBytes`, verifies source and destination identities, and returns a disposable descriptor with copy-method and byte-count receipts. ([#839](https://github.com/openclaw/fs-safe/pull/839))
64
+ - **No-replace error classifier:** export `isNoReplaceUnsupported(error)` from the root package and `errors` subpath for stable `helper-unavailable` capability diagnostics. ([#838](https://github.com/openclaw/fs-safe/pull/838))
65
+
66
+ ### Fixes
67
+
68
+ - **Linux no-replace moves:** `Root.move()` uses retained-parent file link/unlink or directory rename when `RENAME_NOREPLACE` is unsupported in `auto`, rejects file collisions, and preserves publication/source-removal details after partial failures. ([#838](https://github.com/openclaw/fs-safe/pull/838)) Thanks @StarGazer1995, @bbs2i58, and @MertBasar0.
69
+
70
+ ### Performance
71
+
72
+ - **Synchronous copy acceleration:** reuse the native bounded copy engine for APFS clones, Linux reflinks, and `copy_file_range` before byte copying when `copyRootFileSync` is called with `clone: "auto"`. ([#839](https://github.com/openclaw/fs-safe/pull/839))
73
+
74
+ ### Compatibility
75
+
76
+ - **Move fallback limits:** native `require` keeps refusing unsupported no-replace moves. In `auto`, the directory fallback can replace a concurrently created empty directory; nonempty directories and non-directory competitors reject. File fallback requires hard-link support and fails closed if links are denied or unsupported. ([#838](https://github.com/openclaw/fs-safe/pull/838))
77
+ - **Synchronous copy contract:** `copyRootFileSync` defaults to `clone: "never"`, requires existing parents and an absent destination, and performs direct exclusive creation. Use a private capture directory; it does not promise atomic publication, crash durability, or a coherent snapshot of concurrent in-place edits. ([#839](https://github.com/openclaw/fs-safe/pull/839))
78
+
79
+ ## 0.23.1 - 2026-10-04
80
+
81
+ ### Highlights
82
+
83
+ - **gVisor and 9p file creation:** `create()` and other no-replace publication fall back in `auto` mode on filesystems that reject `renameat2` with `RENAME_NOREPLACE`; `require` mode fails closed. ([#834](https://github.com/openclaw/fs-safe/pull/834), fixes [#828](https://github.com/openclaw/fs-safe/issues/828)) Thanks @vishnukool.
84
+ - **Container copy compatibility:** seccomp or LSM policies denying `FICLONE` no longer break `clone: "auto"` copies, for both single files and `copyTree`. ([#829](https://github.com/openclaw/fs-safe/pull/829), [#835](https://github.com/openclaw/fs-safe/pull/835)) Thanks @Mohl, with co-author credit to @foxsky.
85
+ - **macOS watching:** a FIFO at a watched name no longer freezes watching across the process. ([#830](https://github.com/openclaw/fs-safe/pull/830)) Thanks @SebTardif.
86
+
87
+ ### Fixes
88
+
89
+ - **gVisor file creation:** fall back safely when the filesystem rejects native no-replace publication, clean unpublished stages, and report the missing capability in native-required mode. ([#834](https://github.com/openclaw/fs-safe/pull/834)) Thanks @vishnukool.
90
+ - **ZIP collision checks:** reject directory and file names that differ only by case, Unicode normalization, or a trailing separator before extraction or bounded reads. ([#832](https://github.com/openclaw/fs-safe/pull/832)) Thanks @SebTardif.
91
+ - **macOS entry watch:** prevent a FIFO at a watched name from blocking every watcher in the process. ([#830](https://github.com/openclaw/fs-safe/pull/830)) Thanks @SebTardif.
92
+ - **Disk-full diagnostics:** preserve disk-full (`ENOSPC`) diagnostics and cleanup receipts through combined write and disposal failures instead of reporting a misleading file-type error. ([#833](https://github.com/openclaw/fs-safe/pull/833)) Thanks @steipete.
93
+ - **Linux clone fallback:** copies with `clone: "auto"` fall back to guarded byte copying when container seccomp or LSM policy denies `FICLONE`; `clone: "always"` reports `unsupported-platform` and removes its stage. ([#829](https://github.com/openclaw/fs-safe/pull/829)) Thanks @Mohl, with co-author credit to @foxsky.
94
+ - **Linux tree clone fallback:** `copyTree` with `clone: "auto"` copies bytes when XFS/ZFS file reflinks are unavailable or denied by container policy; `"always"` reports `unsupported-platform` after partial-tree cleanup. ([#835](https://github.com/openclaw/fs-safe/pull/835)) Thanks @steipete.
95
+
5
96
  ## 0.23.0 - 2026-10-02
6
97
 
7
98
  ### Highlights
@@ -22,7 +22,7 @@ await fs.write("../escape.txt", "x"); // throws FsSafeError("outside-
22
22
 
23
23
  That's the whole pitch. `root()` is the product; the rest of the package — JSON stores, atomic writes, secret files, archive extraction, temp workspaces — is supporting cast for the same boundary.
24
24
 
25
- Full docs and reference at **[fs-safe.io](https://fs-safe.io)**.
25
+ Full docs and reference at **[fs-safe.io](https://fs-safe.io)**. For synchronous plugin captures, [guarded file copies and batches](docs/copy.md#synchronous-guarded-file-copies) return owned destination descriptors while preserving per-file checks.
26
26
 
27
27
  ## Contents
28
28
 
@@ -1,3 +1,4 @@
1
+ export { copyRootFileSync, createRootFileCopyBatchSync, type RootFileCopyBatchSync, type CopyRootFileSyncOptions, type CopiedRootFileSync } from "./copy-root-file-sync.js";
1
2
  export { createAsyncLock } from "./async-lock.js";
2
3
  export { createDirectory, createDirectorySync, createFileSync, type CreateDirectoryOptions, type CreateFileOptions, type OwnedFileDescriptorSync, } from "./create.js";
3
4
  export { copyFileHandle, copyFileDescriptorSync, type CopyFileHandleOptions } from "./file-handle-transfer.js";
@@ -1,3 +1,4 @@
1
+ export { copyRootFileSync, createRootFileCopyBatchSync } from "./copy-root-file-sync.js";
1
2
  // Advanced composition surface. These exports are less stable than the focused
2
3
  // public subpaths; prefer root/json/store/temp/archive unless you are building a
3
4
  // higher-level primitive.
@@ -65,7 +65,7 @@ export async function finalizeArchivePublication(params) {
65
65
  const check = () => params.deadline?.check();
66
66
  const assertGuards = async (guards) => {
67
67
  for (const guard of [params.destinationGuard, ...guards, params.sourceGuard]) {
68
- await assertDirectoryIdentityGuard(guard);
68
+ assertDirectoryIdentityGuard(guard);
69
69
  check();
70
70
  }
71
71
  };
@@ -63,13 +63,17 @@ export function stripArchivePath(entryPath, stripComponents) {
63
63
  export function createArchiveOutputPathTracker() {
64
64
  const seen = new Set();
65
65
  return (entryPath, originalPath) => {
66
- const normalized = path.posix.normalize(normalizeArchiveEntryPath(entryPath));
66
+ // Extraction passes stripArchivePath output. Native manifests and physical
67
+ // admission pass raw names, and path.posix.normalize keeps a directory's
68
+ // trailing slash, so "A/" and "a" would not share a key.
69
+ const canonical = stripArchivePath(entryPath, 0)
70
+ ?? path.posix.normalize(normalizeArchiveEntryPath(entryPath));
67
71
  // Archive policy must not depend on the destination volume's case or
68
72
  // Unicode-normalization behavior. Otherwise the JavaScript and native
69
73
  // writers can disagree about which of two colliding entries wins.
70
- const collisionKey = lowerCaseNfc(normalized);
74
+ const collisionKey = lowerCaseNfc(canonical);
71
75
  if (seen.has(collisionKey)) {
72
- throw new ArchiveSecurityError("entry-path", `archive entries collide at output path ${formatErrorDetail(normalized)}: ${formatErrorDetail(originalPath)}`);
76
+ throw new ArchiveSecurityError("entry-path", `archive entries collide at output path ${formatErrorDetail(canonical)}: ${formatErrorDetail(originalPath)}`);
73
77
  }
74
78
  seen.add(collisionKey);
75
79
  };
@@ -98,15 +98,15 @@ async function mergeTree(params, publication, durable = true, entryUmask = 0) {
98
98
  assertSourceDirectory(ancestor);
99
99
  };
100
100
  const assertGuards = async () => {
101
- await assertDirectoryIdentityGuard(destinationGuard);
101
+ assertDirectoryIdentityGuard(destinationGuard);
102
102
  check();
103
103
  for (const ancestor of ancestors) {
104
- await assertDirectoryIdentityGuard(ancestor.guard);
104
+ assertDirectoryIdentityGuard(ancestor.guard);
105
105
  check();
106
106
  await ancestor.owner.verify(check);
107
107
  check();
108
108
  }
109
- await assertDirectoryIdentityGuard(sourceGuard);
109
+ assertDirectoryIdentityGuard(sourceGuard);
110
110
  check();
111
111
  };
112
112
  const walk = async (sourceDir) => {
@@ -184,7 +184,7 @@ async function mergeTree(params, publication, durable = true, entryUmask = 0) {
184
184
  await owner.apply(mode, { check, beforeChmod: async () => {
185
185
  await assertGuards();
186
186
  assertSourceAncestors();
187
- await assertDirectoryIdentityGuard(guard);
187
+ assertDirectoryIdentityGuard(guard);
188
188
  check();
189
189
  await assertResolvedInsideDestination({
190
190
  destinationRealDir, targetPath: destinationPath, originalPath,
@@ -213,7 +213,8 @@ async function readNativeBufferEntry(native, buffer, kind, requested, displayPat
213
213
  }
214
214
  }
215
215
  export async function readArchiveEntry(archivePath, entryPath, options) {
216
- if (!Number.isSafeInteger(options.maxBytes) || options.maxBytes < 0) {
216
+ const maxBytes = options.maxBytes;
217
+ if (!Number.isSafeInteger(maxBytes) || maxBytes < 0) {
217
218
  throw new RangeError("maxBytes must be a non-negative safe integer");
218
219
  }
219
220
  const kind = options.kind ?? resolveArchiveKind(archivePath);
@@ -228,7 +229,7 @@ export async function readArchiveEntry(archivePath, entryPath, options) {
228
229
  admitZipBuffer(buffer, resolveExtractLimits(), entry => { zipEntries.push(entry); });
229
230
  const native = getNativeBinding();
230
231
  if (native)
231
- return await readNativeBufferEntry(native, buffer, kind, requestedEntry, entryPath, options.maxBytes, zipEntries);
232
- return kind === "zip" ? await readZipEntry(buffer, requestedEntry, options.maxBytes, zipEntries)
233
- : await readTarEntry(buffer, requestedEntry, options.maxBytes, kind);
232
+ return await readNativeBufferEntry(native, buffer, kind, requestedEntry, entryPath, maxBytes, zipEntries);
233
+ return kind === "zip" ? await readZipEntry(buffer, requestedEntry, maxBytes, zipEntries)
234
+ : await readTarEntry(buffer, requestedEntry, maxBytes, kind);
234
235
  }
@@ -5,7 +5,7 @@ import { ArchiveSecurityError } from "./archive-errors.js";
5
5
  export type ArchiveDirectoryGuard = AsyncDirectoryGuard<BigIntStats>;
6
6
  export declare function createArchiveSymlinkTraversalError(originalPath: string): ArchiveSecurityError;
7
7
  export declare function createDirectoryIdentityGuard(dir: string): Promise<ArchiveDirectoryGuard>;
8
- export declare function assertDirectoryIdentityGuard(guard: ArchiveDirectoryGuard): Promise<void>;
8
+ export declare function assertDirectoryIdentityGuard(guard: ArchiveDirectoryGuard): void;
9
9
  export declare function prepareArchiveDestinationGuard(destDir: string): Promise<ArchiveDirectoryGuard>;
10
10
  export declare function prepareArchiveDestinationDir(destDir: string): Promise<string>;
11
11
  export declare function assertResolvedInsideDestination(params: {
@@ -1,7 +1,7 @@
1
1
  import fsSync, {} from "node:fs";
2
2
  import fs from "node:fs/promises";
3
3
  import path from "node:path";
4
- import { assertAsyncDirectoryGuard, createAsyncDirectoryGuard, } from "./directory-guard.js";
4
+ import { assertDirectoryGuard, createAsyncDirectoryGuard, } from "./directory-guard.js";
5
5
  import { ownExtractionDestinationMutation, } from "./archive-deadline.js";
6
6
  import { ArchiveSecurityError, } from "./archive-errors.js";
7
7
  import { FsSafeError } from "./errors.js";
@@ -38,9 +38,9 @@ export async function createDirectoryIdentityGuard(dir) {
38
38
  throw err;
39
39
  }
40
40
  }
41
- export async function assertDirectoryIdentityGuard(guard) {
41
+ export function assertDirectoryIdentityGuard(guard) {
42
42
  try {
43
- await assertAsyncDirectoryGuard(guard);
43
+ assertDirectoryGuard(guard, "native");
44
44
  }
45
45
  catch (err) {
46
46
  if (err instanceof FsSafeError || isNotFoundPathError(err)) {
@@ -200,7 +200,7 @@ async function prepareOutputPath(params, assertGuards, privateWorkingMode = fals
200
200
  if (existingDestinationGuard && assertGuards)
201
201
  await assertGuards();
202
202
  else {
203
- await assertDirectoryIdentityGuard(destinationGuard);
203
+ assertDirectoryIdentityGuard(destinationGuard);
204
204
  checkExtractionDeadline(params.deadline);
205
205
  await assertGuards?.();
206
206
  }
@@ -213,46 +213,30 @@ async function prepareOutputPath(params, assertGuards, privateWorkingMode = fals
213
213
  originalPath: params.originalPath,
214
214
  });
215
215
  checkExtractionDeadline(params.deadline);
216
- if (params.isDirectory) {
217
- await getFsSafeTestHooks()?.beforeArchiveOutputMutation?.("mkdir", params.outPath);
216
+ const relativePath = params.isDirectory ? relPath : path.posix.dirname(relPath);
217
+ if (params.isDirectory || relativePath !== ".") {
218
+ const directoryPath = params.isDirectory ? params.outPath : path.dirname(params.outPath);
219
+ await getFsSafeTestHooks()?.beforeArchiveOutputMutation?.("mkdir", directoryPath);
218
220
  checkExtractionDeadline(params.deadline);
219
221
  await ownExtractionDestinationMutation(params.deadline, async () => {
220
222
  await assertOutputGuards();
221
223
  checkExtractionDeadline(params.deadline);
222
- await mkdirArchiveOutput({
223
- targetRoot,
224
- relativePath: relPath,
225
- originalPath: params.originalPath,
226
- });
227
- checkExtractionDeadline(params.deadline);
228
- await assertDirectoryIdentityGuard(destinationGuard);
224
+ await mkdirArchiveOutput({ targetRoot, relativePath, originalPath: params.originalPath });
229
225
  checkExtractionDeadline(params.deadline);
230
- await assertResolvedInsideDestination({
231
- destinationRealDir: params.destinationRealDir,
232
- targetPath: params.outPath,
233
- originalPath: params.originalPath,
234
- });
235
- checkExtractionDeadline(params.deadline);
236
- });
237
- return;
238
- }
239
- const parentRel = path.posix.dirname(relPath);
240
- if (parentRel !== ".") {
241
- await getFsSafeTestHooks()?.beforeArchiveOutputMutation?.("mkdir", path.dirname(params.outPath));
242
- checkExtractionDeadline(params.deadline);
243
- await ownExtractionDestinationMutation(params.deadline, async () => {
244
- await assertOutputGuards();
245
- checkExtractionDeadline(params.deadline);
246
- await mkdirArchiveOutput({
247
- targetRoot,
248
- relativePath: parentRel,
249
- originalPath: params.originalPath,
250
- });
251
- checkExtractionDeadline(params.deadline);
252
- await assertDirectoryIdentityGuard(destinationGuard);
226
+ assertDirectoryIdentityGuard(destinationGuard);
253
227
  checkExtractionDeadline(params.deadline);
228
+ if (params.isDirectory) {
229
+ await assertResolvedInsideDestination({
230
+ destinationRealDir: params.destinationRealDir,
231
+ targetPath: params.outPath,
232
+ originalPath: params.originalPath,
233
+ });
234
+ checkExtractionDeadline(params.deadline);
235
+ }
254
236
  });
255
237
  }
238
+ if (params.isDirectory)
239
+ return;
256
240
  await assertResolvedInsideDestination({
257
241
  destinationRealDir: params.destinationRealDir,
258
242
  targetPath: path.dirname(params.outPath),
@@ -292,12 +276,12 @@ export async function withStagedArchiveDestination(params) {
292
276
  const stagingGuard = await createDirectoryIdentityGuard(stagingDir);
293
277
  try {
294
278
  await fs.chmod(stagingDir, ARCHIVE_STAGING_MODE).catch(() => undefined);
295
- await assertDirectoryIdentityGuard(stagingGuard);
279
+ assertDirectoryIdentityGuard(stagingGuard);
296
280
  return await params.run(stagingDir);
297
281
  }
298
282
  finally {
299
283
  try {
300
- await assertDirectoryIdentityGuard(stagingGuard);
284
+ assertDirectoryIdentityGuard(stagingGuard);
301
285
  await fs.rm(stagingDir, { recursive: true, force: true }).catch(() => undefined);
302
286
  }
303
287
  catch {
@@ -0,0 +1,17 @@
1
+ import { type AnyAsyncDirectoryGuard } from "./directory-guard.js";
2
+ export type CopyPath = {
3
+ rootPath: string;
4
+ absolutePath: string;
5
+ };
6
+ declare function admitUncachedCopyPath(input: CopyPath, create?: boolean, admittedRoot?: AnyAsyncDirectoryGuard): {
7
+ path: string;
8
+ parent: AnyAsyncDirectoryGuard;
9
+ assertCurrent: () => void;
10
+ rootCanonicalPath: string | undefined;
11
+ };
12
+ /** Metadata custody lasts only for the owning copy or explicit batch. */
13
+ export declare function createCopyPathAdmissionCache(): {
14
+ admit: (input: CopyPath, create?: boolean) => ReturnType<typeof admitUncachedCopyPath>;
15
+ clear(): void;
16
+ };
17
+ export {};