@openclaw/openshell-sandbox 2026.9.6 → 2026.9.7

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 (244) hide show
  1. package/assets/icon.png +0 -0
  2. package/dist/index.js +75 -229
  3. package/node_modules/@openclaw/fs-safe/CHANGELOG.md +102 -0
  4. package/node_modules/@openclaw/fs-safe/README.md +24 -6
  5. package/node_modules/@openclaw/fs-safe/dist/advanced.d.ts +6 -1
  6. package/node_modules/@openclaw/fs-safe/dist/advanced.js +3 -1
  7. package/node_modules/@openclaw/fs-safe/dist/archive-durability.js +1 -1
  8. package/node_modules/@openclaw/fs-safe/dist/archive-merge.js +1 -2
  9. package/node_modules/@openclaw/fs-safe/dist/archive-plan.d.ts +2 -7
  10. package/node_modules/@openclaw/fs-safe/dist/archive-read.js +9 -18
  11. package/node_modules/@openclaw/fs-safe/dist/archive-staging.d.ts +1 -2
  12. package/node_modules/@openclaw/fs-safe/dist/archive-staging.js +3 -3
  13. package/node_modules/@openclaw/fs-safe/dist/archive-zip-directory.js +4 -0
  14. package/node_modules/@openclaw/fs-safe/dist/archive-zip-entry.d.ts +11 -11
  15. package/node_modules/@openclaw/fs-safe/dist/archive-zip-entry.js +3 -35
  16. package/node_modules/@openclaw/fs-safe/dist/archive-zip-integrity.d.ts +2 -2
  17. package/node_modules/@openclaw/fs-safe/dist/archive-zip-integrity.js +2 -12
  18. package/node_modules/@openclaw/fs-safe/dist/archive-zip-loader.d.ts +7 -3
  19. package/node_modules/@openclaw/fs-safe/dist/archive-zip-loader.js +13 -10
  20. package/node_modules/@openclaw/fs-safe/dist/archive-zip-manifest.js +3 -1
  21. package/node_modules/@openclaw/fs-safe/dist/archive-zip-preflight.d.ts +2 -3
  22. package/node_modules/@openclaw/fs-safe/dist/archive-zip-preflight.js +16 -8
  23. package/node_modules/@openclaw/fs-safe/dist/archive.d.ts +6 -3
  24. package/node_modules/@openclaw/fs-safe/dist/archive.js +22 -19
  25. package/node_modules/@openclaw/fs-safe/dist/atomic.d.ts +1 -1
  26. package/node_modules/@openclaw/fs-safe/dist/copy-publication.d.ts +1 -3
  27. package/node_modules/@openclaw/fs-safe/dist/copy-publication.js +2 -6
  28. package/node_modules/@openclaw/fs-safe/dist/deny-mutation-match.d.ts +2 -0
  29. package/node_modules/@openclaw/fs-safe/dist/deny-mutation-match.js +71 -0
  30. package/node_modules/@openclaw/fs-safe/dist/deny-mutations.js +4 -3
  31. package/node_modules/@openclaw/fs-safe/dist/directory-guard.d.ts +0 -1
  32. package/node_modules/@openclaw/fs-safe/dist/directory-guard.js +0 -3
  33. package/node_modules/@openclaw/fs-safe/dist/directory-mode-node.d.ts +20 -2
  34. package/node_modules/@openclaw/fs-safe/dist/directory-mode-node.js +77 -1
  35. package/node_modules/@openclaw/fs-safe/dist/directory-receipt.js +5 -7
  36. package/node_modules/@openclaw/fs-safe/dist/effective-uid.js +1 -4
  37. package/node_modules/@openclaw/fs-safe/dist/errors.d.ts +3 -1
  38. package/node_modules/@openclaw/fs-safe/dist/errors.js +3 -2
  39. package/node_modules/@openclaw/fs-safe/dist/file-identity.js +10 -2
  40. package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-acquisition.js +1 -1
  41. package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-admission.js +5 -1
  42. package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root-held.d.ts +4 -11
  43. package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root-held.js +1 -4
  44. package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root-io.d.ts +3 -6
  45. package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root-io.js +1 -1
  46. package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root-options.d.ts +2 -10
  47. package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root.d.ts +2 -4
  48. package/node_modules/@openclaw/fs-safe/dist/file-lock-sync.js +2 -2
  49. package/node_modules/@openclaw/fs-safe/dist/file-store-boundary.d.ts +3 -7
  50. package/node_modules/@openclaw/fs-safe/dist/file-store-boundary.js +7 -10
  51. package/node_modules/@openclaw/fs-safe/dist/file-store-prune.js +7 -6
  52. package/node_modules/@openclaw/fs-safe/dist/file-store.d.ts +4 -7
  53. package/node_modules/@openclaw/fs-safe/dist/file-store.js +19 -21
  54. package/node_modules/@openclaw/fs-safe/dist/guest-native-python.js +23 -31
  55. package/node_modules/@openclaw/fs-safe/dist/guest.js +21 -12
  56. package/node_modules/@openclaw/fs-safe/dist/json-document-store.d.ts +4 -9
  57. package/node_modules/@openclaw/fs-safe/dist/json-durable-queue-ownership.d.ts +1 -5
  58. package/node_modules/@openclaw/fs-safe/dist/json-durable-queue.d.ts +0 -1
  59. package/node_modules/@openclaw/fs-safe/dist/json-durable-queue.js +2 -7
  60. package/node_modules/@openclaw/fs-safe/dist/local-file-access.js +2 -5
  61. package/node_modules/@openclaw/fs-safe/dist/local-file-descriptor.d.ts +2 -5
  62. package/node_modules/@openclaw/fs-safe/dist/local-roots.d.ts +2 -7
  63. package/node_modules/@openclaw/fs-safe/dist/move-path-cleanup.js +4 -4
  64. package/node_modules/@openclaw/fs-safe/dist/mutation-authority.js +5 -0
  65. package/node_modules/@openclaw/fs-safe/dist/native-binding.d.ts +51 -14
  66. package/node_modules/@openclaw/fs-safe/dist/native-pinned-write.js +8 -2
  67. package/node_modules/@openclaw/fs-safe/dist/native-staged-symlink.d.ts +13 -0
  68. package/node_modules/@openclaw/fs-safe/dist/native-staged-symlink.js +303 -0
  69. package/node_modules/@openclaw/fs-safe/dist/owner-dacl-batch-worker.d.ts +1 -0
  70. package/node_modules/@openclaw/fs-safe/dist/owner-dacl-batch-worker.js +54 -0
  71. package/node_modules/@openclaw/fs-safe/dist/owner-dacl-batch.d.ts +5 -0
  72. package/node_modules/@openclaw/fs-safe/dist/owner-dacl-batch.js +64 -0
  73. package/node_modules/@openclaw/fs-safe/dist/owner-dacl.d.ts +2 -0
  74. package/node_modules/@openclaw/fs-safe/dist/owner-dacl.js +3 -0
  75. package/node_modules/@openclaw/fs-safe/dist/path.js +17 -1
  76. package/node_modules/@openclaw/fs-safe/dist/permission-exec.d.ts +6 -0
  77. package/node_modules/@openclaw/fs-safe/dist/permission-exec.js +3 -6
  78. package/node_modules/@openclaw/fs-safe/dist/permissions-public.d.ts +3 -1
  79. package/node_modules/@openclaw/fs-safe/dist/permissions-public.js +1 -0
  80. package/node_modules/@openclaw/fs-safe/dist/permissions-windows.d.ts +3 -6
  81. package/node_modules/@openclaw/fs-safe/dist/permissions.d.ts +3 -10
  82. package/node_modules/@openclaw/fs-safe/dist/permissions.js +0 -1
  83. package/node_modules/@openclaw/fs-safe/dist/pinned-mutation-admission.d.ts +0 -1
  84. package/node_modules/@openclaw/fs-safe/dist/pinned-mutation-admission.js +4 -2
  85. package/node_modules/@openclaw/fs-safe/dist/pinned-mutation-shared-route.d.ts +2 -8
  86. package/node_modules/@openclaw/fs-safe/dist/pinned-open.d.ts +1 -1
  87. package/node_modules/@openclaw/fs-safe/dist/pinned-open.js +2 -3
  88. package/node_modules/@openclaw/fs-safe/dist/publish-copy-stage.js +4 -0
  89. package/node_modules/@openclaw/fs-safe/dist/read-opened-file.d.ts +2 -5
  90. package/node_modules/@openclaw/fs-safe/dist/regular-file.js +3 -3
  91. package/node_modules/@openclaw/fs-safe/dist/replace-directory.js +5 -17
  92. package/node_modules/@openclaw/fs-safe/dist/replace-file-buffer.d.ts +4 -0
  93. package/node_modules/@openclaw/fs-safe/dist/replace-file-buffer.js +36 -0
  94. package/node_modules/@openclaw/fs-safe/dist/replace-file-copy-fallback.d.ts +4 -10
  95. package/node_modules/@openclaw/fs-safe/dist/replace-file-copy-fallback.js +66 -38
  96. package/node_modules/@openclaw/fs-safe/dist/replace-file-descriptor.d.ts +7 -11
  97. package/node_modules/@openclaw/fs-safe/dist/replace-file-descriptor.js +10 -2
  98. package/node_modules/@openclaw/fs-safe/dist/replace-file-destination.d.ts +21 -0
  99. package/node_modules/@openclaw/fs-safe/dist/replace-file-destination.js +123 -0
  100. package/node_modules/@openclaw/fs-safe/dist/replace-file-mutation.d.ts +26 -0
  101. package/node_modules/@openclaw/fs-safe/dist/replace-file-mutation.js +47 -0
  102. package/node_modules/@openclaw/fs-safe/dist/replace-file-temp-owner.d.ts +2 -2
  103. package/node_modules/@openclaw/fs-safe/dist/replace-file-temp-owner.js +16 -4
  104. package/node_modules/@openclaw/fs-safe/dist/replace-file-types.d.ts +55 -0
  105. package/node_modules/@openclaw/fs-safe/dist/replace-file-types.js +1 -0
  106. package/node_modules/@openclaw/fs-safe/dist/replace-file.d.ts +3 -55
  107. package/node_modules/@openclaw/fs-safe/dist/replace-file.js +31 -11
  108. package/node_modules/@openclaw/fs-safe/dist/retained-directory-replacement.d.ts +1 -0
  109. package/node_modules/@openclaw/fs-safe/dist/retained-directory-replacement.js +1 -1
  110. package/node_modules/@openclaw/fs-safe/dist/retained-file-types.d.ts +50 -0
  111. package/node_modules/@openclaw/fs-safe/dist/retained-file-types.js +1 -0
  112. package/node_modules/@openclaw/fs-safe/dist/retained-file.d.ts +3 -0
  113. package/node_modules/@openclaw/fs-safe/dist/retained-file.js +121 -0
  114. package/node_modules/@openclaw/fs-safe/dist/root-context.js +3 -2
  115. package/node_modules/@openclaw/fs-safe/dist/root-directory-entry.d.ts +9 -0
  116. package/node_modules/@openclaw/fs-safe/dist/root-directory-entry.js +28 -0
  117. package/node_modules/@openclaw/fs-safe/dist/root-directory-list.d.ts +9 -1
  118. package/node_modules/@openclaw/fs-safe/dist/root-directory-list.js +88 -37
  119. package/node_modules/@openclaw/fs-safe/dist/root-file-final-admission.d.ts +1 -1
  120. package/node_modules/@openclaw/fs-safe/dist/root-file-final-admission.js +1 -6
  121. package/node_modules/@openclaw/fs-safe/dist/root-file.js +2 -5
  122. package/node_modules/@openclaw/fs-safe/dist/root-handle-context.d.ts +4 -0
  123. package/node_modules/@openclaw/fs-safe/dist/root-handle-context.js +12 -0
  124. package/node_modules/@openclaw/fs-safe/dist/root-impl.d.ts +3 -3
  125. package/node_modules/@openclaw/fs-safe/dist/root-impl.js +8 -5
  126. package/node_modules/@openclaw/fs-safe/dist/root-move-noreplace.d.ts +2 -7
  127. package/node_modules/@openclaw/fs-safe/dist/root-move-noreplace.js +3 -3
  128. package/node_modules/@openclaw/fs-safe/dist/root-observed-path.d.ts +0 -1
  129. package/node_modules/@openclaw/fs-safe/dist/root-observed-path.js +0 -3
  130. package/node_modules/@openclaw/fs-safe/dist/root-path-observation.d.ts +4 -11
  131. package/node_modules/@openclaw/fs-safe/dist/root-path.js +7 -10
  132. package/node_modules/@openclaw/fs-safe/dist/root-paths.d.ts +2 -6
  133. package/node_modules/@openclaw/fs-safe/dist/root-remove-identity.d.ts +1 -3
  134. package/node_modules/@openclaw/fs-safe/dist/root-walk.d.ts +19 -12
  135. package/node_modules/@openclaw/fs-safe/dist/root-walk.js +56 -20
  136. package/node_modules/@openclaw/fs-safe/dist/root-write-admission.js +1 -6
  137. package/node_modules/@openclaw/fs-safe/dist/root-write-complete-parent.d.ts +2 -0
  138. package/node_modules/@openclaw/fs-safe/dist/root-write-complete-parent.js +1 -1
  139. package/node_modules/@openclaw/fs-safe/dist/safe-path-segment.d.ts +1 -0
  140. package/node_modules/@openclaw/fs-safe/dist/safe-path-segment.js +8 -2
  141. package/node_modules/@openclaw/fs-safe/dist/secret-file.d.ts +6 -2
  142. package/node_modules/@openclaw/fs-safe/dist/secret-file.js +12 -16
  143. package/node_modules/@openclaw/fs-safe/dist/secret-read-policy.js +1 -1
  144. package/node_modules/@openclaw/fs-safe/dist/secure-file-windows.js +5 -14
  145. package/node_modules/@openclaw/fs-safe/dist/secure-file.js +3 -2
  146. package/node_modules/@openclaw/fs-safe/dist/secure-temp-dir.js +2 -7
  147. package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-acquire.js +4 -4
  148. package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-admission-parser.d.ts +1 -2
  149. package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-handle.d.ts +2 -8
  150. package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-policy.d.ts +2 -7
  151. package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-reclaim.d.ts +2 -2
  152. package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-reclaim.js +11 -12
  153. package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-stale-admission.d.ts +1 -5
  154. package/node_modules/@openclaw/fs-safe/dist/sidecar-lock.js +9 -7
  155. package/node_modules/@openclaw/fs-safe/dist/staged-symlink-types.d.ts +39 -0
  156. package/node_modules/@openclaw/fs-safe/dist/staged-symlink-types.js +1 -0
  157. package/node_modules/@openclaw/fs-safe/dist/stat-observation.js +3 -10
  158. package/node_modules/@openclaw/fs-safe/dist/strict-file-identity.d.ts +2 -0
  159. package/node_modules/@openclaw/fs-safe/dist/strict-file-identity.js +6 -6
  160. package/node_modules/@openclaw/fs-safe/dist/symlink-parents.js +58 -7
  161. package/node_modules/@openclaw/fs-safe/dist/temp-target.js +8 -10
  162. package/node_modules/@openclaw/fs-safe/dist/temp-workspace-admission.js +28 -33
  163. package/node_modules/@openclaw/fs-safe/dist/temp-workspace-child-admission.d.ts +1 -1
  164. package/node_modules/@openclaw/fs-safe/dist/temp-workspace-child-admission.js +16 -17
  165. package/node_modules/@openclaw/fs-safe/dist/temp-workspace-descriptor.js +1 -2
  166. package/node_modules/@openclaw/fs-safe/dist/temp-workspace-owner.js +4 -9
  167. package/node_modules/@openclaw/fs-safe/dist/temp-workspace-ownership.d.ts +8 -0
  168. package/node_modules/@openclaw/fs-safe/dist/temp-workspace-ownership.js +52 -0
  169. package/node_modules/@openclaw/fs-safe/dist/test-hooks.d.ts +5 -1
  170. package/node_modules/@openclaw/fs-safe/dist/text-atomic.d.ts +2 -1
  171. package/node_modules/@openclaw/fs-safe/dist/text-atomic.js +2 -0
  172. package/node_modules/@openclaw/fs-safe/dist/trash.js +27 -1
  173. package/node_modules/@openclaw/fs-safe/dist/walk.d.ts +2 -5
  174. package/node_modules/@openclaw/fs-safe/dist/watch-alias.d.ts +6 -0
  175. package/node_modules/@openclaw/fs-safe/dist/watch-alias.js +88 -0
  176. package/node_modules/@openclaw/fs-safe/dist/watch-hints.d.ts +9 -0
  177. package/node_modules/@openclaw/fs-safe/dist/watch-hints.js +93 -0
  178. package/node_modules/@openclaw/fs-safe/dist/watch-native.d.ts +36 -0
  179. package/node_modules/@openclaw/fs-safe/dist/watch-native.js +73 -0
  180. package/node_modules/@openclaw/fs-safe/dist/watch-scan.d.ts +28 -0
  181. package/node_modules/@openclaw/fs-safe/dist/watch-scan.js +300 -0
  182. package/node_modules/@openclaw/fs-safe/dist/watch-stream.d.ts +8 -0
  183. package/node_modules/@openclaw/fs-safe/dist/watch-stream.js +32 -0
  184. package/node_modules/@openclaw/fs-safe/dist/watch-types.d.ts +60 -0
  185. package/node_modules/@openclaw/fs-safe/dist/watch-types.js +1 -0
  186. package/node_modules/@openclaw/fs-safe/dist/watch.d.ts +5 -0
  187. package/node_modules/@openclaw/fs-safe/dist/watch.js +530 -0
  188. package/node_modules/@openclaw/fs-safe/dist/windows-owner.d.ts +3 -7
  189. package/node_modules/@openclaw/fs-safe/dist/windows-owner.js +0 -1
  190. package/node_modules/@openclaw/fs-safe/dist/windows-security-bridge.cs +6 -6
  191. package/node_modules/@openclaw/fs-safe/dist/windows-security-bridge.ps1 +78 -3
  192. package/node_modules/@openclaw/fs-safe/dist/windows-security-command.d.ts +8 -0
  193. package/node_modules/@openclaw/fs-safe/dist/windows-security-command.js +66 -15
  194. package/node_modules/@openclaw/fs-safe/dist/windows-security-facts.d.ts +4 -0
  195. package/node_modules/@openclaw/fs-safe/dist/windows-security-facts.js +6 -2
  196. package/node_modules/@openclaw/fs-safe/docs/advanced.md +4 -2
  197. package/node_modules/@openclaw/fs-safe/docs/archive.md +16 -0
  198. package/node_modules/@openclaw/fs-safe/docs/atomic.md +93 -3
  199. package/node_modules/@openclaw/fs-safe/docs/contributing.md +102 -4
  200. package/node_modules/@openclaw/fs-safe/docs/copy.md +2 -0
  201. package/node_modules/@openclaw/fs-safe/docs/durability.md +7 -0
  202. package/node_modules/@openclaw/fs-safe/docs/file-store.md +5 -0
  203. package/node_modules/@openclaw/fs-safe/docs/guest.md +7 -1
  204. package/node_modules/@openclaw/fs-safe/docs/index.md +1 -0
  205. package/node_modules/@openclaw/fs-safe/docs/install.md +30 -0
  206. package/node_modules/@openclaw/fs-safe/docs/native-helper.md +20 -4
  207. package/node_modules/@openclaw/fs-safe/docs/native.md +63 -1
  208. package/node_modules/@openclaw/fs-safe/docs/permissions.md +66 -0
  209. package/node_modules/@openclaw/fs-safe/docs/public-api.md +30 -3
  210. package/node_modules/@openclaw/fs-safe/docs/retained-file.md +115 -0
  211. package/node_modules/@openclaw/fs-safe/docs/root.md +35 -9
  212. package/node_modules/@openclaw/fs-safe/docs/secret-file.md +19 -0
  213. package/node_modules/@openclaw/fs-safe/docs/security-model.md +4 -1
  214. package/node_modules/@openclaw/fs-safe/docs/sidecar-lock.md +5 -1
  215. package/node_modules/@openclaw/fs-safe/docs/staged-symlink.md +124 -0
  216. package/node_modules/@openclaw/fs-safe/docs/store.md +3 -1
  217. package/node_modules/@openclaw/fs-safe/docs/temp.md +24 -4
  218. package/node_modules/@openclaw/fs-safe/docs/testing.md +210 -0
  219. package/node_modules/@openclaw/fs-safe/docs/types.md +6 -0
  220. package/node_modules/@openclaw/fs-safe/docs/walk.md +34 -1
  221. package/node_modules/@openclaw/fs-safe/docs/watch.md +251 -0
  222. package/node_modules/@openclaw/fs-safe/docs/writing.md +21 -0
  223. package/node_modules/@openclaw/fs-safe/package.json +13 -9
  224. package/node_modules/@openclaw/fs-safe-darwin-arm64/fs-safe-native.node +0 -0
  225. package/node_modules/@openclaw/fs-safe-darwin-arm64/package.json +1 -1
  226. package/node_modules/@openclaw/fs-safe-darwin-x64/fs-safe-native.node +0 -0
  227. package/node_modules/@openclaw/fs-safe-darwin-x64/package.json +1 -1
  228. package/node_modules/@openclaw/fs-safe-linux-arm64-gnu/fs-safe-native.node +0 -0
  229. package/node_modules/@openclaw/fs-safe-linux-arm64-gnu/package.json +1 -1
  230. package/node_modules/@openclaw/fs-safe-linux-arm64-musl/fs-safe-native.node +0 -0
  231. package/node_modules/@openclaw/fs-safe-linux-arm64-musl/package.json +1 -1
  232. package/node_modules/@openclaw/fs-safe-linux-x64-gnu/fs-safe-native.node +0 -0
  233. package/node_modules/@openclaw/fs-safe-linux-x64-gnu/package.json +1 -1
  234. package/node_modules/@openclaw/fs-safe-linux-x64-musl/fs-safe-native.node +0 -0
  235. package/node_modules/@openclaw/fs-safe-linux-x64-musl/package.json +1 -1
  236. package/node_modules/@openclaw/fs-safe-win32-x64-msvc/fs-safe-native.node +0 -0
  237. package/node_modules/@openclaw/fs-safe-win32-x64-msvc/package.json +1 -1
  238. package/node_modules/p-limit/index.d.ts +2 -0
  239. package/node_modules/p-limit/index.js +17 -1
  240. package/node_modules/p-limit/package.json +1 -1
  241. package/node_modules/p-limit/readme.md +2 -0
  242. package/package.json +6 -6
  243. package/node_modules/@openclaw/fs-safe/dist/directory-mode-owner.d.ts +0 -20
  244. package/node_modules/@openclaw/fs-safe/dist/directory-mode-owner.js +0 -78
