@openclaw/fs-safe 0.14.0 → 0.16.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 (370) hide show
  1. package/CHANGELOG.md +88 -0
  2. package/README.md +38 -10
  3. package/dist/advanced.d.ts +2 -1
  4. package/dist/advanced.d.ts.map +1 -1
  5. package/dist/advanced.js +2 -1
  6. package/dist/archive-kind.d.ts +0 -1
  7. package/dist/archive-kind.d.ts.map +1 -1
  8. package/dist/archive-kind.js +5 -17
  9. package/dist/archive-merge.d.ts.map +1 -1
  10. package/dist/archive-merge.js +113 -46
  11. package/dist/archive-parser.wasm +0 -0
  12. package/dist/archive-read.d.ts.map +1 -1
  13. package/dist/archive-read.js +10 -11
  14. package/dist/archive-tar-stream.d.ts +3 -0
  15. package/dist/archive-tar-stream.d.ts.map +1 -1
  16. package/dist/archive-tar-stream.js +56 -37
  17. package/dist/archive-tar-wasm.d.ts +16 -4
  18. package/dist/archive-tar-wasm.d.ts.map +1 -1
  19. package/dist/archive-tar-wasm.js +134 -34
  20. package/dist/archive-zip-directory.d.ts +4 -0
  21. package/dist/archive-zip-directory.d.ts.map +1 -1
  22. package/dist/archive-zip-directory.js +2 -0
  23. package/dist/archive-zip-entry.d.ts +6 -2
  24. package/dist/archive-zip-entry.d.ts.map +1 -1
  25. package/dist/archive-zip-entry.js +23 -8
  26. package/dist/archive-zip-integrity.d.ts.map +1 -1
  27. package/dist/archive-zip-integrity.js +3 -4
  28. package/dist/archive-zip-loader.d.ts.map +1 -1
  29. package/dist/archive-zip-loader.js +107 -31
  30. package/dist/archive-zip-names.d.ts +1 -0
  31. package/dist/archive-zip-names.d.ts.map +1 -1
  32. package/dist/archive-zip-names.js +6 -0
  33. package/dist/archive.d.ts.map +1 -1
  34. package/dist/archive.js +11 -11
  35. package/dist/bounded-read-stream.d.ts +0 -1
  36. package/dist/bounded-read-stream.d.ts.map +1 -1
  37. package/dist/bounded-read-stream.js +0 -6
  38. package/dist/clone-metadata.d.ts +1 -0
  39. package/dist/clone-metadata.d.ts.map +1 -1
  40. package/dist/clone-metadata.js +6 -2
  41. package/dist/copy-publication.d.ts +6 -0
  42. package/dist/copy-publication.d.ts.map +1 -1
  43. package/dist/copy-publication.js +3 -0
  44. package/dist/copy-tree-portable.d.ts.map +1 -1
  45. package/dist/copy-tree-portable.js +44 -24
  46. package/dist/copy.d.ts.map +1 -1
  47. package/dist/copy.js +29 -11
  48. package/dist/create-directory.d.ts +20 -0
  49. package/dist/create-directory.d.ts.map +1 -0
  50. package/dist/create-directory.js +130 -0
  51. package/dist/create-file-async.d.ts +7 -0
  52. package/dist/create-file-async.d.ts.map +1 -0
  53. package/dist/create-file-async.js +121 -0
  54. package/dist/create-file.d.ts +8 -0
  55. package/dist/create-file.d.ts.map +1 -0
  56. package/dist/create-file.js +190 -0
  57. package/dist/create-owned-file.d.ts +8 -0
  58. package/dist/create-owned-file.d.ts.map +1 -0
  59. package/dist/create-owned-file.js +16 -0
  60. package/dist/create.d.ts +4 -0
  61. package/dist/create.d.ts.map +1 -0
  62. package/dist/create.js +2 -0
  63. package/dist/creation-darwin.d.ts +7 -0
  64. package/dist/creation-darwin.d.ts.map +1 -0
  65. package/dist/creation-darwin.js +79 -0
  66. package/dist/creation-file-state.d.ts +19 -0
  67. package/dist/creation-file-state.d.ts.map +1 -0
  68. package/dist/creation-file-state.js +118 -0
  69. package/dist/creation-path.d.ts +21 -0
  70. package/dist/creation-path.d.ts.map +1 -0
  71. package/dist/creation-path.js +71 -0
  72. package/dist/creation-permissions.d.ts +19 -0
  73. package/dist/creation-permissions.d.ts.map +1 -0
  74. package/dist/creation-permissions.js +125 -0
  75. package/dist/directory-durability.d.ts +1 -1
  76. package/dist/directory-durability.d.ts.map +1 -1
  77. package/dist/directory-durability.js +22 -80
  78. package/dist/directory-guard.d.ts +3 -0
  79. package/dist/directory-guard.d.ts.map +1 -1
  80. package/dist/directory-mode-node.d.ts +2 -0
  81. package/dist/directory-mode-node.d.ts.map +1 -1
  82. package/dist/directory-mode-node.js +8 -0
  83. package/dist/directory-mode-owner.js +5 -5
  84. package/dist/directory-receipt.d.ts +24 -0
  85. package/dist/directory-receipt.d.ts.map +1 -0
  86. package/dist/directory-receipt.js +127 -0
  87. package/dist/file-cleanup.d.ts +19 -0
  88. package/dist/file-cleanup.d.ts.map +1 -0
  89. package/dist/file-cleanup.js +78 -0
  90. package/dist/file-handle-transfer.d.ts +2 -0
  91. package/dist/file-handle-transfer.d.ts.map +1 -1
  92. package/dist/file-handle-transfer.js +57 -2
  93. package/dist/file-identity.d.ts.map +1 -1
  94. package/dist/file-identity.js +18 -4
  95. package/dist/file-lock-sync-admission.d.ts +19 -0
  96. package/dist/file-lock-sync-admission.d.ts.map +1 -0
  97. package/dist/file-lock-sync-admission.js +93 -0
  98. package/dist/file-lock-sync-root-acquire.d.ts +4 -0
  99. package/dist/file-lock-sync-root-acquire.d.ts.map +1 -0
  100. package/dist/file-lock-sync-root-acquire.js +370 -0
  101. package/dist/file-lock-sync-root-arbitration.d.ts +18 -0
  102. package/dist/file-lock-sync-root-arbitration.d.ts.map +1 -0
  103. package/dist/file-lock-sync-root-arbitration.js +66 -0
  104. package/dist/file-lock-sync-root-held.d.ts +34 -0
  105. package/dist/file-lock-sync-root-held.d.ts.map +1 -0
  106. package/dist/file-lock-sync-root-held.js +393 -0
  107. package/dist/file-lock-sync-root-io.d.ts +44 -0
  108. package/dist/file-lock-sync-root-io.d.ts.map +1 -0
  109. package/dist/file-lock-sync-root-io.js +209 -0
  110. package/dist/file-lock-sync-root-mutation.d.ts +17 -0
  111. package/dist/file-lock-sync-root-mutation.d.ts.map +1 -0
  112. package/dist/file-lock-sync-root-mutation.js +277 -0
  113. package/dist/file-lock-sync-root-options.d.ts +20 -0
  114. package/dist/file-lock-sync-root-options.d.ts.map +1 -0
  115. package/dist/file-lock-sync-root-options.js +58 -0
  116. package/dist/file-lock-sync-root-registration.d.ts +2 -0
  117. package/dist/file-lock-sync-root-registration.d.ts.map +1 -0
  118. package/dist/file-lock-sync-root-registration.js +90 -0
  119. package/dist/file-lock-sync-root.d.ts +36 -0
  120. package/dist/file-lock-sync-root.d.ts.map +1 -0
  121. package/dist/file-lock-sync-root.js +361 -0
  122. package/dist/file-lock-sync-stale-admission.d.ts +24 -0
  123. package/dist/file-lock-sync-stale-admission.d.ts.map +1 -0
  124. package/dist/file-lock-sync-stale-admission.js +205 -0
  125. package/dist/file-lock-sync.d.ts.map +1 -1
  126. package/dist/file-lock-sync.js +245 -205
  127. package/dist/file-observation.d.ts +1 -1
  128. package/dist/file-observation.d.ts.map +1 -1
  129. package/dist/file-store-boundary.d.ts +2 -6
  130. package/dist/file-store-boundary.d.ts.map +1 -1
  131. package/dist/file-store-boundary.js +3 -9
  132. package/dist/file-store-prune.d.ts.map +1 -1
  133. package/dist/file-store-prune.js +5 -1
  134. package/dist/file-store-sync-write.d.ts.map +1 -1
  135. package/dist/file-store-sync-write.js +5 -8
  136. package/dist/file-store.d.ts.map +1 -1
  137. package/dist/file-store.js +47 -12
  138. package/dist/guarded-mkdir.d.ts +1 -0
  139. package/dist/guarded-mkdir.d.ts.map +1 -1
  140. package/dist/guarded-mkdir.js +36 -7
  141. package/dist/json-document-store.d.ts.map +1 -1
  142. package/dist/json-document-store.js +22 -15
  143. package/dist/json-durable-queue-ownership.d.ts +0 -1
  144. package/dist/json-durable-queue-ownership.d.ts.map +1 -1
  145. package/dist/json-durable-queue-ownership.js +0 -6
  146. package/dist/move-path.js +1 -1
  147. package/dist/native-binding.d.ts +13 -1
  148. package/dist/native-binding.d.ts.map +1 -1
  149. package/dist/native-fallback-warning.d.ts +4 -0
  150. package/dist/native-fallback-warning.d.ts.map +1 -0
  151. package/dist/native-fallback-warning.js +11 -0
  152. package/dist/native-operations.d.ts +0 -2
  153. package/dist/native-operations.d.ts.map +1 -1
  154. package/dist/native-operations.js +0 -24
  155. package/dist/native-parent-admission.d.ts +5 -2
  156. package/dist/native-parent-admission.d.ts.map +1 -1
  157. package/dist/native-parent-admission.js +27 -7
  158. package/dist/native-pinned-write-windows.d.ts +1 -1
  159. package/dist/native-pinned-write-windows.d.ts.map +1 -1
  160. package/dist/native-pinned-write-windows.js +174 -29
  161. package/dist/native-pinned-write.d.ts.map +1 -1
  162. package/dist/native-pinned-write.js +26 -14
  163. package/dist/native-policy-parent-windows.d.ts +14 -0
  164. package/dist/native-policy-parent-windows.d.ts.map +1 -0
  165. package/dist/native-policy-parent-windows.js +209 -0
  166. package/dist/native-rename-outcome.d.ts +4 -0
  167. package/dist/native-rename-outcome.d.ts.map +1 -0
  168. package/dist/native-rename-outcome.js +8 -0
  169. package/dist/native-staged-file.d.ts +3 -2
  170. package/dist/native-staged-file.d.ts.map +1 -1
  171. package/dist/native-staged-file.js +121 -72
  172. package/dist/output.d.ts.map +1 -1
  173. package/dist/output.js +12 -8
  174. package/dist/owner-dacl.d.ts.map +1 -1
  175. package/dist/owner-dacl.js +10 -4
  176. package/dist/path-prefix.d.ts.map +1 -1
  177. package/dist/path-prefix.js +30 -8
  178. package/dist/path-suffix-aliases.d.ts +2 -0
  179. package/dist/path-suffix-aliases.d.ts.map +1 -1
  180. package/dist/path-suffix-aliases.js +25 -17
  181. package/dist/permission-exec.d.ts +2 -0
  182. package/dist/permission-exec.d.ts.map +1 -1
  183. package/dist/permission-exec.js +150 -21
  184. package/dist/permissions-windows.js +1 -1
  185. package/dist/pinned-mutation-admission.d.ts.map +1 -1
  186. package/dist/pinned-mutation-admission.js +10 -5
  187. package/dist/pinned-mutation-observation.d.ts +0 -1
  188. package/dist/pinned-mutation-observation.d.ts.map +1 -1
  189. package/dist/pinned-mutation-observation.js +0 -19
  190. package/dist/pinned-mutation-shared-route.d.ts +1 -0
  191. package/dist/pinned-mutation-shared-route.d.ts.map +1 -1
  192. package/dist/pinned-mutation-shared-route.js +1 -1
  193. package/dist/pinned-write-input.d.ts +4 -0
  194. package/dist/pinned-write-input.d.ts.map +1 -0
  195. package/dist/pinned-write-input.js +25 -0
  196. package/dist/pinned-write-mode.d.ts +5 -0
  197. package/dist/pinned-write-mode.d.ts.map +1 -0
  198. package/dist/pinned-write-mode.js +24 -0
  199. package/dist/pinned-write-staged.d.ts +6 -0
  200. package/dist/pinned-write-staged.d.ts.map +1 -0
  201. package/dist/pinned-write-staged.js +187 -0
  202. package/dist/pinned-write-types.d.ts +5 -0
  203. package/dist/pinned-write-types.d.ts.map +1 -1
  204. package/dist/pinned-write.d.ts.map +1 -1
  205. package/dist/pinned-write.js +35 -145
  206. package/dist/private-directory.d.ts.map +1 -1
  207. package/dist/private-directory.js +18 -4
  208. package/dist/private-producer-handoff-sync.d.ts +14 -0
  209. package/dist/private-producer-handoff-sync.d.ts.map +1 -0
  210. package/dist/private-producer-handoff-sync.js +114 -0
  211. package/dist/private-producer-handoff.d.ts +22 -4
  212. package/dist/private-producer-handoff.d.ts.map +1 -1
  213. package/dist/private-producer-handoff.js +140 -77
  214. package/dist/private-temp-workspace.d.ts.map +1 -1
  215. package/dist/private-temp-workspace.js +75 -121
  216. package/dist/publish-copy-stage.d.ts +2 -1
  217. package/dist/publish-copy-stage.d.ts.map +1 -1
  218. package/dist/publish-copy-stage.js +16 -7
  219. package/dist/publish-file.d.ts.map +1 -1
  220. package/dist/publish-file.js +2 -2
  221. package/dist/regular-file.d.ts.map +1 -1
  222. package/dist/regular-file.js +1 -15
  223. package/dist/replace-directory.d.ts.map +1 -1
  224. package/dist/replace-directory.js +256 -18
  225. package/dist/replace-file-copy-fallback.d.ts.map +1 -1
  226. package/dist/replace-file-copy-fallback.js +62 -70
  227. package/dist/replace-file-copy-source.d.ts.map +1 -1
  228. package/dist/replace-file-copy-source.js +10 -12
  229. package/dist/replace-file-temp-owner.d.ts +5 -9
  230. package/dist/replace-file-temp-owner.d.ts.map +1 -1
  231. package/dist/replace-file-temp-owner.js +56 -72
  232. package/dist/replace-file.js +6 -6
  233. package/dist/retained-directory-replacement.d.ts +26 -0
  234. package/dist/retained-directory-replacement.d.ts.map +1 -0
  235. package/dist/retained-directory-replacement.js +193 -0
  236. package/dist/root-boundary.d.ts +1 -0
  237. package/dist/root-boundary.d.ts.map +1 -1
  238. package/dist/root-boundary.js +4 -0
  239. package/dist/root-context.d.ts +0 -8
  240. package/dist/root-context.d.ts.map +1 -1
  241. package/dist/root-context.js +0 -3
  242. package/dist/root-create-input.d.ts +8 -1
  243. package/dist/root-create-input.d.ts.map +1 -1
  244. package/dist/root-create-input.js +17 -4
  245. package/dist/root-directory-creation.d.ts +3 -3
  246. package/dist/root-directory-creation.d.ts.map +1 -1
  247. package/dist/root-directory-creation.js +15 -3
  248. package/dist/root-directory-list.d.ts +1 -0
  249. package/dist/root-directory-list.d.ts.map +1 -1
  250. package/dist/root-directory-list.js +1 -0
  251. package/dist/root-impl.d.ts.map +1 -1
  252. package/dist/root-impl.js +46 -17
  253. package/dist/root-move-noreplace.d.ts.map +1 -1
  254. package/dist/root-move-noreplace.js +24 -15
  255. package/dist/root-options.d.ts +12 -4
  256. package/dist/root-options.d.ts.map +1 -1
  257. package/dist/root-path-errors.d.ts +1 -0
  258. package/dist/root-path-errors.d.ts.map +1 -1
  259. package/dist/root-path-errors.js +11 -2
  260. package/dist/root-path-existing.d.ts.map +1 -1
  261. package/dist/root-path-existing.js +11 -35
  262. package/dist/root-path-stat.d.ts.map +1 -1
  263. package/dist/root-path-stat.js +59 -7
  264. package/dist/root-path.js +1 -13
  265. package/dist/root-remove.d.ts +1 -0
  266. package/dist/root-remove.d.ts.map +1 -1
  267. package/dist/root-remove.js +4 -0
  268. package/dist/root-walk.d.ts +1 -1
  269. package/dist/root-walk.d.ts.map +1 -1
  270. package/dist/root-walk.js +17 -2
  271. package/dist/root-write-admission.d.ts +0 -2
  272. package/dist/root-write-admission.d.ts.map +1 -1
  273. package/dist/root-write-admission.js +1 -15
  274. package/dist/root-write-complete-parent.d.ts.map +1 -1
  275. package/dist/root-write-complete-parent.js +7 -23
  276. package/dist/root-write-publication.js +1 -1
  277. package/dist/root-write-verification.d.ts.map +1 -1
  278. package/dist/root-write-verification.js +29 -42
  279. package/dist/secret-file.d.ts.map +1 -1
  280. package/dist/secret-file.js +3 -24
  281. package/dist/secret-read-async.d.ts.map +1 -1
  282. package/dist/secret-read-async.js +3 -24
  283. package/dist/secret-read-policy.d.ts +6 -2
  284. package/dist/secret-read-policy.d.ts.map +1 -1
  285. package/dist/secret-read-policy.js +26 -2
  286. package/dist/secure-file-windows.d.ts +6 -0
  287. package/dist/secure-file-windows.d.ts.map +1 -1
  288. package/dist/secure-file-windows.js +34 -117
  289. package/dist/secure-file.js +2 -2
  290. package/dist/sibling-temp.d.ts.map +1 -1
  291. package/dist/sibling-temp.js +23 -12
  292. package/dist/sidecar-lock-acquire.d.ts +2 -28
  293. package/dist/sidecar-lock-acquire.d.ts.map +1 -1
  294. package/dist/sidecar-lock-acquire.js +288 -199
  295. package/dist/sidecar-lock-admission-context.d.ts +19 -0
  296. package/dist/sidecar-lock-admission-context.d.ts.map +1 -0
  297. package/dist/sidecar-lock-admission-context.js +60 -0
  298. package/dist/sidecar-lock-admission-parser.d.ts +43 -0
  299. package/dist/sidecar-lock-admission-parser.d.ts.map +1 -0
  300. package/dist/sidecar-lock-admission-parser.js +113 -0
  301. package/dist/sidecar-lock-admission.d.ts +35 -0
  302. package/dist/sidecar-lock-admission.d.ts.map +1 -0
  303. package/dist/sidecar-lock-admission.js +7 -0
  304. package/dist/sidecar-lock-reclaim.d.ts +9 -4
  305. package/dist/sidecar-lock-reclaim.d.ts.map +1 -1
  306. package/dist/sidecar-lock-reclaim.js +80 -25
  307. package/dist/sidecar-lock-root.d.ts.map +1 -1
  308. package/dist/sidecar-lock-root.js +2 -1
  309. package/dist/sidecar-lock-stale-admission.d.ts +39 -0
  310. package/dist/sidecar-lock-stale-admission.d.ts.map +1 -0
  311. package/dist/sidecar-lock-stale-admission.js +232 -0
  312. package/dist/sidecar-lock-target.d.ts +8 -0
  313. package/dist/sidecar-lock-target.d.ts.map +1 -0
  314. package/dist/sidecar-lock-target.js +55 -0
  315. package/dist/sidecar-lock.d.ts.map +1 -1
  316. package/dist/sidecar-lock.js +100 -16
  317. package/dist/staged-directory.d.ts.map +1 -1
  318. package/dist/staged-directory.js +6 -6
  319. package/dist/staged-file-settlement.d.ts +17 -0
  320. package/dist/staged-file-settlement.d.ts.map +1 -0
  321. package/dist/staged-file-settlement.js +57 -0
  322. package/dist/temp-workspace-descriptor.d.ts.map +1 -1
  323. package/dist/temp-workspace-descriptor.js +9 -27
  324. package/dist/temp-workspace-owner.d.ts.map +1 -1
  325. package/dist/temp-workspace-owner.js +8 -8
  326. package/dist/walk.d.ts +5 -1
  327. package/dist/walk.d.ts.map +1 -1
  328. package/dist/walk.js +19 -6
  329. package/dist/windows-owner.d.ts.map +1 -1
  330. package/dist/windows-owner.js +4 -3
  331. package/dist/windows-security-bridge.cs +336 -0
  332. package/dist/windows-security-bridge.ps1 +15 -0
  333. package/dist/windows-security-command.d.ts +26 -0
  334. package/dist/windows-security-command.d.ts.map +1 -0
  335. package/dist/windows-security-command.js +363 -0
  336. package/dist/windows-security-facts.d.ts +6 -0
  337. package/dist/windows-security-facts.d.ts.map +1 -0
  338. package/dist/windows-security-facts.js +108 -0
  339. package/docs/advanced.md +4 -2
  340. package/docs/archive.md +97 -46
  341. package/docs/atomic.md +85 -8
  342. package/docs/config.md +6 -2
  343. package/docs/contributing.md +44 -4
  344. package/docs/copy.md +37 -0
  345. package/docs/creation.md +128 -0
  346. package/docs/durability.md +24 -0
  347. package/docs/file-store.md +19 -0
  348. package/docs/install.md +31 -7
  349. package/docs/json-store.md +5 -0
  350. package/docs/migrating-to-0.5.md +15 -6
  351. package/docs/migrating-to-0.6.md +9 -4
  352. package/docs/native-helper.md +32 -12
  353. package/docs/native.md +38 -7
  354. package/docs/output.md +6 -0
  355. package/docs/path-prefix.md +10 -0
  356. package/docs/path-suffix-aliases.md +51 -6
  357. package/docs/permissions.md +50 -14
  358. package/docs/public-api.md +3 -2
  359. package/docs/root.md +56 -3
  360. package/docs/secret-file.md +11 -2
  361. package/docs/secure-file.md +9 -4
  362. package/docs/sidecar-lock.md +114 -8
  363. package/docs/staged-file.md +12 -3
  364. package/docs/temp.md +20 -3
  365. package/docs/walk.md +67 -1
  366. package/docs/writing.md +76 -6
  367. package/package.json +19 -16
  368. package/dist/darwin-acl.d.ts +0 -4
  369. package/dist/darwin-acl.d.ts.map +0 -1
  370. package/dist/darwin-acl.js +0 -24
