@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/dist/root-impl.js CHANGED
@@ -56,28 +56,32 @@ import fs from "node:fs/promises";
56
56
  import path from "node:path";
57
57
  import { normalizeMaxBytes } from "./byte-budget.js";
58
58
  import { assertCopySourceCurrent, resolveFileCopyCloneMode } from "./copy-file-input.js";
59
- import { assertAsyncDirectoryGuard, createAsyncDirectoryGuard, createNearestExistingDirectoryGuard } from "./directory-guard.js";
59
+ import { assertAsyncDirectoryGuard, assertSyncDirectoryGuard, createAsyncDirectoryGuard, createNearestExistingDirectoryGuard } from "./directory-guard.js";
60
60
  import { FsSafeError } from "./errors.js";
61
61
  import { syncDirectoryBestEffort } from "./directory-durability.js";
62
- import { sameFileIdentity, sameFileIdentityForCleanup, } from "./file-identity.js";
63
- import { mkdirPathComponentsWithGuards } from "./guarded-mkdir.js";
64
62
  import { withAsyncDirectoryGuards } from "./guarded-mutation.js";
65
- import { assertMutationNotDenied, mergeDenyMutationPolicies, } from "./deny-mutations.js";
66
- import { resolveOpenedFileRealPathForFd, resolveOpenedFileRealPathForHandle } from "./opened-realpath.js";
67
- import { openedPathResolutionError, recordExclusiveCreateFailure, recordFileOpenFailure, recordOpenedFileFailure, recordPreOpenFileChange } from "./opened-file-failure.js";
68
- import { runPinnedWriteHelper, runPinnedWriteWithRenamePolicy, } from "./pinned-write.js";
63
+ import { openLocalFileDescriptor } from "./local-file-descriptor.js";
64
+ import { assertMutationNotDenied, mergeDenyMutationPolicies } from "./deny-mutations.js";
65
+ import { resolveOpenedFileRealPathForFd } from "./opened-realpath.js";
66
+ import { openedPathResolutionError, recordExclusiveCreateFailure, recordOpenedFileFailure } from "./opened-file-failure.js";
67
+ import { runPinnedWriteHelper, runPinnedWriteWithRenamePolicy } from "./pinned-write.js";
68
+ import { preparePinnedWriteMutationAdmission, snapshotPinnedMutationPolicy } from "./pinned-mutation-admission.js";
69
69
  import { getNativeBinding } from "./native.js";
70
70
  import { validatePinnedOperationPayload } from "./pinned-operation.js";
71
- import { assertNoPathAliasEscape, PATH_ALIAS_POLICIES } from "./path-policy.js";
72
- import { assertNoNulPathInput, assertNoUnsafeDeviceReadPath, hasNodeErrorCode, isNotFoundPathError, isPathInside, isSymlinkOpenError, } from "./path.js";
71
+ import { PATH_ALIAS_POLICIES } from "./path-policy.js";
72
+ import { assertNoNulPathInput, hasNodeErrorCode, isNotFoundPathError, isSymlinkOpenError, } from "./path.js";
73
73
  import { readOpenedFileSafely } from "./read-opened-file.js";
74
- import { cleanupPinnedFilePath } from "./replace-file-temp-owner.js";
75
- import { resolveReadOpenFlags } from "./read-open-flags.js";
74
+ import { cleanupPinnedFilePath, removePathIfIdentityUnchanged } from "./replace-file-temp-owner.js";
76
75
  import { realpathSync } from "./realpath.js";
76
+ import { mkdirPathFallback, prepareRootWriteTarget, tryMkdirAtExactParent } from "./root-directory-creation.js";
77
77
  import { isNonRegularWriteOpenError, resolveNonblockingWriteFlag } from "./write-open-flags.js";
78
- import { resolveRootPath } from "./root-path.js";
79
- import { openRootDirectoryListing, listDirectoryPath, pathStatFromStats } from "./root-directory-list.js";
78
+ import { resolveRootPath, resolveRootPathForRemoval } from "./root-path.js";
79
+ import { RemovalPathReceipts } from "./root-remove-receipt.js";
80
+ import { admitPathInsideRoot } from "./root-boundary.js";
81
+ import { listDirectoryPath, openRootDirectoryListing } from "./root-directory-list.js";
82
+ import { statResolvedPathInRoot } from "./root-path-stat.js";
80
83
  import { entriesInRoot } from "./root-entries.js";
84
+ import { assertMoveMutationAllowed } from "./root-move-preflight.js";
81
85
  import { assertRootIdentityCurrent, assertValidRootDestinationPath, assertValidRootRelativePath, ensureTrailingSep, expandRelativePathWithHome, resolvePathInRoot, resolveRootContext, rootRelativeReadPath, } from "./root-context.js";
82
86
  import { fileNotFoundError, hardlinkedPathNotAllowedError, isAlreadyExistsError, normalizePinnedPathError, normalizePinnedWriteError, outsideWorkspaceError, } from "./root-errors.js";
83
87
  import { getFsSafeTestHooks } from "./test-hooks.js";
@@ -87,12 +91,19 @@ import { registerTempPathForExit } from "./temp-cleanup.js";
87
91
  import { removePathInRootFallback, validateRemoveOptions } from "./root-remove.js";
88
92
  import { serializePathWrite } from "./write-queue.js";
89
93
  import { verifyAtomicWriteResult } from "./root-write-verification.js";
90
- import { inheritWriteTargetMode } from "./root-write-mode.js";
91
- import { inspectFileIdentity } from "./strict-file-identity.js";
94
+ import { assertRootWritePathSelectionSync, assertRootWriteSelectionSync, createRootWriteSelectionForFd, prepareGuardedRootWritePathSelection, resolveGuardedWritePathInRoot, resolveGuardedWriteTargetInRoot, resolvePinnedWriteTargetInRoot, refreshRetainedRootWriteAdmission, refreshRootWritePathSelection, retainRootWriteSelection, takeRootWriteSelection, } from "./root-write-admission.js";
95
+ import { prepareSharedRootWriteTarget } from "./root-write-complete-parent.js";
96
+ import { finishRootFallbackWrite } from "./root-write-publication.js";
97
+ import { assertRootFallbackWritePath, withRootFallbackCompatibilityLock } from "./root-write-compatibility.js";
98
+ import { inspectFileIdentity, inspectFileIdentitySync } from "./strict-file-identity.js";
99
+ import { movePathNoReplaceNative } from "./root-move-noreplace.js";
100
+ import { admitRootReadHandle, inspectOpenedPathIdentitySync } from "./root-read-admission.js";
92
101
  import { createCopyPublicationObserver, onCopyPublication } from "./copy-publication.js";
93
102
  import { writeAllToFile } from "./write-file-handle.js";
94
103
  import { createInputOptions, rethrowCreateInputError, rootWriteInput } from "./root-create-input.js";
95
104
  import { assertFinalSymlinkRejected, mutationSymlinkResolution, readSymlinkResolution } from "./root-symlink-policy.js";
105
+ import { assertNoWindowsPathAlias, resolvePathPreservingWindowsRoot } from "./windows-path-alias.js";
106
+ import { resolvePinnedObservedPathInRoot } from "./root-observed-path.js";
96
107
  import { mergeReadOptions, readDefaults, } from "./root-options.js";
97
108
  import { composeMutationAssertions, MutationAuthorityError, rethrowMutationAuthorityError } from "./mutation-authority.js";
98
109
  export { DEFAULT_ROOT_MAX_BYTES } from "./root-options.js";
@@ -103,8 +114,6 @@ function logWarn(message) {
103
114
  }
104
115
  }
