@openclaw/fs-safe 0.11.0 → 0.13.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 (416) hide show
  1. package/CHANGELOG.md +119 -0
  2. package/README.md +15 -8
  3. package/dist/absolute-path.d.ts.map +1 -1
  4. package/dist/absolute-path.js +32 -11
  5. package/dist/advanced.d.ts +3 -1
  6. package/dist/advanced.d.ts.map +1 -1
  7. package/dist/advanced.js +3 -1
  8. package/dist/archive-entry.d.ts.map +1 -1
  9. package/dist/archive-entry.js +4 -3
  10. package/dist/archive-gzip-tail.d.ts.map +1 -1
  11. package/dist/archive-gzip-tail.js +13 -9
  12. package/dist/archive-input.d.ts.map +1 -1
  13. package/dist/archive-input.js +6 -2
  14. package/dist/archive-merge.d.ts.map +1 -1
  15. package/dist/archive-merge.js +15 -2
  16. package/dist/archive-native.d.ts.map +1 -1
  17. package/dist/archive-native.js +7 -19
  18. package/dist/archive-plan.js +1 -1
  19. package/dist/archive-read.d.ts.map +1 -1
  20. package/dist/archive-read.js +55 -46
  21. package/dist/archive-staging.d.ts.map +1 -1
  22. package/dist/archive-staging.js +43 -10
  23. package/dist/archive-tar-inspect.d.ts.map +1 -1
  24. package/dist/archive-tar-inspect.js +4 -1
  25. package/dist/archive-zip-admission.d.ts +1 -1
  26. package/dist/archive-zip-admission.d.ts.map +1 -1
  27. package/dist/archive-zip-admission.js +2 -2
  28. package/dist/archive-zip-directory.d.ts +3 -0
  29. package/dist/archive-zip-directory.d.ts.map +1 -1
  30. package/dist/archive-zip-directory.js +13 -2
  31. package/dist/archive-zip-loader.d.ts +3 -2
  32. package/dist/archive-zip-loader.d.ts.map +1 -1
  33. package/dist/archive-zip-loader.js +30 -4
  34. package/dist/archive-zip-manifest.d.ts +5 -0
  35. package/dist/archive-zip-manifest.d.ts.map +1 -0
  36. package/dist/archive-zip-manifest.js +22 -0
  37. package/dist/archive-zip-names.d.ts +6 -1
  38. package/dist/archive-zip-names.d.ts.map +1 -1
  39. package/dist/archive-zip-names.js +50 -17
  40. package/dist/archive-zip-preflight.d.ts.map +1 -1
  41. package/dist/archive-zip-preflight.js +3 -2
  42. package/dist/archive.d.ts.map +1 -1
  43. package/dist/archive.js +9 -4
  44. package/dist/bounded-read.js +2 -2
  45. package/dist/darwin-acl.d.ts +4 -0
  46. package/dist/darwin-acl.d.ts.map +1 -0
  47. package/dist/darwin-acl.js +24 -0
  48. package/dist/deny-mutations.d.ts.map +1 -1
  49. package/dist/deny-mutations.js +8 -2
  50. package/dist/device-path.d.ts.map +1 -1
  51. package/dist/device-path.js +5 -3
  52. package/dist/directory-durability.d.ts.map +1 -1
  53. package/dist/directory-durability.js +67 -23
  54. package/dist/directory-entry-path.d.ts +3 -0
  55. package/dist/directory-entry-path.d.ts.map +1 -0
  56. package/dist/directory-entry-path.js +21 -0
  57. package/dist/directory-guard.d.ts +17 -1
  58. package/dist/directory-guard.d.ts.map +1 -1
  59. package/dist/directory-guard.js +134 -48
  60. package/dist/directory-mode-node.d.ts +12 -0
  61. package/dist/directory-mode-node.d.ts.map +1 -1
  62. package/dist/directory-mode-node.js +102 -4
  63. package/dist/effective-uid.d.ts +2 -0
  64. package/dist/effective-uid.d.ts.map +1 -0
  65. package/dist/effective-uid.js +25 -0
  66. package/dist/file-handle-transfer.d.ts.map +1 -1
  67. package/dist/file-handle-transfer.js +98 -27
  68. package/dist/file-hash.d.ts.map +1 -1
  69. package/dist/file-hash.js +12 -2
  70. package/dist/file-lock-sync.d.ts.map +1 -1
  71. package/dist/file-lock-sync.js +36 -6
  72. package/dist/file-lock.d.ts.map +1 -1
  73. package/dist/file-lock.js +29 -9
  74. package/dist/file-observation.d.ts +1 -1
  75. package/dist/file-observation.d.ts.map +1 -1
  76. package/dist/file-store-boundary.d.ts +6 -2
  77. package/dist/file-store-boundary.d.ts.map +1 -1
  78. package/dist/file-store-boundary.js +20 -65
  79. package/dist/file-store-copy-source.d.ts +5 -0
  80. package/dist/file-store-copy-source.d.ts.map +1 -0
  81. package/dist/file-store-copy-source.js +31 -0
  82. package/dist/file-store-path.d.ts.map +1 -1
  83. package/dist/file-store-path.js +4 -1
  84. package/dist/file-store-prune.d.ts.map +1 -1
  85. package/dist/file-store-prune.js +9 -1
  86. package/dist/file-store-sync-directory.d.ts +16 -0
  87. package/dist/file-store-sync-directory.d.ts.map +1 -0
  88. package/dist/file-store-sync-directory.js +349 -0
  89. package/dist/file-store.d.ts.map +1 -1
  90. package/dist/file-store.js +11 -28
  91. package/dist/filename.d.ts +1 -0
  92. package/dist/filename.d.ts.map +1 -1
  93. package/dist/filename.js +85 -23
  94. package/dist/fs.d.ts.map +1 -1
  95. package/dist/fs.js +3 -2
  96. package/dist/guarded-mkdir.d.ts +10 -0
  97. package/dist/guarded-mkdir.d.ts.map +1 -1
  98. package/dist/guarded-mkdir.js +217 -33
  99. package/dist/guest.d.ts.map +1 -1
  100. package/dist/guest.js +16 -5
  101. package/dist/home-dir.d.ts.map +1 -1
  102. package/dist/home-dir.js +73 -10
  103. package/dist/install-path.d.ts +6 -0
  104. package/dist/install-path.d.ts.map +1 -1
  105. package/dist/install-path.js +67 -19
  106. package/dist/json-durable-queue-ownership.d.ts +8 -0
  107. package/dist/json-durable-queue-ownership.d.ts.map +1 -1
  108. package/dist/json-durable-queue-ownership.js +134 -43
  109. package/dist/json-durable-queue-paths.d.ts +11 -0
  110. package/dist/json-durable-queue-paths.d.ts.map +1 -0
  111. package/dist/json-durable-queue-paths.js +42 -0
  112. package/dist/json-durable-queue-read.d.ts +6 -0
  113. package/dist/json-durable-queue-read.d.ts.map +1 -0
  114. package/dist/json-durable-queue-read.js +61 -0
  115. package/dist/json-durable-queue.d.ts +1 -1
  116. package/dist/json-durable-queue.d.ts.map +1 -1
  117. package/dist/json-durable-queue.js +83 -134
  118. package/dist/json-store.d.ts.map +1 -1
  119. package/dist/json-store.js +5 -1
  120. package/dist/json.d.ts.map +1 -1
  121. package/dist/json.js +54 -22
  122. package/dist/local-file-access.d.ts.map +1 -1
  123. package/dist/local-file-access.js +4 -0
  124. package/dist/local-file-descriptor.d.ts +17 -0
  125. package/dist/local-file-descriptor.d.ts.map +1 -0
  126. package/dist/local-file-descriptor.js +84 -0
  127. package/dist/local-roots.d.ts.map +1 -1
  128. package/dist/local-roots.js +35 -7
  129. package/dist/move-path-cleanup.d.ts +2 -0
  130. package/dist/move-path-cleanup.d.ts.map +1 -1
  131. package/dist/move-path-cleanup.js +44 -18
  132. package/dist/move-path.d.ts.map +1 -1
  133. package/dist/move-path.js +31 -6
  134. package/dist/native-binding.d.ts +21 -0
  135. package/dist/native-binding.d.ts.map +1 -1
  136. package/dist/native-directory-observation.d.ts +17 -0
  137. package/dist/native-directory-observation.d.ts.map +1 -0
  138. package/dist/native-directory-observation.js +37 -0
  139. package/dist/native-parent-admission.d.ts +31 -0
  140. package/dist/native-parent-admission.d.ts.map +1 -0
  141. package/dist/native-parent-admission.js +124 -0
  142. package/dist/native-pinned-write-windows.d.ts +0 -1
  143. package/dist/native-pinned-write-windows.d.ts.map +1 -1
  144. package/dist/native-pinned-write-windows.js +0 -3
  145. package/dist/native-pinned-write.d.ts.map +1 -1
  146. package/dist/native-pinned-write.js +309 -40
  147. package/dist/output.d.ts.map +1 -1
  148. package/dist/output.js +15 -5
  149. package/dist/overwrite-file-handle.d.ts.map +1 -1
  150. package/dist/overwrite-file-handle.js +5 -1
  151. package/dist/owner-dacl.d.ts.map +1 -1
  152. package/dist/owner-dacl.js +2 -0
  153. package/dist/path-policy.d.ts.map +1 -1
  154. package/dist/path-policy.js +7 -2
  155. package/dist/path-prefix.d.ts +7 -0
  156. package/dist/path-prefix.d.ts.map +1 -0
  157. package/dist/path-prefix.js +82 -0
  158. package/dist/path-scope-lexical.d.ts +14 -0
  159. package/dist/path-scope-lexical.d.ts.map +1 -0
  160. package/dist/path-scope-lexical.js +37 -0
  161. package/dist/path-segment-route.d.ts +7 -0
  162. package/dist/path-segment-route.d.ts.map +1 -0
  163. package/dist/path-segment-route.js +24 -0
  164. package/dist/path-suffix-aliases.d.ts +10 -0
  165. package/dist/path-suffix-aliases.d.ts.map +1 -0
  166. package/dist/path-suffix-aliases.js +386 -0
  167. package/dist/path.d.ts.map +1 -1
  168. package/dist/path.js +35 -6
  169. package/dist/permissions-windows.d.ts.map +1 -1
  170. package/dist/permissions-windows.js +14 -3
  171. package/dist/permissions.d.ts.map +1 -1
  172. package/dist/permissions.js +37 -8
  173. package/dist/pinned-mutation-admission.d.ts +25 -0
  174. package/dist/pinned-mutation-admission.d.ts.map +1 -0
  175. package/dist/pinned-mutation-admission.js +425 -0
  176. package/dist/pinned-mutation-observation.d.ts +34 -0
  177. package/dist/pinned-mutation-observation.d.ts.map +1 -0
  178. package/dist/pinned-mutation-observation.js +142 -0
  179. package/dist/pinned-mutation-shared-route.d.ts +24 -0
  180. package/dist/pinned-mutation-shared-route.d.ts.map +1 -0
  181. package/dist/pinned-mutation-shared-route.js +70 -0
  182. package/dist/pinned-open.d.ts +6 -0
  183. package/dist/pinned-open.d.ts.map +1 -1
  184. package/dist/pinned-open.js +28 -10
  185. package/dist/pinned-write-types.d.ts +75 -0
  186. package/dist/pinned-write-types.d.ts.map +1 -0
  187. package/dist/pinned-write-types.js +1 -0
  188. package/dist/pinned-write.d.ts +7 -33
  189. package/dist/pinned-write.d.ts.map +1 -1
  190. package/dist/pinned-write.js +152 -17
  191. package/dist/private-directory.d.ts.map +1 -1
  192. package/dist/private-directory.js +2 -0
  193. package/dist/private-producer-handoff.d.ts +16 -0
  194. package/dist/private-producer-handoff.d.ts.map +1 -0
  195. package/dist/private-producer-handoff.js +272 -0
  196. package/dist/private-temp-workspace.d.ts +2 -39
  197. package/dist/private-temp-workspace.d.ts.map +1 -1
  198. package/dist/private-temp-workspace.js +183 -77
  199. package/dist/publish-file.d.ts.map +1 -1
  200. package/dist/publish-file.js +33 -29
  201. package/dist/regular-file.d.ts.map +1 -1
  202. package/dist/regular-file.js +56 -45
  203. package/dist/replace-directory.d.ts.map +1 -1
  204. package/dist/replace-directory.js +12 -5
  205. package/dist/replace-file-temp-owner.d.ts +1 -0
  206. package/dist/replace-file-temp-owner.d.ts.map +1 -1
  207. package/dist/replace-file-temp-owner.js +12 -1
  208. package/dist/replace-file.d.ts.map +1 -1
  209. package/dist/replace-file.js +24 -24
  210. package/dist/root-boundary.d.ts +29 -0
  211. package/dist/root-boundary.d.ts.map +1 -0
  212. package/dist/root-boundary.js +182 -0
  213. package/dist/root-context.d.ts +12 -1
  214. package/dist/root-context.d.ts.map +1 -1
  215. package/dist/root-context.js +89 -16
  216. package/dist/root-directory-creation.d.ts +13 -0
  217. package/dist/root-directory-creation.d.ts.map +1 -0
  218. package/dist/root-directory-creation.js +212 -0
  219. package/dist/root-directory-list.d.ts +16 -4
  220. package/dist/root-directory-list.d.ts.map +1 -1
  221. package/dist/root-directory-list.js +182 -33
  222. package/dist/root-directory.d.ts +23 -0
  223. package/dist/root-directory.d.ts.map +1 -0
  224. package/dist/root-directory.js +141 -0
  225. package/dist/root-errors.d.ts.map +1 -1
  226. package/dist/root-errors.js +12 -1
  227. package/dist/root-file-final-admission.d.ts +21 -0
  228. package/dist/root-file-final-admission.d.ts.map +1 -0
  229. package/dist/root-file-final-admission.js +83 -0
  230. package/dist/root-file.d.ts.map +1 -1
  231. package/dist/root-file.js +103 -24
  232. package/dist/root-impl.d.ts +1 -0
  233. package/dist/root-impl.d.ts.map +1 -1
  234. package/dist/root-impl.js +316 -325
  235. package/dist/root-move-noreplace.d.ts +14 -0
  236. package/dist/root-move-noreplace.d.ts.map +1 -0
  237. package/dist/root-move-noreplace.js +202 -0
  238. package/dist/root-move-preflight.d.ts +8 -0
  239. package/dist/root-move-preflight.d.ts.map +1 -0
  240. package/dist/root-move-preflight.js +16 -0
  241. package/dist/root-observed-path.d.ts +9 -0
  242. package/dist/root-observed-path.d.ts.map +1 -0
  243. package/dist/root-observed-path.js +95 -0
  244. package/dist/root-path-errors.d.ts +12 -0
  245. package/dist/root-path-errors.d.ts.map +1 -0
  246. package/dist/root-path-errors.js +13 -0
  247. package/dist/root-path-existing.d.ts +2 -0
  248. package/dist/root-path-existing.d.ts.map +1 -1
  249. package/dist/root-path-existing.js +63 -22
  250. package/dist/root-path-observation.d.ts +63 -0
  251. package/dist/root-path-observation.d.ts.map +1 -0
  252. package/dist/root-path-observation.js +180 -0
  253. package/dist/root-path-stat.d.ts +5 -0
  254. package/dist/root-path-stat.d.ts.map +1 -0
  255. package/dist/root-path-stat.js +101 -0
  256. package/dist/root-path-symlink.d.ts.map +1 -1
  257. package/dist/root-path-symlink.js +23 -4
  258. package/dist/root-path.d.ts +13 -0
  259. package/dist/root-path.d.ts.map +1 -1
  260. package/dist/root-path.js +263 -73
  261. package/dist/root-paths-lexical.d.ts +9 -0
  262. package/dist/root-paths-lexical.d.ts.map +1 -0
  263. package/dist/root-paths-lexical.js +22 -0
  264. package/dist/root-paths.d.ts +3 -29
  265. package/dist/root-paths.d.ts.map +1 -1
  266. package/dist/root-paths.js +89 -178
  267. package/dist/root-read-admission.d.ts +26 -0
  268. package/dist/root-read-admission.d.ts.map +1 -0
  269. package/dist/root-read-admission.js +94 -0
  270. package/dist/root-remove-identity.d.ts +15 -0
  271. package/dist/root-remove-identity.d.ts.map +1 -0
  272. package/dist/root-remove-identity.js +89 -0
  273. package/dist/root-remove-receipt.d.ts +17 -0
  274. package/dist/root-remove-receipt.d.ts.map +1 -0
  275. package/dist/root-remove-receipt.js +37 -0
  276. package/dist/root-remove.d.ts +2 -1
  277. package/dist/root-remove.d.ts.map +1 -1
  278. package/dist/root-remove.js +172 -10
  279. package/dist/root-walk.d.ts.map +1 -1
  280. package/dist/root-walk.js +2 -1
  281. package/dist/root-write-admission.d.ts +68 -0
  282. package/dist/root-write-admission.d.ts.map +1 -0
  283. package/dist/root-write-admission.js +322 -0
  284. package/dist/root-write-compatibility.d.ts +13 -0
  285. package/dist/root-write-compatibility.d.ts.map +1 -0
  286. package/dist/root-write-compatibility.js +74 -0
  287. package/dist/root-write-complete-parent.d.ts +42 -0
  288. package/dist/root-write-complete-parent.d.ts.map +1 -0
  289. package/dist/root-write-complete-parent.js +195 -0
  290. package/dist/root-write-mode.d.ts +2 -0
  291. package/dist/root-write-mode.d.ts.map +1 -1
  292. package/dist/root-write-mode.js +20 -7
  293. package/dist/root-write-publication.d.ts +24 -0
  294. package/dist/root-write-publication.d.ts.map +1 -0
  295. package/dist/root-write-publication.js +85 -0
  296. package/dist/root-write-verification.d.ts.map +1 -1
  297. package/dist/root-write-verification.js +18 -7
  298. package/dist/safe-path-segment.d.ts.map +1 -1
  299. package/dist/safe-path-segment.js +3 -1
  300. package/dist/secret-file.d.ts.map +1 -1
  301. package/dist/secret-file.js +62 -24
  302. package/dist/secret-read-async.d.ts.map +1 -1
  303. package/dist/secret-read-async.js +18 -13
  304. package/dist/secret-read-policy.d.ts.map +1 -1
  305. package/dist/secret-read-policy.js +5 -1
  306. package/dist/secure-file-windows.d.ts +10 -0
  307. package/dist/secure-file-windows.d.ts.map +1 -0
  308. package/dist/secure-file-windows.js +186 -0
  309. package/dist/secure-file.d.ts.map +1 -1
  310. package/dist/secure-file.js +98 -10
  311. package/dist/secure-temp-dir.d.ts +6 -3
  312. package/dist/secure-temp-dir.d.ts.map +1 -1
  313. package/dist/secure-temp-dir.js +121 -101
  314. package/dist/secure-temp-repair.d.ts +35 -0
  315. package/dist/secure-temp-repair.d.ts.map +1 -0
  316. package/dist/secure-temp-repair.js +106 -0
  317. package/dist/sibling-staged-file.d.ts +3 -1
  318. package/dist/sibling-staged-file.d.ts.map +1 -1
  319. package/dist/sibling-staged-file.js +181 -50
  320. package/dist/sibling-temp.d.ts.map +1 -1
  321. package/dist/sibling-temp.js +27 -12
  322. package/dist/sidecar-lock-acquire.d.ts.map +1 -1
  323. package/dist/sidecar-lock-acquire.js +45 -15
  324. package/dist/sidecar-lock-root.d.ts.map +1 -1
  325. package/dist/sidecar-lock-root.js +4 -2
  326. package/dist/sidecar-lock.d.ts.map +1 -1
  327. package/dist/sidecar-lock.js +2 -3
  328. package/dist/staged-directory.d.ts +14 -5
  329. package/dist/staged-directory.d.ts.map +1 -1
  330. package/dist/staged-directory.js +54 -3
  331. package/dist/standalone-publication-path.d.ts +2 -0
  332. package/dist/standalone-publication-path.d.ts.map +1 -0
  333. package/dist/standalone-publication-path.js +6 -0
  334. package/dist/stat-observation.d.ts +11 -0
  335. package/dist/stat-observation.d.ts.map +1 -0
  336. package/dist/stat-observation.js +64 -0
  337. package/dist/strict-file-identity.d.ts.map +1 -1
  338. package/dist/strict-file-identity.js +39 -2
  339. package/dist/symlink-parents.d.ts.map +1 -1
  340. package/dist/symlink-parents.js +7 -2
  341. package/dist/temp-target.d.ts +2 -0
  342. package/dist/temp-target.d.ts.map +1 -1
  343. package/dist/temp-target.js +133 -4
  344. package/dist/temp-workspace-admission.d.ts +22 -0
  345. package/dist/temp-workspace-admission.d.ts.map +1 -0
  346. package/dist/temp-workspace-admission.js +374 -0
  347. package/dist/temp-workspace-child-admission.d.ts +13 -0
  348. package/dist/temp-workspace-child-admission.d.ts.map +1 -0
  349. package/dist/temp-workspace-child-admission.js +95 -0
  350. package/dist/temp-workspace-descriptor.d.ts +48 -0
  351. package/dist/temp-workspace-descriptor.d.ts.map +1 -0
  352. package/dist/temp-workspace-descriptor.js +363 -0
  353. package/dist/temp-workspace-identity.d.ts +15 -0
  354. package/dist/temp-workspace-identity.d.ts.map +1 -0
  355. package/dist/temp-workspace-identity.js +41 -0
  356. package/dist/temp-workspace-owner.d.ts +7 -6
  357. package/dist/temp-workspace-owner.d.ts.map +1 -1
  358. package/dist/temp-workspace-owner.js +121 -61
  359. package/dist/temp-workspace-permissions.d.ts +4 -0
  360. package/dist/temp-workspace-permissions.d.ts.map +1 -0
  361. package/dist/temp-workspace-permissions.js +32 -0
  362. package/dist/temp-workspace-types.d.ts +40 -0
  363. package/dist/temp-workspace-types.d.ts.map +1 -0
  364. package/dist/temp-workspace-types.js +1 -0
  365. package/dist/temp.d.ts +1 -1
  366. package/dist/temp.d.ts.map +1 -1
  367. package/dist/temp.js +1 -1
  368. package/dist/test-hooks.d.ts +7 -0
  369. package/dist/test-hooks.d.ts.map +1 -1
  370. package/dist/text-atomic.d.ts.map +1 -1
  371. package/dist/text-atomic.js +3 -1
  372. package/dist/trash.d.ts.map +1 -1
  373. package/dist/trash.js +54 -8
  374. package/dist/walk.d.ts.map +1 -1
  375. package/dist/walk.js +20 -16
  376. package/dist/windows-owner.d.ts.map +1 -1
  377. package/dist/windows-owner.js +5 -0
  378. package/dist/windows-path-alias.d.ts +39 -0
  379. package/dist/windows-path-alias.d.ts.map +1 -0
  380. package/dist/windows-path-alias.js +153 -0
  381. package/docs/advanced.md +28 -1
  382. package/docs/archive.md +29 -2
  383. package/docs/atomic.md +27 -3
  384. package/docs/contributing.md +4 -1
  385. package/docs/copy.md +4 -1
  386. package/docs/durability.md +18 -2
  387. package/docs/errors.md +14 -6
  388. package/docs/file-store.md +30 -3
  389. package/docs/filename.md +21 -7
  390. package/docs/guest.md +5 -0
  391. package/docs/install-path.md +61 -15
  392. package/docs/install.md +12 -8
  393. package/docs/json-store.md +5 -1
  394. package/docs/json.md +11 -0
  395. package/docs/mutation-policy-proof.md +65 -0
  396. package/docs/native-helper.md +28 -9
  397. package/docs/native.md +46 -10
  398. package/docs/output.md +33 -11
  399. package/docs/path-prefix.md +64 -0
  400. package/docs/path-suffix-aliases.md +159 -0
  401. package/docs/path.md +12 -1
  402. package/docs/permissions.md +26 -1
  403. package/docs/private-file-store.md +14 -0
  404. package/docs/public-api.md +14 -0
  405. package/docs/quickstart.md +1 -1
  406. package/docs/reading.md +8 -1
  407. package/docs/root.md +21 -4
  408. package/docs/secret-file.md +3 -0
  409. package/docs/secure-file.md +21 -16
  410. package/docs/security-model.md +76 -9
  411. package/docs/sidecar-lock.md +28 -1
  412. package/docs/store.md +27 -2
  413. package/docs/temp.md +262 -34
  414. package/docs/test-hooks.md +14 -0
  415. package/docs/writing.md +38 -8
  416. package/package.json +10 -10
