@openclaw/fs-safe 0.12.0 → 0.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (395) hide show
  1. package/CHANGELOG.md +73 -0
  2. package/README.md +12 -7
  3. package/dist/absolute-path.d.ts.map +1 -1
  4. package/dist/absolute-path.js +32 -11
  5. package/dist/advanced.d.ts +2 -0
  6. package/dist/advanced.d.ts.map +1 -1
  7. package/dist/advanced.js +2 -0
  8. package/dist/archive-input.d.ts.map +1 -1
  9. package/dist/archive-input.js +6 -2
  10. package/dist/archive-merge.d.ts.map +1 -1
  11. package/dist/archive-merge.js +15 -2
  12. package/dist/archive-native.d.ts.map +1 -1
  13. package/dist/archive-native.js +7 -19
  14. package/dist/archive-plan.js +1 -1
  15. package/dist/archive-read.d.ts.map +1 -1
  16. package/dist/archive-read.js +18 -13
  17. package/dist/archive-staging.d.ts.map +1 -1
  18. package/dist/archive-staging.js +43 -10
  19. package/dist/archive-tar-inspect.d.ts.map +1 -1
  20. package/dist/archive-tar-inspect.js +4 -1
  21. package/dist/archive-zip-admission.d.ts +1 -1
  22. package/dist/archive-zip-admission.d.ts.map +1 -1
  23. package/dist/archive-zip-admission.js +2 -2
  24. package/dist/archive-zip-directory.d.ts +3 -0
  25. package/dist/archive-zip-directory.d.ts.map +1 -1
  26. package/dist/archive-zip-directory.js +13 -2
  27. package/dist/archive-zip-loader.d.ts +3 -2
  28. package/dist/archive-zip-loader.d.ts.map +1 -1
  29. package/dist/archive-zip-loader.js +30 -4
  30. package/dist/archive-zip-manifest.d.ts +5 -0
  31. package/dist/archive-zip-manifest.d.ts.map +1 -0
  32. package/dist/archive-zip-manifest.js +22 -0
  33. package/dist/archive-zip-names.d.ts +6 -1
  34. package/dist/archive-zip-names.d.ts.map +1 -1
  35. package/dist/archive-zip-names.js +35 -14
  36. package/dist/archive-zip-preflight.d.ts.map +1 -1
  37. package/dist/archive-zip-preflight.js +3 -2
  38. package/dist/archive.d.ts.map +1 -1
  39. package/dist/archive.js +9 -4
  40. package/dist/copy-file-input.d.ts +1 -1
  41. package/dist/copy-file-input.d.ts.map +1 -1
  42. package/dist/copy-file-input.js +2 -0
  43. package/dist/darwin-acl.d.ts +4 -0
  44. package/dist/darwin-acl.d.ts.map +1 -0
  45. package/dist/darwin-acl.js +24 -0
  46. package/dist/deny-mutations.d.ts.map +1 -1
  47. package/dist/deny-mutations.js +8 -2
  48. package/dist/directory-durability.d.ts.map +1 -1
  49. package/dist/directory-durability.js +67 -23
  50. package/dist/directory-entry-path.d.ts +3 -0
  51. package/dist/directory-entry-path.d.ts.map +1 -0
  52. package/dist/directory-entry-path.js +21 -0
  53. package/dist/directory-guard.d.ts +17 -1
  54. package/dist/directory-guard.d.ts.map +1 -1
  55. package/dist/directory-guard.js +134 -48
  56. package/dist/directory-mode-node.d.ts +12 -0
  57. package/dist/directory-mode-node.d.ts.map +1 -1
  58. package/dist/directory-mode-node.js +102 -4
  59. package/dist/effective-uid.d.ts +2 -0
  60. package/dist/effective-uid.d.ts.map +1 -0
  61. package/dist/effective-uid.js +25 -0
  62. package/dist/file-handle-transfer.d.ts.map +1 -1
  63. package/dist/file-handle-transfer.js +98 -27
  64. package/dist/file-hash.d.ts.map +1 -1
  65. package/dist/file-hash.js +3 -0
  66. package/dist/file-lock-sync.d.ts.map +1 -1
  67. package/dist/file-lock-sync.js +36 -6
  68. package/dist/file-lock.d.ts.map +1 -1
  69. package/dist/file-lock.js +29 -9
  70. package/dist/file-observation.d.ts +1 -1
  71. package/dist/file-observation.d.ts.map +1 -1
  72. package/dist/file-store-boundary.d.ts +6 -2
  73. package/dist/file-store-boundary.d.ts.map +1 -1
  74. package/dist/file-store-boundary.js +20 -65
  75. package/dist/file-store-copy-source.d.ts +5 -0
  76. package/dist/file-store-copy-source.d.ts.map +1 -0
  77. package/dist/file-store-copy-source.js +31 -0
  78. package/dist/file-store-path.d.ts.map +1 -1
  79. package/dist/file-store-path.js +4 -1
  80. package/dist/file-store-sync-directory.d.ts +16 -0
  81. package/dist/file-store-sync-directory.d.ts.map +1 -0
  82. package/dist/file-store-sync-directory.js +349 -0
  83. package/dist/file-store.d.ts.map +1 -1
  84. package/dist/file-store.js +11 -28
  85. package/dist/filename.d.ts.map +1 -1
  86. package/dist/filename.js +65 -20
  87. package/dist/fs.d.ts.map +1 -1
  88. package/dist/fs.js +3 -2
  89. package/dist/guarded-mkdir.d.ts +8 -0
  90. package/dist/guarded-mkdir.d.ts.map +1 -1
  91. package/dist/guarded-mkdir.js +176 -27
  92. package/dist/guest.d.ts.map +1 -1
  93. package/dist/guest.js +4 -1
  94. package/dist/home-dir.d.ts.map +1 -1
  95. package/dist/home-dir.js +73 -10
  96. package/dist/install-path.d.ts.map +1 -1
  97. package/dist/install-path.js +54 -19
  98. package/dist/json-durable-queue-ownership.d.ts +6 -0
  99. package/dist/json-durable-queue-ownership.d.ts.map +1 -1
  100. package/dist/json-durable-queue-ownership.js +89 -42
  101. package/dist/json-durable-queue-paths.d.ts +11 -0
  102. package/dist/json-durable-queue-paths.d.ts.map +1 -0
  103. package/dist/json-durable-queue-paths.js +42 -0
  104. package/dist/json-durable-queue-read.d.ts.map +1 -1
  105. package/dist/json-durable-queue-read.js +2 -0
  106. package/dist/json-durable-queue.d.ts.map +1 -1
  107. package/dist/json-durable-queue.js +50 -58
  108. package/dist/json-store.d.ts.map +1 -1
  109. package/dist/json-store.js +5 -1
  110. package/dist/json.d.ts.map +1 -1
  111. package/dist/json.js +54 -22
  112. package/dist/local-file-access.d.ts.map +1 -1
  113. package/dist/local-file-access.js +4 -0
  114. package/dist/local-file-descriptor.d.ts +17 -0
  115. package/dist/local-file-descriptor.d.ts.map +1 -0
  116. package/dist/local-file-descriptor.js +84 -0
  117. package/dist/local-roots.d.ts.map +1 -1
  118. package/dist/local-roots.js +35 -7
  119. package/dist/move-path-cleanup.d.ts +2 -0
  120. package/dist/move-path-cleanup.d.ts.map +1 -1
  121. package/dist/move-path-cleanup.js +44 -18
  122. package/dist/move-path.d.ts.map +1 -1
  123. package/dist/move-path.js +31 -6
  124. package/dist/native-binding.d.ts +17 -0
  125. package/dist/native-binding.d.ts.map +1 -1
  126. package/dist/native-binding.js +8 -1
  127. package/dist/native-directory-observation.d.ts +17 -0
  128. package/dist/native-directory-observation.d.ts.map +1 -0
  129. package/dist/native-directory-observation.js +37 -0
  130. package/dist/native-operations.d.ts.map +1 -1
  131. package/dist/native-operations.js +6 -4
  132. package/dist/native-parent-admission.d.ts +32 -0
  133. package/dist/native-parent-admission.d.ts.map +1 -0
  134. package/dist/native-parent-admission.js +127 -0
  135. package/dist/native-pinned-write-windows.d.ts +0 -1
  136. package/dist/native-pinned-write-windows.d.ts.map +1 -1
  137. package/dist/native-pinned-write-windows.js +8 -9
  138. package/dist/native-pinned-write.d.ts.map +1 -1
  139. package/dist/native-pinned-write.js +314 -42
  140. package/dist/native-staged-file.d.ts +4 -4
  141. package/dist/native-staged-file.d.ts.map +1 -1
  142. package/dist/native-staged-file.js +20 -10
  143. package/dist/native.d.ts +1 -1
  144. package/dist/native.d.ts.map +1 -1
  145. package/dist/native.js +7 -2
  146. package/dist/output.d.ts.map +1 -1
  147. package/dist/output.js +15 -5
  148. package/dist/overwrite-file-handle.d.ts.map +1 -1
  149. package/dist/overwrite-file-handle.js +5 -1
  150. package/dist/owner-dacl.d.ts.map +1 -1
  151. package/dist/owner-dacl.js +2 -0
  152. package/dist/path-policy.d.ts.map +1 -1
  153. package/dist/path-policy.js +7 -2
  154. package/dist/path-prefix.d.ts +7 -0
  155. package/dist/path-prefix.d.ts.map +1 -0
  156. package/dist/path-prefix.js +82 -0
  157. package/dist/path-scope-lexical.d.ts.map +1 -1
  158. package/dist/path-scope-lexical.js +18 -8
  159. package/dist/path-segment-route.d.ts +7 -0
  160. package/dist/path-segment-route.d.ts.map +1 -0
  161. package/dist/path-segment-route.js +24 -0
  162. package/dist/path-suffix-aliases.d.ts +10 -0
  163. package/dist/path-suffix-aliases.d.ts.map +1 -0
  164. package/dist/path-suffix-aliases.js +386 -0
  165. package/dist/path.d.ts.map +1 -1
  166. package/dist/path.js +16 -5
  167. package/dist/permissions-windows.d.ts.map +1 -1
  168. package/dist/permissions-windows.js +14 -3
  169. package/dist/permissions.d.ts.map +1 -1
  170. package/dist/permissions.js +37 -8
  171. package/dist/pinned-mutation-admission.d.ts +25 -0
  172. package/dist/pinned-mutation-admission.d.ts.map +1 -0
  173. package/dist/pinned-mutation-admission.js +425 -0
  174. package/dist/pinned-mutation-observation.d.ts +34 -0
  175. package/dist/pinned-mutation-observation.d.ts.map +1 -0
  176. package/dist/pinned-mutation-observation.js +142 -0
  177. package/dist/pinned-mutation-shared-route.d.ts +24 -0
  178. package/dist/pinned-mutation-shared-route.d.ts.map +1 -0
  179. package/dist/pinned-mutation-shared-route.js +70 -0
  180. package/dist/pinned-open.d.ts +6 -0
  181. package/dist/pinned-open.d.ts.map +1 -1
  182. package/dist/pinned-open.js +28 -10
  183. package/dist/pinned-write-types.d.ts +75 -0
  184. package/dist/pinned-write-types.d.ts.map +1 -0
  185. package/dist/pinned-write-types.js +1 -0
  186. package/dist/pinned-write.d.ts +7 -33
  187. package/dist/pinned-write.d.ts.map +1 -1
  188. package/dist/pinned-write.js +151 -17
  189. package/dist/private-directory.d.ts.map +1 -1
  190. package/dist/private-directory.js +2 -0
  191. package/dist/private-producer-handoff.d.ts +16 -0
  192. package/dist/private-producer-handoff.d.ts.map +1 -0
  193. package/dist/private-producer-handoff.js +272 -0
  194. package/dist/private-temp-workspace.d.ts +2 -39
  195. package/dist/private-temp-workspace.d.ts.map +1 -1
  196. package/dist/private-temp-workspace.js +183 -77
  197. package/dist/publish-file.d.ts.map +1 -1
  198. package/dist/publish-file.js +38 -32
  199. package/dist/regular-file.d.ts.map +1 -1
  200. package/dist/regular-file.js +56 -45
  201. package/dist/replace-directory.d.ts.map +1 -1
  202. package/dist/replace-directory.js +12 -5
  203. package/dist/replace-file-temp-owner.d.ts +1 -0
  204. package/dist/replace-file-temp-owner.d.ts.map +1 -1
  205. package/dist/replace-file-temp-owner.js +12 -1
  206. package/dist/replace-file.d.ts.map +1 -1
  207. package/dist/replace-file.js +24 -24
  208. package/dist/root-boundary.d.ts +4 -0
  209. package/dist/root-boundary.d.ts.map +1 -1
  210. package/dist/root-boundary.js +6 -1
  211. package/dist/root-context.d.ts +12 -1
  212. package/dist/root-context.d.ts.map +1 -1
  213. package/dist/root-context.js +57 -11
  214. package/dist/root-directory-creation.d.ts +13 -0
  215. package/dist/root-directory-creation.d.ts.map +1 -0
  216. package/dist/root-directory-creation.js +212 -0
  217. package/dist/root-directory-list.d.ts +16 -4
  218. package/dist/root-directory-list.d.ts.map +1 -1
  219. package/dist/root-directory-list.js +180 -39
  220. package/dist/root-directory.d.ts +23 -0
  221. package/dist/root-directory.d.ts.map +1 -0
  222. package/dist/root-directory.js +141 -0
  223. package/dist/root-errors.d.ts.map +1 -1
  224. package/dist/root-errors.js +12 -1
  225. package/dist/root-file-final-admission.d.ts +21 -0
  226. package/dist/root-file-final-admission.d.ts.map +1 -0
  227. package/dist/root-file-final-admission.js +83 -0
  228. package/dist/root-file.d.ts.map +1 -1
  229. package/dist/root-file.js +103 -24
  230. package/dist/root-impl.d.ts +1 -0
  231. package/dist/root-impl.d.ts.map +1 -1
  232. package/dist/root-impl.js +289 -317
  233. package/dist/root-move-noreplace.d.ts +14 -0
  234. package/dist/root-move-noreplace.d.ts.map +1 -0
  235. package/dist/root-move-noreplace.js +202 -0
  236. package/dist/root-observed-path.d.ts +9 -0
  237. package/dist/root-observed-path.d.ts.map +1 -0
  238. package/dist/root-observed-path.js +95 -0
  239. package/dist/root-path-errors.d.ts +12 -0
  240. package/dist/root-path-errors.d.ts.map +1 -0
  241. package/dist/root-path-errors.js +13 -0
  242. package/dist/root-path-existing.d.ts.map +1 -1
  243. package/dist/root-path-existing.js +53 -20
  244. package/dist/root-path-observation.d.ts +63 -0
  245. package/dist/root-path-observation.d.ts.map +1 -0
  246. package/dist/root-path-observation.js +180 -0
  247. package/dist/root-path-stat.d.ts +5 -0
  248. package/dist/root-path-stat.d.ts.map +1 -0
  249. package/dist/root-path-stat.js +101 -0
  250. package/dist/root-path-symlink.d.ts.map +1 -1
  251. package/dist/root-path-symlink.js +23 -4
  252. package/dist/root-path.d.ts +11 -0
  253. package/dist/root-path.d.ts.map +1 -1
  254. package/dist/root-path.js +215 -53
  255. package/dist/root-paths-lexical.d.ts +9 -0
  256. package/dist/root-paths-lexical.d.ts.map +1 -0
  257. package/dist/root-paths-lexical.js +22 -0
  258. package/dist/root-paths.d.ts +3 -25
  259. package/dist/root-paths.d.ts.map +1 -1
  260. package/dist/root-paths.js +77 -154
  261. package/dist/root-read-admission.d.ts +26 -0
  262. package/dist/root-read-admission.d.ts.map +1 -0
  263. package/dist/root-read-admission.js +94 -0
  264. package/dist/root-remove-identity.d.ts +15 -0
  265. package/dist/root-remove-identity.d.ts.map +1 -0
  266. package/dist/root-remove-identity.js +89 -0
  267. package/dist/root-remove-receipt.d.ts +17 -0
  268. package/dist/root-remove-receipt.d.ts.map +1 -0
  269. package/dist/root-remove-receipt.js +37 -0
  270. package/dist/root-remove.d.ts +2 -1
  271. package/dist/root-remove.d.ts.map +1 -1
  272. package/dist/root-remove.js +172 -10
  273. package/dist/root-write-admission.d.ts +68 -0
  274. package/dist/root-write-admission.d.ts.map +1 -0
  275. package/dist/root-write-admission.js +322 -0
  276. package/dist/root-write-compatibility.d.ts +13 -0
  277. package/dist/root-write-compatibility.d.ts.map +1 -0
  278. package/dist/root-write-compatibility.js +74 -0
  279. package/dist/root-write-complete-parent.d.ts +42 -0
  280. package/dist/root-write-complete-parent.d.ts.map +1 -0
  281. package/dist/root-write-complete-parent.js +195 -0
  282. package/dist/root-write-publication.d.ts +24 -0
  283. package/dist/root-write-publication.d.ts.map +1 -0
  284. package/dist/root-write-publication.js +85 -0
  285. package/dist/root-write-verification.d.ts.map +1 -1
  286. package/dist/root-write-verification.js +6 -4
  287. package/dist/secret-file.d.ts.map +1 -1
  288. package/dist/secret-file.js +62 -24
  289. package/dist/secret-read-async.d.ts.map +1 -1
  290. package/dist/secret-read-async.js +18 -13
  291. package/dist/secret-read-policy.d.ts.map +1 -1
  292. package/dist/secret-read-policy.js +5 -1
  293. package/dist/secure-file.d.ts.map +1 -1
  294. package/dist/secure-file.js +92 -7
  295. package/dist/secure-temp-dir.d.ts +6 -3
  296. package/dist/secure-temp-dir.d.ts.map +1 -1
  297. package/dist/secure-temp-dir.js +121 -101
  298. package/dist/secure-temp-repair.d.ts +35 -0
  299. package/dist/secure-temp-repair.d.ts.map +1 -0
  300. package/dist/secure-temp-repair.js +106 -0
  301. package/dist/sibling-staged-file.d.ts +3 -2
  302. package/dist/sibling-staged-file.d.ts.map +1 -1
  303. package/dist/sibling-staged-file.js +179 -55
  304. package/dist/sibling-temp.d.ts.map +1 -1
  305. package/dist/sibling-temp.js +27 -13
  306. package/dist/sidecar-lock-acquire.d.ts.map +1 -1
  307. package/dist/sidecar-lock-acquire.js +44 -14
  308. package/dist/sidecar-lock-root.d.ts.map +1 -1
  309. package/dist/sidecar-lock-root.js +4 -2
  310. package/dist/staged-directory.d.ts +14 -5
  311. package/dist/staged-directory.d.ts.map +1 -1
  312. package/dist/staged-directory.js +54 -3
  313. package/dist/standalone-publication-path.d.ts +2 -0
  314. package/dist/standalone-publication-path.d.ts.map +1 -0
  315. package/dist/standalone-publication-path.js +6 -0
  316. package/dist/stat-observation.d.ts +11 -0
  317. package/dist/stat-observation.d.ts.map +1 -0
  318. package/dist/stat-observation.js +64 -0
  319. package/dist/strict-file-identity.d.ts.map +1 -1
  320. package/dist/strict-file-identity.js +39 -2
  321. package/dist/symlink-parents.d.ts.map +1 -1
  322. package/dist/symlink-parents.js +7 -2
  323. package/dist/temp-target.d.ts +2 -0
  324. package/dist/temp-target.d.ts.map +1 -1
  325. package/dist/temp-target.js +130 -3
  326. package/dist/temp-workspace-admission.d.ts +22 -0
  327. package/dist/temp-workspace-admission.d.ts.map +1 -0
  328. package/dist/temp-workspace-admission.js +374 -0
  329. package/dist/temp-workspace-child-admission.d.ts +13 -0
  330. package/dist/temp-workspace-child-admission.d.ts.map +1 -0
  331. package/dist/temp-workspace-child-admission.js +95 -0
  332. package/dist/temp-workspace-descriptor.d.ts +48 -0
  333. package/dist/temp-workspace-descriptor.d.ts.map +1 -0
  334. package/dist/temp-workspace-descriptor.js +363 -0
  335. package/dist/temp-workspace-identity.d.ts +15 -0
  336. package/dist/temp-workspace-identity.d.ts.map +1 -0
  337. package/dist/temp-workspace-identity.js +41 -0
  338. package/dist/temp-workspace-owner.d.ts +7 -6
  339. package/dist/temp-workspace-owner.d.ts.map +1 -1
  340. package/dist/temp-workspace-owner.js +121 -61
  341. package/dist/temp-workspace-permissions.d.ts +4 -0
  342. package/dist/temp-workspace-permissions.d.ts.map +1 -0
  343. package/dist/temp-workspace-permissions.js +32 -0
  344. package/dist/temp-workspace-types.d.ts +40 -0
  345. package/dist/temp-workspace-types.d.ts.map +1 -0
  346. package/dist/temp-workspace-types.js +1 -0
  347. package/dist/temp.d.ts +1 -1
  348. package/dist/temp.d.ts.map +1 -1
  349. package/dist/temp.js +1 -1
  350. package/dist/test-hooks.d.ts +7 -0
  351. package/dist/test-hooks.d.ts.map +1 -1
  352. package/dist/text-atomic.d.ts.map +1 -1
  353. package/dist/text-atomic.js +3 -1
  354. package/dist/trash.d.ts.map +1 -1
  355. package/dist/trash.js +54 -8
  356. package/dist/walk.d.ts.map +1 -1
  357. package/dist/walk.js +9 -6
  358. package/dist/windows-owner.d.ts.map +1 -1
  359. package/dist/windows-owner.js +5 -0
  360. package/dist/windows-path-alias.d.ts +39 -0
  361. package/dist/windows-path-alias.d.ts.map +1 -0
  362. package/dist/windows-path-alias.js +153 -0
  363. package/docs/advanced.md +25 -0
  364. package/docs/archive.md +23 -2
  365. package/docs/atomic.md +27 -3
  366. package/docs/copy.md +3 -0
  367. package/docs/durability.md +14 -0
  368. package/docs/errors.md +14 -6
  369. package/docs/file-store.md +24 -3
  370. package/docs/filename.md +14 -7
  371. package/docs/install-path.md +2 -2
  372. package/docs/install.md +8 -7
  373. package/docs/json-store.md +5 -1
  374. package/docs/json.md +11 -0
  375. package/docs/mutation-policy-proof.md +65 -0
  376. package/docs/native-helper.md +41 -11
  377. package/docs/native.md +32 -8
  378. package/docs/output.md +33 -11
  379. package/docs/path-prefix.md +64 -0
  380. package/docs/path-suffix-aliases.md +159 -0
  381. package/docs/path.md +11 -0
  382. package/docs/private-file-store.md +14 -0
  383. package/docs/public-api.md +9 -0
  384. package/docs/quickstart.md +1 -1
  385. package/docs/reading.md +8 -1
  386. package/docs/root.md +18 -3
  387. package/docs/secret-file.md +3 -0
  388. package/docs/secure-file.md +6 -2
  389. package/docs/security-model.md +75 -8
  390. package/docs/sidecar-lock.md +28 -1
  391. package/docs/store.md +5 -1
  392. package/docs/temp.md +260 -36
  393. package/docs/test-hooks.md +14 -0
  394. package/docs/writing.md +38 -8
  395. package/package.json +10 -10
