@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
@@ -87,6 +87,12 @@ be exactly representable. Ambiguous identity fails closed.
87
87
  Returned receipts are frozen descriptive snapshots, not mutable authority.
88
88
  Changing a supplied receipt after admission cannot retarget the lifecycle.
89
89
 
90
+ Use `fs.lstat(path, { bigint: true })` for caller-created directory receipts:
91
+ bigint stats retain exact Windows file IDs. Copied or reconstructed receipts
92
+ with unsafe Number identities reject with `FsSafeError("path-mismatch")`.
93
+ Original library receipts retain their private exact identity even when their
94
+ public numeric metadata cannot represent every bit.
95
+
90
96
  `StagedFileReceipt` contains `directory` (`path`, `realPath`, and exact bigint
91
97
  `identity.dev`/`identity.ino`), `temporaryBasename`, and the prepared file's
92
98
  `identity` (`dev`, `ino`, `size`, `nlink`, `mtimeNs`, `ctimeNs` as bigint;
@@ -197,3 +203,15 @@ they do not gain this cleanup authority.
197
203
  - [Advanced composition](advanced.md)
198
204
  - [Native architecture](native.md)
199
205
  - [Security model](security-model.md)
206
+
207
+ ## Round-tripping identities
208
+
209
+ `StagedFileReceipt.identity` and `directory.identity` already contain exact
210
+ bigint `dev`/`ino` values. Retain those values directly; do not convert them to
211
+ Number. `PublishedFileReceipt.staged.identity` is the same staged file's identity,
212
+ not a new pathname observation. File metadata such as size and timestamps is a
213
+ preparation-time snapshot and can change during writing. Publication callbacks
214
+ using `PublishedWriteIdentity` likewise already return exact bigint identities.
215
+
216
+ For the standalone `publishFileExclusive()` API, retain its `exactIdentity`
217
+ companion instead of its legacy `identity: Stats`; see [directory durability](durability.md#exclusive-file-publication).
@@ -84,6 +84,21 @@ Batch loading skips invalid entry names, malformed, oversized, or unreadable ent
84
84
 
85
85
  Failed destinations are create-only. Quarantine publishes the claimed file by hardlink, so the queue and failed directories must share a filesystem with hardlink support. If `failed/<id>.json` already exists, quarantine rejects while preserving both that earlier evidence and the current claimed entry instead of overwriting either file. The `read` callback continues to receive the logical `.json` path even though bytes are read and migrations are written through the claimed path.
86
86
 
87
+ Claim, quarantine, and retirement recovery require hardlinks. If a link syscall
88
+ is refused with `EACCES`, `EPERM`, `EXDEV`, `ENOTSUP`, `EOPNOTSUPP`, or `ENOSYS`,
89
+ the operation rejects with `FsSafeError("helper-unavailable")` and retains the
90
+ original error in `cause`. This includes Android/Termux environments that deny
91
+ hardlinks. The refusal leaves the pending entry, processing claim, or retirement
92
+ record intact for retry after the capability or permission problem is resolved;
93
+ it does not acknowledge delivery. Batch loading propagates this capability
94
+ failure too. The queue does not fall back to copying or overwriting rename:
95
+ those operations cannot preserve its shared-inode recovery and create-only
96
+ collision evidence. Enqueue alone does not prove that an entry can be claimed.
97
+
98
+ The [link-free queue design](queue-link-free-design.md) describes an unimplemented
99
+ v2 candidate and its mixed-version barriers. It does not change these helpers or
100
+ provide Android/Termux queue support.
101
+
87
102
  Migrations stay bound to the exact processing file opened for that load. The
88
103
  read descriptor remains pinned while the callback runs outside the transfer
89
104
  lock; after the callback returns, migration reacquires the lock and rechecks the
@@ -47,6 +47,26 @@ under trusted ancestry. Do not make shared `/tmp` private or take ownership of
47
47
  another user's directory. A caller's preferred root takes precedence over
48
48
  `TMPDIR`; changing `TMPDIR` only changes the resolver's fallback location.
49
49
 
50
+ On Android only (`process.platform === "android"`), ancestors may also be owned
51
+ by Android's OS system identity, AID_SYSTEM (UID 1000). The system is trusted like
52
+ root; this does not trust arbitrary foreign UIDs or stop the walk at `HOME` or
53
+ `PREFIX`. Mode/sticky checks still apply, with one Android group-write exception:
54
+ an ancestor's group may write when both owner and group are AID_SYSTEM and the
55
+ caller belongs to neither that effective nor supplementary group, or when both
56
+ are the calling app's private Android UID/GID (app ID 10000–19999, including
57
+ multi-user UID offsets). Android reserves those groups for the system or the
58
+ same app identity. Unknown group membership fails closed for system-owned
59
+ group-writable ancestors. World-write still needs sticky protection.
60
+
61
+ This admits Termux's system-owned `/data` and `/data/data` at `0771` and its
62
+ app-owned `files` ancestor at `0771`, without allowing another app or an
63
+ untrusted writable group to replace the next child. Canonical ancestors are
64
+ rechecked through `/`; symlink/type and identity checks remain in force. The
65
+ supplied temp root and created workspace must still be owned by the effective
66
+ user and reject **all** group/world write, including these Android groups and
67
+ sticky directories. No Android native-addon or Linux user-namespace assumption
68
+ is made by this policy.
69
+
50
70
  On Linux, non-identity `/proc/self/uid_map` and `/proc/self/gid_map` evidence
51
71
  permits ancestors whose UID and GID equal unmapped kernel overflow IDs,
52
72
  provided the same ancestor mode checks pass.
@@ -209,6 +229,17 @@ identity before opening it, rejects mount crossings, and traverses descendants
209
229
  only through opened directory handles; symlinks/reparse entries are removed as
210
230
  leaves and never traversed. Windows marks the exact opened objects for deletion by handle.
211
231
 
232
+ Empty quarantines can be removed without directory enumeration: the native
233
+ helper can attempt a descriptor-relative empty-directory removal, and the
234
+ compatible POSIX JavaScript path can attempt `rmdir`. Unchanged creation timestamps
235
+ select this attempt as a performance hint, never as emptiness or ownership proof;
236
+ all fresh identity checks still run. Changed timestamps and nonempty directories
237
+ continue through the same guarded recursive removal. The native empty attempt is bounded
238
+ and synchronous, including for async workspaces; recursive native cleanup stays
239
+ asynchronous. Older bindings and Windows compatible cleanup retain their
240
+ existing recursive route; Windows can defer directory timestamp changes, so
241
+ the empty-attempt hint does not select a faster compatible path there.
242
+
212
243
  POSIX has no unlink-by-fd or expected-inode unlink for directory entries. After
213
244
  the final identity check, each `unlinkat` can still be raced; the possible side
214
245
  effect is bounded to one substituted non-directory leaf or one empty directory
@@ -754,3 +785,11 @@ it("processes a fixture", async () => {
754
785
  - [Atomic writes](atomic.md) — `replaceDirectoryAtomic` for whole-directory swaps.
755
786
  - [`root()`](root.md) — `fs.copyIn(rel, sourceAbs)` for moving files from a temp into a `Root`.
756
787
  - [File lock](sidecar-lock.md) — when many processes share a temp tree.
788
+
789
+ ## Exact workspace receipts
790
+
791
+ `tempWorkspace()` and `tempWorkspaceSync()` return `exactIdentity` alongside
792
+ their unchanged legacy `identity`. Preserve the bigint companion for identity
793
+ comparisons. `tempFile().identity` already uses bigint `dev` and `ino` and needs
794
+ no conversion. Cleanup retains its own exact authority; editing or discarding a
795
+ public receipt does not rebind cleanup to another file or directory.
@@ -135,6 +135,19 @@ Lifecycle memory is sampled across ten intervals, with
135
135
  the first two excluded as warm-up; idle's Linux wakeup bound scales with the
136
136
  requested duration (three per minute). Adversarial cases remain unchanged.
137
137
 
138
+ ## Native syscall budgets
139
+
140
+ After building the host addon and package, run the Linux syscall proof with
141
+ `node scripts/native-call-budget-proof.mjs --operation remove` (or `mkdir`,
142
+ `ensureRoot`, `move`, `append`, `openWritable`, `create`, `write`, `tempWorkspace`).
143
+ It traces all threads with `strace -f -yy` and enforces exact per-mode syscall
144
+ and async-resource counts. Canonicalization ancestors and worker-thread I/O
145
+ remain counted. The only runtime filesystem exclusion is Node/V8 Abseil's
146
+ read-only open of `/sys/devices/system/cpu/cpu0/tsc_freq_khz`, a lazy CPU-clock
147
+ probe unrelated to the temporary fixture that can occur on the main thread.
148
+ Other sysfs calls and subsequent I/O remain counted. Failed proofs print the
149
+ retained raw trace path for diagnosis.
150
+
138
151
  ## Linux openat2 fallback
139
152
 
140
153
  Build the host addon and package first. The test hook is cached with the native
@@ -38,7 +38,7 @@ type PathStat = {
38
38
  };
39
39
  ```
40
40
 
41
- The shape returned by `Root.stat()`. It is a serializable view of the identity,
41
+ The shape returned by `Root.stat()`. Its enumerable fields are a serializable legacy view of the identity,
42
42
  ownership, mode, size, timestamp, link count, and three file-kind facts the
43
43
  boundary uses. Unlike Node's `Stats`, `isFile`, `isDirectory`, and
44
44
  `isSymbolicLink` are boolean fields rather than methods.
@@ -79,103 +79,68 @@ Used by helpers that accept either an encoding (returning a string) or `null` (r
79
79
 
80
80
  ## `OpenResult` / `ReadResult`
81
81
 
82
- Returned by `Root.open()` and `Root.read()`:
82
+ Import the result types rather than copying their shapes:
83
83
 
84
84
  ```ts
85
- type OpenResult = {
86
- handle: import("node:fs/promises").FileHandle;
87
- containment: "kernel-atomic" | "best-effort";
88
- realPath: string;
89
- stat: import("node:fs").Stats;
90
- };
91
-
92
- type ReadResult = {
93
- buffer: Buffer;
94
- containment: "kernel-atomic" | "best-effort";
95
- realPath: string;
96
- stat: import("node:fs").Stats;
97
- };
85
+ import type { OpenResult, ReadResult, WritableOpenResult } from "@openclaw/fs-safe/root";
98
86
  ```
99
87
 
100
88
  `realPath` is the canonical real path the read or open landed on, after symlink resolution; `stat` is the verified `fstat` result. Public root results currently report `containment: "best-effort"`; the union also describes direct native `openBeneath()` results, which report `"kernel-atomic"` on Linux. See the [security model](security-model.md#containment-guarantees-by-platform).
101
89
 
90
+ `ReadResult` carries the read `buffer`. `OpenResult` and `WritableOpenResult`
91
+ carry an owned Node `FileHandle` as `handle` and implement
92
+ `[Symbol.asyncDispose]()`; use `await using` or explicitly close the handle.
93
+ See [reading](reading.md#fs-open-rel-options) and
94
+ [writable handles](writing.md#openwritable-for-streaming) for ownership and streaming.
95
+
102
96
  ## `RootDefaults` / `RootOptions`
103
97
 
104
98
  ```ts
105
- type RenameIdentityPolicy = "strict" | "verify-content-with-lock";
106
-
107
- type RootDefaults = {
108
- assertBeforeMutation?: () => void;
109
- denyMutations?: DenyMutationPolicy;
110
- durable?: boolean; // default true for write/create/writeJson/createJson/append
111
- hardlinks?: "reject" | "allow";
112
- maxBytes?: number;
113
- mkdir?: boolean; // default true for mutation methods
114
- mode?: number;
115
- renameIdentity?: RenameIdentityPolicy;
116
- symlinks?: "reject" | "follow-within-root" | "follow-parents-within-root";
117
- mutationSymlinks?: MutationSymlinkPolicy;
118
- };
119
-
120
- type DenyMutationPolicy = {
121
- paths?: readonly string[];
122
- prefixes?: readonly string[];
123
- };
124
-
125
- type RootOptions = {
126
- rootDir: string;
127
- defaults?: RootDefaults;
128
- };
99
+ import type {
100
+ DenyMutationPolicy,
101
+ RenameIdentityPolicy,
102
+ RootDefaults,
103
+ RootOptions,
104
+ } from "@openclaw/fs-safe/root";
129
105
  ```
130
106
 
131
- `RootDefaults` is what `root(rootDir, defaults)` accepts. See [`root()`](root.md) for the per-method options that override these. `denyMutations` and `assertBeforeMutation` are exceptions: deny entries are merged, and the root authority assertion runs before the per-call assertion.
107
+ `RootDefaults` is what `root(rootDir, defaults)` accepts. `RootOptions` is the
108
+ `{ rootDir, defaults? }` record. See the [Root signature and defaults](root.md#signature)
109
+ for fields and behavior. `denyMutations` and `assertBeforeMutation` compose with
110
+ per-call restrictions: deny entries are merged, and the root authority assertion
111
+ runs before the per-call assertion.
132
112
 
133
113
  ## `RootReadOptions` / `RootWriteOptions` / `RootCopyOptions`
134
114
 
135
115
  ```ts
136
- import type { CopyCloneMode, RootCopyPublicationReceipt } from "@openclaw/fs-safe";
137
-
138
- type RootReadOptions = Pick<RootDefaults, "hardlinks" | "maxBytes" | "symlinks">;
139
- type RootWriteOptions = Pick<RootDefaults, "assertBeforeMutation" | "denyMutations" | "durable" | "mkdir" | "mode" | "renameIdentity" | "mutationSymlinks"> & {
140
- encoding?: BufferEncoding;
141
- overwrite?: boolean;
142
- };
143
- type RootCopyOptions = Pick<RootDefaults, "assertBeforeMutation" | "denyMutations" | "durable" | "maxBytes" | "mkdir" | "mode" | "mutationSymlinks"> & {
144
- sourceHardlinks?: "reject" | "allow";
145
- overwrite?: boolean;
146
- clone?: CopyCloneMode;
147
- signal?: AbortSignal;
148
- preserveSourceMode?: boolean;
149
- onDestinationPublished?: (receipt: RootCopyPublicationReceipt) => void;
150
- };
151
- type RootOpenWritableOptions = Pick<RootDefaults, "assertBeforeMutation" | "denyMutations" | "mkdir" | "mode" | "mutationSymlinks"> & {
152
- writeMode?: "replace" | "append" | "update";
153
- };
154
- type RootWriteJsonOptions = RootWriteOptions & {
155
- replacer?: Parameters<typeof JSON.stringify>[1];
156
- space?: Parameters<typeof JSON.stringify>[2];
157
- trailingNewline?: boolean;
158
- };
159
- type RootAppendOptions = RootWriteOptions & {
160
- prependNewlineIfNeeded?: boolean;
161
- };
162
- type RootMoveOptions = Pick<RootDefaults, "assertBeforeMutation" | "denyMutations" | "mutationSymlinks"> & {
163
- overwrite?: boolean;
164
- };
165
- type RootRemoveOptions = Pick<RootDefaults, "assertBeforeMutation" | "denyMutations" | "mutationSymlinks"> & {
166
- recursive?: boolean;
167
- force?: boolean;
168
- order?: "filesystem" | "sorted";
169
- maxEntries?: number;
170
- maxDepth?: number;
171
- signal?: AbortSignal;
172
- };
173
- type RootMkdirOptions = Pick<RootDefaults, "assertBeforeMutation" | "denyMutations" | "mutationSymlinks"> & {
174
- private?: boolean;
175
- };
116
+ import type {
117
+ RootAppendOptions,
118
+ RootCopyOptions,
119
+ RootCreateJsonOptions,
120
+ RootCreateOptions,
121
+ RootCreateStreamOptions,
122
+ RootMkdirOptions,
123
+ RootMoveOptions,
124
+ RootOpenOptions,
125
+ RootOpenWritableOptions,
126
+ RootReadOptions,
127
+ RootRemoveOptions,
128
+ RootWriteJsonOptions,
129
+ RootWriteOptions,
130
+ } from "@openclaw/fs-safe/root";
176
131
  ```
177
132
 
178
- Per-method option shapes. Each picks the `RootDefaults` keys that apply, plus method-specific extras.
133
+ Each method accepts only its applicable defaults and method-specific options:
134
+
135
+ | Types | Contract |
136
+ |---|---|
137
+ | `RootReadOptions` | Read symlink/hardlink policy and `maxBytes`; see [read options](reading.md#read-options). |
138
+ | `RootOpenOptions` | Read link policies, without `maxBytes`. The returned handle's I/O belongs to the caller. |
139
+ | `RootWriteOptions`, `RootWriteJsonOptions`, `RootAppendOptions` | [Write options](writing.md#write-options), JSON formatting, and append newline handling. |
140
+ | `RootCreateOptions`, `RootCreateJsonOptions`, `RootCreateStreamOptions` | [Exclusive creation](writing.md#atomic-buffered-creation), private permissions, and `durable: "file"`; streamed creation also accepts byte limits and cancellation. |
141
+ | `RootCopyOptions` | [Guarded copying](writing.md#write-verbs), source policy, `CopyCloneMode`, and `RootCopyPublicationReceipt`. |
142
+ | `RootOpenWritableOptions` | [Writable handles](writing.md#openwritable-for-streaming) with `writeMode`, without buffered-write durability or byte limits. |
143
+ | `RootMoveOptions`, `RootRemoveOptions`, `RootMkdirOptions` | [Mutation methods](root.md#writes) with their collision, removal-budget, and directory-privacy options. |
179
144
 
180
145
  ## `SymlinkPolicy` / `MutationSymlinkPolicy` / `HardlinkPolicy`
181
146
 
@@ -202,3 +167,8 @@ policy; omission preserves each mutation method's existing behavior.
202
167
  - [`root()`](root.md) — how `RootDefaults` and `Root*Options` are used.
203
168
  - [Errors](errors.md) — the closed code union in context.
204
169
  - [Reading](reading.md), [Writing](writing.md) — option shapes per verb.
170
+
171
+ `PathStat.exactIdentity` is an immutable bigint `dev`/`ino` companion, present on
172
+ returned values and non-enumerable to preserve the numeric JSON shape. Typed
173
+ `list()` and `entries()` results have the same field. Copy it explicitly when
174
+ spreading metadata that will later be used for identity checks.
@@ -171,7 +171,10 @@ Without `maxEntries`, sorted walks reuse a full directory metadata snapshot
171
171
  from the `Root.list()` owner. This preserves the existing fast complete-scan
172
172
  behavior and its snapshot semantics: changes made after a directory is listed
173
173
  do not alter its already-captured entries. Supply an entry budget or use
174
- filesystem order when metadata work must remain incremental. Sorted entries
174
+ filesystem order when metadata work must remain incremental. Complete snapshots
175
+ are consumed directly between public iterator yields, without per-entry listing
176
+ promises. Directory admission, cancellation, and awaited-filter identity fences
177
+ remain in place. Sorted entries
175
178
  describe the observations captured in their directory snapshot or batch.
176
179
 
177
180
  Use `order: "filesystem"` when a wide directory must not be fully enumerated:
@@ -127,8 +127,9 @@ pending detail and queued batches are bounded. The last removal stops and joins
127
127
  the native thread. No Worker threads, eval programs, or JS `fs.watch` are used.
128
128
 
129
129
  macOS entry scopes use nonrecursive `EVFILT_VNODE` watches on the admitted parent
130
- and, when present, the entry itself. Parent activity requests a guarded scan
131
- without supplying filenames. The entry descriptor covers content and attribute
130
+ and, when present, the entry itself. Child activity requests a guarded scan only
131
+ while the selected entry is missing. Once it exists, sibling activity does not
132
+ wake JavaScript; the entry descriptor covers replacement, content and attribute
132
133
  changes and is replaced when a guarded scan admits a new identity. Missing paths
133
134
  use their nearest admitted ancestor. Descriptors are opened without following
134
135
  symlinks; a symlink entry uses `O_SYMLINK` to observe the link itself. All
@@ -223,12 +223,12 @@ await fs.createJson("config/settings.json", { enabled: true }, { atomic: true })
223
223
  await fs.create("config/flushed.json", initial, { atomic: true, durable: "file" });
224
224
  ```
225
225
 
226
- `atomic: true` keeps the destination absent until all bytes have been written.
226
+ `atomic: true` finishes all bytes in a sibling stage before publication.
227
227
  The native backend uses its no-replace rename; the JavaScript fallback hardlinks
228
228
  the completed stage and unlinks its temporary name in the same JavaScript turn.
229
- The fallback requires hardlink support and fails without publishing partial bytes
230
- when that mechanism is unavailable. Other processes can briefly observe both
231
- names. Existing and raced entries are preserved, including dangling symlinks;
229
+ When the OS refuses hardlinks, it uses the [placeholder-rename fallback](#publication-without-hardlinks).
230
+ Other processes can briefly observe both linked names, or an empty placeholder
231
+ before the atomic replacement. Entries present at the exclusive claim are preserved, including dangling symlinks;
232
232
  ordinary confinement, type, hardlink, and symlink-policy rejections still apply.
233
233
  `assertBeforeMutation` retains its live checks through content writes and publication.
234
234
 
@@ -254,7 +254,7 @@ complete publication, and an indeterminate link or native rename must preserve n
254
254
  recovery. A cleanup or close failure also retains the original operation failure.
255
255
  The JavaScript fallback checks the destination again immediately before attempting
256
256
  publication. A collision observed there leaves publication unattempted and cleans
257
- the owned stage. A later collision or other error from the link call remains
257
+ the owned stage. A later collision or unclassified error from the link call remains
258
258
  indeterminate, including `EEXIST`; the error code alone does not prove that the
259
259
  filesystem left both names unchanged.
260
260
  No later verification, mode, or synchronization failure authorizes deleting an
@@ -293,9 +293,10 @@ unlimited. Zero permits an empty input only. Invalid limits reject before I/O.
293
293
  Unlike buffered creation's JavaScript fallback, streamed creation stages all
294
294
  chunks before publishing the final name. Native mode uses no-replace rename;
295
295
  the JavaScript fallback hardlinks the completed stage and removes its temporary
296
- name in the same JavaScript turn. That fallback requires a filesystem supporting
297
- hardlinks; other processes may briefly observe both names. An existing or
298
- concurrently created destination is preserved. Existing-target preflight does
296
+ name in the same JavaScript turn. If hardlinks are refused, it uses the
297
+ [placeholder-rename fallback](#publication-without-hardlinks); other processes may
298
+ briefly observe an empty placeholder. A destination present at the exclusive
299
+ claim is preserved. Existing-target preflight does
299
300
  not consume the input. The final mode and durability policy use the same guarded
300
301
  writer as other Root operations.
301
302
 
@@ -408,8 +409,103 @@ With `overwrite: false`, an existing destination produces `already-exists` and
408
409
  is never altered. Copying prepares a private sibling file before publishing its
409
410
  completed contents. Native mode uses no-replace rename. The guarded JavaScript
410
411
  fallback links the completed stage and removes its temporary name in the same
411
- JavaScript turn; the filesystem must support hardlinks. Other processes can
412
- briefly observe both names. The source is never hardlinked to the destination.
412
+ JavaScript turn. If the OS refuses hardlinks, it uses the placeholder-rename
413
+ fallback below. Other processes can briefly observe both linked names or an
414
+ empty placeholder. The source is never hardlinked to the destination.
415
+
416
+ ### Copy metadata and source links
417
+
418
+ `preserveMetadata: true` restores metadata to the completed staging entry before
419
+ publication. It defaults to `false`; it does not select permissions, ownership,
420
+ ACLs, or extended attributes. Use the existing `preserveSourceMode` option as
421
+ well when source permissions should be selected. An explicit `mode` or Root
422
+ mode default continues to take precedence over `preserveSourceMode`.
423
+
424
+ | State | `preserveMetadata: true` | Permission selection |
425
+ | --- | --- | --- |
426
+ | POSIX access and modification times | Preserved | Not affected by `mode` |
427
+ | POSIX permission and executable bits | Not selected by this option | Existing `mode` / `preserveSourceMode` rules |
428
+ | Windows creation, access and write times | Preserved by native copying | Not affected by `mode` |
429
+ | Windows hidden, system, archive, temporary, offline, not-content-indexed | Preserved by native copying | Not affected by `mode` |
430
+ | Windows readonly | Not selected by this option | Existing `mode` / `preserveSourceMode` rules |
431
+ | Ownership, ACLs, POSIX extended attributes | Not copied | Existing destination policy |
432
+ | Windows sparse/integrity flags | Not set through metadata copying | Owned by the clone/FSCTL mechanism |
433
+
434
+ This applies to clones, native byte copies, and JavaScript byte copies. On
435
+ POSIX, native timestamp restoration retains nanoseconds; JavaScript restoration
436
+ uses Node's timestamp precision, as the portable tree copier does. On
437
+ Windows without the native metadata helper, JavaScript can restore only access
438
+ and write times; creation time and the non-permission attributes are not
439
+ preserved. Native `require` mode rejects a missing helper before publication.
440
+ The Windows attribute mask and POSIX timestamp conversion share the
441
+ [tree-copy implementation](copy.md#api); native tree copying has additional
442
+ metadata guarantees that this narrower option does not request.
443
+
444
+ `sourceSymlinks` defaults to `"reject"`, preserving the existing rejection of
445
+ symlinked absolute string sources with code `symlink` and message
446
+ `symlink not allowed`. There is no follow mode. Set
447
+ `sourceSymlinks: "copy-link"` to copy a source link itself, including a dangling
448
+ file or directory link. Its literal target remains data: copying never resolves
449
+ it. Windows uses the link's own directory attribute and copies the reparse
450
+ payload through the same native mechanism as `copyTree`.
451
+
452
+ ```ts
453
+ await destination.copyIn("template-link", "/srv/templates/template-link", {
454
+ sourceSymlinks: "copy-link",
455
+ preserveMetadata: true,
456
+ });
457
+ ```
458
+
459
+ Link destinations are always exclusive. Omit `overwrite` or set it to `false`;
460
+ explicit `overwrite: true` with a link source fails with `invalid-path` before
461
+ mutation. Regular files retain the usual overwrite behavior. Destination Root
462
+ confinement, denied paths, parent-link policy, and mutation-authority checks
463
+ apply to copied links too.
464
+ macOS link permissions use the selected mode on the link itself; Linux link
465
+ permissions remain fixed by the kernel. The link target's permissions are never
466
+ changed.
467
+
468
+ `"copy-link"` requires an absolute-path string source. A `{ root, relativePath }`
469
+ source fails with `invalid-path` before either source method is called or a
470
+ destination is mutated: its `open` and `stat` capabilities cannot expose literal
471
+ link text or Windows link type without following the link. With the default
472
+ `"reject"`, capability sources continue to use their own Root read policies.
473
+
474
+ macOS and Windows link copying requires native support and otherwise reports
475
+ `helper-unavailable` before destination mutation. macOS `link(2)` follows its
476
+ source, so it cannot publish dangling symlink inodes exclusively. Linux can
477
+ publish the staged symlink inode with guarded hardlink/unlink when native
478
+ support is off or automatic no-replace rename is unavailable. This has the
479
+ same documented pathname identity windows as the JavaScript write fallback;
480
+ no operation follows the link target. Successful native publication uses
481
+ no-replace rename. No link publication uses the regular-file placeholder
482
+ fallback.
483
+
484
+ ### Publication without hardlinks
485
+
486
+ For staged JavaScript no-replace copies and writes, `EACCES`, `EPERM`, `EXDEV`,
487
+ `ENOTSUP`, `EOPNOTSUPP`, and `ENOSYS` from the link operation permit a
488
+ **placeholder-rename** fallback, including Android/Termux hardlink restrictions.
489
+ It exclusively creates a mode-`0600`, no-follow destination, closes it after
490
+ capturing its exact identity, rechecks the parent and stage, and requires the
491
+ same empty, singly linked regular placeholder before renaming the completed
492
+ stage over it. A real creation-permission failure still rejects. `EEXIST` from
493
+ the link operation does not permit fallback or establish non-publication.
494
+
495
+ An empty placeholder is observable before the atomic replacement, and a crash
496
+ can leave that placeholder and the completed stage. Detected placeholder swaps
497
+ reject without removing foreign entries; failed pre-rename operations remove
498
+ only a verified owned empty placeholder. Failed cleanup reports `helper-failed`
499
+ with `details.publication: "not-published"` and `details.cleanup: "failed"`.
500
+ After rename, verification requires the stage name to be absent and the target
501
+ to retain the staged identity; the complete published file is preserved on failure.
502
+
503
+ These pathname checks are best-effort, not compare-and-swap: a same-directory
504
+ writer can replace the placeholder after the final check, and rename can replace
505
+ that intervening file or symlink (without following it). Protect the parent from
506
+ untrusted mutation when strict concurrent no-clobber is required. This fallback
507
+ preserves existing receipt shapes; `RootCopyPublicationReceipt` reports identity
508
+ and path, with no method or fallback field.
413
509
 
414
510
  `clone` chooses the file-data transfer strategy through `CopyCloneMode`, shared
415
511
  with [`copyTree`](copy.md#api). File copies default to `"never"`; tree copies
@@ -421,11 +517,19 @@ default to `"auto"`:
421
517
  | `auto` | Try native file cloning, then copy offload or ordinary byte copying when cloning is unavailable. |
422
518
  | `always` | Require native cloning; fail when the binding or filesystem cannot provide it. |
423
519
 
424
- Native file cloning supports APFS and supported Linux filesystems. On Linux, a
520
+ Native file cloning supports APFS, supported Linux filesystems, and Windows ReFS
521
+ (including Dev Drive). On Linux, a
425
522
  `FICLONE` request denied with `EPERM`, as container seccomp profiles commonly
426
523
  do, counts as unavailable cloning: `auto` copies bytes and `always` fails with
427
- `unsupported-platform`. Windows currently uses byte copying for `never` and
428
- `auto`; `always` fails. Clone choice
524
+ `unsupported-platform`. On Windows, `always` requires source and destination on
525
+ the same ReFS volume; other volumes report `unsupported-platform`. `auto` falls
526
+ back to bytes only for unavailable cloning capabilities, and `never` copies bytes.
527
+ ReFS named streams are rejected rather than silently discarded by cloning.
528
+ For successful native ReFS clones through either `copyIn` or `copyTree`, all full
529
+ clusters are block-cloned; a final partial cluster may be copied. Exact tail bytes and logical file size are
530
+ preserved. See the [shared ReFS contract](copy.md) and Microsoft's
531
+ [cluster-alignment and EOF requirements](https://learn.microsoft.com/en-us/windows/win32/fileio/block-cloning#restrictions-and-remarks).
532
+ Clone choice
429
533
  does not change modes, durability, root confinement, or source and publication
430
534
  identity checks. The shared strategy does not replace Root's guarded regular-file
431
535
  contract with `copyTree`'s caller-owned immutable-tree and metadata contract.
@@ -560,6 +664,8 @@ await fs.remove("scratch/finished-job", {
560
664
  Budgets must be non-negative safe integers or explicit `Infinity`, and require
561
665
  `recursive: true`. Omitted budgets retain their finite defaults. An entry or
562
666
  depth limit throws `too-large` before processing the over-budget entry.
667
+ Once the parent is admitted, `maxEntries: 0` rejects before inspecting even a
668
+ missing requested entry; `force: true` does not bypass that budget.
563
669
 
564
670
  The default filesystem order streams each directory once, using memory and open
565
671
  handles proportional to depth rather than directory width. Sorted order also
@@ -643,7 +749,8 @@ for the precise parent-pinning guarantee and remaining same-call limitations.
643
749
 
644
750
  ### `fs.mkdir(rel)`
645
751
 
646
- `mkdir -p`. Creates missing parents.
752
+ `mkdir -p`. Creates missing parents. An existing regular file at the requested
753
+ directory name rejects with `not-file` in every native mode and remains unchanged.
647
754
 
648
755
  `require` mode creates each missing component relative to a retained directory,
649
756
  opens the child without following a final symlink, and checks its identity before
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/fs-safe",
3
- "version": "0.25.0",
3
+ "version": "0.26.5",
4
4
  "description": "Capability-style filesystem roots for Node.js apps that handle untrusted relative paths.",
5
5
  "keywords": [
6
6
  "filesystem",
@@ -25,8 +25,7 @@
25
25
  },
26
26
  "files": [
27
27
  "dist/archive-parser.wasm",
28
- "dist/windows-security-bridge.cs",
29
- "dist/windows-security-bridge.ps1",
28
+ "dist/windows-security-helper.exe",
30
29
  "dist/**/*.js",
31
30
  "dist/**/*.d.ts",
32
31
  "docs/**/*.md",
@@ -83,6 +82,10 @@
83
82
  "types": "./dist/secret.d.ts",
84
83
  "default": "./dist/secret.js"
85
84
  },
85
+ "./pipe": {
86
+ "types": "./dist/pipe.d.ts",
87
+ "default": "./dist/pipe.js"
88
+ },
86
89
  "./permissions": {
87
90
  "types": "./dist/permissions-public.d.ts",
88
91
  "default": "./dist/permissions-public.js"
@@ -140,7 +143,9 @@
140
143
  "scripts": {
141
144
  "benchmark": "node scripts/benchmark.mjs",
142
145
  "benchmark:methods": "node benchmarks/runner.mjs",
146
+ "benchmark:node": "node benchmarks/node-baseline.mjs",
143
147
  "benchmark:publish": "pnpm build && node scripts/bench-publish.mjs",
148
+ "build:windows-helper": "node scripts/build-windows-helper.mjs",
144
149
  "build": "node scripts/prepack-build.mjs",
145
150
  "lint:file-size": "node scripts/check-file-size.mjs",
146
151
  "lint:fs-boundary": "node scripts/check-fs-boundary-primitives.mjs",
@@ -156,6 +161,7 @@
156
161
  "docs:check": "node scripts/check-doc-examples.mjs",
157
162
  "docs:site": "node scripts/build-docs-site.mjs",
158
163
  "native:build": "pnpm --filter @openclaw/fs-safe-native-build build",
164
+ "native:build:freebsd": "node scripts/build-freebsd.mjs",
159
165
  "native:test": "cargo test --workspace",
160
166
  "pack:check": "pnpm build && node scripts/check-pack.mjs",
161
167
  "public-api:update": "pnpm build && node scripts/check-pack.mjs --update-public-api",
@@ -173,19 +179,22 @@
173
179
  "archive:producer-smoke": "node scripts/archive-producer-smoke.mjs"
174
180
  },
175
181
  "optionalDependencies": {
176
- "@openclaw/fs-safe-darwin-arm64": "0.25.0",
177
- "@openclaw/fs-safe-darwin-x64": "0.25.0",
178
- "@openclaw/fs-safe-linux-arm64-gnu": "0.25.0",
179
- "@openclaw/fs-safe-linux-arm64-musl": "0.25.0",
180
- "@openclaw/fs-safe-linux-x64-gnu": "0.25.0",
181
- "@openclaw/fs-safe-linux-x64-musl": "0.25.0",
182
- "@openclaw/fs-safe-win32-x64-msvc": "0.25.0",
182
+ "@openclaw/fs-safe-darwin-arm64": "0.26.5",
183
+ "@openclaw/fs-safe-darwin-x64": "0.26.5",
184
+ "@openclaw/fs-safe-freebsd-arm64": "0.26.5",
185
+ "@openclaw/fs-safe-freebsd-x64": "0.26.5",
186
+ "@openclaw/fs-safe-linux-arm64-gnu": "0.26.5",
187
+ "@openclaw/fs-safe-linux-arm64-musl": "0.26.5",
188
+ "@openclaw/fs-safe-linux-x64-gnu": "0.26.5",
189
+ "@openclaw/fs-safe-linux-x64-musl": "0.26.5",
190
+ "@openclaw/fs-safe-win32-arm64-msvc": "0.26.5",
191
+ "@openclaw/fs-safe-win32-x64-msvc": "0.26.5",
183
192
  "jszip": "^3.10.2"
184
193
  },
185
194
  "devDependencies": {
186
195
  "@emnapi/runtime": "2.0.0-alpha.6",
187
196
  "@napi-rs/cli": "3.10.8",
188
- "@types/node": "^26.6.4",
197
+ "@types/node": "^26.6.5",
189
198
  "@vitest/coverage-v8": "5.0.3",
190
199
  "fast-check": "^4.10.2",
191
200
  "istanbul-lib-coverage": "3.2.2",
@@ -194,7 +203,7 @@
194
203
  "sigstore": "5.0.0",
195
204
  "tar": "7.5.22",
196
205
  "typescript": "^7.0.2",
197
- "vite": "8.3.3",
206
+ "vite": "8.3.4",
198
207
  "vitest": "^5.0.3"
199
208
  },
200
209
  "engines": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/fs-safe-darwin-arm64",
3
- "version": "0.25.0",
3
+ "version": "0.26.5",
4
4
  "description": "macOS arm64 native binding for @openclaw/fs-safe.",
5
5
  "license": "MIT",
6
6
  "author": "OpenClaw Team <dev@openclaw.ai>",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/fs-safe-darwin-x64",
3
- "version": "0.25.0",
3
+ "version": "0.26.5",
4
4
  "description": "macOS x64 native binding for @openclaw/fs-safe.",
5
5
  "license": "MIT",
6
6
  "author": "OpenClaw Team <dev@openclaw.ai>",