@openclaw/fs-safe 0.14.0 → 0.16.0

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 (370) hide show
  1. package/CHANGELOG.md +88 -0
  2. package/README.md +38 -10
  3. package/dist/advanced.d.ts +2 -1
  4. package/dist/advanced.d.ts.map +1 -1
  5. package/dist/advanced.js +2 -1
  6. package/dist/archive-kind.d.ts +0 -1
  7. package/dist/archive-kind.d.ts.map +1 -1
  8. package/dist/archive-kind.js +5 -17
  9. package/dist/archive-merge.d.ts.map +1 -1
  10. package/dist/archive-merge.js +113 -46
  11. package/dist/archive-parser.wasm +0 -0
  12. package/dist/archive-read.d.ts.map +1 -1
  13. package/dist/archive-read.js +10 -11
  14. package/dist/archive-tar-stream.d.ts +3 -0
  15. package/dist/archive-tar-stream.d.ts.map +1 -1
  16. package/dist/archive-tar-stream.js +56 -37
  17. package/dist/archive-tar-wasm.d.ts +16 -4
  18. package/dist/archive-tar-wasm.d.ts.map +1 -1
  19. package/dist/archive-tar-wasm.js +134 -34
  20. package/dist/archive-zip-directory.d.ts +4 -0
  21. package/dist/archive-zip-directory.d.ts.map +1 -1
  22. package/dist/archive-zip-directory.js +2 -0
  23. package/dist/archive-zip-entry.d.ts +6 -2
  24. package/dist/archive-zip-entry.d.ts.map +1 -1
  25. package/dist/archive-zip-entry.js +23 -8
  26. package/dist/archive-zip-integrity.d.ts.map +1 -1
  27. package/dist/archive-zip-integrity.js +3 -4
  28. package/dist/archive-zip-loader.d.ts.map +1 -1
  29. package/dist/archive-zip-loader.js +107 -31
  30. package/dist/archive-zip-names.d.ts +1 -0
  31. package/dist/archive-zip-names.d.ts.map +1 -1
  32. package/dist/archive-zip-names.js +6 -0
  33. package/dist/archive.d.ts.map +1 -1
  34. package/dist/archive.js +11 -11
  35. package/dist/bounded-read-stream.d.ts +0 -1
  36. package/dist/bounded-read-stream.d.ts.map +1 -1
  37. package/dist/bounded-read-stream.js +0 -6
  38. package/dist/clone-metadata.d.ts +1 -0
  39. package/dist/clone-metadata.d.ts.map +1 -1
  40. package/dist/clone-metadata.js +6 -2
  41. package/dist/copy-publication.d.ts +6 -0
  42. package/dist/copy-publication.d.ts.map +1 -1
  43. package/dist/copy-publication.js +3 -0
  44. package/dist/copy-tree-portable.d.ts.map +1 -1
  45. package/dist/copy-tree-portable.js +44 -24
  46. package/dist/copy.d.ts.map +1 -1
  47. package/dist/copy.js +29 -11
  48. package/dist/create-directory.d.ts +20 -0
  49. package/dist/create-directory.d.ts.map +1 -0
  50. package/dist/create-directory.js +130 -0
  51. package/dist/create-file-async.d.ts +7 -0
  52. package/dist/create-file-async.d.ts.map +1 -0
  53. package/dist/create-file-async.js +121 -0
  54. package/dist/create-file.d.ts +8 -0
  55. package/dist/create-file.d.ts.map +1 -0
  56. package/dist/create-file.js +190 -0
  57. package/dist/create-owned-file.d.ts +8 -0
  58. package/dist/create-owned-file.d.ts.map +1 -0
  59. package/dist/create-owned-file.js +16 -0
  60. package/dist/create.d.ts +4 -0
  61. package/dist/create.d.ts.map +1 -0
  62. package/dist/create.js +2 -0
  63. package/dist/creation-darwin.d.ts +7 -0
  64. package/dist/creation-darwin.d.ts.map +1 -0
  65. package/dist/creation-darwin.js +79 -0
  66. package/dist/creation-file-state.d.ts +19 -0
  67. package/dist/creation-file-state.d.ts.map +1 -0
  68. package/dist/creation-file-state.js +118 -0
  69. package/dist/creation-path.d.ts +21 -0
  70. package/dist/creation-path.d.ts.map +1 -0
  71. package/dist/creation-path.js +71 -0
  72. package/dist/creation-permissions.d.ts +19 -0
  73. package/dist/creation-permissions.d.ts.map +1 -0
  74. package/dist/creation-permissions.js +125 -0
  75. package/dist/directory-durability.d.ts +1 -1
  76. package/dist/directory-durability.d.ts.map +1 -1
  77. package/dist/directory-durability.js +22 -80
  78. package/dist/directory-guard.d.ts +3 -0
  79. package/dist/directory-guard.d.ts.map +1 -1
  80. package/dist/directory-mode-node.d.ts +2 -0
  81. package/dist/directory-mode-node.d.ts.map +1 -1
  82. package/dist/directory-mode-node.js +8 -0
  83. package/dist/directory-mode-owner.js +5 -5
  84. package/dist/directory-receipt.d.ts +24 -0
  85. package/dist/directory-receipt.d.ts.map +1 -0
  86. package/dist/directory-receipt.js +127 -0
  87. package/dist/file-cleanup.d.ts +19 -0
  88. package/dist/file-cleanup.d.ts.map +1 -0
  89. package/dist/file-cleanup.js +78 -0
  90. package/dist/file-handle-transfer.d.ts +2 -0
  91. package/dist/file-handle-transfer.d.ts.map +1 -1
  92. package/dist/file-handle-transfer.js +57 -2
  93. package/dist/file-identity.d.ts.map +1 -1
  94. package/dist/file-identity.js +18 -4
  95. package/dist/file-lock-sync-admission.d.ts +19 -0
  96. package/dist/file-lock-sync-admission.d.ts.map +1 -0
  97. package/dist/file-lock-sync-admission.js +93 -0
  98. package/dist/file-lock-sync-root-acquire.d.ts +4 -0
  99. package/dist/file-lock-sync-root-acquire.d.ts.map +1 -0
  100. package/dist/file-lock-sync-root-acquire.js +370 -0
  101. package/dist/file-lock-sync-root-arbitration.d.ts +18 -0
  102. package/dist/file-lock-sync-root-arbitration.d.ts.map +1 -0
  103. package/dist/file-lock-sync-root-arbitration.js +66 -0
  104. package/dist/file-lock-sync-root-held.d.ts +34 -0
  105. package/dist/file-lock-sync-root-held.d.ts.map +1 -0
  106. package/dist/file-lock-sync-root-held.js +393 -0
  107. package/dist/file-lock-sync-root-io.d.ts +44 -0
  108. package/dist/file-lock-sync-root-io.d.ts.map +1 -0
  109. package/dist/file-lock-sync-root-io.js +209 -0
  110. package/dist/file-lock-sync-root-mutation.d.ts +17 -0
  111. package/dist/file-lock-sync-root-mutation.d.ts.map +1 -0
  112. package/dist/file-lock-sync-root-mutation.js +277 -0
  113. package/dist/file-lock-sync-root-options.d.ts +20 -0
  114. package/dist/file-lock-sync-root-options.d.ts.map +1 -0
  115. package/dist/file-lock-sync-root-options.js +58 -0
  116. package/dist/file-lock-sync-root-registration.d.ts +2 -0
  117. package/dist/file-lock-sync-root-registration.d.ts.map +1 -0
  118. package/dist/file-lock-sync-root-registration.js +90 -0
  119. package/dist/file-lock-sync-root.d.ts +36 -0
  120. package/dist/file-lock-sync-root.d.ts.map +1 -0
  121. package/dist/file-lock-sync-root.js +361 -0
  122. package/dist/file-lock-sync-stale-admission.d.ts +24 -0
  123. package/dist/file-lock-sync-stale-admission.d.ts.map +1 -0
  124. package/dist/file-lock-sync-stale-admission.js +205 -0
  125. package/dist/file-lock-sync.d.ts.map +1 -1
  126. package/dist/file-lock-sync.js +245 -205
  127. package/dist/file-observation.d.ts +1 -1
  128. package/dist/file-observation.d.ts.map +1 -1
  129. package/dist/file-store-boundary.d.ts +2 -6
  130. package/dist/file-store-boundary.d.ts.map +1 -1
  131. package/dist/file-store-boundary.js +3 -9
  132. package/dist/file-store-prune.d.ts.map +1 -1
  133. package/dist/file-store-prune.js +5 -1
  134. package/dist/file-store-sync-write.d.ts.map +1 -1
  135. package/dist/file-store-sync-write.js +5 -8
  136. package/dist/file-store.d.ts.map +1 -1
  137. package/dist/file-store.js +47 -12
  138. package/dist/guarded-mkdir.d.ts +1 -0
  139. package/dist/guarded-mkdir.d.ts.map +1 -1
  140. package/dist/guarded-mkdir.js +36 -7
  141. package/dist/json-document-store.d.ts.map +1 -1
  142. package/dist/json-document-store.js +22 -15
  143. package/dist/json-durable-queue-ownership.d.ts +0 -1
  144. package/dist/json-durable-queue-ownership.d.ts.map +1 -1
  145. package/dist/json-durable-queue-ownership.js +0 -6
  146. package/dist/move-path.js +1 -1
  147. package/dist/native-binding.d.ts +13 -1
  148. package/dist/native-binding.d.ts.map +1 -1
  149. package/dist/native-fallback-warning.d.ts +4 -0
  150. package/dist/native-fallback-warning.d.ts.map +1 -0
  151. package/dist/native-fallback-warning.js +11 -0
  152. package/dist/native-operations.d.ts +0 -2
  153. package/dist/native-operations.d.ts.map +1 -1
  154. package/dist/native-operations.js +0 -24
  155. package/dist/native-parent-admission.d.ts +5 -2
  156. package/dist/native-parent-admission.d.ts.map +1 -1
  157. package/dist/native-parent-admission.js +27 -7
  158. package/dist/native-pinned-write-windows.d.ts +1 -1
  159. package/dist/native-pinned-write-windows.d.ts.map +1 -1
  160. package/dist/native-pinned-write-windows.js +174 -29
  161. package/dist/native-pinned-write.d.ts.map +1 -1
  162. package/dist/native-pinned-write.js +26 -14
  163. package/dist/native-policy-parent-windows.d.ts +14 -0
  164. package/dist/native-policy-parent-windows.d.ts.map +1 -0
  165. package/dist/native-policy-parent-windows.js +209 -0
  166. package/dist/native-rename-outcome.d.ts +4 -0
  167. package/dist/native-rename-outcome.d.ts.map +1 -0
  168. package/dist/native-rename-outcome.js +8 -0
  169. package/dist/native-staged-file.d.ts +3 -2
  170. package/dist/native-staged-file.d.ts.map +1 -1
  171. package/dist/native-staged-file.js +121 -72
  172. package/dist/output.d.ts.map +1 -1
  173. package/dist/output.js +12 -8
  174. package/dist/owner-dacl.d.ts.map +1 -1
  175. package/dist/owner-dacl.js +10 -4
  176. package/dist/path-prefix.d.ts.map +1 -1
  177. package/dist/path-prefix.js +30 -8
  178. package/dist/path-suffix-aliases.d.ts +2 -0
  179. package/dist/path-suffix-aliases.d.ts.map +1 -1
  180. package/dist/path-suffix-aliases.js +25 -17
  181. package/dist/permission-exec.d.ts +2 -0
  182. package/dist/permission-exec.d.ts.map +1 -1
  183. package/dist/permission-exec.js +150 -21
  184. package/dist/permissions-windows.js +1 -1
  185. package/dist/pinned-mutation-admission.d.ts.map +1 -1
  186. package/dist/pinned-mutation-admission.js +10 -5
  187. package/dist/pinned-mutation-observation.d.ts +0 -1
  188. package/dist/pinned-mutation-observation.d.ts.map +1 -1
  189. package/dist/pinned-mutation-observation.js +0 -19
  190. package/dist/pinned-mutation-shared-route.d.ts +1 -0
  191. package/dist/pinned-mutation-shared-route.d.ts.map +1 -1
  192. package/dist/pinned-mutation-shared-route.js +1 -1
  193. package/dist/pinned-write-input.d.ts +4 -0
  194. package/dist/pinned-write-input.d.ts.map +1 -0
  195. package/dist/pinned-write-input.js +25 -0
  196. package/dist/pinned-write-mode.d.ts +5 -0
  197. package/dist/pinned-write-mode.d.ts.map +1 -0
  198. package/dist/pinned-write-mode.js +24 -0
  199. package/dist/pinned-write-staged.d.ts +6 -0
  200. package/dist/pinned-write-staged.d.ts.map +1 -0
  201. package/dist/pinned-write-staged.js +187 -0
  202. package/dist/pinned-write-types.d.ts +5 -0
  203. package/dist/pinned-write-types.d.ts.map +1 -1
  204. package/dist/pinned-write.d.ts.map +1 -1
  205. package/dist/pinned-write.js +35 -145
  206. package/dist/private-directory.d.ts.map +1 -1
  207. package/dist/private-directory.js +18 -4
  208. package/dist/private-producer-handoff-sync.d.ts +14 -0
  209. package/dist/private-producer-handoff-sync.d.ts.map +1 -0
  210. package/dist/private-producer-handoff-sync.js +114 -0
  211. package/dist/private-producer-handoff.d.ts +22 -4
  212. package/dist/private-producer-handoff.d.ts.map +1 -1
  213. package/dist/private-producer-handoff.js +140 -77
  214. package/dist/private-temp-workspace.d.ts.map +1 -1
  215. package/dist/private-temp-workspace.js +75 -121
  216. package/dist/publish-copy-stage.d.ts +2 -1
  217. package/dist/publish-copy-stage.d.ts.map +1 -1
  218. package/dist/publish-copy-stage.js +16 -7
  219. package/dist/publish-file.d.ts.map +1 -1
  220. package/dist/publish-file.js +2 -2
  221. package/dist/regular-file.d.ts.map +1 -1
  222. package/dist/regular-file.js +1 -15
  223. package/dist/replace-directory.d.ts.map +1 -1
  224. package/dist/replace-directory.js +256 -18
  225. package/dist/replace-file-copy-fallback.d.ts.map +1 -1
  226. package/dist/replace-file-copy-fallback.js +62 -70
  227. package/dist/replace-file-copy-source.d.ts.map +1 -1
  228. package/dist/replace-file-copy-source.js +10 -12
  229. package/dist/replace-file-temp-owner.d.ts +5 -9
  230. package/dist/replace-file-temp-owner.d.ts.map +1 -1
  231. package/dist/replace-file-temp-owner.js +56 -72
  232. package/dist/replace-file.js +6 -6
  233. package/dist/retained-directory-replacement.d.ts +26 -0
  234. package/dist/retained-directory-replacement.d.ts.map +1 -0
  235. package/dist/retained-directory-replacement.js +193 -0
  236. package/dist/root-boundary.d.ts +1 -0
  237. package/dist/root-boundary.d.ts.map +1 -1
  238. package/dist/root-boundary.js +4 -0
  239. package/dist/root-context.d.ts +0 -8
  240. package/dist/root-context.d.ts.map +1 -1
  241. package/dist/root-context.js +0 -3
  242. package/dist/root-create-input.d.ts +8 -1
  243. package/dist/root-create-input.d.ts.map +1 -1
  244. package/dist/root-create-input.js +17 -4
  245. package/dist/root-directory-creation.d.ts +3 -3
  246. package/dist/root-directory-creation.d.ts.map +1 -1
  247. package/dist/root-directory-creation.js +15 -3
  248. package/dist/root-directory-list.d.ts +1 -0
  249. package/dist/root-directory-list.d.ts.map +1 -1
  250. package/dist/root-directory-list.js +1 -0
  251. package/dist/root-impl.d.ts.map +1 -1
  252. package/dist/root-impl.js +46 -17
  253. package/dist/root-move-noreplace.d.ts.map +1 -1
  254. package/dist/root-move-noreplace.js +24 -15
  255. package/dist/root-options.d.ts +12 -4
  256. package/dist/root-options.d.ts.map +1 -1
  257. package/dist/root-path-errors.d.ts +1 -0
  258. package/dist/root-path-errors.d.ts.map +1 -1
  259. package/dist/root-path-errors.js +11 -2
  260. package/dist/root-path-existing.d.ts.map +1 -1
  261. package/dist/root-path-existing.js +11 -35
  262. package/dist/root-path-stat.d.ts.map +1 -1
  263. package/dist/root-path-stat.js +59 -7
  264. package/dist/root-path.js +1 -13
  265. package/dist/root-remove.d.ts +1 -0
  266. package/dist/root-remove.d.ts.map +1 -1
  267. package/dist/root-remove.js +4 -0
  268. package/dist/root-walk.d.ts +1 -1
  269. package/dist/root-walk.d.ts.map +1 -1
  270. package/dist/root-walk.js +17 -2
  271. package/dist/root-write-admission.d.ts +0 -2
  272. package/dist/root-write-admission.d.ts.map +1 -1
  273. package/dist/root-write-admission.js +1 -15
  274. package/dist/root-write-complete-parent.d.ts.map +1 -1
  275. package/dist/root-write-complete-parent.js +7 -23
  276. package/dist/root-write-publication.js +1 -1
  277. package/dist/root-write-verification.d.ts.map +1 -1
  278. package/dist/root-write-verification.js +29 -42
  279. package/dist/secret-file.d.ts.map +1 -1
  280. package/dist/secret-file.js +3 -24
  281. package/dist/secret-read-async.d.ts.map +1 -1
  282. package/dist/secret-read-async.js +3 -24
  283. package/dist/secret-read-policy.d.ts +6 -2
  284. package/dist/secret-read-policy.d.ts.map +1 -1
  285. package/dist/secret-read-policy.js +26 -2
  286. package/dist/secure-file-windows.d.ts +6 -0
  287. package/dist/secure-file-windows.d.ts.map +1 -1
  288. package/dist/secure-file-windows.js +34 -117
  289. package/dist/secure-file.js +2 -2
  290. package/dist/sibling-temp.d.ts.map +1 -1
  291. package/dist/sibling-temp.js +23 -12
  292. package/dist/sidecar-lock-acquire.d.ts +2 -28
  293. package/dist/sidecar-lock-acquire.d.ts.map +1 -1
  294. package/dist/sidecar-lock-acquire.js +288 -199
  295. package/dist/sidecar-lock-admission-context.d.ts +19 -0
  296. package/dist/sidecar-lock-admission-context.d.ts.map +1 -0
  297. package/dist/sidecar-lock-admission-context.js +60 -0
  298. package/dist/sidecar-lock-admission-parser.d.ts +43 -0
  299. package/dist/sidecar-lock-admission-parser.d.ts.map +1 -0
  300. package/dist/sidecar-lock-admission-parser.js +113 -0
  301. package/dist/sidecar-lock-admission.d.ts +35 -0
  302. package/dist/sidecar-lock-admission.d.ts.map +1 -0
  303. package/dist/sidecar-lock-admission.js +7 -0
  304. package/dist/sidecar-lock-reclaim.d.ts +9 -4
  305. package/dist/sidecar-lock-reclaim.d.ts.map +1 -1
  306. package/dist/sidecar-lock-reclaim.js +80 -25
  307. package/dist/sidecar-lock-root.d.ts.map +1 -1
  308. package/dist/sidecar-lock-root.js +2 -1
  309. package/dist/sidecar-lock-stale-admission.d.ts +39 -0
  310. package/dist/sidecar-lock-stale-admission.d.ts.map +1 -0
  311. package/dist/sidecar-lock-stale-admission.js +232 -0
  312. package/dist/sidecar-lock-target.d.ts +8 -0
  313. package/dist/sidecar-lock-target.d.ts.map +1 -0
  314. package/dist/sidecar-lock-target.js +55 -0
  315. package/dist/sidecar-lock.d.ts.map +1 -1
  316. package/dist/sidecar-lock.js +100 -16
  317. package/dist/staged-directory.d.ts.map +1 -1
  318. package/dist/staged-directory.js +6 -6
  319. package/dist/staged-file-settlement.d.ts +17 -0
  320. package/dist/staged-file-settlement.d.ts.map +1 -0
  321. package/dist/staged-file-settlement.js +57 -0
  322. package/dist/temp-workspace-descriptor.d.ts.map +1 -1
  323. package/dist/temp-workspace-descriptor.js +9 -27
  324. package/dist/temp-workspace-owner.d.ts.map +1 -1
  325. package/dist/temp-workspace-owner.js +8 -8
  326. package/dist/walk.d.ts +5 -1
  327. package/dist/walk.d.ts.map +1 -1
  328. package/dist/walk.js +19 -6
  329. package/dist/windows-owner.d.ts.map +1 -1
  330. package/dist/windows-owner.js +4 -3
  331. package/dist/windows-security-bridge.cs +336 -0
  332. package/dist/windows-security-bridge.ps1 +15 -0
  333. package/dist/windows-security-command.d.ts +26 -0
  334. package/dist/windows-security-command.d.ts.map +1 -0
  335. package/dist/windows-security-command.js +363 -0
  336. package/dist/windows-security-facts.d.ts +6 -0
  337. package/dist/windows-security-facts.d.ts.map +1 -0
  338. package/dist/windows-security-facts.js +108 -0
  339. package/docs/advanced.md +4 -2
  340. package/docs/archive.md +97 -46
  341. package/docs/atomic.md +85 -8
  342. package/docs/config.md +6 -2
  343. package/docs/contributing.md +44 -4
  344. package/docs/copy.md +37 -0
  345. package/docs/creation.md +128 -0
  346. package/docs/durability.md +24 -0
  347. package/docs/file-store.md +19 -0
  348. package/docs/install.md +31 -7
  349. package/docs/json-store.md +5 -0
  350. package/docs/migrating-to-0.5.md +15 -6
  351. package/docs/migrating-to-0.6.md +9 -4
  352. package/docs/native-helper.md +32 -12
  353. package/docs/native.md +38 -7
  354. package/docs/output.md +6 -0
  355. package/docs/path-prefix.md +10 -0
  356. package/docs/path-suffix-aliases.md +51 -6
  357. package/docs/permissions.md +50 -14
  358. package/docs/public-api.md +3 -2
  359. package/docs/root.md +56 -3
  360. package/docs/secret-file.md +11 -2
  361. package/docs/secure-file.md +9 -4
  362. package/docs/sidecar-lock.md +114 -8
  363. package/docs/staged-file.md +12 -3
  364. package/docs/temp.md +20 -3
  365. package/docs/walk.md +67 -1
  366. package/docs/writing.md +76 -6
  367. package/package.json +19 -16
  368. package/dist/darwin-acl.d.ts +0 -4
  369. package/dist/darwin-acl.d.ts.map +0 -1
  370. package/dist/darwin-acl.js +0 -24