105
116
  const SUPPORTS_NOFOLLOW = process.platform !== "win32" && "O_NOFOLLOW" in fsConstants;
106
- const OPEN_READ_FLAGS = resolveReadOpenFlags();
107
- const OPEN_READ_FOLLOW_FLAGS = resolveReadOpenFlags({ followSymlinks: true });
108
117
  const OPEN_WRITE_EXISTING_FLAGS = fsConstants.O_WRONLY | (SUPPORTS_NOFOLLOW ? fsConstants.O_NOFOLLOW : 0) |
109
118
  resolveNonblockingWriteFlag();
110
119
  const OPEN_WRITE_CREATE_FLAGS = fsConstants.O_WRONLY |
@@ -128,69 +137,9 @@ function openResult(params) {
128
137
  };
129
138
  }
130
139
  async function openVerifiedLocalFile(filePath, options) {
131
- assertNoUnsafeDeviceReadPath(filePath);
132
140
  const fsSafeTestHooks = getFsSafeTestHooks();
133
- let preOpenStat;
134
- let observedBeforeOpen = false;
135
- // Reject directories before opening so we never surface EISDIR to callers (e.g. tool
136
- // results that get sent to messaging channels). See openclaw/openclaw#31186.
141
+ const { handle, stat, identity, preOpenStat } = await openLocalFileDescriptor(filePath, options);
137
142
  try {
138
- preOpenStat = await inspectFileIdentity(async () => {
139
- const stat = fsSync.lstatSync(filePath, { bigint: true });
140
- observedBeforeOpen = true;
141
- if (stat.isSymbolicLink() && options?.symlinks !== "follow-within-root") {
142
- throw new FsSafeError("symlink", "symlink not allowed");
143
- }
144
- if (!stat.isFile() && !stat.isSymbolicLink()) {
145
- throw new FsSafeError("not-file", "not a file");
146
- }
147
- return stat;
148
- });
149
- }
150
- catch (err) {
151
- if (err instanceof FsSafeError || observedBeforeOpen) {
152
- throw err;
153
- }
154
- // Only an initial lookup failure falls through; a failed re-inspection aborts.
155
- }
156
- if (preOpenStat) {
157
- await fsSafeTestHooks?.afterPreOpenLstat?.(filePath);
158
- }
159
- const openFlags = options?.symlinks === "follow-within-root"
160
- ? OPEN_READ_FOLLOW_FLAGS
161
- : OPEN_READ_FLAGS;
162
- await fsSafeTestHooks?.beforeOpen?.(filePath, openFlags);
163
- let handle;
164
- try {
165
- handle = await fs.open(filePath, openFlags).catch((error) => recordFileOpenFailure(isNotFoundPathError(error) ? fileNotFoundError() : error, filePath));
166
- }
167
- catch (err) {
168
- if (isSymlinkOpenError(err)) {
169
- throw new FsSafeError("symlink", "symlink open blocked", { cause: err });
170
- }
171
- // Defensive: if open still throws EISDIR (e.g. race), sanitize so it never leaks.
172
- if (hasNodeErrorCode(err, "EISDIR")) {
173
- throw new FsSafeError("not-file", "not a file");
174
- }
175
- throw err;
176
- }
177
- try {
178
- await fsSafeTestHooks?.afterOpen?.(filePath, handle);
179
- const stat = fsSync.fstatSync(handle.fd);
180
- if (!stat.isFile()) {
181
- throw new FsSafeError("not-file", "not a file");
182
- }
183
- // Keep numeric Stats for the public receipt, never for identity verification.
184
- let openedIdentity;
185
- const identity = await inspectFileIdentity(async () => (openedIdentity = fsSync.fstatSync(handle.fd, { bigint: true })), preOpenStat && !preOpenStat.isSymbolicLink() ? preOpenStat : undefined).catch(async (error) => {
186
- if ([0, 1].includes(stat.nlink)) {
187
- await recordPreOpenFileChange(error, handle, filePath, preOpenStat, openedIdentity);
188
- }
189
- throw error;
190
- });
191
- if (options?.hardlinks === "reject" && stat.nlink > 1) {
192
- throw hardlinkedPathNotAllowedError();
193
- }
194
143
  const inspectPathIdentity = async (inspect) => {
195
144
  try {
196
145
  return await inspectFileIdentity(inspect, identity);
@@ -203,15 +152,7 @@ async function openVerifiedLocalFile(filePath, options) {
203
152
  throw failure;
204
153
  }
205
154
  };
206
- await inspectPathIdentity(async () => {
207
- const pathStat = options?.symlinks === "follow-within-root"
208
- ? fsSync.statSync(filePath, { bigint: true })
209
- : fsSync.lstatSync(filePath, { bigint: true });
210
- if (pathStat.isSymbolicLink() && options?.symlinks !== "follow-within-root") {
211
- throw new FsSafeError("symlink", "symlink not allowed");
212
- }
213
- return pathStat;
214
- });
155
+ await inspectPathIdentity(async () => inspectOpenedPathIdentitySync(filePath, options?.symlinks));
215
156
  await fsSafeTestHooks?.afterOpenedPathIdentityCheck?.(filePath, handle);
216
157
  const resolved = await resolveOpenedFileRealPathForFd(handle.fd, identity, filePath)
217
158
  .catch(async (error) => {
@@ -221,6 +162,7 @@ async function openVerifiedLocalFile(filePath, options) {
221
162
  throw error;
222
163
  });
223
164
  const { realPath } = resolved;
165
+ assertNoWindowsPathAlias(realPath, "filesystem", "resolved file path uses a Windows filesystem namespace alias");
224
166
  let resolvedStat = resolved.stat;
225
167
  await inspectPathIdentity(async () => {
226
168
  // Reuse the post-realpath observation; unknown Windows identities still
@@ -240,12 +182,14 @@ async function openVerifiedLocalFile(filePath, options) {
240
182
  }
241
183
  }
242
184
  export class RootHandle {
185
+ rootGuard;
243
186
  rootIdentity;
244
187
  rootDir;
245
188
  rootReal;
246
189
  rootWithSep;
247
190
  defaults;
248
191
  constructor(context, defaults = {}) {
192
+ this.rootGuard = context.rootGuard;
249
193
  this.rootIdentity = context.rootIdentity;
250
194
  this.rootDir = context.rootDir;
251
195
  this.rootReal = context.rootReal;
@@ -255,6 +199,7 @@ export class RootHandle {
255
199
  get context() {
256
200
  return {
257
201
  rootDir: this.rootDir,
202
+ rootGuard: this.rootGuard,
258
203
  rootIdentity: this.rootIdentity,
259
204
  rootReal: this.rootReal,
260
205
  rootWithSep: this.rootWithSep,
@@ -309,7 +254,7 @@ export class RootHandle {
309
254
  async openWritable(relativePath, options = {}) {
310
255
  assertValidRootDestinationPath(relativePath);
311
256
  const writeMode = options.writeMode ?? "replace";
312
- return await openWritableFileInRoot(this.context, {
257
+ const target = await openWritableFileInRoot(this.context, {
313
258
  relativePath,
314
259
  mkdir: this.defaults.mkdir,
315
260
  mode: this.defaults.mode,
@@ -317,6 +262,7 @@ export class RootHandle {
317
262
  append: writeMode === "append",
318
263
  truncateExisting: writeMode === "replace",
319
264
  }).catch(rethrowMutationAuthorityError);
265
+ return target.opened;
320
266
  }
321
267
  async append(relativePath, data, options = {}) {
322
268
  assertValidRootDestinationPath(relativePath);
@@ -428,6 +374,7 @@ export class RootHandle {
428
374
  assertValidRootDestinationPath(toRelative);
429
375
  validatePinnedOperationPayload({ from: fromRelative, to: toRelative });
430
376
  const { denyMutations, assertBeforeMutation, mutationSymlinks } = this.mutationOptions(options);
377
+ const overwrite = options.overwrite ?? false;
431
378
  await assertMoveMutationAllowed(this.context, {
432
379
  fromRelative,
433
380
  toRelative,
@@ -438,7 +385,7 @@ export class RootHandle {
438
385
  denyMutations,
439
386
  assertBeforeMutation,
440
387
  mutationSymlinks,
441
- overwrite: options.overwrite ?? false,
388
+ overwrite,
442
389
  toRelative,
443
390
  }).catch(rethrowMutationAuthorityError);
444
391
  }
@@ -471,31 +418,36 @@ export async function root(rootDir, defaults = {}) {
471
418
  export function rootFromDirectoryGuard(guard, defaults = {}) {
472
419
  normalizeMaxBytes(defaults.maxBytes);
473
420
  return new RootHandle({
474
- rootDir: path.resolve(guard.dir),
421
+ rootDir: resolvePathPreservingWindowsRoot(guard.dir),
422
+ rootGuard: { dir: guard.realPath, realPath: guard.realPath, stat: guard.stat },
475
423
  rootIdentity: { dev: guard.stat.dev, ino: guard.stat.ino },
476
424
  rootReal: guard.realPath,
477
425
  rootWithSep: ensureTrailingSep(guard.realPath),
478
426
  }, defaults);
479
427
  }
480
428
  async function openFileInRoot(root, params) {
481
- const { rootWithSep, resolved } = await resolvePathInRoot(root, params.relativePath, {
429
+ const { resolved } = await resolvePathInRoot(root, params.relativePath, {
482
430
  allowFinalSymlink: true,
483
431
  rejectUnsafeDeviceReads: true,
484
432
  ...readSymlinkResolution(params.symlinks),
485
433
  resolveCanonical: true,
486
434
  });
487
- const { opened } = await openVerifiedLocalFile(resolved, {
435
+ const fsSafeTestHooks = getFsSafeTestHooks();
436
+ if (fsSafeTestHooks?.afterRootReadPathResolution) {
437
+ await fsSafeTestHooks.afterRootReadPathResolution(resolved);
438
+ }
439
+ const { handle, stat, identity } = await openLocalFileDescriptor(resolved, {
488
440
  symlinks: params.symlinks,
489
441
  });
490
- if (params.hardlinks !== "allow" && opened.stat.nlink > 1) {
491
- await opened.handle.close().catch(() => { });
492
- throw hardlinkedPathNotAllowedError();
493
- }
494
- if (!isPathInside(rootWithSep, opened.realPath)) {
495
- await opened.handle.close().catch(() => { });
496
- throw outsideWorkspaceError();
497
- }
498
- return opened;
442
+ // The admission helper owns the descriptor until the complete root/file/root
443
+ // fence succeeds, then transfers that still-open handle to the caller.
444
+ const admitted = await admitRootReadHandle({
445
+ root, filePath: resolved, opened: { handle, stat }, identity,
446
+ hardlinks: params.hardlinks, symlinks: params.symlinks,
447
+ beforeFinalFence: fsSafeTestHooks?.beforeRootReadFinalFence,
448
+ afterPathIdentityCheck: fsSafeTestHooks?.afterRootReadFinalPathIdentityCheck,
449
+ });
450
+ return openResult(admitted);
499
451
  }
500
452
  async function readFileInRoot(root, params) {
501
453
  const opened = await openFileInRoot(root, params);
@@ -526,8 +478,10 @@ export async function readLocalFileSafely(params) {
526
478
  }
527
479
  }
528
480
  export async function openLocalFileSafely(params) {
529
- assertNoNulPathInput(params.filePath, "file path contains a NUL byte");
530
- return (await openVerifiedLocalFile(params.filePath)).opened;
481
+ const filePath = params.filePath;
482
+ assertNoNulPathInput(filePath, "file path contains a NUL byte");
483
+ assertNoWindowsPathAlias(filePath, "filesystem", "file path uses a Windows filesystem namespace alias");
484
+ return (await openVerifiedLocalFile(filePath)).opened;
531
485
  }
532
486
  function emitWriteBoundaryWarning(reason) {
533
487
  logWarn(`security: fs-safe write boundary warning (${reason})`);
@@ -538,56 +492,38 @@ function buildAtomicWriteTempPath(targetPath) {
538
492
  function rootWriteQueueKey(root, relativePath) {
539
493
  return `${root.rootReal}\0${relativePath}`;
540
494
  }
541
- async function prepareRootWriteTarget(rootReal, targetPath, assertBeforeMutation) {
542
- const parentPath = await mkdirPathComponentsWithGuards({
543
- rootReal,
544
- targetPath: path.dirname(targetPath),
545
- assertBeforeMutation,
546
- });
547
- // Continue through the guarded walk's real parent instead of re-entering
548
- // the original path through a symlinked component.
549
- return path.join(parentPath, path.basename(targetPath));
550
- }
551
- async function resolveGuardedWritePathInRoot(root, params) {
552
- const resolvedPath = await resolvePathInRoot(root, params.relativePath, {
553
- aliasErrorCode: "path-alias",
554
- rejectAmbiguousParents: true,
555
- allowFinalSymlink: params.allowFinalSymlink,
556
- ...mutationSymlinkResolution(params.mutationSymlinks),
557
- });
558
- await assertMutationNotDenied(resolvedPath.resolved, params.denyMutations, params.protectDeniedAncestors ? { protectAncestors: true } : undefined);
559
- if (await (params.shouldAssertNoPathAlias?.(resolvedPath) ?? true)) {
560
- try {
561
- await assertNoPathAliasEscape({
562
- absolutePath: resolvedPath.resolved,
563
- rootPath: resolvedPath.rootReal,
564
- boundaryLabel: "root",
565
- });
566
- }
567
- catch (error) {
568
- throw new FsSafeError("path-alias", "path alias escape blocked", {
569
- cause: error instanceof Error ? error : undefined,
570
- });
571
- }
572
- }
573
- return resolvedPath;
574
- }
575
495
  async function openWritableFileInRoot(root, params) {
576
- const { rootReal, rootWithSep, resolved } = await resolveGuardedWritePathInRoot(root, {
496
+ const guardedTarget = params.denyMutations === undefined && params.mutationSymlinks === undefined
497
+ ? undefined
498
+ : await resolveGuardedWriteTargetInRoot(root, {
499
+ relativePath: params.relativePath,
500
+ denyMutations: params.denyMutations,
501
+ mutationSymlinks: params.mutationSymlinks,
502
+ });
503
+ const { resolved } = guardedTarget?.resolvedPath ?? await resolveGuardedWritePathInRoot(root, {
577
504
  relativePath: params.relativePath,
578
- denyMutations: params.denyMutations,
579
- mutationSymlinks: params.mutationSymlinks,
580
505
  });
581
- let ioPath = params.mkdir === false
582
- ? resolved
583
- : await prepareRootWriteTarget(rootReal, resolved, params.assertBeforeMutation);
506
+ const prepared = guardedTarget ? await prepareSharedRootWriteTarget(root, {
507
+ relativePath: params.relativePath, guardedTarget, mkdir: params.mkdir,
508
+ assertBeforeMutation: params.assertBeforeMutation,
509
+ }) : undefined;
510
+ const mutationAdmission = prepared?.mutationAdmission;
511
+ const preparedParent = prepared?.preparedParent;
512
+ let ioPath = prepared?.targetPath ?? (params.mkdir === false ? resolved :
513
+ await prepareRootWriteTarget(root, resolved, params.assertBeforeMutation));
514
+ const operationTargetPath = ioPath;
584
515
  try {
585
516
  assertFinalSymlinkRejected(ioPath, params.mutationSymlinks !== undefined);
586
517
  const resolvedRealPath = params.mutationSymlinks === undefined ? realpathSync.native(ioPath) : ioPath;
587
- if (!isPathInside(rootWithSep, resolvedRealPath)) {
518
+ const admittedRealPath = admitPathInsideRoot({
519
+ rootPath: root.rootReal,
520
+ candidatePath: resolvedRealPath,
521
+ rootIdentity: root.rootIdentity,
522
+ });
523
+ if (!admittedRealPath) {
588
524
  throw outsideWorkspaceError();
589
525
  }
590
- ioPath = resolvedRealPath;
526
+ ioPath = admittedRealPath.path;
591
527
  }
592
528
  catch (err) {
593
529
  if (err instanceof FsSafeError) {
@@ -597,13 +533,20 @@ async function openWritableFileInRoot(root, params) {
597
533
  throw err;
598
534
  }
599
535
  }
536
+ assertRootFallbackWritePath(params.expectedWritePath, ioPath);
600
537
  const mode = params.mode ?? 0o600;
601
538
  let handle;
602
539
  let createdForWrite = false;
540
+ let writePathSelection = undefined;
603
541
  const existingFlags = params.append ? OPEN_APPEND_EXISTING_FLAGS : OPEN_WRITE_EXISTING_FLAGS;
604
542
  const createFlags = params.append ? OPEN_APPEND_CREATE_FLAGS : OPEN_WRITE_CREATE_FLAGS;
605
543
  try {
544
+ writePathSelection = guardedTarget
545
+ ? await prepareGuardedRootWritePathSelection(guardedTarget, ioPath, operationTargetPath, preparedParent)
546
+ : undefined;
606
547
  try {
548
+ if (writePathSelection)
549
+ assertRootWritePathSelectionSync(root, writePathSelection);
607
550
  handle = await fs.open(ioPath, existingFlags, mode);
608
551
  }
609
552
  catch (err) {
@@ -613,7 +556,11 @@ async function openWritableFileInRoot(root, params) {
613
556
  if (!isNotFoundPathError(err)) {
614
557
  throw err;
615
558
  }
559
+ if (writePathSelection)
560
+ await refreshRootWritePathSelection(writePathSelection);
616
561
  params.assertBeforeMutation?.();
562
+ if (writePathSelection)
563
+ assertRootWritePathSelectionSync(root, writePathSelection);
617
564
  handle = await fs.open(ioPath, createFlags, mode);
618
565
  createdForWrite = true;
619
566
  }
@@ -634,49 +581,91 @@ async function openWritableFileInRoot(root, params) {
634
581
  let createdIdentity = null;
635
582
  try {
636
583
  const stat = fsSync.fstatSync(handle.fd);
637
- if (createdForWrite) {
638
- createdIdentity = stat;
639
- }
640
- if (!stat.isFile()) {
641
- throw new FsSafeError("not-file", "path is not a regular file under root");
642
- }
643
- if (stat.nlink > 1) {
644
- throw hardlinkedPathNotAllowedError();
645
- }
646
- try {
647
- const lstat = fsSync.lstatSync(ioPath);
648
- if (lstat.isSymbolicLink() || !lstat.isFile()) {
649
- throw new FsSafeError(lstat.isSymbolicLink() ? "symlink" : "not-file", "path is not a regular file under root");
650
- }
651
- if (!sameFileIdentity(stat, lstat)) {
652
- throw new FsSafeError("path-mismatch", "path changed during write");
584
+ const identity = inspectFileIdentitySync(() => {
585
+ const observed = fsSync.fstatSync(handle.fd, { bigint: true });
586
+ if (!observed.isFile()) {
587
+ throw new FsSafeError("not-file", "path is not a regular file under root");
653
588
  }
589
+ if (observed.nlink > 1n)
590
+ throw hardlinkedPathNotAllowedError();
591
+ return observed;
592
+ });
593
+ if (createdForWrite)
594
+ createdIdentity = identity;
595
+ let observedIoPath = false;
596
+ try {
597
+ inspectFileIdentitySync(() => {
598
+ const lstat = fsSync.lstatSync(ioPath, { bigint: true });
599
+ observedIoPath = true;
600
+ if (lstat.isSymbolicLink() || !lstat.isFile()) {
601
+ throw new FsSafeError(lstat.isSymbolicLink() ? "symlink" : "not-file", "path is not a regular file under root");
602
+ }
603
+ if (lstat.nlink > 1n)
604
+ throw hardlinkedPathNotAllowedError();
605
+ return lstat;
606
+ }, identity);
654
607
  }
655
608
  catch (err) {
656
- if (!isNotFoundPathError(err)) {
609
+ if (isNotFoundPathError(err) && !observedIoPath) {
610
+ // The opened file may have been renamed before its first pathname
611
+ // observation. The descriptor-bound resolver below must still find it.
612
+ }
613
+ else if (err instanceof FsSafeError && err.code === "path-mismatch") {
614
+ throw new FsSafeError("path-mismatch", "path changed during write", { cause: err });
615
+ }
616
+ else {
657
617
  throw err;
658
618
  }
659
619
  }
660
- const realPath = await resolveOpenedFileRealPathForHandle(handle, ioPath);
661
- realPathForCleanup = realPath;
662
- const realStat = fsSync.statSync(realPath);
663
- if (!sameFileIdentity(stat, realStat)) {
664
- throw new FsSafeError("path-mismatch", "path mismatch");
620
+ let realPath = (await resolveOpenedFileRealPathForFd(handle.fd, identity, ioPath)).realPath;
621
+ assertNoWindowsPathAlias(realPath, "filesystem", "resolved file path uses a Windows filesystem namespace alias");
622
+ try {
623
+ inspectFileIdentitySync(() => {
624
+ const realStat = fsSync.statSync(realPath, { bigint: true });
625
+ if (!realStat.isFile()) {
626
+ throw new FsSafeError("not-file", "path is not a regular file under root");
627
+ }
628
+ if (realStat.nlink > 1n)
629
+ throw hardlinkedPathNotAllowedError();
630
+ return realStat;
631
+ }, identity);
665
632
  }
666
- if (realStat.nlink > 1) {
667
- throw hardlinkedPathNotAllowedError();
633
+ catch (err) {
634
+ if (err instanceof FsSafeError && err.code === "path-mismatch") {
635
+ throw new FsSafeError("path-mismatch", "path mismatch", { cause: err });
636
+ }
637
+ throw err;
668
638
  }
669
- if (!isPathInside(rootWithSep, realPath)) {
639
+ const admittedRealPath = admitPathInsideRoot({
640
+ rootPath: root.rootReal,
641
+ candidatePath: realPath,
642
+ rootIdentity: root.rootIdentity,
643
+ });
644
+ if (!admittedRealPath) {
670
645
  throw outsideWorkspaceError();
671
646
  }
647
+ realPath = admittedRealPath.path;
648
+ realPathForCleanup = realPath;
649
+ assertRootFallbackWritePath(params.expectedWritePath, realPath);
650
+ const writeSelection = writePathSelection
651
+ ? createRootWriteSelectionForFd(writePathSelection, handle.fd)
652
+ : undefined;
672
653
  // Truncate only after boundary and identity checks complete. This avoids
673
654
  // irreversible side effects if a symlink target changes before validation.
674
655
  if (params.append !== true && params.truncateExisting !== false && !createdForWrite) {
656
+ if (writeSelection)
657
+ await refreshRetainedRootWriteAdmission(root, writeSelection, true, handle.fd);
675
658
  assertFinalSymlinkRejected(ioPath, params.mutationSymlinks !== undefined);
676
659
  params.assertBeforeMutation?.();
660
+ if (writeSelection)
661
+ assertRootWriteSelectionSync(root, writeSelection, true, handle.fd);
677
662
  await handle.truncate(0);
678
663
  }
679
- return {
664
+ if (writeSelection) {
665
+ await refreshRetainedRootWriteAdmission(root, writeSelection, true, handle.fd);
666
+ assertRootWriteSelectionSync(root, writeSelection, true, handle.fd);
667
+ }
668
+ const result = {
680
669
  handle,
681
670
  containment: "best-effort",
682
671
  createdForWrite,
@@ -684,6 +673,9 @@ async function openWritableFileInRoot(root, params) {
684
673
  stat,
685
674
  [Symbol.asyncDispose]: () => handle.close().catch(() => undefined),
686
675
  };
676
+ if (writeSelection)
677
+ retainRootWriteSelection(result, writeSelection);
678
+ return { opened: result, identity };
687
679
  }
688
680
  catch (err) {
689
681
  const cleanupCreatedPath = createdForWrite && err instanceof FsSafeError;
@@ -698,7 +690,7 @@ async function openWritableFileInRoot(root, params) {
698
690
  async function appendFileInRoot(root, params) {
699
691
  const env_1 = { stack: [], error: void 0, hasError: false };
700
692
  try {
701
- const target = await openWritableFileInRoot(root, {
693
+ const { opened: target, identity } = await openWritableFileInRoot(root, {
702
694
  relativePath: params.relativePath,
703
695
  mkdir: params.mkdir,
704
696
  mode: params.mode,
@@ -713,7 +705,7 @@ async function appendFileInRoot(root, params) {
713
705
  const cleanup = __addDisposableResource(env_1, {
714
706
  async [Symbol.asyncDispose]() {
715
707
  if (!dispatched && target.createdForWrite) {
716
- await removePathIfIdentityUnchanged(target.realPath, target.stat);
708
+ await removePathIfIdentityUnchanged(target.realPath, identity);
717
709
  }
718
710
  },
719
711
  }, true);
@@ -766,14 +758,16 @@ async function appendFileInRoot(root, params) {
766
758
  }
767
759
  async function removePathInRoot(root, params) {
768
760
  validatePinnedOperationPayload({ relativePath: params.relativePath });
761
+ const removalReceipts = params.recursive ? undefined : new RemovalPathReceipts();
769
762
  const resolved = await resolvePinnedPathInRoot(root, {
770
763
  relativePath: params.relativePath,
771
764
  denyMutations: params.denyMutations,
772
765
  mutationSymlinks: params.mutationSymlinks,
773
766
  remove: true,
767
+ removalReceipts,
774
768
  });
775
769
  try {
776
- await removePathInRootFallback(root, resolved.resolved, params);
770
+ await removePathInRootFallback(root, resolved.resolved, params, removalReceipts);
777
771
  }
778
772
  catch (error) {
779
773
  if (params.recursive)
@@ -783,9 +777,41 @@ async function removePathInRoot(root, params) {
783
777
  }
784
778
  async function mkdirPathInRoot(root, params) {
785
779
  validatePinnedOperationPayload({ relativePath: params.relativePath });
786
- const resolved = await resolvePinnedPathInRoot(root, params);
780
+ const policy = params.denyMutations === undefined && params.mutationSymlinks === undefined
781
+ ? undefined
782
+ : snapshotPinnedMutationPolicy(params.denyMutations, params.mutationSymlinks);
783
+ const resolveCurrent = policy
784
+ ? async () => await resolvePinnedPathInRoot(root, {
785
+ ...params,
786
+ denyMutations: policy.denyMutations,
787
+ mutationSymlinks: policy.mutationSymlinks,
788
+ })
789
+ : undefined;
790
+ const resolved = resolveCurrent
791
+ ? await resolveCurrent()
792
+ : await resolvePinnedPathInRoot(root, params);
793
+ const prepared = policy && resolved.relativePosix !== ""
794
+ ? await preparePinnedWriteMutationAdmission({
795
+ rootReal: resolved.rootReal,
796
+ rootWithSep: ensureTrailingSep(resolved.rootReal),
797
+ rootIdentity: root.rootIdentity,
798
+ resolvedTargetPath: resolved.resolved,
799
+ originalPath: params.relativePath,
800
+ defaultRelativeParentPath: path.posix.dirname(resolved.relativePosix) === "."
801
+ ? ""
802
+ : path.posix.dirname(resolved.relativePosix),
803
+ policy,
804
+ resolveCurrent: resolveCurrent,
805
+ })
806
+ : undefined;
807
+ if (prepared?.mutationAdmission) {
808
+ await getFsSafeTestHooks()?.beforePinnedWriteParentAdmission?.(resolved.resolved);
809
+ }
787
810
  try {
788
- await mkdirPathFallback(resolved, params.assertBeforeMutation, params.mutationSymlinks !== undefined);
811
+ if (prepared?.mutationAdmission && params.assertBeforeMutation === undefined &&
812
+ await tryMkdirAtExactParent(root, resolved.resolved, prepared.mutationAdmission))
813
+ return;
814
+ await mkdirPathFallback(root, resolved, params.assertBeforeMutation, policy?.mutationSymlinks !== undefined, prepared?.mutationAdmission);
789
815
  }
790
816
  catch (error) {
791
817
  throw normalizePinnedPathError(error);
@@ -822,6 +848,7 @@ async function commitPinnedWriteInRoot(root, pinned, params, input) {
822
848
  input,
823
849
  maxBytes: params.maxBytes,
824
850
  rootIdentity: root.rootIdentity,
851
+ mutationAdmission: pinned.mutationAdmission,
825
852
  assertBeforeMutation: params.assertBeforeMutation,
826
853
  verifyPublished: async (fd, expectedIdentity, parentGuard) => {
827
854
  verifyingPublication = true;
@@ -863,6 +890,7 @@ async function copyFileInRoot(root, params) {
863
890
  let sourceIdentity;
864
891
  if (typeof params.source === "string") {
865
892
  assertNoNulPathInput(params.source, "source path contains a NUL byte");
893
+ assertNoWindowsPathAlias(params.source, "filesystem", "source path uses a Windows filesystem namespace alias");
866
894
  ({ opened: source, identity: sourceIdentity } = await openVerifiedLocalFile(params.source, {
867
895
  hardlinks: params.sourceHardlinks,
868
896
  }));
@@ -914,6 +942,7 @@ async function copyFileInRoot(root, params) {
914
942
  onPublished: observer.onPublished,
915
943
  input: { kind: "file", handle: source.handle, size: source.stat.size, clone, signal: params.signal, verifySource },
916
944
  rootIdentity: root.rootIdentity,
945
+ mutationAdmission: pinned.mutationAdmission,
917
946
  });
918
947
  }
919
948
  catch (error) {
@@ -933,61 +962,6 @@ async function copyFileInRoot(root, params) {
933
962
  await source.handle.close().catch(() => { });
934
963
  }
935
964
  }
936
- async function removePathIfIdentityUnchanged(targetPath, identity) {
937
- const parentGuard = await createAsyncDirectoryGuard(path.dirname(targetPath));
938
- await withAsyncDirectoryGuards([parentGuard], async () => {
939
- const current = fsSync.lstatSync(targetPath);
940
- if (current.isSymbolicLink() || !sameFileIdentityForCleanup(current, identity))
941
- return;
942
- await fs.unlink(targetPath);
943
- });
944
- }
945
- async function resolvePinnedWriteTargetInRoot(root, relativePath, requestedMode, denyMutations, overwrite = true, mutationSymlinks) {
946
- const { rootReal, rootWithSep, resolved } = await resolveGuardedWritePathInRoot(root, {
947
- relativePath,
948
- denyMutations,
949
- mutationSymlinks,
950
- });
951
- // resolvePathInRoot already enforces isPathInside, so any actual escape
952
- // is rejected upstream.
953
- const relativeResolved = path.relative(rootReal, resolved);
954
- if (path.isAbsolute(relativeResolved)) {
955
- throw outsideWorkspaceError();
956
- }
957
- const relativePosix = relativeResolved
958
- ? relativeResolved.split(path.sep).join(path.posix.sep)
959
- : "";
960
- const basename = path.posix.basename(relativePosix);
961
- if (!basename || basename === "." || basename === "/") {
962
- throw new FsSafeError("invalid-path", "invalid target path");
963
- }
964
- // Exclusive publication cannot inherit an existing file's mode. Keep type
965
- // and alias checks, but never open a competing owner's record just to reject it.
966
- if (!overwrite) {
967
- try {
968
- const existing = fsSync.statSync(resolved);
969
- if (!existing.isFile())
970
- throw new FsSafeError("not-file", "not a file");
971
- if (existing.nlink > 1)
972
- throw hardlinkedPathNotAllowedError();
973
- throw new FsSafeError("already-exists", "file already exists");
974
- }
975
- catch (error) {
976
- if (!isNotFoundPathError(error))
977
- throw error;
978
- }
979
- }
980
- const mode = overwrite
981
- ? await inheritWriteTargetMode({ targetPath: resolved, rootWithSep, requestedMode })
982
- : requestedMode ?? 0o600;
983
- return {
984
- rootReal,
985
- targetPath: resolved,
986
- relativeParentPath: path.posix.dirname(relativePosix) === "." ? "" : path.posix.dirname(relativePosix),
987
- basename,
988
- mode,
989
- };
990
- }
991
965
  async function resolvePinnedPathInRoot(root, params) {
992
966
  return await resolvePinnedOperationPathInRoot(root, {
993
967
  allowRoot: params.allowRoot,
@@ -996,6 +970,7 @@ async function resolvePinnedPathInRoot(root, params) {
996
970
  protectDenyMutationAncestors: params.remove === true,
997
971
  relativePath: params.relativePath,
998
972
  policy: params.remove ? PATH_ALIAS_POLICIES.unlinkTarget : PATH_ALIAS_POLICIES.strict,
973
+ removalReceipts: params.removalReceipts,
999
974
  });
1000
975
  }
1001
976
  async function resolvePinnedOperationPathInRoot(root, params) {
@@ -1003,6 +978,7 @@ async function resolvePinnedOperationPathInRoot(root, params) {
1003
978
  relativePath: params.relativePath,
1004
979
  policy: params.policy,
1005
980
  mutationSymlinks: params.mutationSymlinks,
981
+ removalReceipts: params.removalReceipts,
1006
982
  });
1007
983
  const relativeResolved = path.relative(resolved.rootReal, resolved.canonicalPath);
1008
984
  if ((relativeResolved === "" || relativeResolved === ".") && params.allowRoot === true) {
@@ -1017,30 +993,38 @@ async function resolvePinnedOperationPathInRoot(root, params) {
1017
993
  throw outsideWorkspaceError();
1018
994
  }
1019
995
  const relativePosix = relativeResolved.split(path.sep).join(path.posix.sep);
1020
- if (!isPathInside(resolved.rootWithSep, resolved.canonicalPath)) {
996
+ const admittedCanonicalPath = admitPathInsideRoot({
997
+ rootPath: resolved.rootReal,
998
+ candidatePath: resolved.canonicalPath,
999
+ rootIdentity: root.rootIdentity,
1000
+ });
1001
+ if (!admittedCanonicalPath) {
1021
1002
  throw outsideWorkspaceError();
1022
1003
  }
1004
+ resolved.canonicalPath = admittedCanonicalPath.path;
1023
1005
  await assertMutationNotDenied(resolved.canonicalPath, params.denyMutations, {
1024
1006
  protectAncestors: params.protectDenyMutationAncestors,
1025
1007
  });
1026
1008
  return { rootReal: resolved.rootReal, resolved: resolved.canonicalPath, relativePosix };
1027
1009
  }
1028
1010
  async function resolvePinnedRootPathInRoot(root, params) {
1029
- await assertRootIdentityCurrent(root);
1011
+ await assertRootIdentityCurrent(root, params.removalReceipts?.observeRoot);
1030
1012
  const rootReal = root.rootReal;
1031
1013
  let resolved;
1032
1014
  try {
1033
1015
  const expandedPath = await expandRelativePathWithHome(params.relativePath);
1034
- resolved = await resolveRootPath({
1016
+ const resolution = {
1035
1017
  absolutePath: path.isAbsolute(expandedPath)
1036
1018
  ? expandedPath
1037
1019
  : `${ensureTrailingSep(rootReal)}${expandedPath}`,
1038
1020
  rootPath: rootReal,
1039
1021
  rootCanonicalPath: rootReal,
1022
+ rootIdentity: root.rootIdentity,
1040
1023
  boundaryLabel: "root",
1041
1024
  policy: params.policy,
1042
1025
  ...mutationSymlinkResolution(params.mutationSymlinks),
1043
- });
1026
+ };
1027
+ resolved = await (params.removalReceipts ? resolveRootPathForRemoval(resolution, params.removalReceipts) : resolveRootPath(resolution));
1044
1028
  }
1045
1029
  catch (err) {
1046
1030
  if (err instanceof FsSafeError && err.code === "symlink")
@@ -1054,44 +1038,20 @@ async function resolvePinnedRootPathInRoot(root, params) {
1054
1038
  canonicalPath: resolved.canonicalPath,
1055
1039
  };
1056
1040
  }
1057
- async function mkdirPathFallback(resolved, assertBeforeMutation, rejectSymlinks = false) {
1058
- await mkdirPathComponentsWithGuards({
1059
- rootReal: resolved.rootReal, targetPath: resolved.resolved, assertBeforeMutation,
1060
- rejectSymlinks,
1061
- beforeComponent: async (componentPath) => await getFsSafeTestHooks()?.beforeRootFallbackMutation?.("mkdir", componentPath),
1062
- });
1063
- }
1064
1041
  async function statPathFallback(root, relativePath) {
1065
- const resolved = await resolvePinnedPathInRoot(root, { relativePath, allowRoot: true });
1066
- try {
1067
- const stat = pathStatFromStats(fsSync.lstatSync(resolved.resolved));
1068
- await assertRootIdentityCurrent(root);
1069
- return stat;
1070
- }
1071
- catch (error) {
1072
- if (isNotFoundPathError(error)) {
1073
- throw fileNotFoundError(error instanceof Error ? error : undefined);
1074
- }
1075
- throw error;
1076
- }
1042
+ const initialObservationHook = getFsSafeTestHooks()?.beforeRootStatInitialObservation;
1043
+ const observed = initialObservationHook
1044
+ ? undefined
1045
+ : await resolvePinnedObservedPathInRoot(root, relativePath, "stat");
1046
+ const resolved = observed ??
1047
+ await resolvePinnedPathInRoot(root, { relativePath, allowRoot: true });
1048
+ return await statResolvedPathInRoot(root, resolved.resolved, resolved.receipt);
1077
1049
  }
1078
1050
  async function listPathFallback(root, relativePath, withFileTypes) {
1079
- const resolved = await resolvePinnedPathInRoot(root, { relativePath, allowRoot: true });
1080
- return await listDirectoryPath(root, resolved.resolved, withFileTypes);
1081
- }
1082
- async function assertMoveMutationAllowed(root, params) {
1083
- // Keep this preflight separate from the pinned resolutions in movePathFallback:
1084
- // mutation denials must take precedence over source alias or identity failures.
1085
- const source = await resolvePathInRoot(root, params.fromRelative, {
1086
- aliasErrorCode: "path-alias",
1087
- allowFinalSymlink: true,
1088
- });
1089
- await assertMutationNotDenied(source.resolved, params.denyMutations, { protectAncestors: true });
1090
- const target = await resolvePathInRoot(root, params.toRelative, {
1091
- aliasErrorCode: "path-alias",
1092
- allowFinalSymlink: true,
1093
- });
1094
- await assertMutationNotDenied(target.resolved, params.denyMutations, { protectAncestors: true });
1051
+ const observed = await resolvePinnedObservedPathInRoot(root, relativePath, "directory");
1052
+ const resolved = observed ??
1053
+ await resolvePinnedPathInRoot(root, { relativePath, allowRoot: true });
1054
+ return await listDirectoryPath(root, resolved.resolved, withFileTypes, resolved.receipt);
1095
1055
  }
1096
1056
  async function movePathFallback(root, params) {
1097
1057
  const source = await resolvePathInRoot(root, params.fromRelative, {
@@ -1100,11 +1060,12 @@ async function movePathFallback(root, params) {
1100
1060
  ...mutationSymlinkResolution(params.mutationSymlinks),
1101
1061
  });
1102
1062
  await assertMutationNotDenied(source.resolved, params.denyMutations, { protectAncestors: true });
1103
- await resolvePinnedRootPathInRoot(root, {
1063
+ const pinnedSource = await resolvePinnedRootPathInRoot(root, {
1104
1064
  relativePath: params.fromRelative,
1105
1065
  policy: PATH_ALIAS_POLICIES.strict,
1106
1066
  mutationSymlinks: params.mutationSymlinks,
1107
1067
  });
1068
+ let pinnedTarget;
1108
1069
  const target = await resolveGuardedWritePathInRoot(root, {
1109
1070
  relativePath: params.toRelative,
1110
1071
  denyMutations: params.denyMutations,
@@ -1112,10 +1073,12 @@ async function movePathFallback(root, params) {
1112
1073
  allowFinalSymlink: true,
1113
1074
  protectDeniedAncestors: true,
1114
1075
  shouldAssertNoPathAlias: async (resolvedTarget) => {
1115
- await resolvePinnedRootPathInRoot(root, {
1076
+ pinnedTarget = await resolvePinnedRootPathInRoot(root, {
1116
1077
  relativePath: params.toRelative,
1117
1078
  policy: PATH_ALIAS_POLICIES.unlinkTarget,
1118
1079
  });
1080
+ if (!params.overwrite)
1081
+ return true;
1119
1082
  let targetStat;
1120
1083
  try {
1121
1084
  targetStat = fsSync.lstatSync(resolvedTarget.resolved);
@@ -1146,18 +1109,16 @@ async function movePathFallback(root, params) {
1146
1109
  throw new FsSafeError("invalid-path", "directory moves require overwrite: true");
1147
1110
  }
1148
1111
  if (!params.overwrite) {
1149
- try {
1150
- fsSync.lstatSync(target.resolved);
1151
- throw new FsSafeError("already-exists", "destination exists");
1152
- }
1153
- catch (error) {
1154
- if (error instanceof FsSafeError) {
1155
- throw error;
1156
- }
1157
- if (!isNotFoundPathError(error)) {
1158
- throw error;
1159
- }
1112
+ if (!pinnedTarget) {
1113
+ throw new FsSafeError("path-mismatch", "destination admission was not completed");
1160
1114
  }
1115
+ await movePathNoReplaceNative(root, params, {
1116
+ sourcePath: source.resolved,
1117
+ sourceParentPath: path.dirname(pinnedSource.canonicalPath),
1118
+ targetPath: target.resolved,
1119
+ targetParentPath: path.dirname(pinnedTarget.canonicalPath),
1120
+ });
1121
+ return;
1161
1122
  }
1162
1123
  const sourceParentGuard = await createAsyncDirectoryGuard(path.dirname(source.resolved));
1163
1124
  const targetParentGuard = await createNearestExistingDirectoryGuard(target.rootReal, path.dirname(target.resolved));
@@ -1190,11 +1151,22 @@ async function movePathFallback(root, params) {
1190
1151
  }
1191
1152
  }
1192
1153
  async function writeFileFallback(root, params) {
1154
+ if (params.renameIdentity !== "verify-content-with-lock")
1155
+ return await writeFileFallbackUnlocked(root, params);
1156
+ const policy = snapshotPinnedMutationPolicy(params.denyMutations, params.mutationSymlinks);
1157
+ if (policy)
1158
+ params = { ...params, ...policy };
1159
+ const { rootReal, resolved } = await resolveGuardedWritePathInRoot(root, params);
1160
+ await withRootFallbackCompatibilityLock({
1161
+ rootPath: rootReal, rootIdentity: root.rootIdentity, targetPath: resolved, assertBeforeMutation: params.assertBeforeMutation,
1162
+ }, async ({ targetPath, ...binding }) => await writeFileFallbackUnlocked(root, { ...params, ...binding }, targetPath));
1163
+ }
1164
+ async function writeFileFallbackUnlocked(root, params, expectedWritePath) {
1193
1165
  if (params.overwrite === false) {
1194
- await writeMissingFileFallback(root, params);
1166
+ await writeMissingFileFallback(root, params, expectedWritePath);
1195
1167
  return;
1196
1168
  }
1197
- const target = await openWritableFileInRoot(root, {
1169
+ const { opened: target, identity: targetIdentity } = await openWritableFileInRoot(root, {
1198
1170
  relativePath: params.relativePath,
1199
1171
  mkdir: params.mkdir,
1200
1172
  // Private, writable placeholder: Windows cannot rename over a read-only file.
@@ -1203,13 +1175,21 @@ async function writeFileFallback(root, params) {
1203
1175
  assertBeforeMutation: params.assertBeforeMutation,
1204
1176
  mutationSymlinks: params.mutationSymlinks,
1205
1177
  truncateExisting: false,
1178
+ expectedWritePath,
1206
1179
  });
1207
- const destinationPath = target.realPath;
1180
+ const policyEnabled = params.denyMutations !== undefined || params.mutationSymlinks !== undefined;
1181
+ const retainedSelection = policyEnabled ? takeRootWriteSelection(target) : undefined;
1182
+ const destinationPath = retainedSelection?.selectedPath ?? target.realPath;
1208
1183
  const mode = params.mode ?? (target.stat.mode & 0o777);
1209
- const destinationGuard = await createAsyncDirectoryGuard(path.dirname(destinationPath), { bigint: true }).catch(async (error) => {
1184
+ if (policyEnabled && !retainedSelection) {
1210
1185
  await target.handle.close().catch(() => undefined);
1211
- throw error;
1212
- });
1186
+ throw new FsSafeError("path-mismatch", "write admission state was not retained");
1187
+ }
1188
+ const destinationGuard = retainedSelection?.parentGuard ??
1189
+ await createAsyncDirectoryGuard(path.dirname(destinationPath), { bigint: true }).catch(async (error) => {
1190
+ await target.handle.close().catch(() => undefined);
1191
+ throw error;
1192
+ });
1213
1193
  let tempPath = null;
1214
1194
  let unregisterTempPath = null;
1215
1195
  let writtenHandle;
@@ -1218,9 +1198,15 @@ async function writeFileFallback(root, params) {
1218
1198
  let published = false;
1219
1199
  try {
1220
1200
  if (target.createdForWrite)
1221
- placeholderIdentity = fsSync.fstatSync(target.handle.fd, { bigint: true });
1201
+ placeholderIdentity = targetIdentity;
1222
1202
  tempPath = buildAtomicWriteTempPath(destinationPath);
1203
+ if (retainedSelection) {
1204
+ await refreshRetainedRootWriteAdmission(root, retainedSelection, true, target.handle.fd);
1205
+ }
1223
1206
  params.assertBeforeMutation?.();
1207
+ if (retainedSelection) {
1208
+ assertRootWriteSelectionSync(root, retainedSelection, true, target.handle.fd);
1209
+ }
1224
1210
  writtenHandle = await fs.open(tempPath, OPEN_WRITE_CREATE_FLAGS, 0o600);
1225
1211
  writtenIdentity = fsSync.fstatSync(writtenHandle.fd, { bigint: true });
1226
1212
  unregisterTempPath = registerTempPathForExit(tempPath, { identity: writtenIdentity, singleLinkFile: true });
@@ -1244,41 +1230,27 @@ async function writeFileFallback(root, params) {
1244
1230
  }
1245
1231
  // Windows cannot replace a destination while its old handle remains open.
1246
1232
  await target.handle.close();
1233
+ if (retainedSelection) {
1234
+ await refreshRetainedRootWriteAdmission(root, retainedSelection, !target.createdForWrite);
1235
+ }
1247
1236
  assertFinalSymlinkRejected(destinationPath, params.mutationSymlinks !== undefined);
1248
1237
  params.assertBeforeMutation?.();
1238
+ if (retainedSelection) {
1239
+ assertRootWriteSelectionSync(root, retainedSelection, !target.createdForWrite);
1240
+ }
1249
1241
  await fs.rename(commitTempPath, destinationPath);
1250
1242
  tempPath = null;
1251
1243
  published = true;
1252
1244
  });
1253
1245
  unregisterTempPath();
1254
1246
  unregisterTempPath = null;
1255
- // Final mode via the retained handle after publication, as the native writer does.
1256
- try {
1257
- await writtenHandle.chmod(mode);
1258
- }
1259
- catch (error) {
1260
- await cleanupPinnedFilePath({
1261
- pathname: destinationPath, handle: writtenHandle, identity: writtenIdentity, parentGuard: destinationGuard,
1262
- });
1263
- throw error;
1264
- }
1265
- if (params.durable !== false)
1266
- await writtenHandle.sync();
1267
- try {
1268
- await verifyAtomicWriteResult({
1269
- root,
1270
- targetPath: destinationPath,
1271
- expectedIdentity: writtenIdentity,
1272
- fd: writtenHandle.fd,
1273
- parentGuard: destinationGuard,
1274
- });
1275
- }
1276
- catch (err) {
1277
- emitWriteBoundaryWarning(`post-write verification failed: ${String(err)}`);
1278
- throw err;
1279
- }
1280
- if (params.durable !== false)
1281
- await syncDirectoryBestEffort(path.dirname(destinationPath));
1247
+ await finishRootFallbackWrite({
1248
+ root, targetPath: destinationPath, handle: writtenHandle, identity: writtenIdentity,
1249
+ parentGuard: destinationGuard, mode, options: params,
1250
+ // Read/write access is needed to sync the accepted destination on Windows.
1251
+ openForCompatibility: () => openVerifiedLocalFile(destinationPath, { hardlinks: "reject", readWrite: true }),
1252
+ onVerificationFailure: err => emitWriteBoundaryWarning(`post-write verification failed: ${String(err)}`),
1253
+ });
1282
1254
  }
1283
1255
  finally {
1284
1256
  if (!published && target.createdForWrite) {
@@ -1296,16 +1268,31 @@ async function writeFileFallback(root, params) {
1296
1268
  unregisterTempPath?.();
1297
1269
  }
1298
1270
  }
1299
- async function writeMissingFileFallback(root, params) {
1300
- const { rootReal, resolved } = await resolveGuardedWritePathInRoot(root, {
1271
+ async function writeMissingFileFallback(root, params, expectedWritePath) {
1272
+ const guardedTarget = params.denyMutations === undefined && params.mutationSymlinks === undefined
1273
+ ? undefined
1274
+ : await resolveGuardedWriteTargetInRoot(root, {
1275
+ relativePath: params.relativePath,
1276
+ denyMutations: params.denyMutations,
1277
+ mutationSymlinks: params.mutationSymlinks,
1278
+ });
1279
+ const { resolved } = guardedTarget?.resolvedPath ?? await resolveGuardedWritePathInRoot(root, {
1301
1280
  relativePath: params.relativePath,
1302
- denyMutations: params.denyMutations,
1303
- mutationSymlinks: params.mutationSymlinks,
1304
1281
  });
1305
- const targetPath = params.mkdir === false
1306
- ? resolved
1307
- : await prepareRootWriteTarget(rootReal, resolved, params.assertBeforeMutation);
1308
- const parentGuard = await createAsyncDirectoryGuard(path.dirname(targetPath), { bigint: true });
1282
+ const prepared = guardedTarget ? await prepareSharedRootWriteTarget(root, {
1283
+ relativePath: params.relativePath, guardedTarget, mkdir: params.mkdir,
1284
+ assertBeforeMutation: params.assertBeforeMutation,
1285
+ }) : undefined;
1286
+ const mutationAdmission = prepared?.mutationAdmission;
1287
+ const preparedParent = prepared?.preparedParent;
1288
+ const targetPath = prepared?.targetPath ?? (params.mkdir === false ? resolved :
1289
+ await prepareRootWriteTarget(root, resolved, params.assertBeforeMutation));
1290
+ assertRootFallbackWritePath(expectedWritePath, targetPath);
1291
+ const pathSelection = guardedTarget
1292
+ ? await prepareGuardedRootWritePathSelection(guardedTarget, targetPath, targetPath, preparedParent)
1293
+ : undefined;
1294
+ const parentGuard = pathSelection?.parentGuard ??
1295
+ await createAsyncDirectoryGuard(path.dirname(targetPath), { bigint: true });
1309
1296
  let created = false;
1310
1297
  let createdIdentity;
1311
1298
  let writtenHandle;
@@ -1314,6 +1301,8 @@ async function writeMissingFileFallback(root, params) {
1314
1301
  const { handle, writtenStat } = await withAsyncDirectoryGuards([parentGuard], async () => {
1315
1302
  assertFinalSymlinkRejected(targetPath, params.mutationSymlinks !== undefined);
1316
1303
  params.assertBeforeMutation?.();
1304
+ if (mutationAdmission)
1305
+ assertSyncDirectoryGuard(parentGuard);
1317
1306
  const handle = await fs.open(targetPath, OPEN_WRITE_CREATE_FLAGS, params.mode ?? 0o600).catch((error) => recordExclusiveCreateFailure(error, targetPath));
1318
1307
  writtenHandle = handle;
1319
1308
  created = true;
@@ -1324,6 +1313,8 @@ async function writeMissingFileFallback(root, params) {
1324
1313
  assertBeforeMutation: () => {
1325
1314
  assertFinalSymlinkRejected(targetPath, params.mutationSymlinks !== undefined);
1326
1315
  params.assertBeforeMutation?.();
1316
+ if (mutationAdmission)
1317
+ assertSyncDirectoryGuard(parentGuard);
1327
1318
  },
1328
1319
  });
1329
1320
  if (params.durable !== false)