@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/native.md CHANGED
@@ -50,12 +50,40 @@ whether a path, archive entry, mode, owner, or cleanup policy is acceptable.
50
50
  race-atomic. macOS uses `renameatx_np(RENAME_EXCL)` and permits
51
51
  `fclonefileat` in an owned, non-shared parent. The clone is normalized inside
52
52
  a private staging directory: flags, ACLs, extended attributes, and broad mode
53
- bits are cleared before no-replace publication.
53
+ bits are cleared before no-replace publication. Clone admission obtains mode,
54
+ owner, exact identity, flags, and ACL state together from the retained descriptor;
55
+ immutable receipts are compared across the private staging operation and against
56
+ fresh no-follow pathname identity reads. Any extended entry on the target parent
57
+ or private staging directory is rejected before cloning bytes. The payload's
58
+ cleared ACL and normalized descriptor facts are verified before publication and
59
+ again, with a fresh published-name identity fence, before its descriptor is
60
+ returned. Unsupported admission before payload
61
+ creation or an unsupported clone syscall may still select the documented
62
+ ordinary-copy path. After the clone creates bytes, normalization and security
63
+ verification failures report terminal `EIO`, retaining the original error
64
+ detail; successful cleanup does not make them eligible for ordinary-copy retry.
65
+ Cleanup failures remain secondary diagnostics, and already-terminal publication
66
+ errors such as `EEXIST` retain their status.
54
67
  - Windows uses handle-relative `NtCreateFile` with `OBJ_DONT_REPARSE` and
55
68
  `FILE_OPEN_REPARSE_POINT`, then explicitly rejects reparse points. Rename and
56
69
  hardlink operations stay rooted in already-open handles. Owner/DACL reads
57
70
  use `GetSecurityInfo`; private directories receive their protected DACL in
58
- the `CreateDirectoryW` call itself.
71
+ an exclusive, handle-relative `NtCreateFile` call. Their created handles remain
72
+ open through ACL and pathname-association checks and own any failure cleanup.
73
+ N-API descriptors cross into and out of
74
+ this layer only through the host executable's paired libuv descriptor bridge;
75
+ missing or partial exports fail with `ENOTSUP` instead of trying a raw HANDLE
76
+ or add-on CRT descriptor namespace.
77
+
78
+ The internal macOS `inspectDarwinAcl(fd)` capability reports `absent`, `empty`,
79
+ or `present` for the opened object's extended ACL. It synchronously owns a
80
+ close-on-exec duplicate for inspection, leaves the caller's descriptor and file
81
+ position alone, and never reopens a pathname. Darwin's `acl_get_entry` returns
82
+ zero for an entry; end-of-list is accepted only for the first entry of a valid,
83
+ privately owned empty ACL. Unsupported, malformed, and failed inspection is not
84
+ reported as absence. These facts do not classify individual ACE permissions,
85
+ prove volume ownership enforcement, or add ACL enforcement to private writers
86
+ and secure readers outside the clone path.
59
87
 
60
88
  ## Archives
61
89
 
@@ -125,22 +153,30 @@ All routes preserve `wx` semantics and the same source/target identity and
125
153
  SHA-256 fencing. Native hashing and Linux whole-file copying run on N-API async
126
154
  workers rather than the JavaScript event loop.
127
155
 
156
+ Linux range copying confirms every zero-byte result with a positioned source
157
+ read at the current transfer offset, including after earlier calls copied data.
158
+ If readable bytes remain, automatic Root copying resumes its byte loop from
159
+ that offset; exclusive publication removes its partial target before retrying
160
+ the guarded byte-copy fallback. EOF checks preserve descriptor cursors and do
161
+ not bypass the byte limit.
162
+
128
163
  ## Mode semantics
129
164
 
130
165
  | Mode | Native loading | Fallback |
131
166
  |---|---|---|
132
- | `auto` | Try once, cache the result | Use guarded JavaScript when unavailable |
167
+ | `auto` | Try once, cache the result | Use guarded JavaScript when safe; reject native-only operations |
133
168
  | `require` | Try once, cache the result | Throw `FsSafeError("helper-unavailable")` |