package/docs/archive.md CHANGED
@@ -3,11 +3,19 @@
3
3
  `@openclaw/fs-safe/archive` extracts ZIP and TAR archives behind one API, with traversal checks, blocked-link-type rejection, and entry-count and byte budgets. When the native binding is available for the current platform, Rust streams ZIP, TAR, gzip, zstd, and bzip2 while TypeScript remains the sole policy owner; every accepted output is created fd-relative in a private staging root. Extraction then merges through the same safe-open boundary used by direct writes — a symlinked entry can't trick the merge into following an out-of-tree path.
4
4
 
5
5
  TAR admission uses one Rust core compiled into both the native binding and a
6
- bundled, import-free WebAssembly module. The guarded JavaScript fallback uses
7
- that module for TAR/gzip and optional `jszip` for ZIP. TAR needs no optional
8
- parser dependency, runtime download, install script, or consumer Rust toolchain.
9
- Installs omitting optional dependencies can import every public subpath and use
10
- TAR/gzip in `auto` or `off`; ZIP fallback still requires `jszip`.
6
+ bundled, import-free WebAssembly module. In `off`, or `auto` when the native
7
+ binding is unavailable, extraction and bounded entry reads use that module for
8
+ plain TAR, gzip, zstd, and bzip2. Zstd and bzip2 use bundled WASM builds of the
9
+ same codec implementations used by native; gzip uses Node's built-in decoder.
10
+ These TAR routes work with all optional dependencies omitted and need no
11
+ runtime interpreter, download, install script, or consumer compiler toolchain.
12
+ ZIP fallback still requires optional `jszip`.
13
+
14
+ `auto` prefers an available native binding; a native operation failure is
15
+ terminal and never retries through WASM. `require` rejects a missing binding
16
+ with `FsSafeError("helper-unavailable")`, including for zstd/bzip2 suffix
17
+ resolution. The separate `inspectTarArchive()` API still accepts only plain TAR
18
+ and gzip.
11
19
 
