@openclaw/openshell-sandbox 2026.10.5-beta.4 → 2026.10.10-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (240) hide show
  1. package/README.md +1 -1
  2. package/node_modules/@openclaw/fs-safe/CHANGELOG.md +148 -0
  3. package/node_modules/@openclaw/fs-safe/README.md +20 -4
  4. package/node_modules/@openclaw/fs-safe/dist/advanced.d.ts +2 -1
  5. package/node_modules/@openclaw/fs-safe/dist/advanced.js +1 -0
  6. package/node_modules/@openclaw/fs-safe/dist/archive-merge.js +28 -3
  7. package/node_modules/@openclaw/fs-safe/dist/archive-parser.wasm +0 -0
  8. package/node_modules/@openclaw/fs-safe/dist/archive-read.js +11 -5
  9. package/node_modules/@openclaw/fs-safe/dist/archive-zip-loader.js +4 -2
  10. package/node_modules/@openclaw/fs-safe/dist/archive.js +2 -2
  11. package/node_modules/@openclaw/fs-safe/dist/atomic-io.js +2 -2
  12. package/node_modules/@openclaw/fs-safe/dist/bounded-read.d.ts +1 -0
  13. package/node_modules/@openclaw/fs-safe/dist/bounded-read.js +9 -2
  14. package/node_modules/@openclaw/fs-safe/dist/clone-metadata.d.ts +2 -0
  15. package/node_modules/@openclaw/fs-safe/dist/clone-metadata.js +2 -1
  16. package/node_modules/@openclaw/fs-safe/dist/config.d.ts +6 -0
  17. package/node_modules/@openclaw/fs-safe/dist/config.js +5 -0
  18. package/node_modules/@openclaw/fs-safe/dist/copy-file-input.d.ts +3 -1
  19. package/node_modules/@openclaw/fs-safe/dist/copy-file-input.js +8 -7
  20. package/node_modules/@openclaw/fs-safe/dist/copy-link.d.ts +26 -0
  21. package/node_modules/@openclaw/fs-safe/dist/copy-link.js +170 -0
  22. package/node_modules/@openclaw/fs-safe/dist/copy-metadata.d.ts +9 -0
  23. package/node_modules/@openclaw/fs-safe/dist/copy-metadata.js +39 -0
  24. package/node_modules/@openclaw/fs-safe/dist/copy-path-admission.js +11 -2
  25. package/node_modules/@openclaw/fs-safe/dist/copy-root-file-sync.d.ts +2 -4
  26. package/node_modules/@openclaw/fs-safe/dist/copy-root-file-sync.js +4 -2
  27. package/node_modules/@openclaw/fs-safe/dist/copy-tree-portable.js +3 -6
  28. package/node_modules/@openclaw/fs-safe/dist/copy.js +3 -3
  29. package/node_modules/@openclaw/fs-safe/dist/create.js +1 -1
  30. package/node_modules/@openclaw/fs-safe/dist/creation-boundary.js +7 -4
  31. package/node_modules/@openclaw/fs-safe/dist/darwin-acl.d.ts +12 -0
  32. package/node_modules/@openclaw/fs-safe/dist/darwin-acl.js +29 -0
  33. package/node_modules/@openclaw/fs-safe/dist/directory-durability.d.ts +7 -4
  34. package/node_modules/@openclaw/fs-safe/dist/directory-guard.js +20 -5
  35. package/node_modules/@openclaw/fs-safe/dist/directory-mode-node.d.ts +3 -0
  36. package/node_modules/@openclaw/fs-safe/dist/directory-mode-node.js +27 -2
  37. package/node_modules/@openclaw/fs-safe/dist/directory-receipt.d.ts +7 -7
  38. package/node_modules/@openclaw/fs-safe/dist/directory-receipt.js +9 -4
  39. package/node_modules/@openclaw/fs-safe/dist/durability.d.ts +2 -1
  40. package/node_modules/@openclaw/fs-safe/dist/entry-publication.js +2 -2
  41. package/node_modules/@openclaw/fs-safe/dist/exact-identity.d.ts +17 -0
  42. package/node_modules/@openclaw/fs-safe/dist/exact-identity.js +29 -0
  43. package/node_modules/@openclaw/fs-safe/dist/file-handle-transfer.js +4 -2
  44. package/node_modules/@openclaw/fs-safe/dist/file-hash.d.ts +1 -1
  45. package/node_modules/@openclaw/fs-safe/dist/file-hash.js +11 -9
  46. package/node_modules/@openclaw/fs-safe/dist/file-identity.d.ts +7 -1
  47. package/node_modules/@openclaw/fs-safe/dist/file-identity.js +27 -5
  48. package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-admission.js +1 -2
  49. package/node_modules/@openclaw/fs-safe/dist/file-lock-sync.js +8 -1
  50. package/node_modules/@openclaw/fs-safe/dist/file-lock.d.ts +1 -0
  51. package/node_modules/@openclaw/fs-safe/dist/file-lock.js +1 -0
  52. package/node_modules/@openclaw/fs-safe/dist/file-write-lease.d.ts +9 -0
  53. package/node_modules/@openclaw/fs-safe/dist/file-write-lease.js +42 -0
  54. package/node_modules/@openclaw/fs-safe/dist/guarded-mkdir.d.ts +1 -0
  55. package/node_modules/@openclaw/fs-safe/dist/guarded-mkdir.js +22 -15
  56. package/node_modules/@openclaw/fs-safe/dist/guest-native-python.js +3 -0
  57. package/node_modules/@openclaw/fs-safe/dist/hardlink-fallback.d.ts +1 -0
  58. package/node_modules/@openclaw/fs-safe/dist/hardlink-fallback.js +5 -0
  59. package/node_modules/@openclaw/fs-safe/dist/index.d.ts +1 -0
  60. package/node_modules/@openclaw/fs-safe/dist/index.js +1 -0
  61. package/node_modules/@openclaw/fs-safe/dist/json-durable-queue-ownership.js +16 -3
  62. package/node_modules/@openclaw/fs-safe/dist/local-file-descriptor.js +24 -13
  63. package/node_modules/@openclaw/fs-safe/dist/native-binding.d.ts +85 -18
  64. package/node_modules/@openclaw/fs-safe/dist/native-config.d.ts +5 -0
  65. package/node_modules/@openclaw/fs-safe/dist/native-config.js +11 -0
  66. package/node_modules/@openclaw/fs-safe/dist/native-directory-observation.js +6 -4
  67. package/node_modules/@openclaw/fs-safe/dist/native-fallback-warning.d.ts +2 -1
  68. package/node_modules/@openclaw/fs-safe/dist/native-fallback-warning.js +26 -2
  69. package/node_modules/@openclaw/fs-safe/dist/native-guidance.d.ts +3 -0
  70. package/node_modules/@openclaw/fs-safe/dist/native-guidance.js +21 -0
  71. package/node_modules/@openclaw/fs-safe/dist/native-move-fallback.d.ts +1 -1
  72. package/node_modules/@openclaw/fs-safe/dist/native-move-fallback.js +2 -0
  73. package/node_modules/@openclaw/fs-safe/dist/native-operations.js +8 -6
  74. package/node_modules/@openclaw/fs-safe/dist/native-parent-admission.d.ts +44 -4
  75. package/node_modules/@openclaw/fs-safe/dist/native-parent-admission.js +85 -15
  76. package/node_modules/@openclaw/fs-safe/dist/native-pinned-write-windows.d.ts +3 -3
  77. package/node_modules/@openclaw/fs-safe/dist/native-pinned-write-windows.js +33 -6
  78. package/node_modules/@openclaw/fs-safe/dist/native-pinned-write.d.ts +4 -1
  79. package/node_modules/@openclaw/fs-safe/dist/native-pinned-write.js +22 -3
  80. package/node_modules/@openclaw/fs-safe/dist/native-policy-directory-observation.js +4 -3
  81. package/node_modules/@openclaw/fs-safe/dist/native-policy-parent.d.ts +1 -1
  82. package/node_modules/@openclaw/fs-safe/dist/native-policy-parent.js +3 -0
  83. package/node_modules/@openclaw/fs-safe/dist/native-staged-file-metadata.d.ts +3 -0
  84. package/node_modules/@openclaw/fs-safe/dist/native-staged-file-metadata.js +16 -0
  85. package/node_modules/@openclaw/fs-safe/dist/native-staged-file.d.ts +1 -1
  86. package/node_modules/@openclaw/fs-safe/dist/native-staged-file.js +15 -5
  87. package/node_modules/@openclaw/fs-safe/dist/native-stat-identity.d.ts +3 -0
  88. package/node_modules/@openclaw/fs-safe/dist/native-stat-identity.js +7 -0
  89. package/node_modules/@openclaw/fs-safe/dist/native-status.d.ts +17 -0
  90. package/node_modules/@openclaw/fs-safe/dist/native-status.js +30 -0
  91. package/node_modules/@openclaw/fs-safe/dist/native.d.ts +31 -4
  92. package/node_modules/@openclaw/fs-safe/dist/native.js +107 -9
  93. package/node_modules/@openclaw/fs-safe/dist/opened-realpath.js +9 -1
  94. package/node_modules/@openclaw/fs-safe/dist/overwrite-file-handle.js +4 -1
  95. package/node_modules/@openclaw/fs-safe/dist/owner-dacl-batch.js +1 -1
  96. package/node_modules/@openclaw/fs-safe/dist/owner-dacl.js +1 -1
  97. package/node_modules/@openclaw/fs-safe/dist/permission-exec.js +9 -1
  98. package/node_modules/@openclaw/fs-safe/dist/permissions-public.d.ts +1 -0
  99. package/node_modules/@openclaw/fs-safe/dist/permissions-public.js +1 -0
  100. package/node_modules/@openclaw/fs-safe/dist/permissions-windows.js +83 -47
  101. package/node_modules/@openclaw/fs-safe/dist/pinned-open.d.ts +2 -0
  102. package/node_modules/@openclaw/fs-safe/dist/pinned-open.js +8 -3
  103. package/node_modules/@openclaw/fs-safe/dist/pinned-write-input.js +13 -0
  104. package/node_modules/@openclaw/fs-safe/dist/pinned-write-staged.js +10 -8
  105. package/node_modules/@openclaw/fs-safe/dist/pinned-write-types.d.ts +2 -1
  106. package/node_modules/@openclaw/fs-safe/dist/pinned-write.js +7 -5
  107. package/node_modules/@openclaw/fs-safe/dist/pipe.d.ts +9 -0
  108. package/node_modules/@openclaw/fs-safe/dist/pipe.js +26 -0
  109. package/node_modules/@openclaw/fs-safe/dist/private-directory.js +1 -1
  110. package/node_modules/@openclaw/fs-safe/dist/private-producer-handoff.js +2 -5
  111. package/node_modules/@openclaw/fs-safe/dist/private-temp-workspace.d.ts +3 -2
  112. package/node_modules/@openclaw/fs-safe/dist/private-temp-workspace.js +38 -10
  113. package/node_modules/@openclaw/fs-safe/dist/publish-copy-stage.js +92 -10
  114. package/node_modules/@openclaw/fs-safe/dist/publish-file-failure.d.ts +3 -0
  115. package/node_modules/@openclaw/fs-safe/dist/publish-file-failure.js +3 -0
  116. package/node_modules/@openclaw/fs-safe/dist/publish-file.d.ts +5 -3
  117. package/node_modules/@openclaw/fs-safe/dist/publish-file.js +26 -20
  118. package/node_modules/@openclaw/fs-safe/dist/read-opened-file.d.ts +2 -0
  119. package/node_modules/@openclaw/fs-safe/dist/read-opened-file.js +3 -4
  120. package/node_modules/@openclaw/fs-safe/dist/realpath.js +32 -0
  121. package/node_modules/@openclaw/fs-safe/dist/regular-file.d.ts +4 -0
  122. package/node_modules/@openclaw/fs-safe/dist/regular-file.js +11 -15
  123. package/node_modules/@openclaw/fs-safe/dist/replace-file-descriptor.js +3 -2
  124. package/node_modules/@openclaw/fs-safe/dist/root-copy.d.ts +18 -0
  125. package/node_modules/@openclaw/fs-safe/dist/root-copy.js +128 -0
  126. package/node_modules/@openclaw/fs-safe/dist/root-create-native.d.ts +8 -13
  127. package/node_modules/@openclaw/fs-safe/dist/root-create-native.js +252 -27
  128. package/node_modules/@openclaw/fs-safe/dist/root-directory-creation.js +5 -1
  129. package/node_modules/@openclaw/fs-safe/dist/root-directory-list-types.d.ts +11 -8
  130. package/node_modules/@openclaw/fs-safe/dist/root-directory-list.js +35 -13
  131. package/node_modules/@openclaw/fs-safe/dist/root-file-open-batch-sync.d.ts +8 -0
  132. package/node_modules/@openclaw/fs-safe/dist/root-file-open-batch-sync.js +108 -0
  133. package/node_modules/@openclaw/fs-safe/dist/root-file.d.ts +7 -0
  134. package/node_modules/@openclaw/fs-safe/dist/root-file.js +15 -0
  135. package/node_modules/@openclaw/fs-safe/dist/root-impl.d.ts +2 -0
  136. package/node_modules/@openclaw/fs-safe/dist/root-impl.js +154 -175
  137. package/node_modules/@openclaw/fs-safe/dist/root-mkdir-native.d.ts +2 -0
  138. package/node_modules/@openclaw/fs-safe/dist/root-mkdir-native.js +239 -0
  139. package/node_modules/@openclaw/fs-safe/dist/root-move-noreplace.js +37 -15
  140. package/node_modules/@openclaw/fs-safe/dist/root-native-creation-path.d.ts +32 -0
  141. package/node_modules/@openclaw/fs-safe/dist/root-native-creation-path.js +56 -0
  142. package/node_modules/@openclaw/fs-safe/dist/root-options.d.ts +2 -0
  143. package/node_modules/@openclaw/fs-safe/dist/root-public-types.d.ts +1 -0
  144. package/node_modules/@openclaw/fs-safe/dist/root-remove-native.js +95 -16
  145. package/node_modules/@openclaw/fs-safe/dist/root-remove.js +6 -4
  146. package/node_modules/@openclaw/fs-safe/dist/root-walk.js +23 -19
  147. package/node_modules/@openclaw/fs-safe/dist/root-write-admission.js +8 -6
  148. package/node_modules/@openclaw/fs-safe/dist/root-write-publication.js +5 -1
  149. package/node_modules/@openclaw/fs-safe/dist/secure-file-windows.js +3 -1
  150. package/node_modules/@openclaw/fs-safe/dist/secure-file.d.ts +2 -0
  151. package/node_modules/@openclaw/fs-safe/dist/secure-file.js +10 -4
  152. package/node_modules/@openclaw/fs-safe/dist/sibling-staged-file.js +1 -1
  153. package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-reclaim.js +2 -2
  154. package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-retained.js +1 -1
  155. package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-target.js +8 -1
  156. package/node_modules/@openclaw/fs-safe/dist/staged-directory.d.ts +13 -0
  157. package/node_modules/@openclaw/fs-safe/dist/staged-directory.js +29 -0
  158. package/node_modules/@openclaw/fs-safe/dist/temp-workspace-child-admission.js +5 -3
  159. package/node_modules/@openclaw/fs-safe/dist/temp-workspace-descriptor.d.ts +8 -0
  160. package/node_modules/@openclaw/fs-safe/dist/temp-workspace-descriptor.js +13 -2
  161. package/node_modules/@openclaw/fs-safe/dist/temp-workspace-owner.js +71 -3
  162. package/node_modules/@openclaw/fs-safe/dist/temp-workspace-ownership.d.ts +2 -1
  163. package/node_modules/@openclaw/fs-safe/dist/temp-workspace-ownership.js +22 -0
  164. package/node_modules/@openclaw/fs-safe/dist/temp-workspace-types.d.ts +3 -0
  165. package/node_modules/@openclaw/fs-safe/dist/test-hooks.d.ts +1 -0
  166. package/node_modules/@openclaw/fs-safe/dist/test-hooks.js +1 -0
  167. package/node_modules/@openclaw/fs-safe/dist/trash.js +2 -2
  168. package/node_modules/@openclaw/fs-safe/dist/types.d.ts +4 -0
  169. package/node_modules/@openclaw/fs-safe/dist/watch-native.js +2 -2
  170. package/node_modules/@openclaw/fs-safe/dist/windows-appcontainer.d.ts +5 -0
  171. package/node_modules/@openclaw/fs-safe/dist/windows-appcontainer.js +39 -0
  172. package/node_modules/@openclaw/fs-safe/dist/windows-owner.d.ts +1 -0
  173. package/node_modules/@openclaw/fs-safe/dist/windows-owner.js +25 -36
  174. package/node_modules/@openclaw/fs-safe/dist/windows-security-command.js +24 -5
  175. package/node_modules/@openclaw/fs-safe/dist/windows-security-facts.d.ts +2 -1
  176. package/node_modules/@openclaw/fs-safe/dist/windows-security-facts.js +4 -0
  177. package/node_modules/@openclaw/fs-safe/dist/windows-security-helper.d.ts +2 -0
  178. package/node_modules/@openclaw/fs-safe/dist/windows-security-helper.exe +0 -0
  179. package/node_modules/@openclaw/fs-safe/dist/windows-security-helper.js +7 -0
  180. package/node_modules/@openclaw/fs-safe/dist/windows-test-hooks.d.ts +16 -0
  181. package/node_modules/@openclaw/fs-safe/dist/windows-test-hooks.js +51 -0
  182. package/node_modules/@openclaw/fs-safe/dist/write-file-handle.d.ts +1 -0
  183. package/node_modules/@openclaw/fs-safe/dist/write-file-handle.js +12 -10
  184. package/node_modules/@openclaw/fs-safe/docs/advanced.md +2 -1
  185. package/node_modules/@openclaw/fs-safe/docs/archive.md +16 -0
  186. package/node_modules/@openclaw/fs-safe/docs/config.md +71 -1
  187. package/node_modules/@openclaw/fs-safe/docs/contributing.md +45 -5
  188. package/node_modules/@openclaw/fs-safe/docs/copy.md +45 -5
  189. package/node_modules/@openclaw/fs-safe/docs/durability.md +34 -7
  190. package/node_modules/@openclaw/fs-safe/docs/errors.md +8 -0
  191. package/node_modules/@openclaw/fs-safe/docs/guest.md +2 -0
  192. package/node_modules/@openclaw/fs-safe/docs/install.md +47 -27
  193. package/node_modules/@openclaw/fs-safe/docs/migrating-to-0.5.md +6 -6
  194. package/node_modules/@openclaw/fs-safe/docs/migrating-to-0.6.md +1 -1
  195. package/node_modules/@openclaw/fs-safe/docs/native-helper.md +79 -6
  196. package/node_modules/@openclaw/fs-safe/docs/native.md +279 -8
  197. package/node_modules/@openclaw/fs-safe/docs/permissions.md +64 -15
  198. package/node_modules/@openclaw/fs-safe/docs/pipe.md +81 -0
  199. package/node_modules/@openclaw/fs-safe/docs/platform-capabilities.md +63 -0
  200. package/node_modules/@openclaw/fs-safe/docs/public-api.md +10 -0
  201. package/node_modules/@openclaw/fs-safe/docs/queue-link-free-design.md +237 -0
  202. package/node_modules/@openclaw/fs-safe/docs/quickstart.md +23 -3
  203. package/node_modules/@openclaw/fs-safe/docs/reading.md +56 -2
  204. package/node_modules/@openclaw/fs-safe/docs/regular-file.md +2 -0
  205. package/node_modules/@openclaw/fs-safe/docs/secret-file.md +5 -5
  206. package/node_modules/@openclaw/fs-safe/docs/secure-file.md +1 -1
  207. package/node_modules/@openclaw/fs-safe/docs/security-model.md +35 -0
  208. package/node_modules/@openclaw/fs-safe/docs/sidecar-lock.md +31 -13
  209. package/node_modules/@openclaw/fs-safe/docs/staged-file.md +18 -0
  210. package/node_modules/@openclaw/fs-safe/docs/store.md +15 -0
  211. package/node_modules/@openclaw/fs-safe/docs/temp.md +39 -0
  212. package/node_modules/@openclaw/fs-safe/docs/testing.md +13 -0
  213. package/node_modules/@openclaw/fs-safe/docs/types.md +51 -81
  214. package/node_modules/@openclaw/fs-safe/docs/walk.md +4 -1
  215. package/node_modules/@openclaw/fs-safe/docs/watch.md +3 -2
  216. package/node_modules/@openclaw/fs-safe/docs/writing.md +121 -14
  217. package/node_modules/@openclaw/fs-safe/package.json +21 -12
  218. package/node_modules/@openclaw/fs-safe-darwin-arm64/fs-safe-native.node +0 -0
  219. package/node_modules/@openclaw/fs-safe-darwin-arm64/package.json +1 -1
  220. package/node_modules/@openclaw/fs-safe-darwin-x64/fs-safe-native.node +0 -0
  221. package/node_modules/@openclaw/fs-safe-darwin-x64/package.json +1 -1
  222. package/node_modules/@openclaw/fs-safe-freebsd-arm64/fs-safe-native.node +0 -0
  223. package/node_modules/@openclaw/fs-safe-freebsd-arm64/package.json +29 -0
  224. package/node_modules/@openclaw/fs-safe-freebsd-x64/fs-safe-native.node +0 -0
  225. package/node_modules/@openclaw/fs-safe-freebsd-x64/package.json +29 -0
  226. package/node_modules/@openclaw/fs-safe-linux-arm64-gnu/fs-safe-native.node +0 -0
  227. package/node_modules/@openclaw/fs-safe-linux-arm64-gnu/package.json +1 -1
  228. package/node_modules/@openclaw/fs-safe-linux-arm64-musl/fs-safe-native.node +0 -0
  229. package/node_modules/@openclaw/fs-safe-linux-arm64-musl/package.json +1 -1
  230. package/node_modules/@openclaw/fs-safe-linux-x64-gnu/fs-safe-native.node +0 -0
  231. package/node_modules/@openclaw/fs-safe-linux-x64-gnu/package.json +1 -1
  232. package/node_modules/@openclaw/fs-safe-linux-x64-musl/fs-safe-native.node +0 -0
  233. package/node_modules/@openclaw/fs-safe-linux-x64-musl/package.json +1 -1
  234. package/node_modules/@openclaw/fs-safe-win32-arm64-msvc/fs-safe-native.node +0 -0
  235. package/node_modules/@openclaw/fs-safe-win32-arm64-msvc/package.json +29 -0
  236. package/node_modules/@openclaw/fs-safe-win32-x64-msvc/fs-safe-native.node +0 -0
  237. package/node_modules/@openclaw/fs-safe-win32-x64-msvc/package.json +1 -1
  238. package/package.json +5 -5
  239. package/node_modules/@openclaw/fs-safe/dist/windows-security-bridge.cs +0 -336
  240. package/node_modules/@openclaw/fs-safe/dist/windows-security-bridge.ps1 +0 -90