134
- | `off` | Never attempt a binding load | Always use guarded JavaScript |
169
+ | `off` | Never attempt a binding load | Use guarded JavaScript when safe; reject native-only operations |
135
170
 
136
171
  `sha256FileSync()` is a synchronous Node implementation in all three modes and
137
172
  does not load the binding. Use asynchronous `sha256File()` for native hashing
138
173
  and cancellation that can respond while JavaScript callbacks run.
139
174
 
140
- Features without a safe JavaScript implementation, including zstd/bzip2 TAR,
141
- Windows private-directory creation, and [retained-directory staging](staged-file.md),
142
- fail with `helper-unavailable` when native support is absent or off. Staging
143
- is currently Linux/macOS only and rejects Windows with `unsupported-platform`.
175
+ Features without a safe JavaScript implementation, including no-clobber
176
+ `Root.move()`, zstd/bzip2 TAR, Windows private-directory creation, and
177
+ [retained-directory staging](staged-file.md), fail with `helper-unavailable`
178
+ when native support is absent or off. Staging is currently Linux/macOS only and
179
+ rejects Windows with `unsupported-platform`.
144
180
 
145
181
  The staged-file owner also serves POSIX native pinned writes, including streaming.
146
182
  Unpublished files remain at `0600`; requested modes are applied through the
@@ -177,12 +213,12 @@ remain TypeScript-owned. What changes is the syscall strength or availability:
177
213
 
178
214
  | Capability | Native path | Guarded JavaScript path |
179
215
  |---|---|---|
180
- | Root-relative opens/mutations | Descriptor-relative beneath operations. Pinned writes create parents and publish both replacement and no-replace targets relative to open directory descriptors. Linux reports `kernel-atomic`; macOS and Windows report `best-effort`. macOS uses `O_RESOLVE_BENEATH` when available plus an `F_GETPATH` detector, while Windows rejects reparse traversal in the object-manager call. | Reports `best-effort`: component-wise alias checks, no-follow opens where Node exposes them, private temp/rename, and post-operation identity verification. A same-privilege peer can replace a writable parent after a guard assertion but before Node resolves the pathname mutation; the mutation may land outside the intended root before the post-check detects it. |
216
+ | Root-relative opens/mutations | Descriptor-relative beneath operations. Pinned writes create parents and publish both replacement and no-replace targets relative to open directory descriptors. No-clobber `Root.move()` admits both parents and uses the native no-replace rename. Linux reports `kernel-atomic`; macOS and Windows report `best-effort`. macOS uses `O_RESOLVE_BENEATH` when available plus an `F_GETPATH` detector, while Windows rejects reparse traversal in the object-manager call. | Reports `best-effort`: component-wise alias checks, no-follow opens where Node exposes them, private temp/rename, and post-operation identity verification. No-clobber `Root.move()` is unsupported because a check followed by a replacing rename is unsafe. A same-privilege peer can replace a writable parent after a guard assertion but before Node resolves another pathname mutation; the mutation may land outside the intended root before the post-check detects it. |
181
217
  | ZIP/TAR/gzip | Rust streaming decode and fd-relative output creation. | Optional JSZip or bundled WASM TAR into guarded private staging, then the same guarded merge policy. |
182
218
  | Zstd/bzip2 TAR | Supported. | Unsupported; typed `helper-unavailable`. |
183
219
  | Publication copy | Clone, Linux `copy_file_range`, async native SHA-256. | Exclusive `wx` byte loop and Node SHA-256 with the same content/identity fences. |
184
220
  | `rename-noreplace` | Atomic platform no-replace rename. | Unsupported; no emulation by check-then-rename. |