12
20
  ```ts
13
21
  import { extractArchive, resolveArchiveKind } from "@openclaw/fs-safe/archive";
@@ -124,7 +132,12 @@ Readable directories do not depend on procfs. A Linux search-only directory
124
132
  needing a mode change requires accessible, genuine procfs; unavailable or
125
133
  untrusted authority rejects explicitly instead of silently accepting a wrong
126
134
  mode. Other unsupported search-only routes also fail closed. Windows retains
127
- its existing bounded lack of POSIX mode enforcement.
135
+ its existing bounded lack of POSIX mode enforcement. Best-effort mode handling
136
+ applies only to the mode change itself: an authority or deadline check that
137
+ fails immediately before dispatch still propagates, including a custom
138
+ one-shot structural check. A check failure observed immediately after dispatch
139
+ is retained while post-dispatch authority verification and final mode inspection
140
+ settle, then propagated with its exact JavaScript value, including falsy values.
128
141
 
129
142
  Extraction and TAR inspection first copy the admitted source into a private
130
143
  staging file. This copy reuses at most 512 KiB of scratch space, reduced for
@@ -148,14 +161,24 @@ must agree with this kind, physical index, size, known path, and UNIX-creator mo
148
161
  before extraction or any member read. Bounded ZIP reads retain this metadata from
149
162
  their single admission pass without another input copy or scan.
150
163
 
151
- Portable ZIP preflight, extraction, and reads also check the decoded kind against
152
- admission. Unsupported JSZip metadata rejects with `ArchiveFormatError` before
153
- filters, including UNIX-only directory attributes without a terminal slash or DOS
154
- directory bit, backslash-only directory names without directory attributes, and
155
- non-UNIX creators whose high-word symlink mode JSZip does not expose. Symlinks that
156
- the decoder represents faithfully remain subject to the existing filter and
157
- blocked-link policy. UNIX creator metadata and permission defaults remain as
158
- described above.
164
+ Portable ZIP preflight, extraction, and reads bind every decoder insertion to
165
+ its admitted physical record before JSZip can coerce its type or discard its
166
+ payload. Names, physical order, original directory/permission metadata,
167
+ compression method, compressed and decoded sizes, and CRC must agree. The
168
+ private loader then applies the admitted kind, preserving UNIX-only directory
169
+ attributes, backslash-only directories, and high-word symlinks from any creator.
170
+ Symlinks remain subject to the existing filter and blocked-link policy.
171
+ UNIX socket and block-device type bits do not turn regular-file payloads into
172
+ empty directories. Directory and symlink callbacks receive their physical
173
+ declared sizes; directory bodies are not published as files. Unsupported
174
+ link-like types remain visible as `other` and are safely omitted when accepted.
175
+ UNIX creator metadata and permission defaults remain as described above.
176
+
177
+ The loader adapter belongs to one private JSZip instance and is removed after
178
+ loading, including failure. Public preflight still returns ordinary JSZip entry
179
+ objects, with directory keys ending in `/` and recognizable symlink type bits.
180
+ Compressed data is retained even for declared-zero entries, so an empty-size
181
+ claim cannot bypass payload-size or CRC verification during extraction or reads.
159
182
 
160
183
  Within one ZIP entry, identical local and central name bytes reuse the same
161
184
  decoded validation. Unicode Path admission is shared only when both the raw names
@@ -173,7 +196,7 @@ collision checks, writes, and mode application agree.
173
196
 
174
197
  An `entryFilter` sees the validated **canonical effective archive path before
175
198
  stripping**, entry kind, and declared size. On every JavaScript and native
176
- ZIP/TAR backend (including gzip and native zstd/bzip2), backslashes become `/`,
199
+ ZIP/TAR backend (including gzip, zstd, and bzip2), backslashes become `/`,
177
200
  empty and `.` components are removed, and trailing separators are removed from
178
201
  directory paths. For example, `./pkg//state\cache/value` is presented as