@@ -42,7 +42,7 @@ POSIX remediation strings shell-quote paths with whitespace or metacharacters
42
42
  and protect option-like paths with `--`, so they can be presented as commands
43
43
  without letting the inspected pathname add shell syntax.
44
44
 
45
- `inspectPathPermissions()` follows symlink targets for the effective mode but tells you whether the original path was a symlink. On POSIX it reports owner/group/world bits. On Windows it delegates to the ACL helpers below and also reports `ownerSid` plus `ownerTrusted` when ownership can be verified. `ownerTrusted` is true only for a local volume owned by the current user, LocalSystem, or built-in Administrators; remote filesystems fail closed. This remains a pathname reporting API with the fallbacks described below. `readSecureFile()` does not use those pathname fallbacks on Windows: it requires descriptor-bound native owner/DACL facts for the exact handle it reads.
45
+ `inspectPathPermissions()` follows symlink targets for the effective mode but tells you whether the original path was a symlink. On POSIX it reports owner/group/world bits. On Windows it delegates to the ACL helpers below and also reports `ownerSid` plus `ownerTrusted` when ownership can be verified. `ownerTrusted` is true only for a local volume owned by the current user, LocalSystem, or built-in Administrators; remote filesystems fail closed. This remains a pathname reporting API with the fallbacks described below. `readSecureFile()` obtains descriptor-bound owner/DACL facts for the exact handle it reads, using native support or the packaged PowerShell/C# bridge in `auto` and `off` modes.
46
46
 