185
- | Windows DACL read | Direct `GetSecurityInfo`; the public facts API exposes ordered basic allow/deny ACE SIDs, masks, and decoded flags without trust policy. | Structured .NET owner/DACL inspection for coarse permission checks; the public raw ACE facts API remains native-only. |
221
+ | Windows DACL read | Direct `GetSecurityInfo`; the public facts API exposes ordered basic allow/deny ACE SIDs, masks, and decoded flags without trust policy. Secure-file reads query the borrowed open descriptor and compare its 32-bit volume serial and 64-bit file-index projection with Node's bigint receipt. | Structured .NET owner/DACL inspection remains available to standalone pathname reporting. Secure-file reads fail closed without the descriptor capability. |
186
222
  | Windows private directory | Creation-time protected DACL. | Unsupported; no weaker pathname-only substitute. |
187
223
 
188
224
  Use `off` in CI to keep the fallback contract exercised. Use `require` when a
package/docs/output.md CHANGED
@@ -69,13 +69,23 @@ when needed as described above. Guarded temporary files used only inside
69
69
  fs-safe have independent names so their length does not grow with the
70
70
  destination basename.
71
71
 
72
+ On Windows, `rootDir` and every target parent reject NTFS alternate-stream and
73
+ directory-index namespace spellings such as `file:stream` and
74
+ `dir::$INDEX_ALLOCATION`. A colon in only the requested basename still follows
75
+ the documented portable filename sanitization above instead of being treated
76
+ as a raw stream path. Ordinary colon-bearing POSIX roots and parents remain
77
+ valid.
78
+
72
79
  ## Choosing a staging mode
73
80
 
74
81
  `staging: "workspace"` is the default. The producer writes in private temp
75
82
  storage, then fs-safe copies through the guarded root boundary. Choose it when
76
83
  the temp and destination filesystems may differ, or when an externally produced
77
84
  partial file must never appear in the destination directory. The final target