package/docs/errors.md CHANGED
@@ -38,6 +38,14 @@ class FsSafeError extends Error {
38
38
 
39
39
  `cause` is available through the standard `Error` `cause` property when the failure was triggered by a `NodeJS.ErrnoException` (e.g. a wrapped `EACCES`). Inspect it for the original `code` / `errno` / `syscall` if you need finer-grained reporting.
40
40
 
41
+ Guarded write preparation describes permission, read-only filesystem, and disk-space
42
+ failures with messages such as `permission denied (EACCES)` or
43
+ `no space left on device (ENOSPC)`. Other errno failures include their code in
44
+ `filesystem write failed (EIO)`. These wrappers retain the existing `invalid-path`
45
+ code and `policy` category for compatibility, along with the original `cause`;
46
+ they do not expose native message text or paths. Already-classified `FsSafeError`
47
+ instances and missing-path errors keep their existing classification.
48
+
41
49
  `details` is an operation-specific receipt, not an alternate error code. For
42
50
  example, `publishFileExclusive()` uses it to report the failing phase, created
43
51
  target identity, cleanup decision, and failed directory-sync outcome. Narrow
@@ -114,7 +122,7 @@ type FsSafeErrorCode =
114
122
  | `device-path` | A read/open target is a known unsafe device or process-fd path. | `/dev/zero`, `/dev/random`, `/dev/stdin`, `/dev/fd/*`, `/proc/*/fd/*`, or a Windows reserved device name. |
115
123
  | `hardlink` | Read or copy with `hardlinks: "reject"` saw `nlink > 1`. | File is hardlinked — possibly an alias of an out-of-tree inode. |
116
124
  | `helper-failed` | A native mechanism or multi-step operational helper failed. | Inspect `cause` and any operation-specific `details`; retrying may be unsafe if the operation partially completed. |
117
- | `helper-unavailable` | Required native binding could not be loaded. | Unsupported platform, omitted/missing/incompatible platform package, or `FS_SAFE_NATIVE_MODE=require`. `auto` falls back where possible; `require` fails closed. |
125
+ | `helper-unavailable` | A required native binding or bounded primitive could not be loaded. | Unsupported platform, omitted/missing/incompatible platform package, `FS_SAFE_NATIVE_MODE=off`, or a no-clobber `Root.move()` without safe native parent admission. `auto` falls back only where a safe fallback exists. |
118
126
  | `insecure-permissions` | A secure file or path permission check found a mode/ACL that allows broader access than requested. | File or directory is group/world writable/readable; Windows ACL grants broad read. |
119
127
  | `invalid-path` | Input was empty, contained NUL, was an unparseable URL, or otherwise unusable; a FileStore key used a noncanonical spelling. | Noncanonical FileStore aliases, backslashes, or complete parent segments; a network path on Windows; a drive-relative segment in a portable relative path or store key; or a leading drive-relative spelling such as `C:name` used as a Root destination. Existing-object Root lookups retain broader confined path compatibility, including legal POSIX drive-like names. |
120
128
  | `not-empty` | Nonrecursive `remove()` on a non-empty directory, or new children appeared during recursive removal. | Use bounded `recursive: true` removal or coordinate concurrent writers. |
@@ -136,11 +144,11 @@ type FsSafeErrorCode =
136
144
 
137
145
  Secret writes reject invalid `mode` / `dirMode` values with `invalid-path` before directory creation. Existing secret directories with a mode different from the requested `dirMode` report `insecure-permissions` without chmod; a created directory whose descriptor ownership no longer matches its initializing effective user reports `not-owned`.
138
146
 
139
- Pathname `sha256File()` also reports `path-mismatch` when pre-open, descriptor,
140
- or current-path identity remains unknown after one bounded Windows retry, even
141
- if the file is benign. It never reopens to recover identity. Preview symlinks
142
- report `symlink`, preview or descriptor non-files report `not-file`, and a
143
- current-path symlink or non-file reports `path-mismatch`.
147
+ Pathname `sha256File()` and `sha256FileSync()` also report `path-mismatch` when
148
+ pre-open, descriptor, or current-path identity remains unknown after one bounded
149
+ Windows retry, even if the file is benign. Neither reopens to recover identity.
150
+ Preview symlinks report `symlink`, preview or descriptor non-files report
151
+ `not-file`, and a current-path symlink or non-file reports `path-mismatch`.
144
152
 
145
153
  ## Branching
146
154
 
@@ -90,13 +90,16 @@ or converts one caller-supplied key onto another:
90
90
  are rejected.
91
91
  - Windows drive-relative segments such as `C:name` or `C:` are rejected
92
92
  anywhere in a key, including `a/C:name`.
93
+ - On Windows, every other colon is rejected too, preventing a key from naming
94
+ an NTFS alternate stream or directory-index alias. POSIX keeps accepting
95
+ ordinary colon-bearing segments that are not drive-relative spellings.
93
96
  - No segment may end in an ASCII dot or space.
94
97
 
95
98
  Violations report `invalid-path` when key validation is reached. Ordinary nested
96
99
  keys, NFC Unicode such as `café/日本語.txt`, `.hidden`, `a..b`, and internal spaces
97
- such as `internal space/a b.txt` are accepted. Colons elsewhere, such as the
98
- timestamp in `logs/2026-08-02T10:30:00Z.log`, remain lexically valid; filesystem
99
- success still depends on the platform and the underlying Root policies.
100
+ such as `internal space/a b.txt` are accepted. On POSIX, colons elsewhere, such
101
+ as the timestamp in `logs/2026-08-02T10:30:00Z.log`, remain lexically valid;
102
+ Windows rejects that spelling as stream syntax.
100
103
 
101
104
  Validation retains each method's operation order. Async reads, `exists`, and
102
105
  `remove` open the root first: if the root is missing, strict methods report
@@ -136,6 +139,24 @@ its exact publication identity cannot be verified. There is no equal-content
136
139
  fallback. The published entry remains present, so callers must inspect or
137
140
  recover that outcome instead of assuming the write did not occur.
138
141
 
142
+ Synchronous directory creation retains exact bigint receipts for the store root
143
+ and every parent component. On POSIX, an existing or newly created directory
144
+ whose complete requested mode differs is reopened without following the final
145
+ name, checked against its receipt and parent chain, and finalized through that
146
+ descriptor. A concurrent root or parent replacement is rejected without
147
+ applying the mode to the replacement. Directories already at the requested mode
148
+ skip the descriptor and mode operation. Windows retains its bounded `mkdir`
149
+ mode request and identity checks without relying on directory descriptors or a
150
+ pathname `chmod`, because Node does not enforce POSIX directory modes there.
151
+
152
+ Node does not expose a portable, `fchmod`-capable search-only directory
153
+ descriptor on Linux. If a mismatched existing directory, or one created under
154
+ an owner-read-removing umask, cannot be opened for reading, the synchronous
155
+ store therefore fails closed with `permission-unverified`; it never falls back
156
+ to pathname `chmod`. On supported macOS x64/arm64 hosts it also tries an
157
+ `O_SEARCH` descriptor, so owner-searchable directories can still be repaired.
158
+ Directories with neither usable read nor search access remain fail-closed.
159
+
139
160
  | Method | Durability support |
140
161
  |---|---|
141
162
  | `write`, `writeText`, `writeJson` (async and sync) | Per-call option overrides store default. |
@@ -256,6 +277,12 @@ type FileStorePruneOptions = {
256
277
 
257
278
  Symlinks are skipped. The walk is best-effort — failures on individual entries don't abort the whole prune. Compares against `mtimeMs`.
258
279
 
280
+ Pruning rechecks that a selected entry is still a regular file and still expired
281
+ immediately before guarded removal. Fresh replacements and in-place timestamp
282
+ refreshes are preserved; replacements that are themselves expired remain
283
+ eligible. This does not require read permission. The existing best-effort
284
+ external-process race window after dispatch still applies.
285
+
259
286
  ## Difference from `Root`
260
287
 
261
288
  | `FileStore` | `Root` |
package/docs/filename.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Filenames
2
2
 
3
- `sanitizeUntrustedFileName(name, fallback)` reduces a filename string from an untrusted source to one traversal-free path segment. Use it as a thin first pass before storing user-supplied names; pair with [`safeDirName`](install-path.md#safedirname) when you need stricter directory-name handling.
3
+ `sanitizeUntrustedFileName(name, fallback)` reduces a filename string from an untrusted source to one traversal-free path segment. Use it as a thin first pass before storing user-supplied names; use [`safePathSegmentHashedV2`](install-path.md#safepathsegmenthashedv2) when mapping untrusted install IDs to separate directory names.
4
4
 
5
5
  ```ts
6
6
  import { sanitizeUntrustedFileName } from "@openclaw/fs-safe/advanced";
@@ -17,15 +17,27 @@ function sanitizeUntrustedFileName(fileName: string, fallbackName: string): stri
17
17
 
18
18
  ## What it does
19
19
 
20
- In order:
20
+ The primary name goes through this pipeline first:
21
21
 
22
- 1. **Trim** whitespace. If the result is empty, return `fallbackName`.
22
+ 1. **Trim** whitespace. An empty result is unusable.
23
23
  2. **Strip path components.** Apply `path.posix.basename` then `path.win32.basename` so neither `foo/bar.txt` nor `foo\bar.txt` survives — only the final segment remains.
24
24
  3. **Strip non-portable characters.** C0/C1 controls (`0x00`–`0x1f`, `0x7f`–`0x9f`) and the Windows-invalid set `< > : " / \\ | ? *` are removed on every platform.
25
25
  4. **Trim again.**
26
- 5. If the result is empty, `"."`, or `".."`, return `fallbackName`.
27
- 6. **Suffix Windows reserved basenames.** Compare the part before the first `.` case-insensitively with the Windows device-name set, including `CON`, `PRN`, `AUX`, `NUL`, `CLOCK$`, `CONIN$`, `CONOUT$`, `COM1..9`, `LPT1..9`, and their superscript `¹`, `²`, and `³` variants. Windows-ignored spaces and dots at the end of that basename do not disguise a device name. A match gains `_` before its extension, preserving the original case and extension on every platform.
28
- 7. **Truncate.** If the cleaned segment is longer than 200 UTF-16 code units, take up to the first 200 without splitting a valid Unicode surrogate pair.
26
+ 5. An empty result, `"."`, or `".."` is unusable.
27
+ 6. **Truncate.** If the cleaned segment is longer than 200 UTF-16 code units, take up to the first 200 without splitting a valid Unicode surrogate pair.
28
+ 7. **Make the final name device-safe.** Compare the part before the first `.` case-insensitively with the Windows device-name set, including `CON`, `PRN`, `AUX`, `NUL`, `CLOCK$`, `CONIN$`, `CONOUT$`, `COM1..9`, `LPT1..9`, and their superscript `¹`, `²`, and `³` variants. Windows-ignored spaces and dots at the end of that basename do not disguise a device name. A match gains `_` before its extension on every platform. If the suffix would exceed 200 code units, the unsuffixed tail is shortened first, so truncation cannot recreate a device name.
29
+
30
+ Only when the primary name is unusable does `fallbackName` go through the same
31
+ nonrecursive pipeline. A safe fallback is preserved exactly; path components,
32
+ controls, reserved device names, and overlong fallback names receive the same
33
+ treatment as the primary name. If both candidates are unusable, the function
34
+ returns the fixed safe literal `"file"`.
35
+
36
+ If truncation itself exposes a reserved-device basename after Windows ignores
37
+ trailing spaces or dots, the result is shortened once more and receives the
38
+ same underscore suffix. A name that reaches the sanitization branch therefore
39
+ remains at most 200 UTF-16 code units and is never a Windows reserved-device
40
+ alias. Fallback names pass through the same checks before they can be returned.
29
41
 
30
42
  That's it. The function stays intentionally small: it removes traversal and
31
43
  the most obvious cross-platform device and character hazards, but it is not a
@@ -41,6 +53,8 @@ sanitizeUntrustedFileName("a\u0000b\tc", "upload"); // "abc"
41
53
  sanitizeUntrustedFileName(" ", "fallback"); // "fallback"
42
54
  sanitizeUntrustedFileName(".", "fallback"); // "fallback"
43
55
  sanitizeUntrustedFileName("..", "fallback"); // "fallback"
56
+ sanitizeUntrustedFileName("<>", "../../etc/passwd"); // "passwd"
57
+ sanitizeUntrustedFileName("<>", "../.."); // "file"
44
58
  sanitizeUntrustedFileName("CON", "fallback"); // "CON_"
45
59
  sanitizeUntrustedFileName("nul.txt", "fallback"); // "nul_.txt"
46
60
  sanitizeUntrustedFileName("aux.c", "fallback"); // "aux_.c"
@@ -92,5 +106,5 @@ await fs.write(`uploads/${safe}`, body); // fs is a Root() handle; rejects trave
92
106
 
93
107
  ## See also
94
108
 
95
- - [Install path helpers](install-path.md) — `safeDirName`, `safePathSegmentHashed` for directory-segment sanitization.
109
+ - [Install path helpers](install-path.md) — legacy directory-segment sanitizers and `safePathSegmentHashedV2` for untrusted install IDs.
96
110
  - [`root()`](root.md) — the boundary you'll write into after sanitizing.
package/docs/guest.md CHANGED
@@ -113,6 +113,11 @@ prefixes and use short random suffixes independent of the destination basename,
113
113
  so legal names near the filesystem's component limit also work for writes and
114
114
  cross-device moves.
115
115
 
116
+ Cross-device symlink moves create the new link in a private destination-side
117
+ staging directory before atomically replacing the destination. Link creation or
118
+ publication failure preserves the existing destination and source link; ordinary
119
+ failure cleanup removes the staging directory.
120
+
116
121
  Cross-device directory moves build a copy manifest and check it during source
117
122
  cleanup. Source changes can leave the published destination and some or all
118
123
  of the source. Regular-file and symlink move fallbacks unlink the source
@@ -8,6 +8,7 @@ import {
8
8
  resolveSafeInstallDir,
9
9
  safeDirName,
10
10
  safePathSegmentHashed,
11
+ safePathSegmentHashedV2,
11
12
  } from "@openclaw/fs-safe/advanced";
12
13
  ```
13
14
 
@@ -22,7 +23,7 @@ function resolveSafeInstallDir(params: {
22
23
  }): { ok: true; path: string } | { ok: false; error: string };
23
24
  ```
24
25
 
25
- Computes the absolute install directory for `id` under `baseDir`, after running `id` through `nameEncoder` (`safeDirName` by default). Verifies the result stays inside `baseDir` — anything that would escape returns `{ ok: false, error: invalidNameMessage }`.
26
+ Computes the absolute install directory for `id` under `baseDir`, after running `id` through `nameEncoder` (`safeDirName` by default). Verifies the result stays inside `baseDir` — anything that would escape returns `{ ok: false, error: invalidNameMessage }`. On Windows, contained alternate-stream and filesystem-namespace aliases are rejected with the same result.
26
27
 
27
28
  ```ts
28
29
  const r = resolveSafeInstallDir({
@@ -35,14 +36,17 @@ if (!r.ok) return reply(400, r.error);
35
36
  await fs.mkdir(r.path, { recursive: true });
36
37
  ```
37
38
 
38
- For ids whose default-sanitized form might collide (e.g. `"foo/bar"` and `"foo\\bar"` both map to `"foo__bar"`), pass `nameEncoder: safePathSegmentHashed` to append a content hash:
39
+ For untrusted IDs that must occupy separate install directories, pass
40
+ `nameEncoder: safePathSegmentHashedV2`. The default `safeDirName` and legacy
41
+ `safePathSegmentHashed` can map distinct IDs to the same directory; the boundary
42
+ check does not establish which ID owns an existing directory.
39
43
 
40
44
  ```ts
41
45
  const r = resolveSafeInstallDir({
42
- baseDir: "/srv/plugins",
46
+ baseDir: "/srv/plugins-v2",
43
47
  id: untrustedId,
44
48
  invalidNameMessage: "invalid plugin name",
45
- nameEncoder: safePathSegmentHashed,
49
+ nameEncoder: safePathSegmentHashedV2,
46
50
  });
47
51
  ```
48
52
 
@@ -60,7 +64,7 @@ function assertCanonicalPathWithinBase(params: {
60
64
  }): Promise<void>;
61
65
  ```
62
66
 
63
- Throws if the candidate resolves outside `baseDir` after `realpath`. The `boundaryLabel` is included in the error message ("Invalid path: must stay within {boundaryLabel}").
67
+ Throws if the candidate resolves outside `baseDir` after `realpath`. On Windows it also throws for alternate-stream or filesystem-namespace aliases in the base, candidate, or canonical path. The `boundaryLabel` is included in the containment-shaped error message ("Invalid path: must stay within {boundaryLabel}").
64
68
 
65
69
  ```ts
66
70
  await assertCanonicalPathWithinBase({
@@ -87,11 +91,48 @@ safeDirName(""); // ""
87
91
 
88
92
  `safeDirName` does *not* try to be exhaustive about Windows-reserved names or special characters. It is purely a separator-stripping pass — `resolveSafeInstallDir` adds the boundary check on top so an `"../../etc"` input cannot escape `baseDir`.
89
93
 
90
- For stricter sanitization, use `safePathSegmentHashed`.
94
+ Use `safePathSegmentHashedV2` when distinct untrusted IDs need separate names.
95
+
96
+ ### `safePathSegmentHashedV2`
97
+
98
+ ```ts
99
+ function safePathSegmentHashedV2(input: string): string;
100
+ ```
101
+
102
+ Hashes every trimmed ID, including ordinary short names, into `id-v2-` followed
103
+ by 64 lowercase hexadecimal SHA-256 digits. The result is always 70 ASCII bytes,
104
+ contains no separators, and avoids Windows device names and ignored suffixes.
105
+ There is no readable prefix to truncate and no unchanged-name branch. Distinct
106
+ trimmed IDs, including an ID that looks like an encoded output, remain distinct
107
+ unless their full SHA-256 digests collide. The lowercase ASCII output also
108
+ preserves that distinction on case-insensitive and Unicode-normalizing volumes.
109
+
110
+ The stable V2 digest recipe is SHA-256 of the UTF-8 bytes of
111
+ `"@openclaw/fs-safe:install-path:v2\0"`, followed by the UTF-16LE bytes of
112
+ `input.trim()`, without a byte-order mark. The NUL-terminated prefix separates
113
+ this use of SHA-256 from other hash domains. UTF-16LE preserves exact JavaScript
114
+ code units, including lone surrogates. Inputs are not case-folded or Unicode
115
+ normalized. Surrounding whitespace, as removed by JavaScript `String.trim()`,
116
+ is the only intentional equivalence; internal whitespace remains significant.
117
+
118
+ ```ts
119
+ const segment = safePathSegmentHashedV2("plugin/v1"); // id-v2-<64 hex digits>
120
+ safePathSegmentHashedV2(" plugin/v1 ") === segment; // true
121
+ safePathSegmentHashedV2("Plugin/v1") === segment; // false
122
+ ```
123
+
124
+ This encoder computes a name; it does not authorize access or prove ownership
125
+ of a directory. Store the original trimmed ID in application-owned metadata and
126
+ verify it before reusing an existing install directory. Keep each install tree
127
+ on one encoding version. Switching to V2 changes existing paths: use a new base
128
+ directory or explicitly migrate directories after verifying their recorded IDs.
129
+ Do not silently fall back to a legacy path when the V2 path is missing.
91
130
 
92
131
  ### `safePathSegmentHashed`
93
132
 
94
- Returns a directory-safe segment **plus** a short content hash when sanitization changed the input or when the safe form is too long. Use this when input collisions matter:
133
+ Legacy readable encoding retained for path compatibility. It appends a short
134
+ content hash when sanitization changed the input or when the safe form is too
135
+ long; ordinary short names remain unchanged. Use V2 for new untrusted-ID mappings.
95
136
 
96
137
  ```ts
97
138
  safePathSegmentHashed("plugin-v1"); // "plugin-v1" (unchanged short input)
@@ -104,31 +145,35 @@ safePathSegmentHashed("."); // "skill-cdb4ee2aea"
104
145
 
105
146
  The sanitization is more aggressive than `safeDirName`: any character not in `[A-Za-z0-9._-]` becomes `-`, runs of `-` collapse, leading and trailing `-` are stripped, the empty/`.`/`..` fallback is `"skill"`. Long results are truncated to 50 chars before the hash is appended.
106
147
 
107
- The suffix is the first 10 hex characters of `sha256(trimmedInput)`. It makes
108
- collisions between distinct trimmed inputs unlikely, but it is a 40-bit
109
- identifier rather than a mathematical uniqueness guarantee. Inputs that differ
110
- only by surrounding whitespace intentionally map to the same output.
148
+ The suffix is the first 10 hex characters of `sha256(trimmedInput)`. This is a
149
+ 40-bit identifier, and the hash is not applied to every ID: short generated
150
+ outputs overlap with accepted literal inputs. Case variants can also share a
151
+ directory on case-insensitive filesystems. Distinct IDs can therefore alias
152
+ without a hash collision. Do not use this legacy encoder as an identity or
153
+ authorization boundary for untrusted IDs. Inputs that differ only by surrounding
154
+ whitespace intentionally map to the same output. Its output and the default
155
+ encoder selected by `resolveSafeInstallDir` remain unchanged for compatibility.
111
156
 
112
157
  ## Common patterns
113
158
 
114
159
  ### Install a plugin
115
160
 
116
161
  ```ts
117
- import { resolveSafeInstallDir, assertCanonicalPathWithinBase, safePathSegmentHashed } from "@openclaw/fs-safe/advanced";
162
+ import { resolveSafeInstallDir, assertCanonicalPathWithinBase, safePathSegmentHashedV2 } from "@openclaw/fs-safe/advanced";
118
163
  import { extractArchive } from "@openclaw/fs-safe/archive";
119
164
  import fs from "node:fs/promises";
120
165
 
121
166
  const r = resolveSafeInstallDir({
122
- baseDir: "/srv/plugins",
167
+ baseDir: "/srv/plugins-v2",
123
168
  id: untrustedName,
124
169
  invalidNameMessage: "invalid plugin name",
125
- nameEncoder: safePathSegmentHashed,
170
+ nameEncoder: safePathSegmentHashedV2,
126
171
  });
127
172
  if (!r.ok) return reply(400, r.error);
128
173
 
129
174
  await fs.mkdir(r.path, { recursive: true, mode: 0o755 });
130
175
  await assertCanonicalPathWithinBase({
131
- baseDir: "/srv/plugins",
176
+ baseDir: "/srv/plugins-v2",
132
177
  candidatePath: r.path,
133
178
  boundaryLabel: "plugin install dir",
134
179
  });
@@ -148,6 +193,7 @@ const snap = resolveSafeInstallDir({
148
193
  baseDir: "/srv/snapshots",
149
194
  id: `${runId}-${version}`,
150
195
  invalidNameMessage: "invalid snapshot id",
196
+ nameEncoder: safePathSegmentHashedV2,
151
197
  });
152
198
  if (!snap.ok) throw new Error(snap.error);
153
199
  await fs.mkdir(snap.path, { recursive: true });
package/docs/install.md CHANGED
@@ -54,7 +54,10 @@ also rejects canonicalization when the addon or its canonicalizer is unavailable
54
54
  Node and Windows use their existing runtime canonicalizers. On Windows, Bun's
55
55
  recursive directory creation receives an absolute spelling that preserves raw
56
56
  path components, working around its rejection of existing relative `.` and `..`
57
- directories. Public paths and caller-supplied filesystem adapters remain unchanged.
57
+ directories. Windows native descriptor-relative operations require Bun to expose
58
+ the paired libuv descriptor bridge from its host executable; a missing or partial
59
+ bridge fails explicitly with `ENOTSUP`. Public paths and caller-supplied filesystem
60
+ adapters remain unchanged.
58
61
 
59
62
  The upstream fix is tracked in [Bun #42374](https://github.com/oven-sh/bun/pull/42374).
60
63
  The adapter can be removed when the supported Bun baseline includes that fix.
@@ -124,9 +127,10 @@ the matching binary. Consumers do not run a native build, download code at
124
127
  runtime, or execute a postinstall step. Omitting optional dependencies keeps
125
128
  non-archive fallback-capable operations working in `auto` or `off`. Native-only
126
129
  features, including strict owned-tree temp cleanup, retained-directory staging,
127
- atomic `rename-noreplace`, zstd/bzip2 TAR handling, and Windows private-directory
128
- creation, remain unavailable. Operations needing the binding in `require` mode fail with
129
- `helper-unavailable` when the matching package is absent or incompatible.
130
+ atomic `rename-noreplace` (including the default no-clobber `Root.move()`),
131
+ zstd/bzip2 TAR handling, and Windows private-directory creation, remain
132
+ unavailable. Operations without a safe fallback fail with `helper-unavailable`
133
+ when the matching package is absent, incompatible, or disabled.
130
134
 
131
135
  Upgrading an existing 0.5 consumer? Follow [Migrating to 0.6](migrating-to-0.6.md)
132
136
  before deploying with native mode `require` or native-only features.
@@ -135,15 +139,15 @@ before deploying with native mode `require` or native-only features.
135
139
 
136
140
  The platform native binaries provide fd-relative open/link/mkdir primitives,
137
141
  atomic no-replace rename, and file identity checks. The default is `auto`: use
138
- the matching binary when it loads, otherwise silently keep the guarded
139
- JavaScript path. Platforms without one of the seven published targets therefore
140
- continue through the documented fallback in `auto` mode.
142
+ the matching binary when it loads, otherwise use the guarded JavaScript path
143
+ where a safe fallback exists. Native-only operations fail with
144
+ `helper-unavailable`.
141
145
 
142
146
  ```ts
143
147
  import { configureFsSafeNative } from "@openclaw/fs-safe/config";
144
148
 
145
149
  configureFsSafeNative({ mode: "auto" }); // default
146
- configureFsSafeNative({ mode: "off" }); // guarded JavaScript only
150
+ configureFsSafeNative({ mode: "off" }); // guarded JavaScript; reject native-only operations
147
151
  configureFsSafeNative({ mode: "require" }); // fail closed if unavailable
148
152
  ```
149
153
 
@@ -1,6 +1,6 @@
1
1
  # JSON store
2
2
 
3
- `jsonStore` is exported from `@openclaw/fs-safe/store`. It is the absolute-path
3
+ `jsonStore` is exported from `@openclaw/fs-safe/store`. It is the single-path
4
4
  convenience wrapper for `fileStore(...).json(...)`: a small read-modify-write
5
5
  handle around a single JSON file. It bakes in atomic writes, explicit fallback
6
6
  reads, and optional cross-process locking via
@@ -71,6 +71,10 @@ type JsonStore<T> = {
71
71
 
72
72
  `jsonStore({ filePath })` resolves `rootDir = dirname(filePath)` and calls
73
73
  `fileStore({ rootDir, private: true }).json(basename(filePath), options)`.
74
+ On Windows, the factory rejects NTFS alternate-stream and directory-index
75
+ namespace spellings before preparing its private parent. An ordinary
76
+ drive-relative path is anchored at entry and `store.filePath` exposes the
77
+ resulting absolute path; ordinary colon-bearing POSIX paths remain valid.
74
78
 
75
79
  `durable: false` keeps sibling-temp replace/rename behavior but skips the
76
80
  temp-file and parent-directory `fsync` calls. Use it only for reconstructible
package/docs/json.md CHANGED
@@ -38,6 +38,17 @@ Use `readJson` when missing-or-malformed is a programmer error you want to surfa
38
38
 
39
39
  `JsonFileReadError` carries `cause` so you can inspect whether the underlying failure was an `ENOENT`, a `SyntaxError`, or something else.
40
40
 
41
+ On Windows, filesystem path inputs reject NTFS alternate-stream and
42
+ directory-index namespace spellings such as `file:stream` and
43
+ `dir::$INDEX_ALLOCATION`; ordinary colon-bearing POSIX names remain valid.
44
+ Standalone JSON writers preserve their released support for an ordinary
45
+ drive-relative destination by anchoring its leading drive designator at entry
46
+ without normalizing the remaining suffix. Any additional colon is still
47
+ rejected before filesystem access.
48
+ Strict standalone readers retain `JsonFileReadError` and expose the
49
+ `invalid-path` rejection as its cause, lenient `tryReadJson*` calls return
50
+ `null`, and root-bounded readers report an `open`/`validation` failure.
51
+
41
52
  ## Reading
42
53
 
43
54
  ### `readJson<T>(filePath, options?)`
@@ -0,0 +1,65 @@
1
+ # Hosted mutation-policy proof
2
+
3
+ The `mutation policy public behavior proof` workflow builds the exact event-head
4
+ package and host addon on Node 24 Linux, macOS, and Windows. Each case executes in
5
+ a fresh process, uses a private temporary fixture, and emits only a bounded,
6
+ canonical receipt. POSIX workers require both real and effective non-root UIDs.
7
+
8
+ Receipt schema `fs-safe-mutation-policy-proof-v2` deliberately does not describe
9
+ final directory emptiness as a mutation-dispatch count. Root replacement records
10
+ the unchanged contents of the original and replacement parent; denied redirect
11
+ records rejection and unchanged denied/displaced parents. The existing JS mkdir
12
+ counters name their exact child or next component. None counts native syscalls
13
+ or excludes transient mutations that leave no final trace.
14
+
15
+ ## Additional hosted cases
16
+
17
+ Each POSIX write/create/copy worker, under both native-off and native-require,
18
+ performs an eligible success control, an exact deeper-parent denial after an
19
+ earlier parent is created, a denied redirect after preflight, and a stale-parent
20
+ replacement at the public pre-mkdir authority boundary. Exact directory listings
21
+ and sentinels verify that denied/current/displaced parents contain no prohibited
22
+ child, target, or stage. Copy sources must retain their original bytes.
23
+
24
+ Redirect injection uses the existing public `@openclaw/fs-safe/test-hooks` subpath
25
+ from `dist`, not a mocked native binding. Only these isolated workers enable
26
+ `NODE_ENV=test`. The exact target and single hook visit are required. The hook is
27
+ after policy preflight but before parent admission; it must not be described as
28
+ a post-parent-admission hook. Stale replacement instead uses the public authority
29
+ callback after child-create policy admission, with an existing sentinel parent
30
+ and a still-missing child. Both implementation files that enforce the following
31
+ freshness check are hash-bound. No unchecked callback ordinal chooses a fault.
32
+
33
+ Representative POSIX `Root.write` workers refuse authority before the first mkdir,
34
+ after one parent has been created and before the next mkdir, before staging, and
35
+ immediately before publication. Epoch selection uses actual fixture state. The
36
+ publication refusal requires a real single-link, caller-owned mode-0600 stage
37
+ containing the complete payload while the destination still contains its original
38
+ sentinel. The same rejection object must escape, no callback may follow refusal,
39
+ and the owned stage must be gone before fixture teardown.
40
+
41
+ Windows workers exercise buffer `Root.write` through a stable final-file symlink,
42
+ then refuse before staging a newly created placeholder, before publishing over a
43
+ new placeholder, and before publishing to an existing symlink-selected destination.
44
+ They verify alias binding, destination preservation, observed placeholder/stage
45
+ states, and cleanup before fixture teardown. The default native-off and explicit
46
+ `verify-content-with-lock` native-require configurations both select the existing
47
+ Windows JS buffer writer. The compatibility route is expected not to load the
48
+ addon; the receipt does not mislabel this as native publication or evidence that
49
+ content-verification fallback or lock contention was exercised.
50
+
51
+ ## Bounds and interpretation
52
+
53
+ The receipt remains below 32 KiB, each worker has a 15-second process timeout and
54
+ 4-KiB stdout limit, and the proof step has a six-minute outer timeout. Worker I/O
55
+ and cleanup retain their existing deadlines. The canonical pending/failed receipt
56
+ is preserved if setup, a worker, provenance validation, or emission fails.
57
+ Source, built modules, the public test seam, harness helper, contract tests, and
58
+ host addon are hashed and checked again after workers.
59
+
60
+ These are deterministic representative observations, not an exhaustive race proof
61
+ or syscall audit. Existing hash-bound internal tests remain complementary for
62
+ other awaited-admission, receipt-refresh, and observation-failure interleavings.
63
+ Windows root replacement and POSIX-specific pinned routes are not claimed on
64
+ Windows. Hosted CI and exact artifact inspection are required before relying on
65
+ new receipts; the proof supplies no performance release clearance.
@@ -15,7 +15,7 @@ consumer Rust build.
15
15
  import { configureFsSafeNative } from "@openclaw/fs-safe/config";
16
16
 
17
17
  configureFsSafeNative({ mode: "auto" }); // default
18
- configureFsSafeNative({ mode: "off" }); // guarded JavaScript only
18
+ configureFsSafeNative({ mode: "off" }); // guarded JavaScript; reject native-only operations
19
19
  configureFsSafeNative({ mode: "require" }); // fail closed when the binding is unavailable
20
20
  ```
21
21
 
@@ -25,8 +25,8 @@ The equivalent environment variables are `FS_SAFE_NATIVE_MODE` and `OPENCLAW_FS_
25
25
 
26
26
  | Mode | Behavior |
27
27
  |---|---|
28
- | `auto` | Prefer native primitives when the current platform package loads; otherwise silently use the guarded JavaScript path. |
29
- | `off` | Do not load a native package. Use the guarded JavaScript path deterministically. |
28
+ | `auto` | Prefer native primitives when the current platform package loads; otherwise use guarded JavaScript where a safe fallback exists and reject native-only operations. |
29
+ | `off` | Do not load a native package. Use guarded JavaScript where safe and reject native-only operations deterministically. |
30
30
  | `require` | Throw `FsSafeError("helper-unavailable")` instead of falling back when an operation needs the native binding and it cannot load. |
31
31
 
32
32
  TAR/gzip in the guarded JavaScript path uses a bundled, import-free WASM build
@@ -62,6 +62,20 @@ change the mode policy of existing fallback-capable APIs.
62
62
 
63
63
  ## Native boundary
64
64
 
65
+ The internal Darwin descriptor ACL inspector requires its matching native
66
+ capability in both `auto` and `require`; `off`, a missing package, or an older
67
+ binding without `inspectDarwinAcl` rejects with `helper-unavailable`. Inspection
68
+ failure or malformed facts reject with `permission-unverified`; there is no
69
+ mode-bit or pathname fallback for this capability. Clone admission uses a fused
70
+ descriptor-bound metadata and ACL observation, then compares immutable receipts
71
+ with fresh no-follow pathname identity fences; pathnames never authorize ACL
72
+ state. The payload ACL-clear readback is part of that fused observation. Once
73
+ a clone payload exists, normalization and verification failures become terminal
74
+ `EIO` errors (with the underlying status and detail retained), not capability
75
+ signals that permit an ordinary-copy retry. Checked cleanup cannot undo that
76
+ terminal classification.
77
+ This addition does not change other APIs' native-mode or permission contracts.
78
+
65
79
  The native layer exposes policy-free filesystem mechanisms: beneath-root
66
80
  open/mkdir/link, replace and no-replace rename, identity reads, archive decode/execution,
67
81
  clone/copy/hash workers, POSIX canonicalization, and Windows security descriptor calls. The TypeScript
@@ -70,12 +84,17 @@ normalization, and the decision to fall back.
70
84
 
71
85
  - Linux uses `openat2` with `RESOLVE_BENEATH | RESOLVE_NO_MAGICLINKS`, `renameat`, and `renameat2(RENAME_NOREPLACE)`. Owned-tree cleanup enumerates and unlinks through retained directory descriptors and rejects device crossings.
72
86
  - macOS 15.4 and newer prefer `O_RESOLVE_BENEATH`; older kernels resolve components with `O_NOFOLLOW` and restart in-root symlinks from the pinned root descriptor. Both routes use an `F_GETPATH` post-open escape detector and report `best-effort` because directory rename races are not atomic with that check. Publication uses `renameat` for replacement and `renameatx_np(RENAME_EXCL)` for no-replace; owned-tree cleanup uses descriptor-relative `openat`/`unlinkat`.
73
- - Windows uses handle-relative `NtCreateFile`, rejects reparse points during root-bounded traversal, uses `FileRenameInfoEx` with replacement selected explicitly by the TypeScript policy layer, and deletes owned trees through exact opened handles with `FileDispositionInfoEx`; symlink/reparse entries in owned trees are removed as leaves and never traversed.
74
-
75
- Native primitives back create-only and replacing pinned writes, async sidecar creation,
76
- guarded publication, archive acceleration, and direct Windows ACL operations.
77
- Equivalent JavaScript paths remain available for documented fallback-capable
78
- features. See [Native architecture](native.md#javascript-fallback-guarantees-and-delta)
87
+ - Windows uses handle-relative `NtCreateFile`, rejects reparse points during root-bounded traversal, uses `FileRenameInfoEx` with replacement selected explicitly by the TypeScript policy layer, and deletes owned trees through exact opened handles with `FileDispositionInfoEx`; symlink/reparse entries in owned trees are removed as leaves and never traversed. Descriptors crossing N-API are converted only by the host executable's paired `uv_get_osfhandle` and `uv_open_osfhandle` exports. A runtime without both exports is unsupported for these native operations; the binding never guesses a raw HANDLE or uses a foreign CRT descriptor table.
88
+
89
+ Native primitives back create-only and replacing pinned writes, no-clobber
90
+ `Root.move()`, async sidecar creation, guarded publication, archive acceleration,
91
+ and direct Windows ACL operations. Windows secure-file reads require
92
+ descriptor-bound owner/DACL facts from the current helper; they do not use the
93
+ standalone pathname inspector's command fallback. No-clobber moves fail with
94
+ `helper-unavailable` when descriptor-relative parent admission or the atomic
95
+ no-replace rename is unavailable; they never use a check followed by a replacing
96
+ rename. Equivalent JavaScript paths remain available for documented
97
+ fallback-capable features. See [Native architecture](native.md#javascript-fallback-guarantees-and-delta)
79
98
  for the exact difference.
80
99
 
81
100
  The guarded JavaScript mutation path is detection-based, not containment-atomic.