179
202
  `pkg/state/cache/value`, even with `stripComponents: 1`. Case and Unicode
@@ -269,11 +292,23 @@ A failure before publication preserves a pre-existing file, and rejection does
269
292
  not grant authority to delete a substituted file or alias. Failed extraction does not
270
293
  restore overwritten contents. Active destination mutations and their guarded
271
294
  cleanup still finish before rejection; no later destination mutation begins.
295
+ Portable ZIP output is not eligible for publication until its stream closes or
296
+ the defensive `FileHandle` close succeeds. If that fallback close rejects,
297
+ `extractArchive()` propagates the error and publishes no entry from the staged
298
+ tree. Cleanup retains its best-effort `FileHandle` close; it does not transfer
299
+ the descriptor to a raw or native closer.
272
300
  New directories whose finalization was never reached can retain their
273
301
  private working mode after failure. Failure cleanup closes retained descriptors;
274
302
  it does not run a cleanup chmod sweep or roll back the archive. The public merge
275
303
  helper still derives modes from its external source tree and must be able to
276
304
  read that source; it never chmods an unreadable external source to admit it.
305
+ It retains the source root and each active child directory's exact identity
306
+ through traversal and copy verification. Each source file is opened once,
307
+ admitted against that root and its earlier exact identity observation, and
308
+ copied from the admitted descriptor; public file modes use that descriptor's
309
+ ordinary permission bits. Replacing a source ancestor or leaf rejects the
310
+ merge before replacement bytes can be published. These checks do not provide
311
+ a snapshot against writes to the same source inode.
277
312
  That helper retains per-copy durability and immediate postorder directory-mode
278
313
  finalization; the deferred pass described above belongs to `extractArchive()`.