78
- still appears only after guarded finalization.
85
+ still appears only after guarded finalization. Its internal `tempFile()` does
86
+ not expose `cleanupSafety` through this API and uses compatible cleanup, with
87
+ the final check-to-pathname-recursive-removal gap documented in
88
+ [`tempFile`](temp.md#tempfile).
79
89
 
80
90
  By default, `staging: "sibling"` gives the producer a randomized temp path in
81
91
  the target directory. Choose it only when that directory itself is the approved writable
@@ -104,19 +114,31 @@ absent file path inside a private child workspace under the target parent, on
104
114
  the target filesystem. Directory cleanup ownership is captured before the
105
115
  callback. A callback exception triggers owned workspace cleanup, including
106
116
  partial output, subject to directory identity checks and I/O failures.
107
- After success, `Root.move` checks source aliases and moves the output to the
108
- ordinary sibling path; an escaping symlink can fail with `path-alias` here.
109
- Rejected output still inside the workspace follows its cleanup contract. Once
110
- output moves to the sibling path, the existing unadmitted-file retention and
111
- single-link regular-file admission, mode, file sync, and final rename rules apply.
117
+ After success, an available native helper uses guarded no-replace `Root.move`.
118
+ With native mode off, the helper opens and identity-fences the completed regular
119
+ file, creates the randomized sibling through an atomic no-clobber hard link,
120
+ verifies its temporary two-link state, then removes the private name. Windows
121
+ first transfers the pin to an independently verified sibling descriptor so the
122
+ source name can disappear before workspace cleanup, including on runtimes with
123
+ legacy deletion behavior. If that unlink clears the producer's read-only
124
+ attribute, the retained descriptor restores it and the helper verifies mode and
125
+ identity before continuing. This keeps the handoff zero-copy while preserving the
126
+ ordinary retained-descriptor, mode,
127
+ file-sync, and final-rename lifecycle. Escaping symlinks still fail with
128
+ `path-alias`; filesystems without hard-link support fail with
129
+ `helper-unavailable`.
112
130
 
113
131
  Exact bigint parent and workspace identities are rechecked before moving
114
132
  output to the sibling path to reject observed replacements. Cleanup uses the
115
- existing [`withTempFile` ownership contract](temp.md#withtempfile). A moved or replaced parent or workspace can
116
- leave original or replacement paths behind; the option does not promise
117
- cleanup through a retained directory after a rename. The existing Windows,
118
- native-off, and JavaScript guard limitations remain, with no additional
119
- permissions or durability guarantee. See the [producer-isolation contract](temp.md#sibling-temp-writes)
133
+ compatible [`withTempFile` ownership contract](temp.md#withtempfile); this
134
+ output option does not expose `cleanupSafety: "require-bounded"`. A moved or
135
+ replaced parent or workspace can leave artifacts, and a workspace substituted
136
+ in the final check-to-pathname-recursive-removal gap can redirect traversal.
137
+ The option does not promise cleanup through a retained directory after a
138
+ rename. The existing Windows and JavaScript pathname-guard limitations remain,
139
+ with no additional permissions or durability guarantee. Native-off publication
140
+ is supported only where hard links are available. See the
141
+ [producer-isolation contract](temp.md#sibling-temp-writes)
120
142
  for cleanup and pathname-race details. The option affects only `staging: "sibling"`;
121
143
  with `staging: "workspace"`, it is redundant and harmless because the producer
122
144
  already uses a private workspace. Omitting it leaves both staging defaults
@@ -0,0 +1,64 @@
1
+ # Resolving an existing path prefix
2
+
3
+ `resolvePathPrefixSync()` follows filesystem path components until the first
4
+ missing entry. It returns a canonical existing prefix and the unprocessed
5
+ suffix separately, including dangling symlink targets.
6
+
7
+ ```ts
8
+ import { resolvePathPrefixSync, type ResolvedPathPrefix } from "@openclaw/fs-safe/advanced";
9
+
10
+ const observed: ResolvedPathPrefix = resolvePathPrefixSync("/srv/data/future/file.json");
11
+ // When /srv/data exists but future does not:
12
+ // observed.existingPath is the canonical spelling of /srv/data.
13
+ // observed.unresolvedSegments is ["future", "file.json"].
14
+ ```
15
+
16
+ The result has three readonly fields:
17
+
18
+ | Field | Meaning |
19
+ | --- | --- |
20
+ | `absolutePath` | Input anchored to the current directory or Windows drive, with raw path components retained and Windows separators converted to backslashes. |
21
+ | `existingPath` | Existing prefix canonicalized by fs-safe's native-realpath owner. This may be a file when the entire path exists. |
22
+ | `unresolvedSegments` | Components from the first missing entry onward, after expanding any earlier symlinks. Empty when the entire path exists. |
23
+
24
+ ## Physical traversal and missing paths
25
+
26
+ The helper resolves `link/..` from the link's physical target. It does not use
27
+ `path.resolve()` on the full input or a symlink target, because lexical
28
+ normalization would erase that traversal. Relative inputs use the current
29
+ directory; Windows drive-relative inputs use Node's current directory for that
30
+ drive. A root-relative Windows symlink target retains its containing link's
31
+ drive or share root. Windows junctions and full UNC share roots are supported.
32
+
33
+ At the first `ENOENT`, traversal stops. A suffix such as
34
+ `missing/../live.sqlite` remains `["missing", "..", "live.sqlite"]`, even if
35
+ `live.sqlite` exists beside the missing component. Dot components, repeated
36
+ separators, and a trailing separator in the unresolved suffix are retained.
37
+ Normalizing that suffix would invent an alias to a file the filesystem cannot
38
+ reach through the missing directory. The caller owns any application-specific
39
+ comparison or prospective-path policy.
40
+
41
+ ## Failures and limits
42
+
43
+ Only `ENOENT` from component inspection produces a missing suffix. Permission,
44
+ I/O, non-directory traversal, symlink-reading, and final canonicalization
45
+ failures propagate. A vanished symlink that was already observed is an error,
46
+ not an unprocessed missing component. NUL bytes reject with `invalid-path`.
47
+
48
+ Resolution rejects with `ELOOP` after 64 symlink expansions or a repeated
49
+ resolution state. The state retains exact bigint device/inode identity and
50
+ the remaining suffix, so revisiting a link with a shorter suffix is permitted.
51
+ Traversing through a non-directory, including `file/..`, `file/.`, or `file/`,
52
+ rejects with `ENOTDIR`.
53
+
54
+ Dot and parent components require the directory's search permission before
55
+ they are collapsed. Native realpath alone does not establish this permission
56
+ on every platform. Empty components from repeated or trailing separators do
57
+ not introduce a `.` lookup.
58
+
59
+ This is a read-only path observation. It neither pins files nor creates a root
60
+ boundary, authorizes access, or guarantees a consistent snapshot during
61
+ concurrent changes. Results can become stale immediately. Use a guarded Root
62
+ operation for subsequent access to untrusted paths; keep any database ownership,
63
+ cache invalidation, and mutation policy with the caller. Native configuration
64
+ and Bun realpath limitations follow the existing [runtime contract](install.md#bun-runtime).
@@ -0,0 +1,159 @@
1
+ ---
2
+ title: Path suffix alias probing
3
+ description: "Bounded local observations for selected missing relative path suffixes."
4
+ ---
5
+
6
+ # Path suffix alias probing
7
+
8
+ `probePathSuffixAliasesSync()` observes whether selected missing relative suffixes
9
+ would alias beneath an existing directory. It returns `boolean | undefined`; it
10
+ does not infer filesystem behavior from the operating system or normalize a path
11
+ into an authorization decision.
12
+
13
+ ```ts
14
+ import { probePathSuffixAliasesSync } from "@openclaw/fs-safe/advanced";
15
+
16
+ const aliases = probePathSuffixAliasesSync({
17
+ directory: "/trusted/existing-directory",
18
+ left: "Reports/Caf\u00e9",
19
+ right: "reports/Cafe\u0301",
20
+ });
21
+ if (aliases === undefined) {
22
+ // The caller must choose an explicit ambiguity policy.
23
+ }
24
+ ```
25
+
26
+ Use this for suffixes that do not yet exist. For local ASCII-case observations,
27
+ including an explicit read-only mode, use
28
+ [`probePathCaseInsensitiveSync()`](path-case.md). Suffix probing has no read-only
29
+ mode: a nontrivial observation can create and remove directories and requires an
30
+ approved writable parent.
31
+
32
+ ## API and validation
33
+
34
+ ```ts
35
+ type ProbePathSuffixAliasesOptions = {
36
+ directory: string;
37
+ left: string;
38
+ right: string;
39
+ shouldProbeCaseVariants?: (leftNfc: string, rightNfc: string) => boolean;
40
+ };
41
+
42
+ function probePathSuffixAliasesSync(
43
+ options: ProbePathSuffixAliasesOptions,
44
+ ): boolean | undefined;
45
+ ```
46
+
47
+ The helper reads and validates `directory`, then resolves it to an absolute path
48
+ before reading the suffixes or predicate. Later option getters or the predicate
49
+ cannot retarget a relative directory by changing the working directory. When
50
+ filesystem observations are needed, the directory is canonicalized and its
51
+ identity is checked; an initial directory alias can be followed.
52
+
53
+ Suffixes must have the same number of ordinary relative path components. Empty
54
+ components, `.` and `..`, absolute suffixes, NUL characters, and non-string path
55
+ inputs are rejected with `TypeError`. Windows additionally rejects drive-relative
56
+ components, colons, and reserved device names, including device aliases with
57
+ extensions or trailing ignored characters. On POSIX, backslashes and colons are
58
+ ordinary filename characters. The optional predicate must be a function.
59
+
60
+ Both suffixes and the predicate are validated before the identical-suffix fast
61
+ path. Identical, valid, within-budget suffixes return `true` without filesystem
62
+ access or a predicate call. This does not prove that the directory exists or that
63
+ the suffix can be created.
64
+
65
+ ## Fixed resource limits
66
+
67
+ These limits apply to one call and cannot be raised through options:
68
+
69
+ | Resource | Limit | On exceeding the limit |
70
+ |---|---|---|
71
+ | Each supplied suffix | 8,192 UTF-16 code units | `RangeError` before mutation |
72
+ | Supplied and resolved directory paths | 32,768 UTF-16 code units each | `RangeError` before mutation |
73
+ | Each suffix's component count | 32 | `RangeError` before mutation |
74
+ | Directory-creation attempts | 128 | `undefined` after cleanup |
75
+ | Successfully created probe directories | 64 | `undefined` after cleanup |
76
+ | Forward filesystem observations | 4,096 | `undefined` after cleanup |
77
+ | Each generated actual path | 32,768 UTF-16 code units | `undefined` after cleanup |
78
+
79
+ Input limits are checked even for identical suffixes. Dynamic budgets count work
80
+ across the whole call, including collision retries; removing a probe does not
81
+ restore its creation budget. Cleanup is still attempted when a forward budget is
82
+ exhausted and is not disabled by that exhausted budget.
83
+
84
+ Candidates are generated lazily, only as needed. The helper does not eagerly
85
+ allocate every possible probe name or consume randomness for unused retries.
86
+ These are count and string-size limits, not a wall-clock guarantee. Synchronous
87
+ filesystem calls, randomness, and caller code can block; the helper cannot
88
+ interrupt them or promise a maximum elapsed time.
89
+
90
+ ## Results and caller policy
91
+
92
+ - `true`: the suffixes are identical, or the requested observations found aliases.
93
+ - `false`: an observation found distinct entries, or the caller's predicate
94
+ excluded a pair.
95
+ - `undefined`: no reliable answer was obtained, including filesystem failures,
96
+ identity changes, exhausted collision candidates or dynamic budgets, generated
97
+ path limits, or incomplete cleanup.
98
+
99
+ `undefined` is not evidence of either case sensitivity or aliasing. The helper
100
+ does not cache observations, choose a fallback, reserve the future destination,
101
+ or establish a root-confinement boundary. A result applies only to the local
102
+ observations made during that call, not to every Unicode pair, mount, or later
103
+ filesystem state.
104
+
105
+ The optional trusted, synchronous `shouldProbeCaseVariants` predicate receives
106
+ NFC-normalized component pairs when they differ and are not equivalent under
107
+ ASCII case folding. Without a predicate, those pairs are eligible for probing.
108
+ It is called in component order. Returning `false` excludes
109
+ that pair and produces `false`; it is caller policy, not a filesystem finding.
110
+ A first-component exclusion needs no probes or randomness. A later exclusion
111
+ can follow earlier mutations, so it does not make the call read-only.
112
+
113
+ A non-boolean predicate result throws `TypeError`; a predicate exception is
114
+ re-thrown unchanged after cleanup attempts. Cleanup problems do not replace the
115
+ original predicate failure. Do not use an asynchronous predicate or rely on the
116
+ resource limits to bound arbitrary callback work.
117
+
118
+ ## Probe design
119
+
120
+ The helper works through corresponding components, creating nested directories
121
+ to observe inherited lookup behavior. ASCII-case probes use generated names.
122
+ Normalization probes retain the original non-ASCII spellings while substituting
123
+ suitable ASCII letters. When a generated pair is unavailable, the exact raw pair
124
+ can be tried inside an owned neutral directory. Requested names are never
125
+ materialized directly in the caller's unowned parent.
126
+
127
+ Generated names are conservatively excluded when their NFC lowercase or uppercase
128
+ forms could collide with either requested component. These folds are only a name
129
+ exclusion rule; they never classify two requested paths as aliases. Short neutral
130
+ and ASCII probe names use at most six characters and are no longer than the
131
+ shorter requested component. Generated normalization pairs are also limited by
132
+ the longer original joined path length. A raw-pair fallback adds a directory
133
+ level; filesystem-specific name or path limits can still make it unavailable.
134
+
135
+ Probes use normal directory creation and the process umask, preserving inherited
136
+ directory behavior. They do not force mode `0700`, change parent permissions, or
137
+ promise private probe names. Creation and removal can affect directory timestamps
138
+ and filesystem watchers.
139
+
140
+ ## Identity and cleanup limitations
141
+
142
+ The requested directory, canonical parent, and owned probe chain are rechecked
143
+ with exact bigint filesystem identities. An alternate spelling must identify the
144
+ same ordinary directory, not a symlink or unrelated collision. Existing files,
145
+ directories, and symlinks at a candidate name are not removed to make room.
146
+
147
+ Cleanup attempts owned directories in reverse order with identity checks and
148
+ non-recursive removal. Nonempty directories and observed replacements are
149
+ preserved. A cleanup failure changes an otherwise boolean result to `undefined`.
150
+ If the first identity observation after a successful creation fails, a directory
151
+ can remain: the helper does not guess ownership to delete it.
152
+
153
+ This is a pathname-based observation helper, not an atomic filesystem
154
+ transaction. There are unavoidable gaps between creation and the first identity
155
+ observation, and between an identity check and a subsequent syscall. No pinned
156
+ directory handle or atomic conditional deletion closes those gaps. Use a trusted,
157
+ approved writable directory and application-level concurrency control where
158
+ needed; do not use this helper as authorization to access attacker-controlled
159
+ paths.
package/docs/path.md CHANGED
@@ -36,7 +36,11 @@ isPathInside("/srv/uploads", "/srv/uploads-other/x"); // false
36
36
  isPathInside("/srv/uploads", "/srv/uploads"); // true (root itself counts)
37
37
  ```
38
38
 
39
- The check is platform-aware: on Windows, paths are normalized for case and separator before comparison.
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");
@@ -42,7 +42,7 @@ POSIX remediation strings shell-quote paths with whitespace or metacharacters
42
42
  and protect option-like paths with `--`, so they can be presented as commands
43
43
  without letting the inspected pathname add shell syntax.
44
44
 
45
- `inspectPathPermissions()` follows symlink targets for the effective mode but tells you whether the original path was a symlink. On POSIX it reports owner/group/world bits. On Windows it delegates to the ACL helpers below and also reports `ownerSid` plus `ownerTrusted` when ownership can be verified. `ownerTrusted` is true only for a local volume owned by the current user, LocalSystem, or built-in Administrators; remote filesystems fail closed. Secure reads and callers that protect credential-bearing execution require `ownerTrusted === true`.
45
+ `inspectPathPermissions()` follows symlink targets for the effective mode but tells you whether the original path was a symlink. On POSIX it reports owner/group/world bits. On Windows it delegates to the ACL helpers below and also reports `ownerSid` plus `ownerTrusted` when ownership can be verified. `ownerTrusted` is true only for a local volume owned by the current user, LocalSystem, or built-in Administrators; remote filesystems fail closed. This remains a pathname reporting API with the fallbacks described below. `readSecureFile()` does not use those pathname fallbacks on Windows: it requires descriptor-bound native owner/DACL facts for the exact handle it reads.
46
46
 
47
47
  ## Advanced Windows ACL helpers
48
48
 
@@ -178,6 +178,31 @@ await openSqlite(path.join(sqliteDirectory, "sessions.sqlite"));
178
178
  On Windows with native support, this creates the directory and applies a
179
179
  protected owner + LocalSystem + Administrators full-control DACL directly with
180
180
  an atomic security descriptor; no PowerShell or `icacls` process is launched.
181
+ The native operation retains the parent and exact created-directory handles
182
+ through ACL and final pathname validation. If validation fails, it attempts only
183
+ nonrecursive deletion through the created handle, preserving any pathname
184
+ replacement. If cleanup also fails, the error retains the original failure and
185
+ includes the cleanup failure.
186
+
187
+ Directory association checks compare the complete 64-bit volume serial and
188
+ 128-bit `FILE_ID_INFO` identity, including on ReFS. If that identity class is
189
+ unavailable, the operation fails closed without a narrower file-index fallback.
190
+ Validation confirms that the created directory is local, its DACL is protected
191
+ from inheritance, and its final public pathname opens the same local directory.
192
+
193
+ This is a point-in-time pathname association check. The function closes its
194
+ handles before returning; callers must keep the pathname's ancestry trusted
195
+ during subsequent use, including opening SQLite databases in the example above.
196
+ The immediate parent and final directory must not be reparse points. Earlier
197
+ ancestor reparse points can be followed; this API does not reject every reparse
198
+ point in the full ancestry.
199
+
200
+ Path components ending in a space or period are rejected before filesystem
201
+ operations to avoid differing Win32 and native pathname interpretations. This
202
+ also rejects explicit `.` and `..` components, including spellings such as
203
+ `.\private` and `parent\..\private`, as a compatibility restriction. Simple
204
+ relative names without these components remain supported.
205
+
181
206
  This API is Windows-only and native-only; it fails closed with
182
207
  `FsSafeError("helper-unavailable")` on other platforms, when native mode is off,
183
208
  or when the binding is unavailable. POSIX callers should create private
@@ -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.
@@ -26,6 +26,11 @@ deprecated native-configuration bridge retains the `FsSafePythonConfig` type.
26
26
 
27
27
  ## `path` and `advanced`
28
28
 
29
+ `safePathSegmentHashedV2` encodes every trimmed install ID with domain-separated
30
+ SHA-256 into a fixed lowercase segment. The legacy `safePathSegmentHashed` keeps
31
+ its existing output but can alias distinct IDs. See [install paths](install-path.md)
32
+ for the exact encoding and migration contract.
33
+
29
34
  The lexical path surface additionally exports `isNodeError`,
30
35
  `isPathRelativeEscape`, `normalizeWindowsPathForComparison`,
31
36
  `resolveSafeRelativePath`, `splitSafeRelativePath`, and
@@ -56,6 +61,15 @@ prefix preparation. See [in-place writes](in-place-write.md).
56
61
  for local ASCII-case observations. An unavailable answer remains `undefined`;
57
62
  the caller selects any fallback. See [path case probing](path-case.md).
58
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
+
59
73
  ## Guest source
60
74
 
61
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
@@ -100,7 +100,9 @@ The read methods also accept an absolute spelling that already resolves inside
100
100
  the root. `readAbsolute()` and `reader()` make that intent explicit and accept
101
101
  both the configured root spelling and its canonical real path when the Root was
102
102
  created through a directory symlink or Windows junction. An absolute path
103
- outside the root is still rejected.
103
+ outside the root is still rejected. On Windows, alternate casing is accepted
104
+ only when the differently cased Root prefix has the Root's exact directory
105
+ identity; the operation then continues under the trusted Root spelling.
104
106
 
105
107
  ### Writes
106
108
 
@@ -112,7 +114,7 @@ fs.createJson(rel, value, options?) // create() variant of writeJson
112
114
  fs.append(rel, data, options?) // append text/buffer; syncs before close by default
113
115
  fs.copyIn(rel, sourceAbsPath, options?) // copy from outside the root, atomically, with size cap
114
116
  fs.openWritable(rel, options?) // FileHandle for streaming writes; supports await using
115
- 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
116
118
  fs.remove(rel, options?) // unlink file, rmdir, or bounded recursive removal
117
119
  fs.mkdir(rel, options?) // mkdir -p (creates missing parents)
118
120
  fs.ensureRoot(options?) // accepts "" / "." as the root itself
@@ -219,7 +221,17 @@ source)` can reject a legal POSIX basename such as `c:photo.png`; callers that
219
221
  derive portable destination names from host files must sanitize or map that
220
222
  basename first.
221
223
 
222
- `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.
223
235
 
224
236
  `remove` leaves non-empty directories unchanged unless `recursive: true` is
225
237
  provided. Recursive removal defaults to streaming entries in filesystem order;
@@ -297,7 +309,12 @@ fs.entries(rel, options?) // nonrecursive AsyncIterable<DirEntry>, includ
297
309
  fs.resolve(rel) // absolute path inside the root, after canonicalization
298
310
  ```
299
311
 
300
- 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.
301
318
 
302
319
  `entries()` streams immediate children in filesystem order by default. It
303
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