47
47
  ## Advanced Windows ACL helpers
48
48
 
@@ -69,9 +69,10 @@ resolveWindowsUserPrincipal(env);
69
69
  ```
70
70
 
71
71
  The fallback Windows inspector reads the owner and DACL together through one
72
- built-in Windows PowerShell/.NET query. It returns canonical SIDs and numeric
73
- access masks, so Unicode paths and account names do not pass through lossy
74
- console display text. `inspectWindowsAcl()` uses native descriptor facts for
72
+ built-in Windows PowerShell/.NET query. The query addresses its JSON command by
73
+ module name and limits module discovery to PowerShell's bundled system modules.
74
+ It returns canonical SIDs and numeric access masks, so Unicode paths and account
75
+ names do not pass through lossy console display text. `inspectWindowsAcl()` uses native descriptor facts for
75
76
  complete local ACLs with nonzero inherited ACEs (or empty/null DACLs) when the
76
77
  optional Windows binding is available. It applies
77
78
  the same classifier to native facts and the fallback query, returning canonical
@@ -102,6 +103,19 @@ characters, including a trailing `…` when truncated. Diagnostics do not copy
102
103
  stdout or read target file contents. The separate `errorCause` retains the
103
104
  original exception for restricted local diagnosis; do not serialize or expose
104
105
  it as display text.
106
+ Custom executors may reject with any JavaScript value. The fallback display
107
+ formatter handles primitives directly and reads only string-valued `name` and
108
+ `message` data descriptors through a small, fixed prototype budget. It does not
109
+ coerce objects, invoke accessors, or inspect proxy targets; unavailable display
110
+ facts use a bounded generic reason. Command fields follow the same best-effort
111
+ data-descriptor rule. Raw string, `Buffer`, or genuine `Uint8Array` stderr
112
+ retains the sanitization above. Byte stderr is copied through captured
113
+ typed-array intrinsics into a private bounded snapshot before replacement-based
114
+ UTF-8 decoding; receiver properties, iterators, constructors, and altered
115
+ prototypes are not consulted. Detached or out-of-bounds byte views contribute
116
+ no stderr detail. These diagnostic limits do not relax permission policy:
117
+ incomplete owner or ACL inspection remains unverified, and `errorCause` remains
118
+ the exact rejected value even when no display metadata is safe to obtain.
105
119
  The parser and remediation command builders remain on the advanced surface for
106
120
  CLIs processing captured `icacls` output or presenting an explicit repair.
107
121
  Runtime inspection does not parse that display text. A null DACL reports
@@ -158,10 +172,12 @@ Object-specific and other ACE layouts are not guessed: they are omitted,
158
172
  `complete` becomes false, and their numeric types appear in
159
173
  `unsupportedAceTypes`, allowing a security-sensitive caller to fail closed.
160
174
  Non-Windows systems return `{ status: "unsupported-platform", platform }`.
161
- Windows requires the native binding; if it is unavailable or forced
162
- off, the call throws `FsSafeError("helper-unavailable")`. The existing coarse
163
- `inspectPathPermissions()` API still owns its compatibility fallback and trust
164
- classification.
175
+ Windows prefers the native binding. In native `auto` or `off` mode, a missing
176
+ binding or capability uses the packaged PowerShell/C# bridge with
177
+ the same raw ACE projection. Native `require` rejects either absence with
178
+ `FsSafeError("helper-unavailable")` and starts no command. An available native
179
+ query's failure is terminal. The existing coarse `inspectPathPermissions()` API
180
+ still owns its compatibility fallback and trust classification.
165
181
 
166
182
  ## Private directories
167
183
 
@@ -175,10 +191,11 @@ await createPrivateDirectory(sqliteDirectory);
175
191
  await openSqlite(path.join(sqliteDirectory, "sessions.sqlite"));
176
192
  ```