package/docs/path.md CHANGED
@@ -37,6 +37,10 @@ isPathInside("/srv/uploads", "/srv/uploads"); // true (root itsel
37
37
  ```
38
38
 
39
39
  The check is platform-aware: on Windows, paths are normalized for case and separator before comparison. This is deliberately a string-only, lexical answer; case folding does not prove that two differently cased prefixes name the same directory on a case-sensitive Windows directory. Root operations perform their own filesystem-identity admission when containment depends on case folding.
40
+ It is a lexical comparison, not a filesystem-admission boundary: it does not
41
+ reject Windows alternate data streams or index-allocation aliases. Use a
42
+ filesystem operation such as `root()` when a caller-controlled path will be
43
+ opened or mutated.
40
44
 
41
45
  ### `isPathInsideWithRealpath(rootDir, target, opts?)`
42
46
 
@@ -54,6 +58,9 @@ type Options = {
54
58
  ```
55
59
 
56
60
  Does not throw on missing inputs — `realpath` failures are absorbed by the underlying `safeRealpathSync`. By default (`requireRealpath: true`) the function returns `false` when either input cannot be resolved. Pass `{ requireRealpath: false }` to fall back to the lexical answer from `isPathInside` instead.
61
+ On Windows it returns `false` for namespace aliases in either raw input or in
62
+ a canonical value returned by `realpath` or the supplied cache. The
63
+ `requireRealpath: false` fallback does not admit those aliases.
57
64
 