279
314
 
@@ -341,14 +376,16 @@ Native gzip, zstd, and bzip2 readers check cancellation before refilling
341
376
  compressed input and before each decoded read, including buffered output. These checks
342
377
  apply to file extraction and in-memory member reads; they cannot interrupt an
343
378
  already-running filesystem read or a decoder step using already-buffered input.
379
+ Portable zstd/bzip2 decoding checks cancellation between bounded codec steps and
380
+ periodically yields to the event loop, including while consuming output-free
381
+ members. An individual WASM call cannot be interrupted. Teardown joins the input,
382
+ parser, and any Node decoder streams before disposing their shared WASM state.
344
383
 
345
384
  ### Raw TAR framing
346
385
 
347
386
  Extraction and bounded reads admit the complete decoded TAR stream through the
348
- shared Rust core. This applies to plain TAR,
349
- gzip, and native-supported zstd/bzip2, without changing native-mode availability
350
- or fallback policy. The native and WASM builds enforce the same
351
- framing rules:
387
+ shared Rust core. This applies to plain TAR, gzip, zstd, and bzip2 on native and
388
+ fallback paths. The native and WASM builds enforce the same framing rules:
352
389
 
353
390
  - Every nonzero header must have a valid unsigned octal checksum, delimited
354
391
  within its field. Checksum validation precedes metadata allocation and member
@@ -393,14 +430,24 @@ returning selected bytes. Unrequested, filtered, and stripped members cannot
393
430
  bypass validation. Decompression remains streaming; no complete decoded archive
394
431
  is retained in memory or written to a decoded spool.
395
432
 
396
- The WASM transport has a fixed 64 KiB input buffer, one pending member event,
397
- and a 256 MiB maximum linear memory per isolated parser instance. JavaScript
398
- gzip decoding emits chunks of at most 64 KiB for both staged files and buffered
399
- inputs, matching that input window. Metadata is
400
- bounded before allocation; allocation failure rejects. Stream backpressure
401
- bounds queued chunks, and completion/error destroys the instance's parser
402
- state. The manifest retains the existing charged budget below; linear memory
403
- is an additional execution resource bound, not a new public limit option.
433
+ The WASM transport has fixed 64 KiB input/output windows, one pending member
434
+ event, and a 256 MiB maximum linear memory per isolated session. The parser and
435
+ portable zstd/bzip2 decoder share that session and memory ceiling. JavaScript
436
+ gzip decoding also emits chunks of at most 64 KiB for both staged files and
437
+ buffered inputs. Metadata is bounded before allocation; codec allocation
438
+ failure rejects. Stream backpressure bounds queued chunks, and completion or
439
+ error releases the session's parser and decoder state after stream teardown.
440
+ The manifest retains the existing charged budget below; linear memory is an
441
+ additional execution resource bound, not a new public limit option.
442
+
443
+ Portable zstd/bzip2 decoding consumes every concatenated member through physical
444
+ EOF and verifies container integrity, including available checksums. Zstd
445
+ skippable frames are consumed without becoming TAR data. Truncated members and
446
+ trailing non-container bytes reject with `ArchiveFormatError` before filters,
447
+ publication, or selected bytes are returned. Decoded TAR EOF and byte-budget
448
+ checks still apply across member boundaries; a second TAR after EOF is not
449
+ silently ignored. The gzip-only compressed-padding policy above does not extend
450
+ to zstd/bzip2 containers.
404
451
 
405
452
  The raw meter enforces `maxEntries` before consuming each logical member's body,
406
453
  including members later skipped by filtering or stripping. PAX/GNU metadata
@@ -497,7 +544,7 @@ parsers from disagreeing about a member's type.
497
544
  `K` validates encoding and NUL structure without authorizing link creation.
498
545
  Normal link/filter policy still governs the described member. Canonical
499
546
  pre-strip filter paths, decoded-stream ceilings, and physical EOF checks apply
500
- to plain/gzip TAR and native zstd/bzip2 alike.
547
+ to plain/gzip TAR and zstd/bzip2 alike.
501
548
 
502
549
  ## `inspectTarArchive`
503
550
 
@@ -563,7 +610,7 @@ import { resolveArchiveKind, type ArchiveKind } from "@openclaw/fs-safe/archive"
563
610
 
564
611
  const kind = resolveArchiveKind("upload.zip"); // "zip"
565
612
  const tar = resolveArchiveKind("upload.tar.gz"); // "tar"
566
- const zstd = resolveArchiveKind("upload.tar.zst"); // "tar-zstd" when native is available
613
+ const zstd = resolveArchiveKind("upload.tar.zst"); // "tar-zstd" in auto/off; require checks native
567
614
  const unknown = resolveArchiveKind("upload.bin"); // null
568
615
  ```
569
616
 
@@ -571,17 +618,18 @@ Recognizes:
571
618
 
572
619
  - `*.zip` → `"zip"`
573
620
  - `*.tar`, `*.tar.gz`, `*.tgz` → `"tar"`
574
- - `*.tar.zst`, `*.tar.zstd`, `*.tzst` → `"tar-zstd"` (native only)
575
- - `*.tar.bz2`, `*.tbz2`, `*.tbz` → `"tar-bzip2"` (native only)
621
+ - `*.tar.zst`, `*.tar.zstd`, `*.tzst` → `"tar-zstd"`
622
+ - `*.tar.bz2`, `*.tbz2`, `*.tbz` → `"tar-bzip2"`
576
623
 
577
624
  Returns `null` for unknown extensions; check the result before calling
578
- `extractArchive` if the filename is caller-controlled. A recognized zstd or
579
- bzip2 TAR extension with no native binding throws the typed
580
- `FsSafeError("helper-unavailable")` with installation guidance. This includes
581
- `mode: "off"`; those two formats have no JavaScript fallback.
625
+ `extractArchive` if the filename is caller-controlled. Recognized zstd and bzip2
626
+ TAR extensions resolve in `auto` and `off` even without a native binding, using
627
+ the bundled codecs for subsequent extraction or reads. Explicit `require`
628
+ still checks native availability during suffix resolution and throws
629
+ `FsSafeError("helper-unavailable")` when the binding cannot load.
582
630
 
583
- For a service whose input contract requires zstd, configure native mode before
584
- the first archive call so a packaging mistake fails at the boundary:
631
+ For a deployment that requires native archive processing, configure native mode
632
+ before the first archive call so a missing binding fails at the boundary:
585
633
 
586
634
  ```ts
587
635
  import { configureFsSafeNative } from "@openclaw/fs-safe/config";