177
193
 
178
- On Windows with native support, this creates the directory and applies a
194
+ On Windows, this creates the directory and applies a
179
195
  protected owner + LocalSystem + Administrators full-control DACL directly with
180
- an atomic security descriptor; no PowerShell or `icacls` process is launched.
181
- The native operation retains the parent and exact created-directory handles
196
+ an atomic security descriptor. The native route launches no command. When its
197
+ binding or capability is unavailable, native `auto` and `off` modes use the
198
+ packaged PowerShell/C# bridge. Both routes retain the parent and exact created-directory handles
182
199
  through ACL and final pathname validation. If validation fails, it attempts only
183
200
  nonrecursive deletion through the created handle, preserving any pathname
184
201
  replacement. If cleanup also fails, the error retains the original failure and
@@ -203,13 +220,32 @@ also rejects explicit `.` and `..` components, including spellings such as
203
220
  `.\private` and `parent\..\private`, as a compatibility restriction. Simple
204
221
  relative names without these components remain supported.
205
222
 
206
- This API is Windows-only and native-only; it fails closed with
207
- `FsSafeError("helper-unavailable")` on other platforms, when native mode is off,
208
- or when the binding is unavailable. POSIX callers should create private
223
+ This API is Windows-only; it fails closed with `FsSafeError("helper-unavailable")`
224
+ on other platforms. Native `require` also fails if the binding or capability is
225
+ missing and never starts a command. An available native operation's failure is
226
+ terminal. POSIX callers should create private
209
227
  directories through their existing trusted-root creation policy rather than a
210
228
  pathname-only compatibility shim. Existing Windows permission inspection still
211
229
  retains its structured .NET compatibility fallback.
212
230
 