58
65
  ### `isWithinDir(rootDir, targetPath)`
59
66
 
@@ -79,12 +86,16 @@ if (real === null) return notFound();
79
86
  ```
80
87
 
81
88
  All `realpath` failures collapse to `null` — there is no distinction between `ENOENT`, `EACCES`, and other I/O errors. Use `fs.realpathSync` directly if you need to branch on the error code.
89
+ This convenience wrapper preserves ordinary Node `realpath` semantics; it is
90
+ not a caller-path admission boundary on its own.
82
91
 
83
92
  ### `safeStatSync(targetPath)`
84
93
 
85
94
  Synchronous `stat` that returns `Stats` on success and `null` on any failure,
86
95
  including missing paths and permission errors. Use `fs.statSync` directly when
87
96
  the distinction matters.
97
+ Like `safeRealpathSync`, it does not apply the pathname-admission policy used
98
+ by the higher-level filesystem boundaries.
88
99
 
89
100
  ```ts
90
101
  const stat = safeStatSync("/srv/uploads/photo.jpg");
@@ -47,6 +47,20 @@ fileStoreSync({ rootDir: "/var/lib/app", private: true }).writeJson("config.json
47
47
  The sync store intentionally exposes a smaller surface: path resolution,
48
48
  lenient reads, and atomic text/JSON writes.
49
49
 
50
+ Sync directory modes remain repair-compatible on POSIX, but repairs are applied
51
+ only through an exact-identity, no-follow directory descriptor after the store
52
+ root and admitted parent name are revalidated. Root or component swaps fail
53
+ without chmodding the substituted directory. Matching modes take the no-open
54
+ fast path. Windows uses its existing `mkdir` mode request plus exact directory
55
+ identity checks and never falls back to pathname chmod.
56
+
57
+ On Linux, Node offers no portable search-only descriptor that can also be
58
+ `fchmod`ed. A mismatched directory without effective read access—including one
59
+ created under an owner-read-removing umask—fails closed with
60
+ `permission-unverified`. Supported macOS x64/arm64 hosts additionally try
61
+ `O_SEARCH` when the directory remains searchable; an inaccessible directory
62
+ still fails rather than restoring the pathname race.
63
+
50
64
  ## See also
51
65
 
52
66
  - [`fileStore`](file-store.md) — full store API.
@@ -61,6 +61,15 @@ prefix preparation. See [in-place writes](in-place-write.md).
61
61
  for local ASCII-case observations. An unavailable answer remains `undefined`;
62
62
  the caller selects any fallback. See [path case probing](path-case.md).
63
63
 
64
+ `resolvePathPrefixSync` and `ResolvedPathPrefix` separate a canonical existing
65
+ path prefix from its raw unresolved suffix after physical symlink traversal.
66
+ See [resolving path prefixes](path-prefix.md).
67
+
68
+ `probePathSuffixAliasesSync` and `ProbePathSuffixAliasesOptions` compare selected
69
+ missing relative suffixes beneath an existing directory using bounded temporary
70
+ directory probes. The caller owns Unicode-pair policy, caching, and the fallback
71
+ for `undefined`. See [path suffix alias probing](path-suffix-aliases.md).
72
+
64
73
  ## Guest source
65
74
 
66
75
  `@openclaw/fs-safe/guest` exports `GUEST_FILESYSTEM_PYTHON`,
@@ -60,7 +60,7 @@ await fs.move("notes/today.txt", "notes/archive/today.txt", { overwrite: true })
60
60
  await fs.remove("notes/archive/today.txt");
61
61
  ```
62
62
 
63
- `move()` defaults to no clobber. Pass `{ overwrite: true }` when replacing the target is intentional. `remove()` works on files and empty directories. For non-empty directories, list and remove children first or use [`replaceDirectoryAtomic`](atomic.md#replacedirectoryatomic).
63
+ `move()` defaults to no clobber. That mode requires the native helper so a concurrent target cannot be replaced between an absence check and the rename; without it, the call fails with `helper-unavailable`. Pass `{ overwrite: true }` when replacing the target is intentional. `remove()` works on files and empty directories. For non-empty directories, list and remove children first or use [`replaceDirectoryAtomic`](atomic.md#replacedirectoryatomic).
64
64
 
65
65
  ## 5. Inspect
66
66
 
package/docs/reading.md CHANGED
@@ -27,10 +27,17 @@ Regardless of shape, every read goes through the same boundary checks:
27
27
  3. Resolve path components and reject anything that escapes the root (`outside-workspace`).
28
28
  4. Reject `..` traversal and absolute spellings when they resolve outside the root. In-root absolute spellings remain accepted; `readAbsolute` makes that intent explicit.
29
29
  5. Open with `O_NOFOLLOW` where available. Any remaining symlink in the path triggers `symlink` unless the call's `symlinks` policy is `follow-within-root`.
30
- 6. Compare the pre-open path identity, the open fd, and the post-open resolved path (`sameFileIdentity`). A swap mid-call triggers `path-mismatch`.
30
+ 6. Compare the pre-open bigint path identity with the open fd, then perform one best-effort final admission: check the captured root identity, compare the policy-aware pathname with the fd, freshly canonicalize and re-admit that target inside the captured root, compare its exact bigint identity without following a final symlink with the fd, and check the root again. Both final pathname observations run even when their spellings match. An observed swap triggers `path-mismatch` or `outside-workspace`; a missing final path triggers `not-found`.
31
31
  7. If `hardlinks: "reject"`, refuse files with `nlink > 1` (`hardlink`).
32
32
  8. If `maxBytes` is set, refuse reads larger than the cap (`too-large`).
33
33
 
34
+ The final fence closes a rejected descriptor before any Root read consumes bytes or
35
+ `open()` hands the descriptor to its caller. It is a sequence of filesystem
36
+ observations, not an atomic kernel pathname/open primitive, so a hostile peer can
37
+ still race the namespace after the last observation. Standalone absolute-file
38
+ helpers have no captured `Root` identity and do not claim this replacement-root
39
+ fence; use a `Root` for untrusted paths.
40
+
34
41
  ## Read shapes
35
42
 
36
43
  ### `fs.read(rel, options?)`
package/docs/root.md CHANGED
@@ -114,7 +114,7 @@ fs.createJson(rel, value, options?) // create() variant of writeJson
114
114
  fs.append(rel, data, options?) // append text/buffer; syncs before close by default
115
115
  fs.copyIn(rel, sourceAbsPath, options?) // copy from outside the root, atomically, with size cap
116
116
  fs.openWritable(rel, options?) // FileHandle for streaming writes; supports await using
117
- fs.move(from, to, options?) // rename within the root; defaults to no clobber
117
+ fs.move(from, to, options?) // rename within the root; native-backed no clobber by default
118
118
  fs.remove(rel, options?) // unlink file, rmdir, or bounded recursive removal
119
119
  fs.mkdir(rel, options?) // mkdir -p (creates missing parents)
120
120
  fs.ensureRoot(options?) // accepts "" / "." as the root itself
@@ -221,7 +221,17 @@ source)` can reject a legal POSIX basename such as `c:photo.png`; callers that
221
221
  derive portable destination names from host files must sanitize or map that
222
222
  basename first.
223
223
 
224
- `openWritable` opens a writable file with options `mode?: number` and `writeMode?: "replace" | "append" | "update"`. `replace` truncates existing files and is the default; `update` keeps existing contents. Use it for streaming output. Prefer `await using` for cleanup.
224
+ On Windows, every Root pathname admission also rejects NTFS alternate-data-stream
225
+ and directory-index aliases: relative names containing `:`, or absolute names
226
+ with a colon beyond the single rooted drive designator, fail with
227
+ `invalid-path` before filesystem access. This includes spellings such as
228
+ `file:stream`, `dir::$INDEX_ALLOCATION`, and `dir:$I30:$INDEX_ALLOCATION`.
229
+ Rooted drive, UNC, and extended-drive paths keep their existing handling, and
230
+ the separate device/network policies remain in force. Ordinary colon-bearing
231
+ names remain valid on POSIX where the operation's existing drive-relative rule
232
+ does not otherwise reject them.
233
+
234
+ `openWritable` opens a writable file with options `mode?: number` and `writeMode?: "replace" | "append" | "update"`. `replace` truncates existing files and is the default; `update` keeps existing contents. Before truncation or handle return, descriptor and pathname identities are compared with lossless bigint metadata; persistently unknown Windows identities fail closed. The returned `stat` remains an ordinary numeric Node `Stats` object. Use it for streaming output. Prefer `await using` for cleanup.
225
235
 
226
236
  `remove` leaves non-empty directories unchanged unless `recursive: true` is
227
237
  provided. Recursive removal defaults to streaming entries in filesystem order;
@@ -299,7 +309,12 @@ fs.entries(rel, options?) // nonrecursive AsyncIterable<DirEntry>, includ
299
309
  fs.resolve(rel) // absolute path inside the root, after canonicalization
300
310
  ```
301
311
 
302
- These do not pin a later operation. They are safe to expose to UIs and decision points; for the actual read or write, use the verb methods so the operation pins identity at the point of use.
312
+ These do not pin a later operation. During `stat()`, the exact selected target and
313
+ parent are checked around metadata collection; `list()` checks one exact selected
314
+ directory around the complete name/metadata batch instead of repeating containment
315
+ work for every child. A detectable redirection rejects with `path-mismatch` rather
316
+ than returning names or metadata from the replacement. Results remain advisory
317
+ after the call returns, so use the verb methods for the actual read or write.
303
318
 
304
319
  `entries()` streams immediate children in filesystem order by default. It
305
320
  supports cancellation, a physical-entry limit that throws on overflow, and
@@ -81,6 +81,9 @@ itself must not be an alias. Hardlinks are rejected by default so another
81
81
  in-tree name cannot alias the credential; pass `rejectHardlinks: false` only
82
82
  when you explicitly trust that layout.
83
83
 
84
+ Read options are captured when the call starts. Mutating a shared options
85
+ object while an asynchronous read is in flight cannot relax its link policy.
86
+
84
87
  These readers do not enforce ownership or mode bits on an existing file. Their
85
88
  read contract covers pinned identity, file type, link policy, and byte bounds;
86
89
  the `0o600` guarantee belongs to the write helpers below. Use
@@ -62,6 +62,10 @@ type SecureFileReadOptions = {
62
62
 
63
63
  `io.maxBytes` must be a non-negative safe integer or positive `Infinity`; zero is an active cap and `Infinity` disables the cap. Invalid limits reject before filesystem admission.
64
64
 
65
+ The helper synchronously snapshots the supplied options, including nested permission and I/O settings, the injection callback, and supplied injection environment values, before opening the file or reaching its first `await`. Mutating those objects after this snapshot does not change that read's policy. This is not an atomic snapshot at invocation entry: caller getters run during snapshot construction and can affect values or working directories that have not yet been captured. `trust.trustedDirs` must be an array with a valid length and an own string entry without null bytes at every index; malformed lengths or entries, including sparse entries filled by inherited properties, reject with `invalid-path` before filesystem admission. An omitted or empty array leaves the read unrestricted by directory.
66
+
67
+ Relative trusted directories (including an empty string) are resolved to absolute lexical paths during this synchronous snapshot using Node's `path.resolve()` semantics. Windows drive-relative entries retain their per-drive current-directory semantics, and extended-length drive roots retain their root separator. Raw alternate-stream and filesystem-namespace aliases reject before normalization. Working-directory changes after the snapshot cannot redirect this allowlist. The existing realpath check still follows trusted-directory symlinks when it runs and falls back to the captured lexical path if realpath lookup fails; the allowlist does not pin directory identities.
68
+
65
69
  `permissions.allowInsecure` is a migration escape hatch. Prefer fixing permissions and using [`formatPermissionRemediation`](permissions.md) to show the user what to run. `trust.allowNetworkPath` is off by default because UNC paths are remote authority, not local filesystem input. `inject` is for tests and platform adapters; production callers usually leave it unset.
66
70
 
67
71
  On an actual Windows process with effective `platform: "win32"`, `inject.env` and `inject.exec` do not replace descriptor inspection. They remain available to simulated Windows checks on non-Windows hosts.
@@ -74,7 +78,7 @@ On an actual Windows process with effective `platform: "win32"`, `inject.env` an
74
78
 
75
79
  | Code | Meaning |
76
80
  |---|---|
77
- | `invalid-path` | `filePath` was not a local absolute path. |
81
+ | `invalid-path` | `filePath` was not a local absolute path, `trust.trustedDirs` contained malformed paths, or a Windows file path/trusted directory used an alternate-stream or filesystem-namespace alias. |
78
82
  | `not-found` | The path could not be stat'd before open. |
79
83
  | `not-file` | The opened target is not a regular file. |
80
84
  | `symlink` | The path is a symlink and `trust.allowSymlink` is false. |
@@ -83,7 +87,7 @@ On an actual Windows process with effective `platform: "win32"`, `inject.env` an
83
87
  | `outside-workspace` | `realPath` is outside `trust.trustedDirs`. |
84
88
  | `permission-unverified` | Required mode/ACL checks could not be completed, including when descriptor-bound Windows inspection is unavailable. |
85
89
  | `insecure-permissions` | Mode bits or ACLs grant broader access than allowed. |
86
- | `not-owned` | POSIX owner uid is not the current process uid. |
90
+ | `not-owned` | POSIX owner uid is not the process's effective uid. |
87
91
  | `too-large` | File size or bytes read exceeded `maxBytes`. |
88
92
  | `timeout` | `timeoutMs` elapsed while reading. |
89
93
 
@@ -28,6 +28,8 @@ You hand a `root()` boundary to a piece of code that takes caller-controlled rel
28
28
  - replaces the destination directory with a symlink right before a write
29
29
  - creates a hardlink that aliases an out-of-tree inode and asks you to read or replace it
30
30
  - asks a read/open primitive to target a known unsafe device or process-fd path
31
+ - uses an NTFS alternate-stream or directory-index pathname to alias a different
32
+ Windows filesystem object than the visible path suggests
31
33
  - triggers a partial write that leaves a half-written file at the destination
32
34
  - ships an archive with `..` paths, absolute paths, or symlinks pointing outside the destination
33
35
 
@@ -47,6 +49,30 @@ If you need full sandboxing, run the worker under reduced privileges (uid, conta
47
49
 
48
50
  Every path is resolved against the canonicalized real path of the root, then checked at that boundary. On Windows, an exact-case structural Root prefix stays on the lexical fast path; a prefix accepted only by case folding must have the Root's exact directory identity and is rebased onto the trusted Root spelling before use. Alias resolution walks components before applying a later `..`, so a symlink cannot change what that parent segment means after validation. Parent traversal, an absolute spelling, or any alias whose canonical result is outside the root throws `outside-workspace`; absolute spellings that remain inside the root are accepted.
49
51
 
52
+ Guarded pathname APIs reject Windows `:` namespace aliases before normalization
53
+ or filesystem access. The only colon admitted in a Windows filesystem path is
54
+ the rooted ASCII drive designator (including extended-drive syntax); relative
55
+ paths admit none. This rule does not authorize device or network paths, which
56
+ retain their independent restrictions. Pure formatters, descriptor-only calls,
57
+ and `walkDirectory` (documented as a non-boundary traversal helper) are outside
58
+ this pathname-admission guarantee. Output helpers continue to sanitize an
59
+ untrusted basename, then validate the resulting path.
60
+
61
+ The low-level existing-object readers `openRootFile()` and
62
+ `openRootFileSync()` preserve their historical Windows drive-relative input:
63
+ they anchor its leading drive designator before this admission check. The raw
64
+ suffix remains unnormalized, and any additional colon is still rejected before
65
+ filesystem access.
66
+
67
+ Trusted-path standalone publication APIs retain the same drive-relative
68
+ compatibility. This includes the atomic file, text, JSON, JSON store/direct
69
+ queue writer, directory-replacement, move, and exclusive-publication helpers.
70
+ They capture the drive's current directory at publication entry and carry the
71
+ anchored spelling through their remaining checks, locks, callbacks, receipts,
72
+ publication, and cleanup. File writers preserve the raw suffix. Root-relative
73
+ APIs and caller-constructed relative directory receipts continue to reject
74
+ drive designators.
75
+
50
76
  ### Symlinks (read side)
51
77
 
52
78
  `open()` and `read()` use `fs.open` with `O_NOFOLLOW` on POSIX where available. The library then `fstat`s the open fd and `realpath`s the original input, asserting the two refer to the same inode. A symlink swap that happens between resolve and open will fail at the identity check rather than silently following the new target.
@@ -60,12 +86,33 @@ checks, so callers do not need their own parent canonicalization.
60
86
 
61
87
  Guarded root reads compare lossless bigint identities from before open, the opened
62
88
  descriptor, the input path, and the canonical target; numeric public `Stats`
63
- receipts are not used as identity evidence. Unknown Windows device/inode values
64
- receive one re-inspection without reopening the file. A definite mismatch or
65
- persistent unknown identity rejects with `path-mismatch` before reading bytes.
66
- Regular-file readers, root-file adapters, and archive input staging use the same
67
- exact admission policy. `copyIn()` retains the admitted source identity for its
68
- checks before and after copying, independently of its numeric metadata receipt.
89
+ receipts are not used as identity evidence. Before returning a handle or reading
90
+ bytes, one best-effort final admission fence checks the originally captured root
91
+ identity, freshly compares the policy-aware pathname with the opened descriptor,
92
+ canonicalizes and re-admits that current target inside the captured root, compares
93
+ the canonical target's exact bigint identity without following a final symlink
94
+ with the descriptor, and checks the root identity again. The two final pathname
95
+ observations remain independent even when the spellings match. Unknown Windows
96
+ device/inode values receive one re-inspection without reopening the file.
97
+ A definite mismatch or persistent unknown identity
98
+ rejects with `path-mismatch`; escaped fresh containment rejects with
99
+ `outside-workspace`. This is not an atomic kernel pathname/open primitive, so the
100
+ namespace can still change after the final observation.
101
+
102
+ Other regular-file readers, root-file adapters, and archive input staging retain
103
+ their documented descriptor/path admission. The exported unrooted
104
+ `openLocalFileSafely()` and `readLocalFileSafely()` helpers have no captured `Root`
105
+ identity and therefore do not provide the replacement-root fence. `copyIn()`
106
+ retains the admitted source identity for its checks before and after copying,
107
+ independently of its numeric metadata receipt.
108
+
109
+ The low-level `openRootFile()` adapters additionally retain the canonical root's
110
+ exact identity from before component traversal. After their existing pathname and
111
+ descriptor checks, they verify the root, freshly resolve and re-admit the consumed
112
+ pathname, compare that canonical leaf with the descriptor, and verify the root
113
+ again before returning ownership. A failed final fence closes the descriptor
114
+ without reading. The checks detect substitutions at each observation boundary;
115
+ they do not make pathname confinement atomic against a continuously racing peer.
69
116
 
70
117
  ### Symlinks (write side)
71
118
 
@@ -75,6 +122,26 @@ directory descriptors. Replacement uses descriptor-relative rename just like
75
122
  no-replace publication, so replacing the parent pathname does not divert the
76
123
  mutation.
77
124
 
125
+ When either `denyMutations` or an explicit `mutationSymlinks` policy applies,
126
+ the POSIX writer binds that exact policy snapshot to parent admission. An existing
127
+ parent is canonicalized and identity-matched to its retained descriptor before
128
+ the actual destination is authorized. A missing-parent walk authorizes each
129
+ prospective directory before `mkdirat`, opens it without following a newly
130
+ introduced link, and authorizes the opened object before continuing. This
131
+ prevents a contained Linux `openat2(RESOLVE_BENEATH | RESOLVE_NO_MAGICLINKS)`
132
+ redirect from reusing policy approval for a different in-root subtree.
133
+ For an ordinary unchanged route, operation-local observations may carry that
134
+ admission across a direct-child creation only after exact parent and child
135
+ fences and a synchronous full-epoch validation. The resulting operation-local
136
+ token authorizes the opened child without an intervening await; stale,
137
+ redirected, incomplete, or foreign evidence returns to the full ordered
138
+ admission. An already-complete fallback parent is likewise retained only after
139
+ full target admission and a fresh exact guard fence. Native acceleration additionally requires an exclusive
140
+ direct-child mkdir result proving that this syscall created the name; a
141
+ collision, legacy helper, or malformed result performs the guarded walk but
142
+ cannot advance the receipt. That boolean is admission provenance only and does
143
+ not grant ownership for cleanup by pathname.
144
+
78
145
  The opt-in `mutationSymlinks` policy applies independently of read policy.
79
146
  `"reject"` rejects symlink components; `"follow-parents-within-root"` resolves
80
147
  contained directory aliases and rejects final symlinks. Publication checks the
@@ -107,13 +174,13 @@ When `hardlinks: "reject"` is set, reads stat the target and refuse if `nlink >
107
174
 
108
175
  ### TOCTOU between resolve and use
109
176
 
110
- `resolve()`, `exists()`, `stat()`, and `list()` are explicitly advisory — they answer a question and return. To act on a path with operation-local identity checks, use `read()`, `open()`, `write()`, `create()`, `copyIn()`, `move()`, or `remove()`. The containment table below states which opens are kernel-atomic and which remain best-effort.
177
+ `resolve()`, `exists()`, `stat()`, and `list()` are explicitly advisory — they answer a question and return. `stat()` checks the exact selected target and parent while collecting metadata, and `list()` checks the exact selected directory around its complete batch, so detectable descendant redirection rejects before results are returned. Those checks do not preserve identity after the call. To act on a path with operation-local identity checks, use `read()`, `open()`, `write()`, `create()`, `copyIn()`, `move()`, or `remove()`. The containment table below states which opens are kernel-atomic and which remain best-effort.
111
178
 
112
179
  A `root()` handle also remembers the canonical root directory identity. Calls fail with `path-mismatch` if that canonical pathname is replaced, including advisory inspection and walking calls, rather than following a replacement root into another tree.
113
180
 
114
181
  ### Denied mutations
115
182
 
116
- `denyMutations` is an opt-in application policy for `root()` mutation methods. It blocks exact absolute paths with `paths` and whole subtrees with `prefixes`, merging root defaults with per-call entries so a call cannot clear root-level denies. This is not an OS permission boundary: code with access to `node:fs`, a shell, or another process with the same filesystem privileges can bypass it.
183
+ `denyMutations` is an opt-in application policy for `root()` mutation methods. It blocks exact absolute paths with `paths` and whole subtrees with `prefixes`, merging root defaults with per-call entries so a call cannot clear root-level denies. POSIX pinned `write`, `create`, and `copyIn` copy the merged entries before awaiting preflight and reapply them to their admitted canonical parent, including before missing parent creation. This is not an OS permission boundary: code with access to `node:fs`, a shell, or another process with the same filesystem privileges can bypass it.
117
184
 
118
185
  ### Atomic writes
119
186
 
@@ -2,6 +2,11 @@
2
2
 
3
3
  `acquireFileLock()` and `withFileLock()` provide a cross-process file lock with retry and process-exit cleanup. The lock is implemented as a sidecar file (e.g. `state.json` ↔ `state.json.lock`) — only one acquirer can create the sidecar with `O_CREAT | O_EXCL` at a time.
4
4
 
5
+ On Windows, both the target and an explicitly supplied `lockPath` reject NTFS
6
+ alternate-stream and directory-index namespace spellings before parent creation,
7
+ in-process reentrant lookup, or sidecar access. Rooted drive paths retain their
8
+ normal meaning, and ordinary colon-bearing POSIX paths remain valid.
9
+
5
10
  JavaScript raw-sidecar creation passes mode `0o600` to that exclusive open. On POSIX, the process umask may further restrict the new file but cannot add group or other access. No pathname `chmod` fallback is used.
6
11
 
7
12
  ```ts
@@ -33,7 +38,7 @@ Each new sidecar also carries an internal random ownership token encoded as JSON
33
38
 
34
39
  The raw sidecar bytes are not a canonical JSON representation: tools that trim or rewrite the trailing whitespace invalidate the ownership token, so release leaves the changed sidecar in place and fails closed. The token distinguishes cooperating acquisitions; it is not a secret and does not make pathname compare-and-remove atomic against a hostile process that can replace files outside the lock protocol.
35
40
 
36
- `release()` propagates an I/O failure that prevents deletion of an unchanged, owned sidecar; it never reports successful cleanup while leaving that lock behind. The handle and manager retain the exact cleanup receipt after a failure, so the same handle can retry `release()` and `manager.drain()` can retry retained cleanup. A changed sidecar remains an ownership mismatch rather than a deletion failure and is left untouched. If both a `withFileLock()` callback and release fail, the release error is the primary `SuppressedError.error` and the callback failure remains available as `SuppressedError.suppressed`. Failed acquisition cleanup uses the same shape, with the cleanup error primary and the acquisition failure suppressed. On Node runtimes without the global `SuppressedError` constructor, fs-safe returns the equivalent `Error` shape with the same name and properties.
41
+ `release()` propagates an I/O failure that prevents deletion of an unchanged, owned sidecar; it never reports successful cleanup while leaving that lock behind. The handle and manager retain the exact cleanup receipt after a failure, so the same handle can retry `release()` and `manager.drain()` can retry retained cleanup. A changed sidecar remains an ownership mismatch rather than a deletion failure and is left untouched. If both a `withFileLock()` callback and release fail, the release error is the primary `SuppressedError.error` and the callback failure remains available as `SuppressedError.suppressed`. Failed asynchronous acquisition cleanup uses the same shape, with the cleanup error primary and the acquisition failure suppressed. On Node runtimes without the global `SuppressedError` constructor, fs-safe returns the equivalent `Error` shape with the same name and properties.
37
42
 
38
43
  ## API
39
44
 
@@ -102,6 +107,16 @@ type FileLockRetryOptions = {
102
107
 
103
108
  `payload` is a function so you can re-evaluate it on each retry (e.g. timestamp, PID).
104
109
 
110
+ Asynchronous acquisition snapshots `targetPath`, an explicit `lockPath`, and
111
+ `lockRoot` before its first asynchronous operation. Cwd-dependent path spellings
112
+ are resolved using Node's platform path-resolution rules at that point, and the
113
+ resulting absolute paths remain fixed through retries, stale recovery,
114
+ verification, and release even if the process later changes its working
115
+ directory. An explicit, fully qualified `lockPath` retains its caller-supplied
116
+ spelling; current-drive-rooted and drive-relative Windows paths are resolved at
117
+ the snapshot boundary. The snapshot adds no normalization beyond what is
118
+ required to remove that cwd or current-drive dependency.
119
+
105
120
  The complete serialized sidecar must fit within 1 MiB (1,048,576 UTF-8 bytes),
106
121
  including pretty-printed JSON, newlines, and the internal ownership token's
107
122
  trailing whitespace. The limit counts bytes, not string characters. Oversized
@@ -215,6 +230,11 @@ Discarding an acquisition observation is not proof that the pathname is absent:
215
230
  another owner may already have created the next record. Every discarded
216
231
  observation consumes the normal retry/deadline budget and requires fresh
217
232
  exclusive creation. It supplies no release, reclaim, or held-lock authority.
233
+ If that successor disappears during the recovery metadata probe, the waiter
234
+ may discard the probe only with an operation-local receipt for an admitted
235
+ regular file with one link, followed by current Root and canonical ancestor
236
+ checks. A generic metadata error does not permit this retry, and public
237
+ `Root.stat()` still rejects a file that changes during observation.
218
238
  Generic `Root.open()` and held-owner/reclaim reads still reject failed opens.
219
239
  Moving an already-matched pinned descriptor without unlinking it, unknown or
220
240
  inexact identities, retargeted ancestors, and unrelated filesystem or caller
@@ -302,6 +322,13 @@ try {
302
322
  }
303
323
  ```
304
324
 
325
+ Failed synchronous acquisition attempts close the created descriptor once even
326
+ if its metadata cannot be read. Cleanup leaves the sidecar in place without an
327
+ exact descriptor identity. A metadata-capture failure does not replace the
328
+ acquisition error; if close or identity-checked removal also fails, the
329
+ `SuppressedError.error` is the acquisition error and `suppressed` is the cleanup
330
+ error.
331
+
305
332
  The sync payload, reclaim, and parsing callbacks must also be synchronous. This
306
333
  shape is appropriate for a short boot migration; it is a poor fit for a server
307
334
  request because retry backoff uses a blocking wait.
package/docs/store.md CHANGED
@@ -32,7 +32,7 @@ import {
32
32
  | Durable JSON queue helpers | Append/load/ack JSON entry files using atomic writes and delivered markers. |
33
33
  | [Private file-store mode](private-file-store.md) | `fileStore({ private: true })` for credentials, tokens, and per-agent state at `0600` files under `0700` directories. |
34
34
 
35
- `fileStore().json("rel.json")` and `jsonStore({ filePath })` are intentionally separate primitives. Use `fileStore().json(...)` when JSON state lives alongside other files in the same managed directory; use `jsonStore({ filePath })` when you have a single absolute path and want the keyed JSON shape directly.
35
+ `fileStore().json("rel.json")` and `jsonStore({ filePath })` are intentionally separate primitives. Use `fileStore().json(...)` when JSON state lives alongside other files in the same managed directory; use `jsonStore({ filePath })` when you have one trusted path, resolved to an absolute path at construction, and want the keyed JSON shape directly.
36
36
 
37
37
  ## Picking a shape
38
38
 
@@ -74,6 +74,10 @@ Queue and failed directory creation fsyncs every newly-created parent edge from
74
74
 
75
75
  `writeJsonDurableQueueEntry()` and migrations share strict parent synchronization inside the atomic writer's retained descriptor and per-path serialization lifetime, followed by published-file identity verification. If sync fails after publication, the write rejects without rolling back the published JSON; retrying `writeJsonDurableQueueEntry()` writes the entry again and must complete its own sync. Loader retries resync an existing processing claim's parent under the transfer lock before calling `read`, even when a version-dependent callback would no longer request migration. Fresh claims and same-directory source retirement already complete that sync. This is not a rollback, deduplication, or exactly-once guarantee. The generic `replaceFileAtomic({ syncParentDir: true })` option remains best-effort.
76
76
 
77
+ The direct queue writer accepts trusted relative paths. On Windows it anchors
78
+ an ordinary drive-relative `filePath` before publication and strict parent
79
+ sync. Other queue lifecycle APIs retain their own root/path admission contracts.
80
+
77
81
  Batch loading skips invalid entry names, malformed, oversized, or unreadable entry content, and caller `read` callback failures. Initially unowned pending entries (hardlinks or unverifiable identities), symlinks, non-files, and absent pending entries are also skipped. Claim, transfer-lock, retirement, and migration write/publication/durability failures reject the batch with the original error, even if earlier entries succeeded. Migration in both loaders strictly syncs the parent directory after successful publication. Visible transitions and earlier processing claims remain for retry; a rejected batch does not acknowledge or roll them back.
78
82
 
79
83
  Failed destinations are create-only. Quarantine publishes the claimed file by hardlink, so the queue and failed directories must share a filesystem with hardlink support. If `failed/<id>.json` already exists, quarantine rejects while preserving both that earlier evidence and the current claimed entry instead of overwriting either file. The `read` callback continues to receive the logical `.json` path even though bytes are read and migrations are written through the claimed path.