@@ -266,7 +266,9 @@ export async function removePathInRootFallback(root, targetPath, options, receip
266
266
  const rootGuard = await createAsyncDirectoryGuard(root.rootReal, { bigint: true });
267
267
  if (!sameFileIdentityForCleanup(rootGuard.stat, root.rootIdentity))
268
268
  throw rootPathChangedError();
269
- const guards = [rootGuard];
269
+ const guards = [
270
+ createRemovalDirectoryAssertion(rootGuard.dir, rootGuard.stat, rootGuard.realPath),
271
+ ];
270
272
  let ancestor = root.rootReal;
271
273
  const parentRelative = path.relative(root.rootReal, path.dirname(targetPath));
272
274
  for (const segment of parentRelative.split(path.sep).filter(Boolean)) {
@@ -287,12 +289,12 @@ export async function removePathInRootFallback(root, targetPath, options, receip
287
289
  if (!isPathInside(guard.realPath, ancestor) || !isPathInside(ancestor, guard.realPath)) {
288
290
  throw new FsSafeError("path-mismatch", "removal ancestor changed during operation");
289
291
  }
290
- guards.push(guard);
292
+ guards.push(createRemovalDirectoryAssertion(guard.dir, guard.stat, guard.realPath));
291
293
  }
292
294
  function assertCurrent() {
293
295
  for (const guard of guards) {
294
296
  try {
295
- assertSyncDirectoryGuard(guard);
297
+ assertRemovalDirectoryCurrent(guard);
296
298
  }
297
299
  catch (error) {
298
300
  // force tolerates a vanished leaf, never a lost traversal boundary.
@@ -342,7 +344,7 @@ export async function removePathInRootFallback(root, targetPath, options, receip
342
344
  !sameFileIdentityForCleanup(directoryGuard.stat, initial)) {
343
345
  throw new FsSafeError("path-mismatch", "removal directory changed during operation");
344
346
  }
345
- guards.push(directoryGuard);
347
+ guards.push(createRemovalDirectoryAssertion(directoryGuard.dir, directoryGuard.stat, directoryGuard.realPath));
346
348
  try {
347
349
  assertCurrent();
348
350
  assertNotAborted(options.signal);
@@ -29,23 +29,9 @@ function entryKind(entry) {
29
29
  function limitEntry(relativePath) {
30
30
  return { relativePath, kind: "truncated", size: 0 };
31
31
  }
32
- export async function* walkRoot(root, relativePath, options) {
33
- if (!["skip", "follow-within-root", "include"].includes(options.symlinkPolicy)) {
34
- throw new TypeError(`invalid root walk symlink policy: ${String(options.symlinkPolicy)}`);
35
- }
36
- if (options.order !== undefined && !["sorted", "filesystem"].includes(options.order)) {
37
- throw new TypeError(`invalid root walk order: ${String(options.order)}`);
38
- }
39
- if (options.limitBehavior !== undefined &&
40
- !["truncate", "throw"].includes(options.limitBehavior)) {
41
- throw new TypeError(`invalid root walk limit behavior: ${String(options.limitBehavior)}`);
42
- }
43
- if (options.onDirectoryError !== undefined &&
44
- !["throw", "skip-and-report"].includes(options.onDirectoryError)) {
45
- throw new TypeError(`invalid root walk directory error behavior: ${String(options.onDirectoryError)}`);
46
- }
47
- const maxDepth = validateBudget("maxDepth", options.maxDepth);
48
- const maxEntries = validateBudget("maxEntries", options.maxEntries);
32
+ export function walkRoot(root, relativePath, options) {
33
+ let maxDepth;
34
+ let maxEntries;
49
35
  const visitedDirectories = new Set();
50
36
  let examined = 0;
51
37
  let truncated = false;
@@ -69,6 +55,24 @@ export async function* walkRoot(root, relativePath, options) {
69
55
  return { relativePath: directory, kind: "directory-error", size: 0, error };
70
56
  };
71
57
  async function* visit(directory, depth, admittedChildPath) {
58
+ if (depth === 0) {
59
+ if (!["skip", "follow-within-root", "include"].includes(options.symlinkPolicy)) {
60
+ throw new TypeError(`invalid root walk symlink policy: ${String(options.symlinkPolicy)}`);
61
+ }
62
+ if (options.order !== undefined && !["sorted", "filesystem"].includes(options.order)) {
63
+ throw new TypeError(`invalid root walk order: ${String(options.order)}`);
64
+ }
65
+ if (options.limitBehavior !== undefined &&
66
+ !["truncate", "throw"].includes(options.limitBehavior)) {
67
+ throw new TypeError(`invalid root walk limit behavior: ${String(options.limitBehavior)}`);
68
+ }
69
+ if (options.onDirectoryError !== undefined &&
70
+ !["throw", "skip-and-report"].includes(options.onDirectoryError)) {
71
+ throw new TypeError(`invalid root walk directory error behavior: ${String(options.onDirectoryError)}`);
72
+ }
73
+ maxDepth = validateBudget("maxDepth", options.maxDepth);
74
+ maxEntries = validateBudget("maxEntries", options.maxEntries);
75
+ }
72
76
  options.signal?.throwIfAborted();
73
77
  let listing;
74
78
  let canonicalDirectory;
@@ -142,7 +146,7 @@ export async function* walkRoot(root, relativePath, options) {
142
146
  while (true) {
143
147
  let next;
144
148
  try {
145
- next = await listing.next();
149
+ next = listing.nextSnapshot ? listing.nextSnapshot() : await listing.next();
146
150
  options.signal?.throwIfAborted();
147
151
  }
148
152
  catch (error) {
@@ -228,5 +232,5 @@ export async function* walkRoot(root, relativePath, options) {
228
232
  }
229
233
  }
230
234
  }
231
- yield* visit(relativePath, 0);
235
+ return visit(relativePath, 0);
232
236
  }
@@ -260,12 +260,14 @@ export async function resolvePinnedWriteTargetInRoot(root, relativePath, request
260
260
  }
261
261
  if (!overwrite) {
262
262
  try {
263
- const existing = fsSync.statSync(resolved);
264
- if (!existing.isFile() && !existing.isDirectory())
265
- throw new FsSafeError("not-file", "not a file");
266
- if (existing.isFile() && existing.nlink > 1)
267
- throw hardlinkedPathNotAllowedError();
268
- throw new FsSafeError("already-exists", "file already exists");
263
+ const existing = fsSync.statSync(resolved, { throwIfNoEntry: false });
264
+ if (existing) {
265
+ if (!existing.isFile() && !existing.isDirectory())
266
+ throw new FsSafeError("not-file", "not a file");
267
+ if (existing.isFile() && existing.nlink > 1)
268
+ throw hardlinkedPathNotAllowedError();
269
+ throw new FsSafeError("already-exists", "file already exists");
270
+ }
269
271
  }
270
272
  catch (error) {
271
273
  if (!isNotFoundPathError(error))
@@ -62,7 +62,11 @@ export async function finishRootFallbackWrite(params) {
62
62
  // Final mode belongs to the accepted object, including on rename-unstable
63
63
  // filesystems; never chmod the old temp and assume its mode was transferred.
64
64
  try {
65
- await handle.chmod(params.mode);
65
+ // Windows chmod only changes the owner-write bit (the read-only attribute).
66
+ const modeChange = process.platform !== "win32" ||
67
+ (fsSync.fstatSync(handle.fd).mode & 0o200) !== (params.mode & 0o200);
68
+ if (modeChange)
69
+ fsSync.fchmodSync(handle.fd, params.mode);
66
70
  }
67
71
  catch (error) {
68
72
  await cleanupPinnedFilePath({
@@ -3,6 +3,7 @@ import { getNativeBinding } from "./native.js";
3
3
  import { getFsSafeNativeConfig } from "./native-config.js";
4
4
  import { warnNativeFallback } from "./native-fallback-warning.js";
5
5
  import { inspectWindowsDescriptorCommand } from "./windows-security-command.js";
6
+ import { assertWindowsHelperAllowed } from "./windows-appcontainer.js";
6
7
  import { unverified as permissionUnverified, validateSecureWindowsSecurityFacts } from "./windows-security-facts.js";
7
8
  const IDENTITY_RE = /^([0-9a-f]{8}):([0-9a-f]{16})$/;
8
9
  const TRUSTED_OWNER_CLASSES = new Set(["current-user", "system", "administrators"]);
@@ -61,7 +62,8 @@ export async function inspectSecureWindowsFile(params) {
61
62
  permissionUnverified("Windows descriptor ACL verification requires an up-to-date native helper");
62
63
  }
63
64
  if (!nativeAvailable) {
64
- warnNativeFallback("windows-secure-file", "Windows descriptor ACL inspection uses a slower built-in system command.");
65
+ assertWindowsHelperAllowed();
66
+ warnNativeFallback("windows-secure-file", "Windows descriptor ACL inspection launches the packaged .NET helper for each check.", "inspectWindowsSecureFileHandle");
65
67
  }
66
68
  let result;
67
69
  try {
@@ -1,3 +1,4 @@
1
+ import { type ExactFileIdentity } from "./exact-identity.js";
1
2
  import { type Stats } from "node:fs";
2
3
  import { type PermissionCheck, type PermissionCheckOptions } from "./permissions.js";
3
4
  export type SecureFileReadOptions = {
@@ -26,6 +27,7 @@ export type SecureFileReadResult = {
26
27
  buffer: Buffer;
27
28
  realPath: string;
28
29
  stat: Stats;
30
+ exactIdentity: ExactFileIdentity;
29
31
  permissions?: PermissionCheck;
30
32
  };
31
33
  export declare function readSecureFile(options: SecureFileReadOptions): Promise<SecureFileReadResult>;
@@ -1,3 +1,4 @@
1
+ import { exactFileIdentity, numberStats } from "./exact-identity.js";
1
2
  import fsSync, {} from "node:fs";
2
3
  import fs from "node:fs/promises";
3
4
  import path from "node:path";
@@ -135,11 +136,16 @@ async function openSecureHandle(options, maxBytes) {
135
136
  throw err;
136
137
  }
137
138
  try {
138
- const openedStat = fsSync.fstatSync(handle.fd);
139
+ const openedIdentity = inspectFileIdentitySync(() => {
140
+ const observed = fsSync.fstatSync(handle.fd, { bigint: true });
141
+ if (!observed.isFile())
142
+ throw new FsSafeError("not-file", `${label(options)} must be a file: ${options.filePath}`);
143
+ return observed;
144
+ });
145
+ const openedStat = numberStats(openedIdentity);
139
146
  if (!openedStat.isFile()) {
140
147
  throw new FsSafeError("not-file", `${label(options)} must be a file: ${options.filePath}`);
141
148
  }
142
- const openedIdentity = inspectFileIdentitySync(() => fsSync.fstatSync(handle.fd, { bigint: true }));
143
149
  assertNotHardlinked(options, openedIdentity);
144
150
  inspectFileIdentitySync(() => {
145
151
  const pathStat = options.trust?.allowSymlink
@@ -255,7 +261,7 @@ async function assertSecurePermissions(options, opened, fd) {
255
261
  return permissions;
256
262
  }
257
263
  async function readHandleWithTimeout(handle, timeoutMs, maxBytes) {
258
- const read = () => maxBytes === undefined ? handle.readFile() : readFileHandleBounded(handle, maxBytes);
264
+ const read = () => readFileHandleBounded(handle, maxBytes ?? Infinity);
259
265
  if (timeoutMs === undefined || !Number.isFinite(timeoutMs) || timeoutMs <= 0) {
260
266
  return await read();
261
267
  }
@@ -290,7 +296,7 @@ export async function readSecureFile(options) {
290
296
  throw new FsSafeError("not-file", `${label(options)} must remain a file: ${options.filePath}`);
291
297
  }
292
298
  assertNotHardlinked(options, finalIdentity);
293
- return { buffer, realPath: opened.realPath, stat: opened.pathStat, permissions };
299
+ return { buffer, realPath: opened.realPath, stat: opened.pathStat, exactIdentity: exactFileIdentity(opened.identity), permissions };
294
300
  }
295
301
  finally {
296
302
  await opened.handle.close().catch(() => undefined);
@@ -75,7 +75,7 @@ async function writeIsolatedProducer(params) {
75
75
  const targetRoot = rootFromDirectoryGuard(parentGuard);
76
76
  // Select the handoff before invoking the producer. Missing required native
77
77
  // support must not leave producer output behind as a discovery side effect.
78
- const native = getNativeBinding();
78
+ const native = getNativeBinding("openBeneath", "renameNoReplace");
79
79
  const cleanupErrors = [];
80
80
  const { target, identity } = await createOwnedTempFile({
81
81
  rootDir: targetRoot.rootReal,
@@ -246,8 +246,8 @@ export async function sidecarReclaimGuardExists(pathname, lockRoot) {
246
246
  try {
247
247
  if (lockRoot)
248
248
  await lockRoot.stat(relativeSidecarLockPath(lockRoot, pathname));
249
- else
250
- fsSync.lstatSync(pathname);
249
+ else if (fsSync.lstatSync(pathname, { throwIfNoEntry: false }) === undefined)
250
+ return false;
251
251
  return true;
252
252
  }
253
253
  catch (error) {
@@ -99,7 +99,7 @@ export function retainCreatedSidecar(lockPath, raw, createdFd, guard, nativePare
99
99
  }
100
100
  } };
101
101
  }
102
- if (!binding.stagedFileMatches || !binding.removeStagedFile)
102
+ if (!binding.openBeneath || !binding.stagedFileMatches || !binding.removeStagedFile)
103
103
  return undefined;
104
104
  const closeFile = captureNativeFdClose(binding);
105
105
  const parentFd = binding.openBeneath(nativeParent.fd, "", nativeOpenFlags(fs.constants.O_RDONLY | fs.constants.O_DIRECTORY)).fd;
@@ -1,3 +1,4 @@
1
+ import fsSync from "node:fs";
1
2
  import fs from "node:fs/promises";
2
3
  import path from "node:path";
3
4
  import { canonicalPathFromExistingAncestor } from "./absolute-path.js";
@@ -23,7 +24,13 @@ export async function resolveSidecarTargetPath(resolved, lockRoot) {
23
24
  assertNoWindowsPathAlias(normalized);
24
25
  return normalized;
25
26
  }
26
- await fs.mkdir(recursiveMkdirPath(dir), { recursive: true });
27
+ let parentExists = false;
28
+ try {
29
+ parentExists = fsSync.statSync(dir).isDirectory();
30
+ }
31
+ catch { /* Preserve mkdir errors below. */ }
32
+ if (!parentExists)
33
+ await fs.mkdir(recursiveMkdirPath(dir), { recursive: true });
27
34
  let parent;
28
35
  try {
29
36
  parent = realpathSync.native(dir);
@@ -18,6 +18,19 @@ export declare function exactIdentityMatches(expected: FileIdentityStat, actual:
18
18
  }>): boolean;
19
19
  export declare function describeStagedDirectory(fd: number, pathname: string): StagedDirectorySnapshot;
20
20
  export declare function assertStagedDirectoryCurrent(receipt: StagedDirectorySnapshot): BigIntStats;
21
+ export declare function assertNativeStagedDirectoryCurrent(binding: NativeBinding, fd: number, directory: StagedDirectorySnapshot): void;
22
+ /** Ordinary publication fences identity, while sibling entries may change. */
23
+ export declare function describeNativeStagedDirectory(binding: NativeBinding, fd: number, canonicalPath: string): {
24
+ directory: Readonly<{
25
+ path: string;
26
+ realPath: string;
27
+ identity: Readonly<{
28
+ dev: bigint;
29
+ ino: bigint;
30
+ }>;
31
+ }>;
32
+ stat: BigIntStats;
33
+ };
21
34
  export declare function describePolicyStagedDirectory(fd: number, pathname: string, binding?: NativeBinding): PolicyStagedDirectory;
22
35
  export declare function assertPolicyStagedDirectoryCurrent(captured: PolicyStagedDirectory): BigIntStats;
23
36
  export declare function refreshPolicyStagedDirectoryObservation(captured: PolicyStagedDirectory): MutationDirectoryObservation;
@@ -34,6 +34,35 @@ export function assertStagedDirectoryCurrent(receipt) {
34
34
  }
35
35
  return current;
36
36
  }
37
+ export function assertNativeStagedDirectoryCurrent(binding, fd, directory) {
38
+ if (typeof binding.stagedDirectoryMatches === "function" && directory.path === directory.realPath) {
39
+ try {
40
+ if (!binding.stagedDirectoryMatches(fd, directory.path, directory.identity.dev, directory.identity.ino)) {
41
+ throw new FsSafeError("path-mismatch", "staging directory pathname changed");
42
+ }
43
+ return;
44
+ }
45
+ catch (error) {
46
+ if (error?.code !== "OBSERVATION_UNAVAILABLE")
47
+ throw error;
48
+ }
49
+ }
50
+ assertStagedDirectoryCurrent(directory);
51
+ }
52
+ /** Ordinary publication fences identity, while sibling entries may change. */
53
+ export function describeNativeStagedDirectory(binding, fd, canonicalPath) {
54
+ const stat = fs.fstatSync(fd, { bigint: true });
55
+ if (!stat.isDirectory())
56
+ throw new FsSafeError("not-file", "staging parent must be a directory");
57
+ const pathname = resolvePathPreservingWindowsRoot(canonicalPath);
58
+ const directory = Object.freeze({
59
+ path: pathname,
60
+ realPath: pathname,
61
+ identity: Object.freeze({ dev: stat.dev, ino: stat.ino }),
62
+ });
63
+ assertNativeStagedDirectoryCurrent(binding, fd, directory);
64
+ return { directory, stat };
65
+ }
37
66
  function sameDirectoryMetadata(left, right) {
38
67
  return left.dev === right.dev && left.ino === right.ino && left.mode === right.mode &&
39
68
  left.nlink === right.nlink;
@@ -3,7 +3,7 @@ import { inspectDirectoryIdentitySync, observeDirectoryIdentitySync } from "./di
3
3
  import { pinNodeDirectoryForMode, pinNodeDirectoryForModeSync } from "./directory-mode-node.js";
4
4
  import { FsSafeError } from "./errors.js";
5
5
  import { fileIdentityMismatchError, inspectFileIdentitySync } from "./strict-file-identity.js";
6
- import { classifyTempWorkspaceOwner, warnUnmappedTempWorkspaceAncestor } from "./temp-workspace-ownership.js";
6
+ import { classifyTempWorkspaceOwner, hasTrustedAndroidTempAncestorGroup, warnUnmappedTempWorkspaceAncestor } from "./temp-workspace-ownership.js";
7
7
  import { privateTempDirectoryFix, tempDirectoryMode } from "./temp-directory-diagnostics.js";
8
8
  export const TEMP_WORKSPACE_NUMERIC_IDENTITY_REPLAY = process.platform === "linux" || process.platform === "darwin";
9
9
  export function projectTempWorkspaceNumericIdentity(identity) {
@@ -54,9 +54,11 @@ export function assertTrustedTempWorkspaceDirectory(stat, uid, privateDirectory,
54
54
  // Unmapped host owners are unverifiable; trust the host directory hierarchy.
55
55
  // Admit sticky ancestors so PrivateUsers keeps host /tmp and PrivateTmp usable.
56
56
  // Leaf roots stay euid-owned/private; non-sticky writable ancestors are rejected.
57
+ const writeMask = !privateDirectory && hasTrustedAndroidTempAncestorGroup(stat, uid, ownership)
58
+ ? 0o002 : 0o022;
57
59
  const writable = typeof stat.mode === "bigint"
58
- ? (stat.mode & 18n) !== 0n
59
- : Number.isSafeInteger(stat.mode) && stat.mode >= 0 && (stat.mode & 0o022) !== 0;
60
+ ? (stat.mode & BigInt(writeMask)) !== 0n
61
+ : Number.isSafeInteger(stat.mode) && stat.mode >= 0 && (stat.mode & writeMask) !== 0;
60
62
  const sticky = typeof stat.mode === "bigint"
61
63
  ? (stat.mode & 512n) !== 0n
62
64
  : Number.isSafeInteger(stat.mode) && stat.mode >= 0 && (stat.mode & 0o1000) !== 0;
@@ -23,6 +23,10 @@ export declare class TempWorkspaceRetainedChild {
23
23
  retained: TempWorkspaceRetainedChild;
24
24
  stat: fsSync.BigIntStats;
25
25
  };
26
+ static adoptCreated(dir: string, fd: number, searchOnly: boolean): {
27
+ retained: TempWorkspaceRetainedChild;
28
+ stat: fsSync.BigIntStats;
29
+ };
26
30
  discardInitialReceipt(): void;
27
31
  finalizeAdmission(ownerUid: number | undefined, mode: number): TempWorkspaceIdentityStat;
28
32
  initializeMode(mode: number, ownerUid: number | undefined, assertParent: () => void): Promise<void>;
@@ -36,6 +40,10 @@ export declare class TempWorkspaceRetainedChild {
36
40
  ino: bigint;
37
41
  }>;
38
42
  directory: RetainedChildDirectory | undefined;
43
+ creationActivity: Readonly<{
44
+ mtimeNs: bigint;
45
+ ctimeNs: bigint;
46
+ }> | undefined;
39
47
  };
40
48
  close(): void;
41
49
  }
@@ -90,6 +90,7 @@ export class TempWorkspaceRetainedChild {
90
90
  #dir;
91
91
  #identity;
92
92
  #numericIdentity;
93
+ #creationActivity;
93
94
  #access;
94
95
  #fd;
95
96
  #modeLease = false;
@@ -102,6 +103,8 @@ export class TempWorkspaceRetainedChild {
102
103
  }
103
104
  this.#dir = dir;
104
105
  this.#identity = Object.freeze({ dev: identity.dev, ino: identity.ino });
106
+ this.#creationActivity = typeof identity.mtimeNs === "bigint" && typeof identity.ctimeNs === "bigint"
107
+ ? Object.freeze({ mtimeNs: identity.mtimeNs, ctimeNs: identity.ctimeNs }) : undefined;
105
108
  this.#numericIdentity = TEMP_WORKSPACE_NUMERIC_IDENTITY_REPLAY
106
109
  ? projectTempWorkspaceNumericIdentity(this.#identity)
107
110
  : undefined;
@@ -127,6 +130,14 @@ export class TempWorkspaceRetainedChild {
127
130
  }
128
131
  static retainCreated(dir) {
129
132
  const descriptor = openRetainedDirectory(dir);
133
+ return TempWorkspaceRetainedChild.#adoptCreated(dir, descriptor);
134
+ }
135
+ static adoptCreated(dir, fd, searchOnly) {
136
+ return TempWorkspaceRetainedChild.#adoptCreated(dir, {
137
+ fd, access: searchOnly ? "search" : "read", proc: searchOnly && process.platform === "linux",
138
+ });
139
+ }
140
+ static #adoptCreated(dir, descriptor) {
130
141
  try {
131
142
  // Only this immediate post-create route installs a receipt. Freezing the
132
143
  // exact fd result keeps its identity and complete mode immutable while
@@ -317,9 +328,9 @@ export class TempWorkspaceRetainedChild {
317
328
  }
318
329
  if (!retainDescriptor) {
319
330
  fsSync.closeSync(fd);
320
- return { dir: this.#dir, identity: this.#identity, directory: undefined };
331
+ return { dir: this.#dir, identity: this.#identity, directory: undefined, creationActivity: this.#creationActivity };
321
332
  }
322
- return { dir: this.#dir, identity: this.#identity, directory: { fd } };
333
+ return { dir: this.#dir, identity: this.#identity, directory: { fd }, creationActivity: this.#creationActivity };
323
334
  }
324
335
  close() {
325
336
  this.#assertModeLeaseReleased();
@@ -191,6 +191,7 @@ export class TempWorkspaceCleanupOwner {
191
191
  #identity;
192
192
  #capability;
193
193
  #directory;
194
+ #creationActivity;
194
195
  #closed = false;
195
196
  #running = false;
196
197
  #exitInterrupted = false;
@@ -202,6 +203,7 @@ export class TempWorkspaceCleanupOwner {
202
203
  this.#identity = child.identity;
203
204
  this.#capability = capability;
204
205
  this.#directory = child.directory;
206
+ this.#creationActivity = child.creationActivity;
205
207
  }
206
208
  #repeat() {
207
209
  return this.#result === "removed" ? "missing" : this.#result;
@@ -307,7 +309,12 @@ export class TempWorkspaceCleanupOwner {
307
309
  !sameFileIdentityForCleanup(quarantined, this.#identity)) {
308
310
  return "indeterminate";
309
311
  }
310
- return { name, path: quarantinePath, nativeRemoval };
312
+ // Timestamps are only a performance hint. Every identity fence still
313
+ // runs, and even an unchanged/coarse timestamp must pass OS empty removal.
314
+ const tryEmpty = this.#creationActivity !== undefined &&
315
+ current.mtimeNs === this.#creationActivity.mtimeNs &&
316
+ current.ctimeNs === this.#creationActivity.ctimeNs;
317
+ return { name, path: quarantinePath, nativeRemoval, tryEmpty };
311
318
  }
312
319
  catch (error) {
313
320
  if (error instanceof FsSafeError && error.code === "helper-unavailable")
@@ -335,11 +342,23 @@ export class TempWorkspaceCleanupOwner {
335
342
  }
336
343
  return result.outcome === "removed" ? "removed" : "indeterminate";
337
344
  }
345
+ #tryRemoveEmptyNative(quarantine) {
346
+ if (!quarantine.tryEmpty)
347
+ return undefined;
348
+ const binding = this.#capability.binding;
349
+ const result = binding.tryRemoveEmptyOwnedTree?.(this.#capability.parent.fd, quarantine.name, this.#directory.fd);
350
+ if (!result || (!result.errorCode && result.outcome === "not-empty"))
351
+ return undefined;
352
+ return this.#mapRemoval(result);
353
+ }
338
354
  async #remove(quarantine) {
339
355
  if (quarantine.nativeRemoval) {
340
356
  const beforeNativeRemoval = getFsSafeTestHooks()?.beforeTempWorkspaceNativeRemoval;
341
357
  if (beforeNativeRemoval)
342
358
  await beforeNativeRemoval(quarantine.path);
359
+ const emptyResult = this.#tryRemoveEmptyNative(quarantine);
360
+ if (emptyResult !== undefined)
361
+ return emptyResult;
343
362
  return this.#mapRemoval(await this.#capability.binding.removeOwnedTree(this.#capability.parent.fd, quarantine.name, this.#directory.fd));
344
363
  }
345
364
  try {
@@ -349,7 +368,31 @@ export class TempWorkspaceCleanupOwner {
349
368
  return "indeterminate";
350
369
  }
351
370
  // Removal failures propagate unchanged; only authority uncertainty becomes a receipt.
352
- await fs.rm(quarantine.path, { recursive: true, force: true });
371
+ const tryEmpty = quarantine.tryEmpty && process.platform !== "win32";
372
+ try {
373
+ if (tryEmpty)
374
+ await fs.rmdir(quarantine.path);
375
+ else
376
+ await fs.rm(quarantine.path, { recursive: true, force: true });
377
+ }
378
+ catch (error) {
379
+ if (!tryEmpty)
380
+ throw error;
381
+ const code = error?.code;
382
+ if (code !== "ENOENT") {
383
+ if (code !== "ENOTEMPTY" && code !== "EEXIST" && code !== "EPERM")
384
+ throw error;
385
+ // rmdir yielded before reporting a nonempty directory. Re-admit the
386
+ // quarantine before allowing pathname-recursive removal to follow it.
387
+ try {
388
+ this.#assertQuarantine(quarantine);
389
+ }
390
+ catch {
391
+ return "indeterminate";
392
+ }
393
+ await fs.rm(quarantine.path, { recursive: true, force: true });
394
+ }
395
+ }
353
396
  try {
354
397
  this.#capability.assertCurrent();
355
398
  return "removed";
@@ -361,6 +404,9 @@ export class TempWorkspaceCleanupOwner {
361
404
  #removeSync(quarantine) {
362
405
  if (quarantine.nativeRemoval) {
363
406
  getFsSafeTestHooks()?.beforeTempWorkspaceNativeRemovalSync?.(quarantine.path);
407
+ const emptyResult = this.#tryRemoveEmptyNative(quarantine);
408
+ if (emptyResult !== undefined)
409
+ return emptyResult;
364
410
  return this.#mapRemoval(this.#capability.binding.removeOwnedTreeSync(this.#capability.parent.fd, quarantine.name, this.#directory.fd));
365
411
  }
366
412
  try {
@@ -369,7 +415,29 @@ export class TempWorkspaceCleanupOwner {
369
415
  catch {
370
416
  return "indeterminate";
371
417
  }
372
- fsSync.rmSync(quarantine.path, { recursive: true, force: true });
418
+ const tryEmpty = quarantine.tryEmpty && process.platform !== "win32";
419
+ try {
420
+ if (tryEmpty)
421
+ fsSync.rmdirSync(quarantine.path);
422
+ else
423
+ fsSync.rmSync(quarantine.path, { recursive: true, force: true });
424
+ }
425
+ catch (error) {
426
+ if (!tryEmpty)
427
+ throw error;
428
+ const code = error?.code;
429
+ if (code !== "ENOENT") {
430
+ if (code !== "ENOTEMPTY" && code !== "EEXIST" && code !== "EPERM")
431
+ throw error;
432
+ try {
433
+ this.#assertQuarantine(quarantine);
434
+ }
435
+ catch {
436
+ return "indeterminate";
437
+ }
438
+ fsSync.rmSync(quarantine.path, { recursive: true, force: true });
439
+ }
440
+ }
373
441
  try {
374
442
  this.#capability.assertCurrent();
375
443
  return "removed";
@@ -2,7 +2,8 @@ type DirectoryOwner = {
2
2
  uid: number | bigint;
3
3
  gid: number | bigint;
4
4
  };
5
- export type TempWorkspaceOwnership = "user" | "root" | "unmapped" | "foreign";
5
+ export type TempWorkspaceOwnership = "user" | "root" | "android-system" | "unmapped" | "foreign";
6
6
  export declare function classifyTempWorkspaceOwner(stat: DirectoryOwner, uid: number): TempWorkspaceOwnership;
7
+ export declare function hasTrustedAndroidTempAncestorGroup(stat: DirectoryOwner, uid: number, ownership: TempWorkspaceOwnership): boolean;
7
8
  export declare function warnUnmappedTempWorkspaceAncestor(): void;
8
9
  export {};
@@ -28,6 +28,8 @@ export function classifyTempWorkspaceOwner(stat, uid) {
28
28
  return "user";
29
29
  if (stat.uid === 0 || stat.uid === 0n)
30
30
  return "root";
31
+ if (process.platform === "android" && (stat.uid === 1000 || stat.uid === 1000n))
32
+ return "android-system";
31
33
  if (process.platform !== "linux")
32
34
  return "foreign";
33
35
  try {
@@ -44,6 +46,26 @@ export function classifyTempWorkspaceOwner(stat, uid) {
44
46
  return "foreign";
45
47
  }
46
48
  }
49
+ // Android reserves the system group and each app's matching UID/GID. This
50
+ // exception is for ancestor group-write only; private leaves still reject it.
51
+ export function hasTrustedAndroidTempAncestorGroup(stat, uid, ownership) {
52
+ if (process.platform !== "android")
53
+ return false;
54
+ if (ownership === "user" && uid % 100000 >= 10000 && uid % 100000 <= 19999) {
55
+ return stat.gid === uid || stat.gid === BigInt(uid);
56
+ }
57
+ if (ownership !== "android-system" || (stat.gid !== 1000 && stat.gid !== 1000n))
58
+ return false;
59
+ try {
60
+ const gid = process.getegid?.();
61
+ const groups = process.getgroups?.();
62
+ return gid !== undefined && Number.isSafeInteger(gid) && gid >= 0 && gid !== 1000 && Array.isArray(groups) &&
63
+ groups.every((group) => Number.isSafeInteger(group) && group >= 0 && group !== 1000);
64
+ }
65
+ catch {
66
+ return false;
67
+ }
68
+ }
47
69
  export function warnUnmappedTempWorkspaceAncestor() {
48
70
  if (warned)
49
71
  return;
@@ -1,3 +1,4 @@
1
+ import type { ExactFileIdentity } from "./exact-identity.js";
1
2
  import type { FileStore, FileStoreSync } from "./file-store.js";
2
3
  import type { TempPathIdentityReceipt } from "./temp-cleanup.js";
3
4
  import type { TempWorkspaceCleanupResult, TempWorkspaceCleanupSafety } from "./temp-workspace-owner.js";
@@ -11,6 +12,7 @@ export type TempWorkspaceOptions = {
11
12
  export type TempWorkspace = {
12
13
  dir: string;
13
14
  identity: TempPathIdentityReceipt;
15
+ exactIdentity?: ExactFileIdentity;
14
16
  store: FileStore;
15
17
  path(fileName: string): string;
16
18
  write(fileName: string, data: string | Uint8Array): Promise<string>;
@@ -26,6 +28,7 @@ export type TempWorkspace = {
26
28
  export type TempWorkspaceSync = {
27
29
  dir: string;
28
30
  identity: TempPathIdentityReceipt;
31
+ exactIdentity?: ExactFileIdentity;
29
32
  store: FileStoreSync;
30
33
  path(fileName: string): string;
31
34
  write(fileName: string, data: string | Uint8Array): string;
@@ -1,5 +1,6 @@
1
1
  import type { FileHandle } from "node:fs/promises";
2
2
  import type { FileIdentityStat } from "./file-identity.js";
3
+ export { holdWindowsSharingLock, setWindowsFileAttributes, readWindowsFileExtents } from "./windows-test-hooks.js";
3
4
  export type FsSafeTestHooks = {
4
5
  beforeWatchRegistration?: (path: string) => void | Promise<void>;
5
6
  afterWatchRegistration?: (path: string) => void | Promise<void>;
@@ -1,3 +1,4 @@
1
+ export { holdWindowsSharingLock, setWindowsFileAttributes, readWindowsFileExtents } from "./windows-test-hooks.js";
1
2
  let fsSafeTestHooks;
2
3
  function allowFsSafeTestHooks() {
3
4
  return process.env.NODE_ENV === "test" || process.env.VITEST === "true";
@@ -97,7 +97,7 @@ function assertAllowedTrashTarget(targetPath, allowedRoots) {
97
97
  if (isForeignWindowsShareOrDevicePath(lexicalTarget, [...allowedRoots, ...resolvedRoots])) {
98
98
  throw new Error(`Refusing to trash path outside allowed roots: ${targetPath}`);
99
99
  }
100
- const stat = fs.lstatSync(lexicalTarget);
100
+ const stat = fs.lstatSync(lexicalTarget, { bigint: true });
101
101
  const resolvedTarget = resolveTrashTargetPath(targetPath);
102
102
  const resolvedTargetPath = resolvedTarget.path;
103
103
  const parent = createSyncDirectoryGuard(path.dirname(lexicalTarget));
@@ -121,7 +121,7 @@ function assertAllowedTrashTarget(targetPath, allowedRoots) {
121
121
  }
122
122
  function assertTrashTargetGuard(guard) {
123
123
  assertSyncDirectoryGuard(guard.parent);
124
- const stat = fs.lstatSync(guard.path);
124
+ const stat = fs.lstatSync(guard.path, { bigint: true });
125
125
  if (!sameFileIdentity(stat, guard.stat)) {
126
126
  throw new Error(`Refusing to trash path after it changed: ${guard.path}`);
127
127
  }