@@ -600,8 +648,10 @@ await extractArchive({
600
648
 
601
649
  `readArchiveEntry(archivePath, entryPath, { maxBytes, kind? })` reads one
602
650
  regular-file entry into a bounded `Buffer` without extracting a tree. It reads
603
- the input through an identity-checked descriptor, rejects link, directory, and duplicate
604
- entries, verifies ZIP CRC and declared size,
651
+ the input through an identity-checked descriptor, rejects a requested link or
652
+ directory, and rejects duplicate entry names anywhere in the archive. Unrequested
653
+ links and directories do not prevent reading a regular file; no links are followed
654
+ or created. It verifies ZIP CRC and declared size,
605
655
  and throws `ArchiveLimitError` if the requested entry's output exceeds
606
656
  `maxBytes`. ZIP output within that cap must match the declared uncompressed
607
657
  size exactly; either a shorter or longer payload throws
@@ -614,7 +664,9 @@ plus the 768 MiB decoded ceiling derived from default extracted/archive byte
614
664
  limits. It does not apply payload budgets to unrequested members. ZIP
615
665
  inputs retain the archive subpath's 256 MiB compressed-input ceiling.
616
666
  With a native binding it uses the same Rust decoders as extraction, including
617
- zstd and bzip2 TAR. Without native it retains the JS ZIP/TAR/gzip implementation.
667
+ zstd and bzip2 TAR. Without native, the guarded fallback uses bundled WASM for
668
+ TAR admission and zstd/bzip2 decoding, Node gunzip for gzip, and optional JSZip
669
+ for ZIP. Native `require` still rejects an unavailable binding.
618
670
  Archive member reads retain their private in-memory input without a disk
619
671
  snapshot. JavaScript ZIP member reads reuse their completed physical admission
620
672
  when loading the decoder, which still checks its decoded names and entry count.
@@ -625,12 +677,11 @@ allocation without another copy where external buffers are supported.
625
677
  Native TAR retains the fully admitted member offsets alongside the same input
626
678
  allocation. Plain TAR copies only the selected payload range after full archive
627
679
  validation. Gzip, zstd, and bzip2 replay bounded decompression and still validate
628
- all framing, trailers, and physical padding before returning. The JavaScript
629
- TAR/gzip fallback copies each input window into WASM once, consuming member
630
- events at offsets within that window. After full admission, plain TAR copies the
631
- selected range directly from its private snapshot; gzip still replays bounded
632
- decompression through the parser. WASM transport and selected output still
633
- require copies.
680
+ all framing, trailers, and physical padding before returning. The fallback also
681
+ retains admitted member offsets. After full admission, plain TAR copies the
682
+ selected range directly from its private snapshot; gzip, zstd, and bzip2 replay
683
+ bounded decompression through the same parser. WASM transport and selected
684
+ output use owned copies, so reusable codec windows cannot escape to callers.
634
685
  Returned buffers own their bytes, so changing a result cannot modify an archive
635
686
  reader or retain an unrelated part of the input through its backing ArrayBuffer.
636
687
 
package/docs/atomic.md CHANGED
@@ -92,6 +92,17 @@ await replaceFileAtomic({
92
92
 
93
93
  If `beforeRename` throws, the rename is skipped and the owned temp file is removed — the destination is unchanged. Cleanup unlinks only the exact admitted single-link file; a substitute observed at the temp name is preserved and removed from cleanup authority. The same identity is rechecked before every rename retry, when entering copy fallback, and at the final name after rename. A post-rename verification failure reports the race without rolling back or deleting the published name.
94
94
 
95
+ JavaScript permits `beforeRename` callbacks to throw any value, including
96
+ `undefined`, `null`, `false`, signed zero, `0n`, an empty string, and `NaN`.
97
+ Once such an operation failure reaches temp-owner settlement, atomic replacement
98
+ preserves that value when cleanup and close succeed. With
99
+ `throwOnCleanupError: true`, an additional owned-temp cleanup failure keeps the
100
+ existing cleanup wrapper whose `cause` is the original thrown value. A later
101
+ descriptor-close failure is reported in an `AggregateError`, in operation/cleanup
102
+ then close order. The default `throwOnCleanupError: false` omits only the cleanup
103
+ failure: the temp stays registered for identity-checked process-exit cleanup, the
104
+ descriptor is still closed, and a close failure remains reportable.
105
+
95
106
  Identity checks and pathname rename/unlink remain separate syscalls, not atomic conditional mutations. Use an approved writable parent plus cooperative locking or OS isolation when arbitrary concurrent namespace mutation is in scope.
96
107
 
97
108
  ### FUSE, Windows exFAT/FAT32, and unstable rename identity
@@ -148,6 +159,19 @@ that same descriptor, and synchronizes the result. Any write, mode, or sync
148
159
  failure triggers a byte-and-mode restore and another sync through the same
149
160
  descriptor.
150
161
 
162
+ With `syncTempFile: false`, an exclusive-create copy fallback does not report
163
+ success until its new destination writer closes successfully. This includes
164
+ `"restore-original"` when the destination did not exist. A close rejection or throw is
165
+ propagated exactly, including falsy values. The destination may already contain
166
+ all or part of the replacement, so a close failure does not prove that the old
167
+ destination survived or that the replacement was published. The outer atomic
168
+ operation still attempts identity-bound cleanup of its owned source temp; an
169
+ unverifiable or substituted temp remains preserved. If writing, mode adjustment,
170
+ or another earlier operation also fails, that earlier value remains the reported
171
+ failure and the destination close is attempted once. Successful synchronized
172
+ fallbacks and in-place `"restore-original"` replacements retain their existing best-effort final-close
173
+ handling.
174
+
151
175
  Restore failures are `FsSafeError("helper-failed")` values with typed
152
176
  `details.cleanup` set to `"restored"` or `"restore-failed"`. An original larger
153
177
  than `maxRestoreBytes` fails with `too-large` before mutation. A missing
@@ -168,7 +192,9 @@ synchronous boot paths or test setup code. It returns the same
168
192
 
169
193
  ## `replaceDirectoryAtomic`
170
194
 
171
- Atomically swap one directory's contents with another, using a temporary backup during the swap.
195
+ Publish one staged directory at a target without overwriting a concurrently
196
+ created entry. Despite the historical name, replacing an existing target is a
197
+ guarded two-rename protocol, not an atomic directory exchange.
172
198
 
173
199
  ```ts
174
200
  import { replaceDirectoryAtomic } from "@openclaw/fs-safe/atomic";
@@ -179,15 +205,66 @@ await replaceDirectoryAtomic({
179
205
  });
180
206
  ```
181
207
 
182
- The helper renames `targetDir` to a generated backup path, renames `stagedDir → targetDir`, then removes the backup. If the second rename fails, it tries to restore the original target before rethrowing.
183
- Concurrent replacements of the same resolved target are serialized inside the
184
- current process so their backup, commit, and cleanup phases cannot interleave.
185
- On Windows, ordinary drive-relative staged and target paths are anchored at
186
- entry before namespace-alias admission and resolution.
208
+ Every publication requires the dedicated native identity-fenced,
209
+ descriptor-relative no-replace rename capability and readable retained
210
+ descriptors for the staged and target parents. Older bindings that expose only
211
+ the legacy four-argument no-replace rename fail with `helper-unavailable`
212
+ before target-parent creation or any other replacement effect. This applies
213
+ when the parents are the same or different. There is no JavaScript rename
214
+ fallback, and a cross-device rename still fails. Replacing an existing target
215
+ additionally requires usable native bounded owned-tree cleanup and a readable
216
+ retained descriptor for the original target.
217
+
218
+ If the target is absent, the helper publishes `stagedDir → targetDir` with a
219
+ single no-replace rename. If the target exists, it renames `targetDir` to a
220
+ randomized sibling backup and then renames `stagedDir → targetDir`. The target
221
+ name is temporarily absent between those two operations. A competing entry is
222
+ never overwritten. Publication rollback is attempted only when the staged
223
+ rename is known not to have committed and the target name is still absent; the
224
+ rollback itself is no-replace, so a competitor is preserved and the backup is
225
+ left for recovery.
226
+
227
+ Exact staged-directory identity and parent checks run before and after each
228
+ rename. On Windows, the native rename opens the source relative to the retained
229
+ parent, compares that handle's exact volume and file-index identity with the
230
+ pre-rename bigint receipt, and only then mutates it. POSIX does not provide a
231
+ rename operation that also compares an expected source inode, so on POSIX a
232
+ source-name substitution in the final check-to-rename gap can be moved briefly
233
+ and then detected by the post-rename verification. Similarly, a successful
234
+ rename can be followed by a verification error. Inspect the error's
235
+ `details.publication` value rather than treating rejection as proof that
236
+ publication did not happen.
237
+
238
+ A Windows source-identity mismatch is rejected before mutation, reported
239
+ publicly as `path-mismatch`, and treated as definitely uncommitted so an earlier
240
+ backup can be rolled back. Other `path-mismatch`-shaped native errors are not
241
+ assumed to be pre-commit failures; their publication outcome remains
242
+ indeterminate and observed competitors are preserved.
243
+
244
+ Any native rename error without explicit pre-dispatch provenance has an
245
+ indeterminate outcome, including ordinary errno such as `ENOENT`, `EEXIST`,
246
+ or `EACCES`: a remote filesystem may commit before losing its reply. The helper
247
+ does not guess whether that rename committed or perform another rename or
248
+ cleanup based on that guess; observed names are preserved for caller-directed
249
+ recovery. An error `details.backupPath`, when present, is only the attempted or
250
+ last-observed backup pathname. It does not prove that the path still exists or
251
+ still names the original directory.
252
+
253
+ After a verified commit, the original backup is removed through its retained
254
+ directory identity and bounded native traversal. Cleanup failures reject after
255
+ publication and can leave the backup. On POSIX, cleanup retains the
256
+ [bounded final-entry unlink limitation](temp.md#private-temp-workspaces).
257
+
258
+ Concurrent calls for the same resolved target are serialized inside the current
259
+ process so their backup, publication, and cleanup phases cannot interleave.
260
+ Other processes are not serialized; no-replace renames provide the competitor
261
+ boundary. On Windows, ordinary drive-relative staged and target paths are
262
+ anchored at entry before namespace-alias admission and resolution.
187
263
  `backupPrefix`, when supplied, is sanitized as one path prefix and cannot contain
188
264
  path separators or NUL bytes; the generated backup tail is randomized.
189
265
 
190
- Use it when callers must see a whole staged tree at the target path. For single-file replacement, `replaceFileAtomic` is the right tool.
266
+ Use it when callers must publish a whole staged tree with these recovery
267
+ semantics. For single-file replacement, `replaceFileAtomic` is the right tool.
191
268
 
192
269
  ## `writeTextAtomic`
193
270
 
@@ -396,7 +473,7 @@ type ReplaceFileAtomicSyncFileSystem = {
396
473
  };
397
474
  ```
398
475
 
399
- The async interface already requires `open()`, whose `FileHandle` supplies `chmod()`, so injecting `node:fs` or another conforming adapter needs no new async member. On POSIX, that `open()` must support no-follow directory descriptors as Node does. A custom synchronous filesystem that passes `mode`, `dirMode`, or `preserveExistingMode` must supply `fchmodSync`; omission fails before any file or directory is created and never falls back to a pathname `chmod`. Existing synchronous adapters that request none of those options may omit it; their parent is still opened and identity-checked through a no-follow directory descriptor. Injecting plain `node:fs` supports explicit file and directory modes. Older adapter literals may continue to include `chmod` or `chmodSync` for source compatibility, but those operations are ignored. Copy fallback applies the file mode through its pinned destination descriptor as well, preserving exact modes despite the process umask.
476
+ The async interface already requires `open()`, whose `FileHandle` supplies `chmod()`, so injecting `node:fs` or another conforming adapter needs no new async member. The async temp owner consumes its retained handle before awaiting `close()` during publication handoff and terminal settlement: if a custom adapter releases the resource and then rejects, that rejection is reported without calling `close()` on the same retained handle again. If publication verification opened a replacement handle before the previous retained handle failed to close, the replacement receives one best-effort close attempt. On POSIX, `open()` must support no-follow directory descriptors as Node does. A custom synchronous filesystem that passes `mode`, `dirMode`, or `preserveExistingMode` must supply `fchmodSync`; omission fails before any file or directory is created and never falls back to a pathname `chmod`. Existing synchronous adapters that request none of those options may omit it; their parent is still opened and identity-checked through a no-follow directory descriptor. Injecting plain `node:fs` supports explicit file and directory modes. Older adapter literals may continue to include `chmod` or `chmodSync` for source compatibility, but those operations are ignored. Copy fallback applies the file mode through its pinned destination descriptor as well, preserving exact modes despite the process umask.
400
477
 
401
478
  ## See also
402
479
 
package/docs/config.md CHANGED
@@ -37,10 +37,14 @@ Set the process-global loading policy. Configure once at startup, before the fir
37
37
 
38
38
  | Mode | Behavior |
39
39
  |---|---|
40
- | `auto` | Default. Prefer the platform binding and use guarded JavaScript when it is unavailable. |
41
- | `off` | Do not load the binding; use guarded JavaScript deterministically. |
40
+ | `auto` | Default. Prefer the platform binding and use supported fallbacks when it is unavailable. |
41
+ | `off` | Do not load the binding; use supported fallbacks and reject native-only operations. |
42
42
  | `require` | Operations that need the binding raise `FsSafeError("helper-unavailable")` when it cannot load. |
43
43
 
44
+ Fallbacks include guarded JavaScript, the bundled TAR/gzip WASM parser, and the
45
+ [packaged Windows security scripts](install.md#windows-security-fallback).
46
+ Windows command fallbacks remain subject to normal system execution policy.
47
+
44
48
  ## `getFsSafeNativeConfig()`
45
49
 
46
50
  ```ts
@@ -19,10 +19,50 @@ manager version declared in `package.json`.
19
19
  pnpm build
20
20
  ```
21
21
 
22
- Runs TypeScript compilation and builds the portable Rust TAR parser for
23
- `wasm32-unknown-unknown`. Contributors need Rust (the native crate's declared
24
- minimum or newer) and `rustup target add wasm32-unknown-unknown`; Alpine's
25
- packaged toolchain uses `rust-wasm`. `pnpm archive:wasm` rebuilds just the parser.
22
+ Runs TypeScript compilation and builds the portable Rust TAR parser and its
23
+ bzip2/zstd codecs for `wasm32-unknown-unknown`. Contributors need Rust (the
24
+ native crate's declared minimum or newer), `rustup target add
25
+ wasm32-unknown-unknown`, and LLVM's WebAssembly-capable `clang` and `llvm-ar`.
26
+ The system's native `ar` is not sufficient. `pnpm archive:wasm` rebuilds just
27
+ the portable module.
28
+
29
+ Linux, macOS, and Windows CI use the same pinned WASI SDK 34 LLVM toolchain;
30
+ Alpine uses its versioned LLVM 22 packages alongside `rust-wasm`. For local
31
+ builds, install LLVM through your package manager or use the official
32
+ [WASI SDK](https://github.com/WebAssembly/wasi-sdk/releases/tag/wasi-sdk-34).
33
+ On macOS, `brew install llvm` supplies the archiver missing from Apple's
34
+ Command Line Tools. On Windows, install the LLVM distribution with both
35
+ `clang.exe` and `llvm-ar.exe`. On Linux, install the matching `clang` and
36
+ `llvm` packages; a GCC-only build toolchain cannot compile these WASM codecs.
37
+
38
+ The build discovers tools on `PATH`, in `LLVM_PATH/bin`, in Homebrew's LLVM
39
+ prefixes, and in Windows' standard LLVM installation. It also checks the
40
+ versioned `clang-18` through `clang-21` and `llvm-ar-18` through `llvm-ar-21`
41
+ executables. To select another installation explicitly, set
42
+ `CC_wasm32_unknown_unknown` and `AR_wasm32_unknown_unknown` to its compiler
43
+ and archiver. The corresponding hyphenated target variables and cc-rs's
44
+ `TARGET_CC`/`TARGET_AR` or `CC`/`AR` overrides are also respected; an unusable
45
+ explicit override fails with a builder diagnostic instead of being ignored.
46
+ Windows build environment names are case-insensitive, including when worker
47
+ processes uppercase them. The build normalizes only its copied child environment.
48
+ Clang's implicit configuration is disabled for this target so the WASI SDK's
49
+ default libc/sysroot cannot leak into the import-free module. These settings
50
+ affect compilation only and do not become runtime dependencies.
51
+
52
+ The build disables release LTO only in the WASM Cargo subprocess. An observed
53
+ Rust 1.98.1 optimized-WASM-LTO allocation/free failure makes that necessary;
54
+ the native release profile stays unchanged. The WASM linker strips debug
55
+ sections to keep the bundled module small without stripping native binaries.
56
+ The build verifies zero host
57
+ imports and one unshared 32-bit memory with the existing 256 MiB maximum
58
+ before copying the artifact. Allocator regression tests build a separate
59
+ instrumented module with `pnpm archive:wasm:allocator-tests` under the Cargo
60
+ target directory. That module is never copied to `dist/` or packaged. `pnpm
61
+ check` and coverage collection build it explicitly before testing. After a
62
+ fresh checkout, run that command before `pnpm test`, `pnpm test:coverage`, or
63
+ focused `test/archive-codec-wasm-allocator.test.ts` runs; the tests fail if
64
+ their prerequisite artifact is missing.
65
+
26
66
  The import-free asset lands at `dist/archive-parser.wasm`; source tests and
27
67
  compiled consumers both resolve that generated artifact. Run `pnpm build`
28
68
  before source tests in a fresh checkout. Do not commit `dist/` or built WASM.
package/docs/copy.md CHANGED
@@ -66,6 +66,8 @@ On Windows, automatic byte copying uses the native binding when available to tra
66
66
 
67
67
  On Linux, automatic byte copying also uses the native binding when available. It reads in chunks up to 1 MiB, using smaller buffers for smaller source-size hints, and leaves leading and trailing zero-filled portions of each chunk unwritten in the new file, avoiding their allocation on filesystems that support sparse files. A final size update preserves trailing holes and all-zero files. This path uses reads and writes, without cloning or copy offload; it still reads the full logical contents and does not promise identical sparse extent layout. `clone: "never"` retains the JavaScript byte-copy path.
68
68
 
69
+ Tree-copy cleanup attempts every acquired close once, even when another close fails. Portable file handles close output before input; completed directories close their pinned source before target; the public wrapper then closes its source before the destination parent. A copy, identity, metadata, native-clone, or cancellation failure already observed at one of those scopes remains the reported value instead of being replaced by cleanup. If that scope otherwise succeeded, its first close failure is reported unchanged. Concurrent siblings have no global structural close order: the first observed sibling failure stops new work while admitted work settles. Portable copying records caller cancellation when it occurs, so a later abort during cleanup cannot replace an earlier copy failure.
70
+
69
71
  Clones preserve file contents, empty directories, timestamps, executable modes where supported, and literal symbolic links. Editing a clone does not modify its source. Unsupported filesystem operations fail; callers may choose their own copy or checkout fallback after the failed operation has settled.
70
72
 
71
73
  Native Windows byte copies can store large zero-filled chunks as sparse ranges when the destination is initially empty and its filesystem supports sparse files. This still reads every source byte and creates an independent copy.
@@ -76,6 +78,8 @@ XFS and ZFS preserve regular-file and directory modes, timestamps, extended attr
76
78
 
77
79
  `readCloneFileMetadata(files)` asynchronously reads APFS data-stream identities and file metadata in one native batch. Results correspond to input order; missing or unsupported entries return `undefined`. The returned `CloneFileMetadata` includes clone ID, device/inode, size, mode, ownership, and timestamps. These are point-in-time observations, not authorization or proof that later reads remain unchanged. Consumers such as Git index adapters must validate their own content and timestamp invariants. The reader does not follow leaf symbolic links.
78
80
 
81
+ All input paths must be absolute and valid before native availability is checked. On platforms other than macOS, `auto` and `off` can return one `undefined` per path without the addon, matching native's unsupported result. On macOS, `off` or an unavailable addon still rejects with `helper-unavailable`; JavaScript cannot supply APFS clone IDs. Explicit `require` mode rejects an unavailable addon on every platform, including for an empty batch. Errors from a loaded native helper remain terminal.
82
+
79
83
  ## Borrowed FileHandle transfers
80
84
 
81
85
  `copyFileHandle` from `@openclaw/fs-safe/advanced` copies bytes between two
@@ -137,6 +141,37 @@ descriptor inspection does not prove that the source remained unchanged while
137
141
  copying. Keep existing source-fingerprint and publication checks around the
138
142
  transfer when building snapshot operations.
139
143
 
144
+ ### Synchronous descriptor transfers
145
+
146
+ `copyFileDescriptorSync(sourceFd, targetFd, options?)` provides the same
147
+ zero-origin byte transfer for borrowed numeric descriptors. It shares
148
+ `CopyFileHandleOptions` and returns the copied byte count synchronously:
149
+
150
+ ```ts
151
+ import { copyFileDescriptorSync } from "@openclaw/fs-safe/advanced";
152
+
153
+ const bytes = copyFileDescriptorSync(sourceFd, targetFd, {
154
+ maxBytes: expectedSize,
155
+ assertBeforeMutation: assertSnapshotOwnerCurrent,
156
+ });
157
+ ```
158
+
159
+ The same regular-file and exact-identity admission, byte limits, short-I/O
160
+ handling, cursor preservation, and caller-owned cleanup apply. The target must
161
+ be opened without append mode, and both descriptors must remain open and free
162
+ of concurrent I/O, including inside callbacks. Neither helper makes a mutable
163
+ source into a consistent snapshot or truncates an existing destination suffix.
164
+
165
+ The synchronous helper snapshots the four options once and invokes both
166
+ callbacks with no receiver (`this` is `undefined` in strict callbacks).
167
+ `onChunk` receives a borrowed view that must be consumed immediately without
168
+ retaining or mutating it. Both callbacks must finish synchronously; thenables
169
+ throw `TypeError` before the current write. Cancellation is cooperative: a
170
+ pre-aborted signal or an abort triggered by a callback stops the transfer before
171
+ the next write. Timers cannot interrupt synchronous filesystem calls while the
172
+ event loop is blocked. Authority runs before every partial write; an abort
173
+ triggered by that assertion prevents the same write.
174
+
140
175
  ## Ownership and cancellation
141
176
 
142
177
  These are low-level operations on caller-owned absolute paths, not Root-relative methods. The source and destination parent must be real directories. The library pins their descriptors and verifies their identities; it does not establish the caller's authorization to use them. Keep the source immutable for the operation, including writes through other aliases, and keep the destination namespace under the caller's control. Literal symlinks in the cloned contents are preserved rather than followed or sanitized.
@@ -145,6 +180,8 @@ An already aborted signal prevents dispatch. In-flight cancellation stops cancel
145
180
 
146
181
  Completion is not a crash-durability guarantee. The API is suitable for reconstructible templates and checkouts; it does not sync every file or replace application-level publication and recovery rules.
147
182
 
183
+ A close-only rejection reports descriptor settlement, not whether bytes reached stable storage. The destination can already be complete when a close fails, just as other failed or cancelled calls can leave caller-owned output. Inspect or remove that output only after the copying promise settles and under the same source and destination authority assumptions.
184
+
148
185
  Byte copying retains fractional file and directory access/modification timestamps to the precision supported by Node's timestamp APIs and the destination filesystem. This includes dates before 1970 on Unix. On Windows, [Node's unsigned stat seconds](https://github.com/nodejs/node/blob/v26.8.2/src/node_file-inl.h#L93-L104) can report pre-1970 timestamps as dates about 136 years later; byte copying inherits that upstream limitation.
149
186
 
150
187
  ## Platform tests and benchmarks