@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
@@ -23,7 +23,7 @@ function resolveSafeInstallDir(params: {
23
23
  }): { ok: true; path: string } | { ok: false; error: string };
24
24
  ```
25
25
 
26
- Computes the absolute install directory for `id` under `baseDir`, after running `id` through `nameEncoder` (`safeDirName` by default). Verifies the result stays inside `baseDir` — anything that would escape returns `{ ok: false, error: invalidNameMessage }`.
26
+ Computes the absolute install directory for `id` under `baseDir`, after running `id` through `nameEncoder` (`safeDirName` by default). Verifies the result stays inside `baseDir` — anything that would escape returns `{ ok: false, error: invalidNameMessage }`. On Windows, contained alternate-stream and filesystem-namespace aliases are rejected with the same result.
27
27
 
28
28
  ```ts
29
29
  const r = resolveSafeInstallDir({
@@ -64,7 +64,7 @@ function assertCanonicalPathWithinBase(params: {
64
64
  }): Promise<void>;
65
65
  ```
66
66
 
67
- Throws if the candidate resolves outside `baseDir` after `realpath`. The `boundaryLabel` is included in the error message ("Invalid path: must stay within {boundaryLabel}").
67
+ Throws if the candidate resolves outside `baseDir` after `realpath`. On Windows it also throws for alternate-stream or filesystem-namespace aliases in the base, candidate, or canonical path. The `boundaryLabel` is included in the containment-shaped error message ("Invalid path: must stay within {boundaryLabel}").
68
68
 
69
69
  ```ts
70
70
  await assertCanonicalPathWithinBase({
package/docs/install.md CHANGED
@@ -127,9 +127,10 @@ the matching binary. Consumers do not run a native build, download code at
127
127
  runtime, or execute a postinstall step. Omitting optional dependencies keeps
128
128
  non-archive fallback-capable operations working in `auto` or `off`. Native-only
129
129
  features, including strict owned-tree temp cleanup, retained-directory staging,
130
- atomic `rename-noreplace`, zstd/bzip2 TAR handling, and Windows private-directory
131
- creation, remain unavailable. Operations needing the binding in `require` mode fail with
132
- `helper-unavailable` when the matching package is absent or incompatible.
130
+ atomic `rename-noreplace` (including the default no-clobber `Root.move()`),
131
+ zstd/bzip2 TAR handling, and Windows private-directory creation, remain
132
+ unavailable. Operations without a safe fallback fail with `helper-unavailable`
133
+ when the matching package is absent, incompatible, or disabled.
133
134
 
134
135
  Upgrading an existing 0.5 consumer? Follow [Migrating to 0.6](migrating-to-0.6.md)
135
136
  before deploying with native mode `require` or native-only features.
@@ -138,15 +139,15 @@ before deploying with native mode `require` or native-only features.
138
139
 
139
140
  The platform native binaries provide fd-relative open/link/mkdir primitives,
140
141
  atomic no-replace rename, and file identity checks. The default is `auto`: use
141
- the matching binary when it loads, otherwise silently keep the guarded
142
- JavaScript path. Platforms without one of the seven published targets therefore
143
- continue through the documented fallback in `auto` mode.
142
+ the matching binary when it loads, otherwise use the guarded JavaScript path
143
+ where a safe fallback exists. Native-only operations fail with
144
+ `helper-unavailable`.
144
145
 
145
146
  ```ts
146
147
  import { configureFsSafeNative } from "@openclaw/fs-safe/config";
147
148
 
148
149
  configureFsSafeNative({ mode: "auto" }); // default
149
- configureFsSafeNative({ mode: "off" }); // guarded JavaScript only
150
+ configureFsSafeNative({ mode: "off" }); // guarded JavaScript; reject native-only operations
150
151
  configureFsSafeNative({ mode: "require" }); // fail closed if unavailable
151
152
  ```
152
153
 
@@ -1,6 +1,6 @@
1
1
  # JSON store
2
2
 
3
- `jsonStore` is exported from `@openclaw/fs-safe/store`. It is the absolute-path
3
+ `jsonStore` is exported from `@openclaw/fs-safe/store`. It is the single-path
4
4
  convenience wrapper for `fileStore(...).json(...)`: a small read-modify-write
5
5
  handle around a single JSON file. It bakes in atomic writes, explicit fallback
6
6
  reads, and optional cross-process locking via
@@ -71,6 +71,10 @@ type JsonStore<T> = {
71
71
 
72
72
  `jsonStore({ filePath })` resolves `rootDir = dirname(filePath)` and calls
73
73
  `fileStore({ rootDir, private: true }).json(basename(filePath), options)`.
74
+ On Windows, the factory rejects NTFS alternate-stream and directory-index
75
+ namespace spellings before preparing its private parent. An ordinary
76
+ drive-relative path is anchored at entry and `store.filePath` exposes the
77
+ resulting absolute path; ordinary colon-bearing POSIX paths remain valid.
74
78
 
75
79
  `durable: false` keeps sibling-temp replace/rename behavior but skips the
76
80
  temp-file and parent-directory `fsync` calls. Use it only for reconstructible
package/docs/json.md CHANGED
@@ -38,6 +38,17 @@ Use `readJson` when missing-or-malformed is a programmer error you want to surfa
38
38
 
39
39
  `JsonFileReadError` carries `cause` so you can inspect whether the underlying failure was an `ENOENT`, a `SyntaxError`, or something else.
40
40
 
41
+ On Windows, filesystem path inputs reject NTFS alternate-stream and
42
+ directory-index namespace spellings such as `file:stream` and
43
+ `dir::$INDEX_ALLOCATION`; ordinary colon-bearing POSIX names remain valid.
44
+ Standalone JSON writers preserve their released support for an ordinary
45
+ drive-relative destination by anchoring its leading drive designator at entry
46
+ without normalizing the remaining suffix. Any additional colon is still
47
+ rejected before filesystem access.
48
+ Strict standalone readers retain `JsonFileReadError` and expose the
49
+ `invalid-path` rejection as its cause, lenient `tryReadJson*` calls return
50
+ `null`, and root-bounded readers report an `open`/`validation` failure.
51
+
41
52
  ## Reading
42
53
 
43
54
  ### `readJson<T>(filePath, options?)`
@@ -0,0 +1,65 @@
1
+ # Hosted mutation-policy proof
2
+
3
+ The `mutation policy public behavior proof` workflow builds the exact event-head
4
+ package and host addon on Node 24 Linux, macOS, and Windows. Each case executes in
5
+ a fresh process, uses a private temporary fixture, and emits only a bounded,
6
+ canonical receipt. POSIX workers require both real and effective non-root UIDs.
7
+
8
+ Receipt schema `fs-safe-mutation-policy-proof-v2` deliberately does not describe
9
+ final directory emptiness as a mutation-dispatch count. Root replacement records
10
+ the unchanged contents of the original and replacement parent; denied redirect
11
+ records rejection and unchanged denied/displaced parents. The existing JS mkdir
12
+ counters name their exact child or next component. None counts native syscalls
13
+ or excludes transient mutations that leave no final trace.
14
+
15
+ ## Additional hosted cases
16
+
17
+ Each POSIX write/create/copy worker, under both native-off and native-require,
18
+ performs an eligible success control, an exact deeper-parent denial after an
19
+ earlier parent is created, a denied redirect after preflight, and a stale-parent
20
+ replacement at the public pre-mkdir authority boundary. Exact directory listings
21
+ and sentinels verify that denied/current/displaced parents contain no prohibited
22
+ child, target, or stage. Copy sources must retain their original bytes.
23
+
24
+ Redirect injection uses the existing public `@openclaw/fs-safe/test-hooks` subpath
25
+ from `dist`, not a mocked native binding. Only these isolated workers enable
26
+ `NODE_ENV=test`. The exact target and single hook visit are required. The hook is
27
+ after policy preflight but before parent admission; it must not be described as
28
+ a post-parent-admission hook. Stale replacement instead uses the public authority
29
+ callback after child-create policy admission, with an existing sentinel parent
30
+ and a still-missing child. Both implementation files that enforce the following
31
+ freshness check are hash-bound. No unchecked callback ordinal chooses a fault.
32
+
33
+ Representative POSIX `Root.write` workers refuse authority before the first mkdir,
34
+ after one parent has been created and before the next mkdir, before staging, and
35
+ immediately before publication. Epoch selection uses actual fixture state. The
36
+ publication refusal requires a real single-link, caller-owned mode-0600 stage
37
+ containing the complete payload while the destination still contains its original
38
+ sentinel. The same rejection object must escape, no callback may follow refusal,
39
+ and the owned stage must be gone before fixture teardown.
40
+
41
+ Windows workers exercise buffer `Root.write` through a stable final-file symlink,
42
+ then refuse before staging a newly created placeholder, before publishing over a
43
+ new placeholder, and before publishing to an existing symlink-selected destination.
44
+ They verify alias binding, destination preservation, observed placeholder/stage
45
+ states, and cleanup before fixture teardown. The default native-off and explicit
46
+ `verify-content-with-lock` native-require configurations both select the existing
47
+ Windows JS buffer writer. The compatibility route is expected not to load the
48
+ addon; the receipt does not mislabel this as native publication or evidence that
49
+ content-verification fallback or lock contention was exercised.
50
+
51
+ ## Bounds and interpretation
52
+
53
+ The receipt remains below 32 KiB, each worker has a 15-second process timeout and
54
+ 4-KiB stdout limit, and the proof step has a six-minute outer timeout. Worker I/O
55
+ and cleanup retain their existing deadlines. The canonical pending/failed receipt
56
+ is preserved if setup, a worker, provenance validation, or emission fails.
57
+ Source, built modules, the public test seam, harness helper, contract tests, and
58
+ host addon are hashed and checked again after workers.
59
+
60
+ These are deterministic representative observations, not an exhaustive race proof
61
+ or syscall audit. Existing hash-bound internal tests remain complementary for
62
+ other awaited-admission, receipt-refresh, and observation-failure interleavings.
63
+ Windows root replacement and POSIX-specific pinned routes are not claimed on
64
+ Windows. Hosted CI and exact artifact inspection are required before relying on
65
+ new receipts; the proof supplies no performance release clearance.
@@ -15,7 +15,7 @@ consumer Rust build.
15
15
  import { configureFsSafeNative } from "@openclaw/fs-safe/config";
16
16
 
17
17
  configureFsSafeNative({ mode: "auto" }); // default
18
- configureFsSafeNative({ mode: "off" }); // guarded JavaScript only
18
+ configureFsSafeNative({ mode: "off" }); // guarded JavaScript; reject native-only operations
19
19
  configureFsSafeNative({ mode: "require" }); // fail closed when the binding is unavailable
20
20
  ```
21
21
 
@@ -25,8 +25,8 @@ The equivalent environment variables are `FS_SAFE_NATIVE_MODE` and `OPENCLAW_FS_
25
25
 
26
26
  | Mode | Behavior |
27
27
  |---|---|
28
- | `auto` | Prefer native primitives when the current platform package loads; otherwise silently use the guarded JavaScript path. |
29
- | `off` | Do not load a native package. Use the guarded JavaScript path deterministically. |
28
+ | `auto` | Prefer native primitives when the current platform package loads; otherwise use guarded JavaScript where a safe fallback exists and reject native-only operations. |
29
+ | `off` | Do not load a native package. Use guarded JavaScript where safe and reject native-only operations deterministically. |
30
30
  | `require` | Throw `FsSafeError("helper-unavailable")` instead of falling back when an operation needs the native binding and it cannot load. |
31
31
 
32
32
  TAR/gzip in the guarded JavaScript path uses a bundled, import-free WASM build
@@ -43,6 +43,19 @@ when admitting a temp workspace. Containment and identity checks stay intact.
43
43
 
44
44
  Configure the mode once during startup. Loading is lazy and cached; changing from `auto` to `require` after a failed load changes failure policy but does not repeatedly probe the binary.
45
45
 
46
+ Native-created descriptors retain their originating native close operation through
47
+ normal and error cleanup, including later mode changes. Node-created roots and
48
+ directory handles keep Node's close operation, and borrowed handles keep their
49
+ caller-owned lifetime. This preserves Node worker-thread descriptor tracking
50
+ without unmanaged-descriptor warnings. A helper missing native close support is
51
+ unavailable before descriptor allocation.
52
+
53
+ Close retained native resources and let in-flight operations finish before
54
+ forcibly terminating a worker. Native-created descriptors are not registered
55
+ with Node's automatic worker-exit cleanup; `Worker.terminate()` can leave them
56
+ open until process exit. The native close operation handles explicit cleanup,
57
+ not forced worker termination.
58
+
46
59
  [`tempWorkspace()` and its scoped/sync variants](temp.md#private-temp-workspaces)
47
60
  remain available in every mode. Their default compatible cleanup uses guarded
48
61
  JavaScript quarantine when owned native tree removal is unavailable.
@@ -62,6 +75,20 @@ change the mode policy of existing fallback-capable APIs.
62
75
 
63
76
  ## Native boundary
64
77
 
78
+ The internal Darwin descriptor ACL inspector requires its matching native
79
+ capability in both `auto` and `require`; `off`, a missing package, or an older
80
+ binding without `inspectDarwinAcl` rejects with `helper-unavailable`. Inspection
81
+ failure or malformed facts reject with `permission-unverified`; there is no
82
+ mode-bit or pathname fallback for this capability. Clone admission uses a fused
83
+ descriptor-bound metadata and ACL observation, then compares immutable receipts
84
+ with fresh no-follow pathname identity fences; pathnames never authorize ACL
85
+ state. The payload ACL-clear readback is part of that fused observation. Once
86
+ a clone payload exists, normalization and verification failures become terminal
87
+ `EIO` errors (with the underlying status and detail retained), not capability
88
+ signals that permit an ordinary-copy retry. Checked cleanup cannot undo that
89
+ terminal classification.
90
+ This addition does not change other APIs' native-mode or permission contracts.
91
+
65
92
  The native layer exposes policy-free filesystem mechanisms: beneath-root
66
93
  open/mkdir/link, replace and no-replace rename, identity reads, archive decode/execution,
67
94
  clone/copy/hash workers, POSIX canonicalization, and Windows security descriptor calls. The TypeScript
@@ -70,14 +97,17 @@ normalization, and the decision to fall back.
70
97
 
71
98
  - Linux uses `openat2` with `RESOLVE_BENEATH | RESOLVE_NO_MAGICLINKS`, `renameat`, and `renameat2(RENAME_NOREPLACE)`. Owned-tree cleanup enumerates and unlinks through retained directory descriptors and rejects device crossings.
72
99
  - macOS 15.4 and newer prefer `O_RESOLVE_BENEATH`; older kernels resolve components with `O_NOFOLLOW` and restart in-root symlinks from the pinned root descriptor. Both routes use an `F_GETPATH` post-open escape detector and report `best-effort` because directory rename races are not atomic with that check. Publication uses `renameat` for replacement and `renameatx_np(RENAME_EXCL)` for no-replace; owned-tree cleanup uses descriptor-relative `openat`/`unlinkat`.
73
- - Windows uses handle-relative `NtCreateFile`, rejects reparse points during root-bounded traversal, uses `FileRenameInfoEx` with replacement selected explicitly by the TypeScript policy layer, and deletes owned trees through exact opened handles with `FileDispositionInfoEx`; symlink/reparse entries in owned trees are removed as leaves and never traversed. Descriptors crossing N-API are converted only by the host executable's paired `uv_get_osfhandle` and `uv_open_osfhandle` exports. A runtime without both exports is unsupported for these native operations; the binding never guesses a raw HANDLE or uses a foreign CRT descriptor table.
74
-
75
- Native primitives back create-only and replacing pinned writes, async sidecar creation,
76
- guarded publication, archive acceleration, and direct Windows ACL operations. Windows
77
- secure-file reads require descriptor-bound owner/DACL facts from the current helper;
78
- they do not use the standalone pathname inspector's command fallback.
79
- Equivalent JavaScript paths remain available for documented fallback-capable
80
- features. See [Native architecture](native.md#javascript-fallback-guarantees-and-delta)
100
+ - Windows uses handle-relative `NtCreateFile`, rejects reparse points during root-bounded traversal, uses `FileRenameInfoEx` with replacement selected explicitly by the TypeScript policy layer, and deletes owned trees through exact opened handles with `FileDispositionInfoEx`; symlink/reparse entries in owned trees are removed as leaves and never traversed. Descriptors crossing N-API are converted only by the host executable's paired `uv_get_osfhandle` and `uv_open_osfhandle` exports. A runtime without both exports is unsupported for these native operations; the binding never guesses a raw HANDLE or uses a foreign CRT descriptor table. Descriptor-producing operations also require that same host's synchronous libuv close and request-management APIs before exporting an owned descriptor.
101
+
102
+ Native primitives back create-only and replacing pinned writes, no-clobber
103
+ `Root.move()`, async sidecar creation, guarded publication, archive acceleration,
104
+ and direct Windows ACL operations. Windows secure-file reads require
105
+ descriptor-bound owner/DACL facts from the current helper; they do not use the
106
+ standalone pathname inspector's command fallback. No-clobber moves fail with
107
+ `helper-unavailable` when descriptor-relative parent admission or the atomic
108
+ no-replace rename is unavailable; they never use a check followed by a replacing
109
+ rename. Equivalent JavaScript paths remain available for documented
110
+ fallback-capable features. See [Native architecture](native.md#javascript-fallback-guarantees-and-delta)
81
111
  for the exact difference.
82
112
 
83
113
  The guarded JavaScript mutation path is detection-based, not containment-atomic.
package/docs/native.md CHANGED
@@ -50,7 +50,20 @@ 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
@@ -62,6 +75,16 @@ whether a path, archive entry, mode, owner, or cleanup policy is acceptable.
62
75
  missing or partial exports fail with `ENOTSUP` instead of trying a raw HANDLE
63
76
  or add-on CRT descriptor namespace.
64
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.
87
+
65
88
  ## Archives
66
89
 
67
90
  Native ZIP and TAR entry reads retain a private, unpooled input Buffer in
@@ -141,18 +164,19 @@ not bypass the byte limit.
141
164
 
142
165
  | Mode | Native loading | Fallback |
143
166
  |---|---|---|
144
- | `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 |
145
168
  | `require` | Try once, cache the result | Throw `FsSafeError("helper-unavailable")` |
146
- | `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 |
147
170
 
148
171
  `sha256FileSync()` is a synchronous Node implementation in all three modes and
149
172
  does not load the binding. Use asynchronous `sha256File()` for native hashing
150
173
  and cancellation that can respond while JavaScript callbacks run.
151
174
 
152
- Features without a safe JavaScript implementation, including zstd/bzip2 TAR,
153
- Windows private-directory creation, and [retained-directory staging](staged-file.md),
154
- fail with `helper-unavailable` when native support is absent or off. Staging
155
- 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`.
156
180
 
157
181
  The staged-file owner also serves POSIX native pinned writes, including streaming.
158
182
  Unpublished files remain at `0600`; requested modes are applied through the
@@ -189,7 +213,7 @@ remain TypeScript-owned. What changes is the syscall strength or availability:
189
213
 
190
214
  | Capability | Native path | Guarded JavaScript path |
191
215
  |---|---|---|
192
- | 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. |
193
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. |
194
218
  | Zstd/bzip2 TAR | Supported. | Unsupported; typed `helper-unavailable`. |
195
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. |
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.