231
+ The raw owner/DACL and private-directory fallbacks each emit one path-free
232
+ `FS_SAFE_NATIVE_FALLBACK` warning per process. PowerShell startup and C#
233
+ compilation add overhead to each call; install the native package for frequent
234
+ operations. These routes run the package's readable, fixed scripts under normal
235
+ system PowerShell policy; see the [Windows security fallback prerequisites](install.md#windows-security-fallback).
236
+ If command support is unavailable, disallowed, or fails, the operation rejects.
237
+ Private-directory creation never falls back to inherited permissions.
238
+ The asynchronous creation command has a 30-second deadline. After a timeout or
239
+ transport failure, fs-safe requests termination and waits at most one further
240
+ second before rejecting and closing its output pipes. The error distinguishes
241
+ observed process exit from an unconfirmed termination attempt. If the OS refuses
242
+ termination, the command can still create the directory after rejection. An
243
+ already-created object retains its protected DACL, but pathname validation and
244
+ owned-handle cleanup may not finish. An error therefore does not prove the
245
+ pathname is absent; a retry can report `EEXIST`. Before retrying or using the
246
+ pathname, establish that the earlier operation stopped and verify any existing
247
+ directory's security. The library does not attempt pathname-based cleanup.
248
+
213
249
  Use `createIcaclsResetCommand()` when you need a structured command and argv pair. Use `formatIcaclsResetCommand()` when you only need a remediation string for a user-facing message.
214
250
 
215
251
  ## Types
@@ -43,8 +43,9 @@ The advanced root-file primitive exports `OpenRootFileParams`,
43
43
  `RootFileOpenFailureReason`. These are composition types for callers building
44
44
  their own pinned-open flow, not substitutes for the higher-level `Root` verbs.
45
45
 
46
- `copyFileHandle` and `CopyFileHandleOptions` transfer bytes between already-open
47
- regular files without taking over their cursors, lifetime, or publication.
46
+ `copyFileHandle` and `copyFileDescriptorSync` share `CopyFileHandleOptions` to
47
+ transfer bytes between already-open regular files without taking over their
48
+ cursors, lifetime, or publication.
48
49
  See [borrowed-handle transfers](copy.md#borrowed-filehandle-transfers).
49
50
 
50
51
  `readDirectoryIdentity`, `assertDirectoryIdentitySync`, and `DirectoryIdentity`
package/docs/root.md CHANGED
@@ -72,12 +72,22 @@ The default `order: "sorted"` enumerates and sorts each directory's names;
72
72
  wide directories. Budget exhaustion yields a `"truncated"` marker by
73
73
  default or throws `FsSafeError("too-large")` with `limitBehavior: "throw"`.
74
74
  Use `entryFilter(entry)` to return `"include"`, `"skip"`, or
75
- `"skip-subtree"`. `"skip"` omits the current entry but still descends into a
76
- directory; `"skip-subtree"` omits a directory and all of its descendants.
75
+ `"skip-subtree"`, directly or through a Promise. `"skip"` omits the current
76
+ entry but still descends into a directory; `"skip-subtree"` omits a directory
77
+ and all of its descendants.
78
+ Filters run serially outside metadata batches, with the options object as their
79
+ `this` receiver. After an awaited filter resolves, the walk checks cancellation
80
+ and revalidates the current listing directory and Root identities before using
81
+ the decision. Captured entry metadata retains its snapshot semantics.
82
+
83
+ Cancellation and iterator disposal wait for a pending filter to settle; they do
84
+ not race the callback or close its directory while it is running. Callback
85
+ throws and promise rejections reject the walk through normal cleanup.
77
86
  Directory reads remain fail-fast by default. With
78
87
  `onDirectoryError: "skip-and-report"`, the iterator instead yields
79
88
  `{ relativePath, kind: "directory-error", size: 0, error }` and continues with
80
- the remaining tree.
89
+ the remaining tree. That policy also covers identity-check failures after an
90
+ awaited filter, while callback failures always reject.
81
91
  See [Directory walking](walk.md) for the pure-Node guarantees and the contrast
82
92
  with the standalone best-effort walkers.
83
93
 
@@ -120,8 +130,32 @@ fs.mkdir(rel, options?) // mkdir -p (creates missing parents)
120
130
  fs.ensureRoot(options?) // accepts "" / "." as the root itself
121
131
  ```
122
132
 
133
+ `mkdir`, `ensureRoot`, `create`, and `createJson` accept `private: true`.
134
+ Missing directories are created with private permissions, and an existing
135
+ requested directory must already be private. Existing ancestors are not
136
+ chmodded or assigned new ACLs. Private files use owner-only POSIX permissions
137
+ or a protected Windows DACL granting access to the current user, System, and
138
+ Administrators. On macOS, private directories and files must also have no ACL;
139
+ creation rejects relevant inheritable parent ACLs, while noninheriting parent
140
+ ACLs remain allowed. A native helper with `inspectDarwinAcl` is required. Native
141
+ `off`, a missing helper, or an older helper without that capability rejects with
142
+ `helper-unavailable` before creating parents or stages. See [creation](creation.md)
143
+ for platform support, synchronous leaf creation, and failure handling.
144
+
145
+ ```ts
146
+ await fs.mkdir("private-data", { private: true });
147
+ await fs.create("private-data/credential", "synthetic credential", { private: true });
148
+ ```
149
+
123
150
  `write`, `create`, `append`, `writeJson`, and `createJson` accept `mode?: number`; use `0o600` for credentials and other private state. `writeJson` also accepts the same options as `JSON.stringify` plus `trailingNewline?: boolean` (defaults `true` so the file ends in `\n`).
124
151
 
152
+ Buffered `create` and `createJson` also accept `atomic?: boolean`. With `true`,
153
+ complete content is staged before exclusive publication even in native-off mode;
154
+ the fallback requires hardlinks. Omitted or `false` keeps the existing buffered
155
+ publication behavior. Streamed creates always stage complete content. The flag
156
+ does not change `durable` or promise stronger containment or crash durability.
157
+ See [atomic creation and settlement](writing.md#atomic-buffered-creation).
158
+
125
159
  `create` also accepts `AsyncIterable<Uint8Array>` with `RootCreateStreamOptions`:
126
160
  the same path, authority, mode, and durability options, plus `maxBytes` and
127
161
  `signal`, without `encoding` or `renameIdentity`. It consumes one chunk at a
@@ -142,6 +176,11 @@ and parent-directory fsync calls. Use it only for reconstructible data: a crash
142
176
  may lose the write or leave the previous file. See [Writing](writing.md#write-options)
143
177
  for platform details.
144
178
 
179
+ `create` and `createJson` additionally accept `durable: "file"` to require file
180
+ synchronization, including propagating `EPERM`. Parent-directory synchronization
181
+ retains its existing best-effort behavior. This option applies to buffered and
182
+ streamed creation and does not select a publication strategy.
183
+
145
184
  `copyIn` accepts a `RootCopySource`: a trusted absolute source path or a file
146
185
  within another Root. The guarded form supplies `root` with only its `open` and
147
186
  `stat` read capabilities, plus `relativePath`:
@@ -290,6 +329,20 @@ the caller, which must check authority before its own later writes.
290
329
 
291
330
  All mutation methods accept `denyMutations?: { paths?: string[]; prefixes?: string[] }`. Entries must be absolute paths. `paths` blocks those exact paths; `prefixes` blocks those paths and their descendants. fs-safe preserves path strings exactly and canonicalizes through existing ancestors before comparing, so a symlinked ancestor to a denied location is still denied. Denied mutations throw `FsSafeError` with code `denied-path`. Use this for caller-specific sensitive paths, not as a replacement for the root boundary, symlink, or hardlink checks.
292
331
 
332
+ `move()` snapshots its merged default and per-call mutation policy before
333
+ asynchronous preparation. Later changes to the original policy objects or arrays
334
+ apply to subsequent calls. Use `assertBeforeMutation` for live revocation of an
335
+ in-flight move.
336
+
337
+ For writes, creates, streams, and copies, parent creation admits the prospective
338
+ file and each missing directory before creating that directory, including on the
339
+ Windows native route. An exact deny on an existing parent does not prevent using
340
+ that parent to write an allowed child. If a deeper missing parent is denied,
341
+ earlier admitted directories may remain; the denied directory and file are not
342
+ created. With `mkdir: false`, missing parents are never created. Native Windows
343
+ policy-aware creation requires the direct-child helper and fails with
344
+ `helper-unavailable` if it is absent.
345
+
293
346
  All mutation methods also accept `mutationSymlinks`. `"reject"` rejects symlink
294
347
  components; `"follow-parents-within-root"` resolves contained parent directory
295
348
  aliases but rejects the final component if it is a symlink, including a dangling
@@ -133,6 +133,15 @@ startWebhookVerifier(signingKey);
133
133
 
134
134
  Async. Creates the parent directory at `dirMode` (default `0o700`) if missing, writes content to a sibling temp file, finalizes `mode` (default `0o600`) through an owned descriptor after content writes, and atomically renames over the destination. Publication verification checks the final file identity and mode.
135
135
 
136
+ On POSIX, both native and JavaScript writers verify actual `0o600` permission
137
+ bits through the retained descriptor before writing content. A filesystem that
138
+ reports successful chmod without enforcing those bits fails with
139
+ `insecure-permissions` before any payload is written, including when an explicit
140
+ `dirMode` permits other users to traverse the parent. The requested final `mode`
141
+ is still applied after content writes, including restrictive and special-bit
142
+ overrides. This mode-bit check does not require native ACL inspection; JavaScript
143
+ secret writes remain available on macOS.
144
+
136
145
  Concurrent writes to distinct leaves may share creation of a missing parent.
137
146
  After a parent-creation race, the helper re-inspects the entry and requires a
138
147
  non-symlink directory, then revalidates root/parent guards, containment, and
@@ -270,9 +279,9 @@ await withTimeout(
270
279
 
271
280
  ## Threat model notes
272
281
 
273
- - These helpers protect the secret file from **other processes with the same UID** that respect filesystem permissions. They do not defend against root or against attackers who can read process memory.
282
+ - On POSIX, the default `0600` file and `0700` directory modes restrict group and other access. They do not protect against processes with the same UID, root, attackers who can read process memory, or access granted by additional ACL entries.
274
283
  - Validation failures are tripwires, not authorization. Investigate before clearing a rejected credential file.
275
- - If the destination directory is on a tmpfs that does not honor mode bits, the helpers will set the mode bits but the OS may ignore them. Audit your platform.
284
+ - On POSIX, a file that still reports a mode other than `0600` after initialization is rejected with `insecure-permissions` before payload is written. Matching mode reports alone cannot prove that an arbitrary filesystem actually enforces those permissions.
276
285
 
277
286
  ## See also
278
287
 
@@ -27,9 +27,13 @@ The helper:
27
27
  - enforces `maxBytes` before and after reading
28
28
  - closes the handle on success, error, and timeout
29
29
 
30
- On POSIX, unsafe permissions mean group/world writable, and group/world readable unless `permissions.allowReadableByOthers` is true. On Windows, the helper queries owner, DACL, and locality from the same open descriptor that supplies the bytes. The native query returns the 32-bit volume serial and 64-bit file-index projection used by Node, which must equal Node's bigint descriptor receipt before its ACL facts are trusted. This avoids JavaScript number rounding but does not represent the full 128-bit file identity available on ReFS. Only the current user, LocalSystem, and built-in Administrators are trusted owner classes.
30
+ On POSIX, unsafe permissions mean group/world writable, and group/world readable unless `permissions.allowReadableByOthers` is true. On Windows, the helper queries owner, DACL, and locality from the same open descriptor that supplies the bytes. Both native and system-command queries return the 32-bit volume serial and 64-bit file-index projection used by Node, which must equal Node's bigint descriptor receipt before its ACL facts are trusted. This avoids JavaScript number rounding but does not represent the full 128-bit file identity available on ReFS. Only the current user, LocalSystem, and built-in Administrators are trusted owner classes.
31
31
 
32
- Windows secure reads require the matching current optional native package. A missing or stale helper, fd-to-handle conversion failure, denied `READ_CONTROL`, remote handle, incomplete descriptor, or unsupported ACE form rejects with `permission-unverified` before content is read. A malformed or different handle identity rejects with `path-mismatch`. There is no pathname-command fallback for `readSecureFile()`; the standalone reporting APIs in [`permissions`](permissions.md) retain their documented fallbacks. `permissions.allowInsecure` remains the explicit escape hatch and bypasses the ACL query.
32
+ Windows secure reads prefer the matching optional native package. In native `auto` or `off` mode, a missing binding or descriptor-inspection capability uses a packaged, readable PowerShell script and adjacent C# source to inspect the borrowed file handle. This route requires the [Windows security fallback prerequisites](install.md#windows-security-fallback), including permission to run the scripts under normal system policy. The command does not read file contents or reopen the pathname. Successful inspection waits for the child to exit and its output pipes to close. This emits a path-free `FS_SAFE_NATIVE_FALLBACK` warning once per process for secure reads and adds PowerShell startup and compilation overhead to each inspection.
33
+
34
+ Descriptor commands have a 30-second deadline. After a timeout or transport failure, fs-safe requests termination, waits at most one further second, and then rejects even if process exit or pipe closure remains unconfirmed. It closes its own output pipes and reports the observed exit separately from the termination attempt in the error cause. If the OS refuses termination, the child may retain its independently inherited Windows handle; closing the caller's descriptor cannot retarget that handle. No file bytes are returned from a failed inspection.
35
+
36
+ Native `require` still rejects a missing binding or capability with `permission-unverified`, without starting a command. An available native helper's failure is terminal. On either route, fd-to-handle conversion failure, denied `READ_CONTROL`, a remote handle, incomplete descriptor, unsupported ACE form, or unavailable command support rejects with `permission-unverified` before content is read. A malformed or different handle identity rejects with `path-mismatch`. The standalone reporting APIs in [`permissions`](permissions.md) retain their documented pathname fallbacks. `permissions.allowInsecure` remains the explicit escape hatch and bypasses the ACL query.
33
37
 
34
38
  Descriptor, pathname, and realpath identity checks use bigint stats internally to avoid JavaScript number rounding. The returned `stat` remains a normal Node `Stats` object with numeric fields. A zero Windows device or inode is unverified, never a match: the helper re-inspects that identity once using the same descriptor or pathname, then rejects persistent ambiguity with `path-mismatch`. A definite mismatch rejects immediately; retries retain known identity components and still enforce symlink policy.
35
39
 
@@ -92,13 +96,14 @@ On an actual Windows process with effective `platform: "win32"`, `inject.env` an
92
96
  | `timeout` | `timeoutMs` elapsed while reading. |
93
97
 
94
98
  Windows descriptor-inspection failures are operational `permission-unverified`
95
- errors and refuse the read. The original native exception is retained as
99
+ errors and refuse the read. The original native or descriptor-command exception is retained as
96
100
  `cause`; treat causes as restricted local diagnostic data. No pathname or ACL
97
101
  content is copied into the display message. Test adapters that simulate Windows
98
102
  on another operating system retain the standalone pathname inspector's
99
103
  structured command diagnostics (`ownerError`, `command`, `durationMs`,
100
104
  `timedOut`, `exitCode`, `signal`, and bounded escaped `stderr`). Actual Windows
101
- secure reads do not start those commands. No retries are performed.
105
+ secure reads never invoke the injected pathname inspector: their optional
106
+ command route inspects the borrowed descriptor instead. No retries are performed.
102
107
 
103
108
  ## See also
104
109
 
@@ -9,6 +9,13 @@ normal meaning, and ordinary colon-bearing POSIX paths remain valid.
9
9
 
10
10
  JavaScript raw-sidecar creation passes mode `0o600` to that exclusive open. On POSIX, the process umask may further restrict the new file but cannot add group or other access. No pathname `chmod` fallback is used.
11
11
 
12
+ Root-backed lock records and reclaim guards also claim their final name
13
+ exclusively, including with the native backend. The native path retains the
14
+ admitted parent descriptor and removes incomplete claims only while their exact
15
+ identity remains owned. Ordinary Root creates still stage privately; lock records
16
+ use this internal exclusive-create path so racing contenders can retry without
17
+ an ambiguous rename outcome.
18
+
12
19
  ```ts
13
20
  import { acquireFileLock } from "@openclaw/fs-safe/file-lock";
14
21
 
@@ -28,13 +35,32 @@ try {
28
35
 
29
36
  The lock file sits next to the protected resource. If a process crashes mid-lock, the next acquirer notices the held entry, inspects its payload (PID, host, acquired-at timestamp), and decides — via `shouldReclaim` (defaulting to "is the lock older than `staleMs`?") — whether it should keep waiting or fail.
30
37
 
31
- On natural event-loop shutdown, a globally deduplicated `process.on("beforeExit")` handler attempts asynchronous cleanup of held Root-backed locks through their retained Root capability and ownership receipt. The synchronous `process.on("exit")` handler provides last-chance cleanup for raw locks and reclaim guards. Changed sidecars and failed Root cleanup remain in place; cleanup does not keep retrying during shutdown unless another acquisition re-arms it. Locks acquired with `retainOnExit: true` are exempt from both handlers: their sidecar stays in place after exit and is governed only by the caller's own stale policy. Because exit handlers are globally deduplicated across package copies, `retainOnExit` fails closed with `helper-unavailable` if an older copy that cannot honor it registered the handlers first.
38
+ On natural event-loop shutdown, a globally deduplicated `process.on("beforeExit")` handler attempts asynchronous cleanup of held Root-backed locks through their retained Root capability and ownership receipt. The synchronous `process.on("exit")` handler provides last-chance cleanup for raw locks and raw-path reclaim guards. Changed sidecars and failed Root cleanup remain in place; cleanup does not keep retrying during shutdown unless another acquisition re-arms it. Locks acquired with `retainOnExit: true` are exempt from both handlers: their sidecar stays in place after exit and is governed only by the caller's own stale policy. Because exit handlers are globally deduplicated across package copies, `retainOnExit` fails closed with `helper-unavailable` if an older copy that cannot honor it registered the handlers first.
39
+
40
+ Asynchronous Root-backed stale recovery uses a regular file at the
41
+ `.reclaim` name, created, verified, and removed through that Root. Its ownership
42
+ token and exact bytes are checked after awaited decisions and before stale
43
+ removal; Root mutation policies also apply to the guard. Raw and synchronous
44
+ Root reclaimers use directories and recognize these files as occupied guards.
45
+ Each asynchronous attempt owns its guard directly, outside process-exit cleanup,
46
+ so `beforeExit` cannot release an exclusion still needed by an unsettled attempt.
47
+ Normal completion removes it through the Root. Interrupted creation, revoked
48
+ cleanup authority, identity changes, or process exit can leave the guard in
49
+ place; recover it only after an application-owned liveness check proves the
50
+ attempt has ended. There is no raw-path cleanup fallback. These token/byte
51
+ checks retain the sidecar protocol's cooperative, non-atomic removal boundary.
52
+ The final guard check follows the last sidecar snapshot and parser call, before
53
+ removal. Parsers can run before guard ownership is established or verified;
54
+ invocation is not mutation authority. A failing final parser keeps its error
55
+ even if guard ownership has also changed.
56
+ `manager.reset()` invalidates admission bookkeeping but preserves a pending
57
+ Root guard; let its original attempt settle before retrying that guarded path.
32
58
 
33
59
  Always release locks in a `finally` block. Application-managed graceful shutdown can await `release()` or `manager.drain()` before terminating. Explicit `process.exit()`, uncaught failures, crashes, default signal handling, and fatal termination (including `SIGKILL`) do not reliably run asynchronous Root cleanup and may leave sidecars. Recover only after an application-owned liveness policy proves the holder cannot still be writing.
34
60
 
35
- Exit cleanup tolerates shared managers created by older package copies that lack reclaim-guard state, and continues through later manager domains. Handler ownership remains first-registration-wins: loading an updated copy does not replace an older copy's registered handler. Restart with the updated copy registering first to obtain the fix. After building, `node scripts/legacy-lock-exit-proof.mjs` checks clean exit, removal of legacy and modern raw locks, and preservation of a retained lock using synthetic temporary files.
61
+ Exit cleanup tolerates shared managers created by older package copies that lack reclaim-guard state, and continues through later manager domains. Handler ownership remains first-registration-wins: loading an updated copy does not replace an older copy's registered handler. First registration is committed only after Node accepts the listener; synchronous `newListener` reentry fails closed and a thrown registration rolls back so a later acquisition can retry. Restart with the updated copy registering first to obtain the fix. After building, `node scripts/legacy-lock-exit-proof.mjs` checks clean exit, removal of legacy and modern raw locks, and preservation of a retained lock using synthetic temporary files.
36
62
 
37
- Each new sidecar also carries an internal random ownership token encoded as JSON trailing whitespace. `JSON.parse()` and every payload callback still see exactly the caller-provided object. Only the process that successfully created the sidecar keeps that token as release authority; merely reading token-shaped bytes from disk does not enable this mode. Release compares the in-memory token and exact serialized bytes, and requires the pathname to remain a regular file, instead of requiring an opened descriptor and pathname lookup to report the same inode identity. This preserves ownership checks on filesystems such as Docker Desktop VirtioFS where those two views can legitimately differ. Sidecars created by older releases have no token and retain the legacy identity-plus-content check.
63
+ Each new sidecar also carries an internal random ownership token encoded as JSON trailing whitespace. `JSON.parse()` and every payload callback still see exactly the caller-provided object. Only the process that successfully created the sidecar keeps that token as release authority; merely reading token-shaped bytes from disk does not enable this mode. Release compares the in-memory token and exact serialized bytes, and requires the pathname to remain a regular file, instead of requiring an opened descriptor and pathname lookup to report the same inode identity. This preserves ownership checks on filesystems such as Docker Desktop VirtioFS where those two views can legitimately differ. Sidecars created by older releases have no token and retain the legacy identity-plus-content check. If Windows reports a zero device or inode for either identity observation, that check is inconclusive and removal is skipped.
38
64
 
39
65
  The raw sidecar bytes are not a canonical JSON representation: tools that trim or rewrite the trailing whitespace invalidate the ownership token, so release leaves the changed sidecar in place and fails closed. The token distinguishes cooperating acquisitions; it is not a secret and does not make pathname compare-and-remove atomic against a hostile process that can replace files outside the lock protocol.
40
66
 
@@ -62,6 +88,41 @@ function withFileLockSync<T, TPayload>(targetPath: string, options: FileLockSync
62
88
 
63
89
  `managerKey` is an optional identifier used to keep state isolated across multiple lock domains in the same process. Use distinct keys for distinct domains (`"snapshot"`, `"compact"`, `"build"`). If omitted, fs-safe derives one from the target path.
64
90
 
91
+ Within one manager domain, the canonical target path is the in-process
92
+ arbitration key even when callers supply different explicit `lockPath` values.
93
+ Admission remains pending while payload serialization and stale-policy callbacks
94
+ run, and becomes reentrant only after a matching owner is fully published.
95
+ Foreign owners wait or reach the configured timeout without opening their
96
+ alternate sidecar. Each unguarded attempt still invokes and serializes `payload`
97
+ before a completed foreign holder consumes retry budget, preserving callback and
98
+ retry compatibility; the holder is rechecked before delayed option accessors or
99
+ sidecar I/O. When the candidate resolves to the holder's actual sidecar (the
100
+ default path or an explicitly identical path), asynchronous retries also preserve
101
+ the existing parser observation: bytes are observed first, then its accessor is
102
+ read and the current holder bytes are parsed once per unguarded attempt.
103
+ Distinct alternate sidecars do not trigger that observation. Async acquisition releases
104
+ pending admission before retry backoff;
105
+ synchronous callback reentry cannot let the active stack progress, so it fails
106
+ closed with the normal `file_lock_timeout` fields. Async payload, serialization,
107
+ delayed-option, and stale-policy callbacks carry a process-shared ancestry scope:
108
+ a nested acquisition of the same canonical target in the same manager domain
109
+ fails before waiting on its ancestor, while independent tasks, different targets,
110
+ and different manager domains retain their normal retry behavior. The synchronous
111
+ API uses one process-wide domain. An ancestry snapshot keeps each ancestor that
112
+ is active when the child acquisition starts, even if the current callback's own
113
+ scope already became inactive; later deactivation cannot reclassify that child.
114
+ Detached work started only after every matching ancestor has finished is not
115
+ retained as a descendant. Promise-like callback results are assimilated inside
116
+ that ancestry scope, and the resolved payload crosses the internal return
117
+ boundary in a non-thenable envelope. The helper therefore does not observe a
118
+ stateful payload `then` accessor again outside the reservation.
119
+
120
+ The pending-admission registry coordinates package copies that implement this
121
+ protocol without placing incomplete state in the legacy held-lock map. An older
122
+ already-loaded executable copy does not consult that registry, so a mixed-version
123
+ process cannot rely on the new in-process arbitration until every copy is updated
124
+ and the process is restarted.
125
+
65
126
  ## Acquire options
66
127
 
67
128
  ```ts
@@ -105,7 +166,11 @@ type FileLockRetryOptions = {
105
166
  };
106
167
  ```
107
168
 
108
- `payload` is a function so you can re-evaluate it on each retry (e.g. timestamp, PID).
169
+ `payload` is a function so you can re-evaluate it on each retry (e.g. timestamp,
170
+ PID). Callback-valued option accessors are otherwise captured once for an
171
+ acquisition; the asynchronous same-sidecar parser observation above reads
172
+ `parsePayload` once per unguarded attempt. Callback invocation keeps its
173
+ established receiver behavior.
109
174
 
110
175
  Asynchronous acquisition snapshots `targetPath`, an explicit `lockPath`, and
111
176
  `lockRoot` before its first asynchronous operation. Cwd-dependent path spellings
@@ -230,10 +295,11 @@ Discarding an acquisition observation is not proof that the pathname is absent:
230
295
  another owner may already have created the next record. Every discarded
231
296
  observation consumes the normal retry/deadline budget and requires fresh
232
297
  exclusive creation. It supplies no release, reclaim, or held-lock authority.
233
- If that successor disappears during the recovery metadata probe, the waiter
234
- may discard the probe only with an operation-local receipt for an admitted
235
- regular file with one link, followed by current Root and canonical ancestor
236
- checks. A generic metadata error does not permit this retry, and public
298
+ If that successor disappears or is replaced during the recovery metadata probe,
299
+ the waiter may discard the probe only with an operation-local receipt for an
300
+ admitted regular file with one link. Replacement also requires a single exact
301
+ observation of a different regular file with one link. Current Root and canonical
302
+ ancestor checks must still pass. A generic metadata error does not permit this retry, and public
237
303
  `Root.stat()` still rejects a file that changes during observation.
238
304
  Generic `Root.open()` and held-owner/reclaim reads still reject failed opens.
239
305
  Moving an already-matched pinned descriptor without unlinking it, unknown or
@@ -295,6 +361,46 @@ deadline budget. Errors from descriptor reads/stats or parsing are not treated
295
361
  as missing snapshots, even when their code is `ENOENT`. Held verification,
296
362
  release, and reclaim do not retry open denials.
297
363
 
364
+ Synchronous `lockRoot` is an authority boundary, not only a containment hint.
365
+ It requires a genuine `Root` returned by the same loaded package copy; a
366
+ structural/custom Root lookalike or a handle constructed by another installed
367
+ copy fails with `helper-unavailable` before any remaining acquisition option or
368
+ nested retry getter, payload evaluation, or filesystem effects. After reading
369
+ `lockRoot` once, the genuine Root and its policies are snapshotted before those
370
+ getters run. Construct `lockRoot` through the same import instance that provides
371
+ the synchronous lock function. The acquirer retains the original Root context,
372
+ exact root, parent, and file identities, and the Root's entry-time read, hardlink,
373
+ mutation-symlink, `denyMutations`, and `assertBeforeMutation` policies. Those
374
+ receipts remain authoritative through same-owner reuse, compromise checks,
375
+ reclaim, explicit release, and process-exit cleanup. If the Root, an admitted
376
+ parent, or the owned entry changes, cleanup leaves the ambiguous path in place.
377
+ Root-backed synchronous records and their exit handler use a separate versioned
378
+ global domain; legacy raw-lock handlers and legacy package copies cannot adopt or
379
+ pathname-delete those records. Root and raw acquisitions never share a
380
+ reentrant reference, even when their owner strings match.
381
+
382
+ As with asynchronous Root-backed acquisition, synchronous target normalization
383
+ does not create the target's parent. An explicit in-root `lockPath` can therefore
384
+ guard an external or not-yet-created target key without creating anything next
385
+ to that target. Missing directories for the sidecar itself are created one
386
+ component at a time through the retained Root policy; the returned `lockPath`
387
+ uses the admitted canonical spelling. This strengthens earlier synchronous
388
+ behavior that treated `lockRoot` as a one-time lexical/canonical bound and used
389
+ raw pathname operations afterward.
390
+
391
+ Windows Root-backed target keys use native existing-ancestor canonicalization,
392
+ so long and short spellings of the same target parent share an arbitration key.
393
+ Sidecar admission applies both the retained mutation policy and read/final-link
394
+ policy before payload evaluation; a dangling final sidecar link is rejected
395
+ without creating its target.
396
+
397
+ Exact Root, parent, and file receipts narrow replacement races but do not make a
398
+ pathname check and the following `open`, `mkdir`, `unlink`, or `rmdir` one atomic
399
+ filesystem operation. A hostile peer with direct write access can still race the
400
+ final syscall. An observed mismatch fails closed and ambiguous entries remain;
401
+ use OS-enforced directory permissions or a native descriptor-relative primitive
402
+ when that attacker model must be excluded.
403
+
298
404
  Both synchronous helpers consume the [process-wide lock defaults](config.md#configurefssafelocks-config).
299
405
  A synchronous retry sleep is clamped to the remaining finite deadline, so a long or jittered backoff cannot extend the configured timeout or block forever.
300
406
  Per-call options take precedence, including zero values; a per-call `retry`
@@ -65,6 +65,11 @@ interface StagedFile extends AsyncDisposable {
65
65
  Strings are UTF-8. `mode` is the requested **published** mode and defaults to
66
66
  `0600`; exact final modes, including `000`, are supported. The unpublished file
67
67
  stays at `0600` throughout preparation and any awaited application checks.
68
+ The retained descriptor's actual mode is checked before writing payload bytes,
69
+ by `assertCurrent()`, and before publication; a successful but ineffective
70
+ `chmod` fails with `insecure-permissions`. Final mode verification after
71
+ publication can fail with a `published` receipt while preserving the completed
72
+ file. These are POSIX mode checks, not ACL or ownership admission.
68
73
  After rename succeeds and the published entry passes identity validation, the
69
74
  owner applies the requested mode through its retained file descriptor. Content
70
75
  was synchronized during preparation; publication always synchronizes the parent.
@@ -105,9 +110,13 @@ touching descriptors.
105
110
  basename. Empty, dot, dotdot, separators, absolute paths, NUL, control characters,
106
111
  drive-relative spellings, and the stage's own name are rejected.
107
112
 
108
- With `overwrite: false`, publication is genuine kernel no-replace rename; a
109
- collision leaves both names unchanged and raises `FsSafeError("already-exists")`.
110
- The stage may then be cleaned or published under another name. With
113
+ With `overwrite: false`, publication is genuine kernel no-replace rename.
114
+ A native collision raises `FsSafeError("already-exists")`, but ordinary errno
115
+ does not prove that a remote rename never committed. Native rename failures
116
+ without explicit pre-dispatch provenance therefore report `indeterminate`:
117
+ cleanup preserves names and closes descriptors, and further publication rejects.
118
+ Only rejection before rename dispatch leaves the stage eligible for cleanup or
119
+ publication under another name. With
111
120
  `overwrite: true`, publication is plain atomic replacement. Neither route
112
121
  copies. Both source and destination resolve through the retained original
113
122
  parent, with checks immediately before rename and after publication.
package/docs/temp.md CHANGED
@@ -221,9 +221,15 @@ A missing workspace returns `"missing"`. A replacement observed at the public
221
221
  name before quarantine returns `"identity-mismatch"` when the parent is stable;
222
222
  an ambiguous parent returns `"indeterminate"`. After successful removal,
223
223
  repeated cleanup returns `"missing"` without touching a recreated public name.
224
- Other statuses remain stable. Operational removal errors propagate and later
225
- cleanup returns `"indeterminate"` without retrying. Disposal and scoped helpers
226
- ignore returned statuses, while manual cleanup exposes the result.
224
+ Other statuses remain stable. Compatible recursive-removal failures propagate
225
+ the exact thrown value, including `undefined`, `null`, `false`, positive or
226
+ negative numeric zero, bigint zero, an empty string, and `NaN`; they are never
227
+ inferred from value identity or truthiness. Uncertain quarantine and
228
+ retained-parent checks instead return
229
+ `"indeterminate"`. After a propagated removal failure, later cleanup returns
230
+ `"indeterminate"` without retrying. Disposal and scoped helpers ignore returned
231
+ statuses, while manual cleanup exposes the result. A terminal descriptor-close
232
+ failure retains its existing precedence if it also fails during settlement.
227
233
 
228
234
  When cleanup is part of a retention or audit decision, inspect the receipt
229
235
  instead of treating cleanup as fire-and-forget:
@@ -410,6 +416,12 @@ files, hardlinks, and changes between the pre-open pathname, opened descriptor,
410
416
  and current pathname are rejected. The callback must finish and close its
411
417
  writer before returning. Its return value is preserved as `result`.
412
418
 
419
+ Each option is read once before directory creation starts, including both
420
+ callbacks, the temp prefix, isolation, directory and file modes, and sync flags.
421
+ Later changes to the options object do not change the in-flight operation.
422
+ `writeTemp` and `resolveFinalPath` retain their shared internal staging object
423
+ as the callback receiver.
424
+
413
425
  Generated temp filenames suffix Windows reserved-device basenames on every
414
426
  platform. Before either an ordinary or isolated producer runs, the completed staging
415
427
  name must be a nonempty, non-dot path component with no POSIX or Windows
@@ -536,6 +548,11 @@ await writeViaSiblingTempPath({
536
548
  If `replaceFileAtomic` does what you need, prefer that. Use
537
549
  `writeViaSiblingTempPath` when the producer needs a concrete temp pathname but
538
550
  the final destination still needs root-boundary checks.
551
+
552
+ The root, target, callback, fallback filename, and temp prefix are read once
553
+ before setup. Later changes to the parameters do not affect the in-flight
554
+ operation; `writeTemp` retains the original parameters object as its receiver.
555
+
539
556
  Its private workspace uses `tempFile()`'s compatible identity-aware cleanup.
540
557
  It preserves replacements observed before removal, but retains the final
541
558
  pathname-recursive-removal gap described above; this helper does not expose