@@ -130,10 +130,22 @@ features, including strict owned-tree temp cleanup, retained-directory staging,
130
130
  and atomic `rename-noreplace` (including the default no-clobber `Root.move()`),
131
131
  remain unavailable. Operations without a safe fallback fail with `helper-unavailable`
132
132
  when the matching package is absent, incompatible, or disabled.
133
+ No-clobber `Root.move()` preserves a native loader failure in the error's
134
+ `cause`, including the original missing-library or incompatible-glibc diagnostic.
133
135
 
134
136
  Upgrading an existing 0.5 consumer? Follow [Migrating to 0.6](migrating-to-0.6.md)
135
137
  before deploying with native mode `require` or native-only features.
136
138
 
139
+ ### Older Linux kernels and seccomp
140
+
141
+ The native addon supports kernels without `openat2` (before Linux 5.6) and
142
+ containers returning `ENOSYS` or probe-time `EPERM` for that syscall. Beneath
143
+ opens use a guarded no-follow component walk and report `best-effort`.
144
+ Nested no-clobber moves retain atomic `renameat2(RENAME_NOREPLACE)` and exact
145
+ identity checks; keep native mode enabled. Strict bounded cleanup still
146
+ requires `openat2` with `RESOLVE_NO_XDEV` and reports `helper-unavailable`
147
+ without it. See [Linux capability behavior and limits](native.md#linux-without-openat2).
148
+
137
149
  ### Windows security fallback
138
150
 
139
151
  Windows raw owner/DACL inspection, private-directory creation, and secure-file
@@ -161,6 +173,24 @@ available native operation's error never triggers a command retry. See
161
173
 
162
174
  ## Native helper policy
163
175
 
176
+ ### Supported native platforms
177
+
178
+ Prebuilt bindings cover Linux x64/arm64 (GNU glibc **2.28 or newer**, or musl),
179
+ macOS x64/arm64, and Windows x64. The GNU baseline includes RHEL 8, Rocky Linux 8,
180
+ and AlmaLinux 8. A compatible Node 22+ runtime and the kernel/filesystem features
181
+ required by each operation are still necessary; a loadable addon alone does not
182
+ guarantee every native capability. Systems older than glibc 2.28 are outside the
183
+ GNU binary support floor.
184
+
185
+ A missing or incompatible optional binding (including `ERR_DLOPEN_FAILED` from
186
+ glibc) does not prevent importing fs-safe. In `auto`, supported JavaScript/WASM
187
+ fallbacks remain available. Native-only operations such as the default
188
+ no-clobber `Root.move()` still fail closed with `helper-unavailable`; `require`
189
+ also rejects fallback-capable operations and retains the binding load error as
190
+ the cause.
191
+
192
+ ### Loading modes
193
+
164
194
  The platform native binaries provide fd-relative open/link/mkdir primitives,
165
195
  atomic no-replace rename, and file identity checks. The default is `auto`: use
166
196
  the matching binary when it loads, otherwise use the guarded JavaScript path
@@ -103,10 +103,16 @@ clone/copy/hash workers, POSIX canonicalization, and Windows security descriptor
103
103
  layer owns policy, retries, filters, budgets, modes, cleanup, error
104
104
  normalization, and the decision to fall back.
105
105
 
106
- - Linux uses `openat2` with `RESOLVE_BENEATH | RESOLVE_NO_MAGICLINKS`, `renameat`, and `renameat2(RENAME_NOREPLACE)`. Direct-child no-replace renames borrow already-retained parent descriptors; deeper relative paths retain the guarded reopen. Owned-tree cleanup enumerates and unlinks through retained directory descriptors and rejects device crossings.
106
+ - Linux uses `openat2` with `RESOLVE_BENEATH | RESOLVE_NO_MAGICLINKS`, `renameat`, and `renameat2(RENAME_NOREPLACE)`. Without `openat2`, beneath opens use the [no-follow component walk](native.md#linux-without-openat2) with exact identity checks and report `best-effort`. Direct-child no-replace renames borrow already-retained parent descriptors; deeper relative paths retain the guarded reopen. Owned-tree cleanup still requires `openat2` with `RESOLVE_NO_XDEV` and fails closed when unavailable.
107
107
  - macOS 15.4 and newer prefer `O_RESOLVE_BENEATH`; older kernels resolve components with `O_NOFOLLOW` and restart in-root symlinks from the pinned root descriptor. Both routes use an `F_GETPATH` post-open escape detector and report `best-effort` because directory rename races are not atomic with that check. Direct-child no-replace renames borrow already-retained parents without another `F_GETPATH`; deeper paths retain the guarded reopen. Publication uses `renameat` for replacement and `renameatx_np(RENAME_EXCL)` for no-replace; owned-tree cleanup uses descriptor-relative `openat`/`unlinkat`.
108
108
  - Windows uses handle-relative `NtCreateFile`, rejects reparse points during root-bounded traversal, and uses `FileRenameInfoEx` with replacement selected explicitly by the TypeScript policy layer. The dedicated retained-directory `renameNoReplaceWithIdentity` primitive compares its required exact source receipt with the handle opened for rename before mutation. Its internal mismatch status is normalized to public `path-mismatch`; the legacy four-argument `renameNoReplace` export and its other callers are unchanged. Owned trees are deleted through exact opened handles with `FileDispositionInfoEx`; symlink/reparse entries in owned trees are removed as leaves and never traversed. Descriptors crossing N-API are converted only by the host executable's paired `uv_get_osfhandle` and `uv_open_osfhandle` exports. A runtime without both exports is unsupported for these native operations; the binding never guesses a raw HANDLE or uses a foreign CRT descriptor table. Descriptor-producing operations also require that same host's synchronous libuv close and request-management APIs before exporting an owned descriptor.
109
109
 
110
+ Linux root lookups reject negative descriptor sentinels before borrowing a handle or resolving a relative path; they never substitute the process working directory for an admitted root. Public Root operations already supply retained, admitted handles.
111
+
112
+ On Linux and macOS, native asynchronous file-copy admission rejects negative source and parent descriptors before creating a stage, preserving source-before-parent error ordering. Callers must keep nonnegative source and parent descriptors open until the operation settles.
113
+
114
+ Low-level Unix query, hash, copy, clone, staging, and owned-tree cleanup calls also reject negative descriptors before using them, preserving each operation's path-validation, cancellation, and cleanup order. Descriptor-relative mutations never accept a working-directory sentinel as a retained capability. On modern macOS, beneath opens reject negative roots with `EBADF` before calling `openat`, rather than returning `EIO` after an OS failure or working-directory operation. This check does not establish the validity of arbitrary nonnegative integers: callers must supply live descriptors and retain them until synchronous calls return or asynchronous operations settle.
115
+
110
116
  `replaceDirectoryAtomic()` requires `renameNoReplaceWithIdentity` before it
111
117
  creates a missing target parent. On POSIX the dedicated entry point keeps the
112
118
  existing pre-dispatch exact receipt fence but dispatches direct-child names
@@ -133,11 +139,12 @@ for the exact difference.
133
139
  The guarded JavaScript mutation path is detection-based, not containment-atomic.
134
140
  If a same-privilege peer can replace a writable parent after its identity guard
135
141
  but before Node resolves a pathname mutation, the mutation can land outside the
136
- intended root before the post-operation guard throws. Select `require` rather
137
- than `auto` or `off` when that concurrent attacker is part of the threat model.
142
+ intended root before the post-operation guard throws. Native `require` ensures the addon is present, but does not require a
143
+ `kernel-atomic` resolver; inspect containment and use OS isolation when that
144
+ concurrent attacker is part of the threat model.
138
145
 
139
146
  `openBeneath()` returns `{ fd, containment }`. `containment` is
140
- `"kernel-atomic"` for Linux `openat2` and `"best-effort"` for macOS and
147
+ `"kernel-atomic"` for Linux `openat2` and `"best-effort"` for the Linux fallback, macOS and
141
148
  Windows. Public JavaScript root open/read/writable results also expose the
142
149
  field and report `"best-effort"`; the label reports mechanism, not policy.
143
150
 
@@ -175,3 +182,12 @@ consumer performs its 0.5 upgrade.
175
182
  - [Durability](durability.md)
176
183
  - [Migrating to 0.5](migrating-to-0.5.md)
177
184
  - [Migrating to 0.6](migrating-to-0.6.md)
185
+
186
+ ### Retained existing Windows file
187
+
188
+ The maintained Windows helper supports the public
189
+ [`retainFileInDirectory`](retained-file.md) lifecycle on fixed local NTFS.
190
+ Private native handles, exact identity/generation checks, writable-section
191
+ admission and explicit close results stay behind that public API. Older helpers
192
+ without this capability are unsupported; there is no pathname deletion fallback.
193
+ No Windows namespace persistence barrier is provided.
@@ -43,6 +43,7 @@ whether a path, archive entry, mode, owner, or cleanup policy is acceptable.
43
43
 
44
44
  - Linux uses `openat2(RESOLVE_BENEATH | RESOLVE_NO_MAGICLINKS)`, fd-relative
45
45
  `mkdirat`/`linkat`/`renameat`/`renameat2`, `FICLONE`, and `copy_file_range`.
46
+ Without `openat2`, beneath opens use the [guarded fallback](#linux-without-openat2).
46
47
  - macOS 15.4 and newer first use `openat(O_RESOLVE_BENEATH)`; older kernels walk
47
48
  components with `openat(O_NOFOLLOW)` and restart in-root symlinks from the
48
49
  pinned root. Both routes apply an `F_GETPATH` post-open containment detector,
@@ -85,6 +86,67 @@ privately owned empty ACL. Unsupported, malformed, and failed inspection is not
85
86
  reported as absence. These facts do not classify individual ACE permissions or
86
87
  prove volume ownership enforcement; each caller applies its own security policy.
87
88
 
89
+ ## Linux without openat2
90
+
91
+ Linux kernels before 5.6 and containers whose seccomp policy denies `openat2`
92
+ can keep native mode `auto` or `require`. The addon caches one harmless
93
+ `openat2(".")` capability probe per process. `ENOSYS`, or `EPERM` on that probe,
94
+ selects the native `openat` fallback. An `EPERM`/`EACCES` from an application
95
+ operation is still a permission error and never triggers a retry. Install
96
+ syscall filters before the first native operation; a later `ENOSYS` fails with
97
+ `ENOTSUP`, rather than changing the cached mechanism during a call.
98
+
99
+ The fallback inspects components with `O_PATH | O_NOFOLLOW`, follows relative
100
+ symlink targets using a stack of retained directory descriptors, and rejects
101
+ absolute targets or `..` past the retained root with `EXDEV`. It permits at most
102
+ 40 link expansions (`ELOOP` beyond that limit). Final `O_NOFOLLOW` and exclusive
103
+ creation retain their syscall semantics, including opening the link itself with
104
+ `O_PATH | O_NOFOLLOW` and creating through a dangling in-root relative link.
105
+ Exact device/inode/type identities are checked before and after the final
106
+ no-follow open; followed links also retain their descriptors and have their
107
+ named identities and target strings rechecked. Detected replacements fail with
108
+ `EXDEV`, including changes to directories left behind by a link's `..` target.
109
+
110
+ The fallback also honors `nosymfollow` mount restrictions on retained links.
111
+ Two conservative restrictions remain: the fallback refuses to follow **any
112
+ procfs symlink** with `ELOOP`, including ordinary links such as `/proc/mounts`.
113
+ Userspace metadata cannot distinguish these from procfs magic links, which
114
+ `RESOLVE_NO_MAGICLINKS` must never follow. Opening a final link itself with
115
+ `O_PATH | O_NOFOLLOW` remains allowed. In a sticky, world-writable directory,
116
+ the fallback refuses all symlink following with `EACCES`, even when the calling
117
+ thread or directory owner owns the link, or `fs.protected_symlinks=0`. This
118
+ preserves Linux's protected-symlink restriction without assuming the thread's
119
+ filesystem UID or treating equal mapped `stat` UIDs as proof of equal kernel
120
+ owners (distinct unmapped owners can both appear as the overflow UID).
121
+
122
+ Public Root policy and canonical-path admission, hardlink rejection, pinned-file checks, and mutation
123
+ identity fences remain in place. `openBeneath()` reports `best-effort`: these
124
+ identity samples detect replacements but cannot make a multi-component walk
125
+ atomic against a hostile process renaming directories between samples. This
126
+ is the same documented containment class as the macOS and JavaScript paths;
127
+ applications requiring atomic beneath resolution must check the result or use
128
+ OS isolation. Rejection after a mutating open does not promise rollback.
129
+
130
+ Nested no-clobber `Root.move()` still admits both parents and uses
131
+ `renameat2(RENAME_NOREPLACE)`. Existing destinations are never overwritten.
132
+ That separate syscall/filesystem capability remains required; if unavailable,
133
+ the operation fails with `helper-unavailable`. Turning native mode `off` still
134
+ disables no-clobber moves because Node has no equivalent atomic rename API.
135
+
136
+ Bounded owned-tree cleanup deliberately has no `openat` fallback:
137
+ `RESOLVE_NO_XDEV` rejects bind mounts even when device numbers match, which
138
+ ordinary identity checks cannot reproduce. `cleanupSafety: "require-bounded"`
139
+ fails before workspace creation with `helper-unavailable`; low-level cleanup
140
+ opens report `ENOTSUP`. Compatible cleanup retains its documented behavior.
141
+ Low-level `O_TMPFILE` anonymous opens also fail before creation with `ENOTSUP`
142
+ in the fallback, because named-entry identity checks cannot verify an unnamed
143
+ file. Public staged-write APIs use exclusive named files and remain available.
144
+
145
+ For tests, set `FS_SAFE_TEST_NO_OPENAT2=1` before starting Node to force the
146
+ fallback (including bounded-cleanup refusal). It is read only at the first
147
+ capability probe. It has no effect on macOS or Windows.
148
+ See [Linux fallback testing](testing.md#linux-openat2-fallback).
149
+
88
150
  ## Archives
89
151
 
90
152
  Native ZIP and TAR entry reads retain a private, unpooled input Buffer in
@@ -252,7 +314,7 @@ See [Root containment guarantees](security-model.md#containment-guarantees-by-pl
252
314
 
253
315
  | Capability | Native path | Guarded JavaScript path |
254
316
  |---|---|---|
255
- | Native beneath opens and Root mutations | Descriptor-relative beneath operations. Pinned writes create parents and publish both replacement and no-replace targets relative to open directory descriptors. No-clobber `Root.move()` admits both parents and uses the native no-replace rename. Native `openBeneath()` reports `kernel-atomic` on Linux and `best-effort` on macOS and Windows. macOS uses `O_RESOLVE_BENEATH` when available plus an `F_GETPATH` detector, while Windows rejects reparse traversal in the object-manager call. | Reports `best-effort`: component-wise alias checks, no-follow opens where Node exposes them, private temp/rename, and post-operation identity verification. No-clobber `Root.move()` is unsupported because a check followed by a replacing rename is unsafe. A same-privilege peer can replace a writable parent after a guard assertion but before Node resolves another pathname mutation; the mutation may land outside the intended root before the post-check detects it. |
317
+ | Native beneath opens and Root mutations | Descriptor-relative beneath operations. Pinned writes create parents and publish both replacement and no-replace targets relative to open directory descriptors. No-clobber `Root.move()` admits both parents and uses the native no-replace rename. Native `openBeneath()` reports `kernel-atomic` with Linux `openat2` and `best-effort` with the guarded Linux fallback, macOS, and Windows. macOS uses `O_RESOLVE_BENEATH` when available plus an `F_GETPATH` detector, while Windows rejects reparse traversal in the object-manager call. | Reports `best-effort`: component-wise alias checks, no-follow opens where Node exposes them, private temp/rename, and post-operation identity verification. No-clobber `Root.move()` is unsupported because a check followed by a replacing rename is unsafe. A same-privilege peer can replace a writable parent after a guard assertion but before Node resolves another pathname mutation; the mutation may land outside the intended root before the post-check detects it. |
256
318
  | ZIP/TAR/gzip | Rust streaming decode and fd-relative output creation. | Optional JSZip or bundled WASM TAR into guarded private staging, then the same guarded merge policy. |
257
319
  | Zstd/bzip2 TAR | Rust streaming decode and fd-relative output creation. | Bundled WASM codecs feed the shared Rust TAR parser, then guarded private staging and the same merge policy; no optional codec dependency. |
258
320
  | Publication copy | Clone, Linux `copy_file_range`, async native SHA-256. | Exclusive `wx` byte loop and Node SHA-256 with the same content/identity fences. |
@@ -185,6 +185,72 @@ the same raw ACE projection. Native `require` rejects either absence with
185
185
  query's failure is terminal. The existing coarse `inspectPathPermissions()` API
186
186
  still owns its compatibility fallback and trust classification.
187
187
 
188
+ ### Asynchronous batches
189
+
190
+ Use `readOwnerAndDaclBatch()` when several paths, such as a directory and its
191
+ ancestors, need inspection without blocking the caller's event loop:
192
+
193
+ ```ts
194
+ import { readOwnerAndDaclBatch } from "@openclaw/fs-safe/permissions";
195
+
196
+ const facts = await readOwnerAndDaclBatch(stagingDirectories, { timeoutMs: 60_000 });
197
+ ```
198
+
199
+ ```ts
200
+ function readOwnerAndDaclBatch(
201
+ paths: readonly string[],
202
+ options?: { timeoutMs?: number },
203
+ ): Promise<OwnerAndDaclResult[]>;
204
+ ```
205
+
206
+ Results use the same raw fact shape and SID spelling as `readOwnerAndDacl()`.
207
+ They correspond one-for-one to input order, including duplicate paths. An empty
208
+ array returns `[]` without dispatch. Other platforms return an
209
+ `unsupported-platform` result for each path. Query failures, malformed facts,
210
+ or missing/reordered response rows reject the entire batch; no partial facts
211
+ are returned. Null DACLs, incomplete ACE lists and nonlocal observations remain
212
+ raw facts for the caller's policy to evaluate.
213
+
214
+ The call captures paths, options and native mode before awaiting. Windows
215
+ relative paths are anchored to the current directory or selected drive at
216
+ entry, without normalizing their remaining `.` or `..` components. Empty,
217
+ nonstring, sparse or NUL-containing path entries and Windows namespace aliases
218
+ reject before dispatch. The UTF-8 JSON input is limited to 16 MiB.
219
+
220
+ An available native capability runs all queries in one isolated process using
221
+ the current runtime executable. Its resolved native mode is forwarded explicitly;
222
+ Node preload and module-search environment overrides are not inherited. An
223
+ available native query's failure is terminal. In `auto` when the binding or
224
+ capability is absent, or in `off`, one packaged PowerShell process reads the
225
+ path array from stdin and compiles the existing C# bridge once. Native `require`
226
+ rejects missing support before launching a process. No route launches one
227
+ process per path or wraps synchronous parent-process queries in promises.
228
+
229
+ `timeoutMs` defaults to 60,000 and applies to the whole process, including
230
+ startup and fallback compilation. It must be an integer from 1 through
231
+ 2,147,483,647. Combined stdout and stderr are limited to 16 MiB. Success waits
232
+ for process exit and both output pipes to close. Timeout and transport failure
233
+ request termination, then retain the existing one-second settlement grace.
234
+ The error's `processExitConfirmed` field, also retained in its cause receipt,
235
+ distinguishes observed exit from an unconfirmed termination attempt. An
236
+ unconfirmed child can still be reading after rejection; the operation returns
237
+ no facts and owns no output files or artifact cleanup. Neither timing out nor
238
+ receiving a successful kill request is reported as confirmed exit.
239
+
240
+ The PowerShell fallback encodes and budgets each result while collecting it,
241
+ instead of retaining every descriptor graph until the entire batch finishes.
242
+ An encoded response that would exceed 16 MiB rejects with `too-large` before
243
+ inspecting later paths. A query failure observed earlier retains its original
244
+ error, and no partial facts are returned. Duplicate paths remain independent
245
+ observations. This bounds accumulated encoded results, not total process memory:
246
+ the current descriptor and row, decoded input, and runtime overhead still exist.
247
+
248
+ Batch observations are point-in-time pathname facts, not a snapshot or a
249
+ retained filesystem capability. The caller still owns ancestry trust, principal
250
+ policy and authorization for subsequent operations. Existing singular queries
251
+ and other command operations retain their 30-second deadline, 1 MiB output
252
+ budget and documented failure behavior.
253
+
188
254
  ## Private directories
189
255
 
190
256
  ```ts
@@ -41,6 +41,11 @@ The lexical path surface additionally exports `isNodeError`,
41
41
  `UnsafeDeviceReadPathMatch`, `UnsafeDeviceReadPathOptions`, and
42
42
  `UnsafeDeviceReadPathReason`.
43
43
 
44
+ `isPathRelativeEscape()` rejects absolute paths and relative paths that step
45
+ above their starting directory at any point. Contained paths such as
46
+ `dir/../file` remain relative. It accepts both separators on Windows; on POSIX,
47
+ a backslash remains a literal filename character.
48
+
44
49
  The advanced root-file primitive exports `OpenRootFileParams`,
45
50
  `OpenRootFileSyncParams`, `RootFileOpenResult`, and
46
51
  `RootFileOpenFailureReason`. These are composition types for callers building
@@ -74,6 +79,16 @@ missing relative suffixes beneath an existing directory using bounded temporary
74
79
  directory probes. The caller owns Unicode-pair policy, caching, and the fallback
75
80
  for `undefined`. See [path suffix alias probing](path-suffix-aliases.md).
76
81
 
82
+ `retainSymlinkInDirectory` holds an explicitly identified POSIX symlink through
83
+ exact-slot no-replace publication; its receipts use the `StagedSymlink*` and
84
+ `PublishedSymlinkReceipt` types. See [staged symlinks](staged-symlink.md).
85
+
86
+ `retainFileInDirectory` retains an existing Windows NTFS file through a native
87
+ handle for explicit identity-bound retirement. It is described by
88
+ `RetainFileInDirectoryOptions`, `RetainedFile`, `RetainedFileAdmission`,
89
+ `RetainedFileExpected`, `RetainedFileIssue`, `RetainedFileReceipt`, and
90
+ `RetainedFileResult`. See [retained Windows files](retained-file.md).
91
+
77
92
  ## Guest source
78
93
 
79
94
  `@openclaw/fs-safe/guest` exports `GUEST_FILESYSTEM_PYTHON`,
@@ -100,7 +115,8 @@ best-effort cleanup helper used by those queue flows.
100
115
  Permission inspection exposes `PermissionCheckOptions` and `SafeStatResult`.
101
116
  Private-directory creation uses `CreatePrivateDirectoryOptions`. Raw Windows
102
117
  descriptor facts use `OwnerAndDaclResult`, `WindowsAccessControlEntry`, and
103
- `WindowsAceFlags`.
118
+ `WindowsAceFlags`. `readOwnerAndDaclBatch()` returns those same facts in input
119
+ order through an isolated asynchronous batch with a whole-process timeout.
104
120
 
105
121
  Secure reads split their option and result shapes into
106
122
  `SecureFileTrustOptions`, `SecureFilePermissionOptions`,
@@ -126,8 +142,11 @@ parent/workspace descriptors; see the
126
142
  Atomic helper option and receipt types include
127
143
  `MovePathWithCopyFallbackOptions`, `ReplaceDirectoryAtomicOptions`,
128
144
  `ReplaceFileAtomicSyncOptions`, `ReplaceFileAtomicResult`,
129
- `ReplaceFileAtomicRestoreCleanup`, `ReplaceFileCopyFallbackRestorePolicy`, and
130
- `ReplaceFileDestinationHardlinkPolicy`.
145
+ `ReplaceFileAtomicRestoreCleanup`, `ReplaceFileCopyFallbackRestorePolicy`,
146
+ `ReplaceFileDestinationHardlinkPolicy`, and `ReplaceFileAtomicDestinationState`.
147
+ The `assertBeforeMutation` option rechecks caller authority before new effects,
148
+ and `onDestinationState` reports retained destination identities as
149
+ `ReplaceFileAtomicDestinationState` values; see [atomic writes](atomic.md).
131
150
 
132
151
  The durability surface also exports the synchronous strict
133
152
  `syncDirectorySync`, plus `DirectoryReceipt`, `DurableDirectoryReceipt`,
@@ -154,6 +173,14 @@ Archive option and policy types are `ExtractArchiveOptions`,
154
173
  used by extractors. `resolvePackedRootDir` finds the single packed root when an
155
174
  archive layout permits it; neither helper weakens entry validation.
156
175
 
176
+ ## `watch`
177
+
178
+ `@openclaw/fs-safe/watch` exports `watch()` plus `WatchScope`, `WatchEntry`,
179
+ `WatchChange`, `WatchInvalidation`, `WatchFailure`, `WatchHealth`,
180
+ `WatchOptions`, and `WatchSubscription`. Invalidations are advisory; guarded
181
+ scans stay authoritative. See [filesystem observation](watch.md) for modes,
182
+ budgets, `persistent`, and lifecycle.
183
+
157
184
  ## Keeping this list honest
158
185
 
159
186
  Every runtime and type name in `test/public-api.json` must appear somewhere in
@@ -0,0 +1,115 @@
1
+ ---
2
+ title: Retained Windows files
3
+ description: "Explicit identity-bound retirement of an existing file; resource settlement is not persistence."
4
+ ---
5
+
6
+ # Retain an existing Windows file
7
+
8
+ `retainFileInDirectory` from `@openclaw/fs-safe/advanced` retains **one existing
9
+ regular direct child**, without creating or deleting anything at admission.
10
+ It requires the matching native package. There is no pathname-unlink fallback.
11
+ The initial implementation supports fixed local NTFS drives only; other systems
12
+ return `unsupported`.
13
+
14
+ ```ts
15
+ import { retainFileInDirectory } from "@openclaw/fs-safe/advanced";
16
+
17
+ const admission = retainFileInDirectory({
18
+ directory: producer.directory, // canonical C:\... spelling
19
+ parent: producer.parentIdentity, // exact bigint dev/ino
20
+ basename: producer.basename,
21
+ expected: producer.expected, // bigint dev/ino/size/mtimeNs/ctimeNs and SHA-256
22
+ assertBeforeMutation: () => producer.assertCurrentExclusiveAuthority(),
23
+ });
24
+ if (admission.status === "retained") {
25
+ using file = admission.file;
26
+ const outcome = file.remove(); // explicit; using alone does NOT delete
27
+ // outcome.persistence is always "not-proven".
28
+ producer.recordRetirementObservation(outcome);
29
+ }
30
+ ```
31
+
32
+ The producer must capture its expected identity, generation and bytes during its
33
+ own creation/ownership protocol. Reading an arbitrary current pathname immediately
34
+ before admission does **not** authenticate the producer. `Number` identities,
35
+ zero/unknown identities, negative/overflowing identities, and malformed digests
36
+ are rejected. A full opaque native volume/file ID is included in the receipt;
37
+ no native handle or numeric descriptor is exposed. Receipts are immutable facts,
38
+ not transferable authority.
39
+
40
+ ## Public types
41
+
42
+ - `RetainFileInDirectoryOptions`: immutable admission inputs and synchronous authority.
43
+ - `RetainedFileExpected`: exact producer identity, write generation and expected digest.
44
+ - `RetainedFileAdmission`: a retained `RetainedFile` or a refusal/settlement result.
45
+ - `RetainedFileReceipt`: immutable admitted facts, never reusable authority.
46
+ - `RetainedFileResult`: separate disposition, namespace, resources and persistence facts.
47
+ - `RetainedFileIssue`: phase, native code/message and optional original authority cause.
48
+
49
+ ## Admission and mutation custody
50
+
51
+ The directory's ancestry is opened component by component without following
52
+ reparse points, with delete sharing denied. Its exact expected parent identity
53
+ and canonical path are checked. The file is opened relative to that retained
54
+ parent, with write/delete sharing denied. Existing writer handles refuse
55
+ admission; a read-oplock grant also excludes preexisting writable mapped
56
+ sections. That oplock request is cancelled and joined before admission returns,
57
+ while the no-write-sharing file handle remains open. No detached request remains.
58
+
59
+ The opened file must match the expected exact NTFS identity, single-link regular
60
+ type, write/change generation, size and SHA-256. Names with stream/device syntax,
61
+ trailing-dot/space aliases and observed named data streams are unsupported.
62
+ Readonly attributes and ACL denials are not repaired or overridden. Verification
63
+ is synchronous and bounded by `maxBytes` (default 16 MiB, maximum 64 MiB).
64
+
65
+ The original producer must retain **exclusive mutation custody for the entire
66
+ file**, including alternate streams, attributes, security and hardlink creation.
67
+ Windows read/write sharing is per-stream; it is not an application lock over
68
+ all possible aliases. The helper rejects observed alternate streams and changed
69
+ generations, but does not turn a point-in-time check into exclusion of concurrent
70
+ alias/attribute operations. The synchronous `assertBeforeMutation` must check
71
+ that this original authority is still current. Callers unable to establish that
72
+ custody must not call `remove`. Privileged/raw-volume/kernel modifications are
73
+ outside this capability's threat model.
74
+
75
+ `remove()` admits authority once, revalidates the retained object, sets native
76
+ handle disposition, closes the file, observes the name under the still-retained
77
+ parent, then closes ancestry. No pathname is passed to unlink. Ordinary
78
+ `dispose()` and `[Symbol.dispose]()` only close resources. Authority callbacks
79
+ returning Promises, thenables, or synchronous or asynchronous generator objects
80
+ are refused without deletion; generators are never advanced. Reentrancy is also
81
+ refused, and even caught reentrancy poisons that attempt.
82
+ Repeated settlement returns the original receipt without another mutation or
83
+ another authority call. A copied receipt cannot be used to remove a replacement.
84
+ `[Symbol.dispose]()` throws `FsSafeError` with the complete result in
85
+ `error.details.result` if resource settlement is uncertain; `dispose()` returns
86
+ that result directly. GC closes only and produces no settlement receipt; use
87
+ explicit disposal.
88
+
89
+ ## Read the facts separately
90
+
91
+ | Field | Meaning |
92
+ | --- | --- |
93
+ | `status` | `unsupported`, `not-attempted`, `preserved-mismatch`, `disposition-accepted`, `name-absent-after-settlement`, `failed`, or `indeterminate`. |
94
+ | `disposition` | Whether native deletion was unattempted, accepted, rejected, or indeterminate. |
95
+ | `namespace` | A post-file-close observation: absent, original, foreign, unknown, or not observed. Absence alone never authenticates prior deletion. |
96
+ | `resources` | `closed` or `close-failed`; operation and close errors are retained together. An uncertain close is never retried against a possibly recycled handle. |
97
+ | `persistence` | Always `not-proven`; no namespace persistence barrier was performed. |
98
+
99
+ An accepted disposition can still have an unknown/foreign/original namespace
100
+ observation or failed close. A later replacement may exist even after observed
101
+ absence. Existing read handles can retain access to old bytes after the name is
102
+ absent. Namespace observation does not certify all foreign handles are closed.
103
+ An unexpected native binding failure is indeterminate, not success.
104
+
105
+ ## No persistence or service-transaction guarantee
106
+
107
+ This API does not flush a volume, implement a journal, certify power-loss-safe
108
+ unlink, or issue an application dependency release. Process-termination tests
109
+ establish process-owned handle lifetime only, not crash/storage durability.
110
+
111
+ An update transaction must independently qualify either a real namespace
112
+ persistence barrier or original-owned durable recovery with correct ordering,
113
+ generation binding and restart consumption. Until then it must retain its
114
+ uncertain recovery/dependency state. Successful removal of one payload does not
115
+ establish a committed multi-file transaction or justify deleting its receipt.
@@ -24,7 +24,7 @@ type RootDefaults = {
24
24
  denyMutations?: DenyMutationPolicy; // absolute paths/prefixes mutation methods may not change
25
25
  maxBytes?: number; // refuse reads larger than this many bytes; defaults to 16 MiB
26
26
  mkdir?: boolean; // create missing parent dirs on write/openWritable/append; default true
27
- mode?: number; // file mode applied to new writes; per-call override available
27
+ mode?: number; // requested file mode; per-call override available
28
28
  nonBlockingRead?: boolean; // compatibility hint; safe opens are already nonblocking where supported
29
29
  renameIdentity?: "strict" | "verify-content-with-lock"; // default "strict"
30
30
  symlinks?: "reject" | "follow-within-root" | "follow-parents-within-root"; // read policy
@@ -68,7 +68,10 @@ fs.walk(rel, options) // root-bounded AsyncIterable<{ relativePath, kin
68
68
 
69
69
  `walk()` is the incremental, root-bounded recursive scan. It supports entry and
70
70
  depth budgets, cancellation, and `symlinkPolicy: "skip" |
71
- "follow-within-root"`. With an entry budget, sorted walks prepare small metadata
71
+ "follow-within-root" | "include"`. Include mode returns links as
72
+ `{ relativePath, kind: "symlink", size }` without resolving or entering their
73
+ targets, including dangling and outside-root links. `size` describes the link,
74
+ not its target. With an entry budget, sorted walks prepare small metadata
72
75
  batches within the remaining budget; unbounded sorted walks reuse the full
73
76
  directory snapshot.
74
77
  The default `order: "sorted"` enumerates and sorts each directory's names;
@@ -92,6 +95,8 @@ Directory reads remain fail-fast by default. With
92
95
  `{ relativePath, kind: "directory-error", size: 0, error }` and continues with
93
96
  the remaining tree. That policy also covers identity-check failures after an
94
97
  awaited filter, while callback failures always reject.
98
+ In include mode, a directory that becomes a symlink before descent is a
99
+ `path-mismatch` directory error; it is never silently omitted or followed.
95
100
  See [Directory walking](walk.md) for the pure-Node guarantees and the contrast
96
101
  with the standalone best-effort walkers.
97
102
 
@@ -117,6 +122,10 @@ created through a directory symlink or Windows junction. An absolute path
117
122
  outside the root is still rejected. On Windows, alternate casing is accepted
118
123
  only when the differently cased Root prefix has the Root's exact directory
119
124
  identity; the operation then continues under the trusted Root spelling.
125
+ Absolute paths keep literal `~` components: `readAbsolute("/srv/root/~/file")`
126
+ reads that entry under the root, without expanding the user's home directory.
127
+ Relative `~/file` inputs still expand the home directory and must remain inside
128
+ the Root; use `./~/file` for a literal relative `~` directory.
120
129
 
121
130
  ### Writes
122
131
 
@@ -153,6 +162,11 @@ await fs.create("private-data/credential", "synthetic credential", { private: tr
153
162
 
154
163
  `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`).
155
164
 
165
+ For `append` and `openWritable`, `mode` only affects new-file creation: POSIX
166
+ permissions remain subject to the process umask. These methods do not chmod
167
+ existing files. Replacement and create-only writes apply their final mode
168
+ through the retained descriptor; see [Writing](writing.md#write-options).
169
+
156
170
  Buffered `create` and `createJson` also accept `atomic?: boolean`. With `true`,
157
171
  complete content is staged before exclusive publication even in native-off mode;
158
172
  the fallback requires hardlinks. Omitted or `false` keeps the existing buffered
@@ -249,8 +263,10 @@ occurred, later cancellation or verification failure preserves the destination.
249
263
  The synchronous optional `onDestinationPublished` callback receives a frozen
250
264
  `RootCopyPublicationReceipt` containing `{ path, dev, ino }`, with exact bigint identity immediately after
251
265
  publication, before later checks can fail. Callback errors also preserve the
252
- published file. This receipt records an outcome; it does not authorize removing
253
- a file that another actor may have edited. Application recovery and cooperative
266
+ published file. Promise, thenable, and synchronous or asynchronous generator
267
+ results reject with `TypeError`; returned generators are never advanced. Other
268
+ synchronous return values are ignored. This receipt records an outcome; it does
269
+ not authorize removing a file that another actor may have edited. Application recovery and cooperative
254
270
  locking remain caller-owned.
255
271
 
256
272
  Existing `copyIn` callers must account for completed destinations retained after
@@ -310,10 +326,12 @@ truncation, append, move, and removal. Buffered writes use bounded chunks and
310
326
  recheck before every partial-write submission; file removal submits a direct
311
327
  unlink request. Native calls that perform multiple filesystem steps are one
312
328
  dispatch. No asynchronous wait separates the check
313
- from that dispatch. A thrown value rejects the operation unchanged; an async
314
- or thenable-returning callback rejects with `TypeError` before that mutation.
315
- Synchronous return values are ignored. Callbacks can run multiple times and
316
- must inspect current authority each time.
329
+ from that dispatch. A thrown value rejects the operation unchanged; a Promise,
330
+ thenable, or synchronous or asynchronous generator result rejects with `TypeError`
331
+ before that mutation. Returned generators are never advanced. Other synchronous
332
+ return values are ignored. Generator detection applies to generator objects
333
+ themselves, not proxy wrappers. Callbacks can run multiple times and must inspect
334
+ current authority during each call; return-value validation does not establish it.
317
335
  Directory creation rechecks the retained parent after the callback and before
318
336
  submitting mkdir, so a replacement is rejected before creating that component.
319
337
  Overwrite moves recheck the retained root, parents, source identity and both
@@ -340,7 +358,9 @@ For `openWritable()`, the callback covers the library's parent creation,
340
358
  exclusive creation, and truncation. The returned raw `FileHandle` belongs to
341
359
  the caller, which must check authority before its own later writes.
342
360
 
343
- 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.
361
+ 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. Missing suffixes also match prospective case and canonical Unicode normalization aliases: case-folding and Unicode NFC/NFD-equivalent spellings cannot bypass a denied path or prefix. A read-only observation of the existing parent can establish that ASCII case variants are distinct. Admission never creates temporary probe files or directories. If sensitivity cannot be established (including empty or unreadable parents, future directories, and Unicode normalization), equivalent suffixes are denied conservatively, even on a filesystem that would allow distinct names. Distinct existing canonical ancestors and unrelated names remain distinct. Observations are local to each synchronous policy check and are refreshed after callbacks or mutations. This conservative fallback does not model other filesystem-specific equivalences, such as HFS+ ignorable formatting characters.
362
+
363
+ 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.
344
364
 
345
365
  `move()` snapshots its merged default and per-call mutation policy before
346
366
  asynchronous preparation. Later changes to the original policy objects or arrays
@@ -375,6 +395,12 @@ fs.entries(rel, options?) // nonrecursive AsyncIterable<DirEntry>, includ
375
395
  fs.resolve(rel) // absolute path inside the root, after canonicalization
376
396
  ```
377
397
 
398
+ Directory enumeration (`list`, `entries`, and `walk`) requires UTF-8 filenames.
399
+ A discovered name that cannot be represented losslessly as a JavaScript string
400
+ rejects with `invalid-path`, before looking up metadata through that name.
401
+ Streaming traversal may already have yielded earlier entries. Literal Unicode
402
+ replacement characters (`U+FFFD`) remain valid names.
403
+
378
404
  These do not pin a later operation. During `stat()`, the exact selected target and
379
405
  parent are checked around metadata collection; `list()` checks one exact selected
380
406
  directory around the complete name/metadata batch instead of repeating containment
@@ -113,6 +113,10 @@ If an already validated descriptor fails while reading, both readers throw an
113
113
  operational `FsSafeError` with `code: "read-failed"`; inspect `cause` for the
114
114
  underlying Node filesystem code such as `EIO`.
115
115
 
116
+ Caught `null` or `undefined` inspection and read failures are reported as
117
+ structured errors with an `Error` cause carrying `"null"` or `"undefined"`,
118
+ instead of an internal `TypeError` while inspecting the thrown value.
119
+
116
120
  A synchronous reader closes its descriptor once. A close failure preserves an
117
121
  earlier read or identity-validation error; after a successful read, the close
118
122
  failure is reported before trimming or rejecting empty content.
@@ -137,6 +141,11 @@ startWebhookVerifier(signingKey);
137
141
 
138
142
  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.
139
143
 
144
+ Both secret writers capture top-level parameter values when called, before
145
+ asynchronous filesystem preparation; a supplied byte buffer is captured by
146
+ reference. A parameter getter throwing `null` or `undefined` rejects with that
147
+ same value before filesystem inspection.
148
+
140
149
  On POSIX, both native and JavaScript writers verify actual `0o600` permission
141
150
  bits through the retained descriptor before writing content. A filesystem that
142
151
  reports successful chmod without enforcing those bits fails with
@@ -219,6 +228,15 @@ if anything already occupies the target path it throws
219
228
  `FsSafeError("secret-exists")` without modifying that entry. Use the distinct
220
229
  name when first-writer-wins is part of the credential protocol.
221
230
 
231
+ Its `durable` option also accepts `"file"`, matching `Root.create()`. This
232
+ requires every file `fsync` to succeed, including on `EPERM`, while parent-directory
233
+ synchronization remains best effort. The default and boolean options retain their
234
+ existing behavior. `writeSecretFileAtomic()` continues to accept boolean durability.
235
+
236
+ Strict file synchronization preserves the existing publication strategy and
237
+ identity-checked cleanup. A failed file flush before staged publication prevents
238
+ publication; a failure after publication can leave the complete file present.
239
+
222
240
  Distinct leaves can share missing-parent creation without a `secret-exists`
223
241
  error. Concurrent creates at the same leaf still have exactly one winner;
224
242
  the loser receives `secret-exists` and leaves the winner's bytes intact.
@@ -235,6 +253,7 @@ try {
235
253
  rootDir: "/var/lib/app/credentials",
236
254
  filePath: "/var/lib/app/credentials/provider.refresh-token",
237
255
  content: refreshToken,
256
+ durable: "file",
238
257
  });
239
258
  } catch (error) {
240
259
  if (!(error instanceof FsSafeError) || error.code !== "secret-exists") throw error;
@@ -232,13 +232,16 @@ The library does not modify or constrain the global Node.js `fs` namespace, and
232
232
 
233
233
  | Mechanism | Reported containment | Boundary |
234
234
  |---|---|---|
235
- | Linux native | `kernel-atomic` | `openat2(RESOLVE_BENEATH | RESOLVE_NO_MAGICLINKS)` resolves and opens under the root in one kernel operation. |
235
+ | Linux native with `openat2` | `kernel-atomic` | `openat2(RESOLVE_BENEATH | RESOLVE_NO_MAGICLINKS)` resolves and opens under the root in one kernel operation. |
236
+ | Linux native without `openat2` | `best-effort` | A no-follow `openat` component walk retains each parent and verifies exact identity associations before and after the final open; all symlink components are rejected. |
236
237
  | macOS native | `best-effort` | macOS 15.4 and newer use `O_RESOLVE_BENEATH` first; older kernels use the guarded `openat(O_NOFOLLOW)` component walk. Both verify the opened descriptor with `F_GETPATH`. |
237
238
  | Windows native | `best-effort` | Handle-relative `NtCreateFile` rejects reparse points, but this package does not claim a Linux-style atomic beneath guarantee. |
238
239
  | JavaScript fallback | `best-effort` | Canonical checks, no-follow opens where Node exposes them, and post-open identity checks form a check-then-use sequence. |
239
240
 
240
241
  The macOS `F_GETPATH` verification is an escape detector, not a race-atomic guarantee. A hostile same-UID process can rename a directory after `O_RESOLVE_BENEATH` or the manual walk and race the post-open sample or a later descriptor-relative mutation. The native result therefore remains `best-effort` on macOS even when the kernel flag is available. No policy decision is attached to these labels; callers can inspect the fact and decide what their own threat model requires.
241
242
 
243
+ The Linux fallback's identity checks are also detection-based: a directory can be renamed between chain samples or before a later descriptor-relative mutation. It cannot provide atomic resolution, and rejection after a mutating open does not promise rollback. See [Linux without openat2](native.md#linux-without-openat2) for the cached capability probe, conservative symlink rejection, and operations that remain unavailable.
244
+
242
245
  The public `OpenResult`, `ReadResult`, and `WritableOpenResult` expose `containment`. Those root APIs currently report `best-effort`; direct native `openBeneath()` reports the platform value above. No-replace publication uses `renameat2(RENAME_NOREPLACE)` on Linux, `renameatx_np(RENAME_EXCL)` on macOS, and `FileRenameInfoEx` with replacement disabled on Windows, but those separate mutation semantics do not upgrade an open result's containment label.
243
246
 
244
247
  ## Limitations to keep in mind