@openclaw/openshell-sandbox 2026.10.5-beta.4 → 2026.10.10-beta.2

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 (240) hide show
  1. package/README.md +1 -1
  2. package/node_modules/@openclaw/fs-safe/CHANGELOG.md +148 -0
  3. package/node_modules/@openclaw/fs-safe/README.md +20 -4
  4. package/node_modules/@openclaw/fs-safe/dist/advanced.d.ts +2 -1
  5. package/node_modules/@openclaw/fs-safe/dist/advanced.js +1 -0
  6. package/node_modules/@openclaw/fs-safe/dist/archive-merge.js +28 -3
  7. package/node_modules/@openclaw/fs-safe/dist/archive-parser.wasm +0 -0
  8. package/node_modules/@openclaw/fs-safe/dist/archive-read.js +11 -5
  9. package/node_modules/@openclaw/fs-safe/dist/archive-zip-loader.js +4 -2
  10. package/node_modules/@openclaw/fs-safe/dist/archive.js +2 -2
  11. package/node_modules/@openclaw/fs-safe/dist/atomic-io.js +2 -2
  12. package/node_modules/@openclaw/fs-safe/dist/bounded-read.d.ts +1 -0
  13. package/node_modules/@openclaw/fs-safe/dist/bounded-read.js +9 -2
  14. package/node_modules/@openclaw/fs-safe/dist/clone-metadata.d.ts +2 -0
  15. package/node_modules/@openclaw/fs-safe/dist/clone-metadata.js +2 -1
  16. package/node_modules/@openclaw/fs-safe/dist/config.d.ts +6 -0
  17. package/node_modules/@openclaw/fs-safe/dist/config.js +5 -0
  18. package/node_modules/@openclaw/fs-safe/dist/copy-file-input.d.ts +3 -1
  19. package/node_modules/@openclaw/fs-safe/dist/copy-file-input.js +8 -7
  20. package/node_modules/@openclaw/fs-safe/dist/copy-link.d.ts +26 -0
  21. package/node_modules/@openclaw/fs-safe/dist/copy-link.js +170 -0
  22. package/node_modules/@openclaw/fs-safe/dist/copy-metadata.d.ts +9 -0
  23. package/node_modules/@openclaw/fs-safe/dist/copy-metadata.js +39 -0
  24. package/node_modules/@openclaw/fs-safe/dist/copy-path-admission.js +11 -2
  25. package/node_modules/@openclaw/fs-safe/dist/copy-root-file-sync.d.ts +2 -4
  26. package/node_modules/@openclaw/fs-safe/dist/copy-root-file-sync.js +4 -2
  27. package/node_modules/@openclaw/fs-safe/dist/copy-tree-portable.js +3 -6
  28. package/node_modules/@openclaw/fs-safe/dist/copy.js +3 -3
  29. package/node_modules/@openclaw/fs-safe/dist/create.js +1 -1
  30. package/node_modules/@openclaw/fs-safe/dist/creation-boundary.js +7 -4
  31. package/node_modules/@openclaw/fs-safe/dist/darwin-acl.d.ts +12 -0
  32. package/node_modules/@openclaw/fs-safe/dist/darwin-acl.js +29 -0
  33. package/node_modules/@openclaw/fs-safe/dist/directory-durability.d.ts +7 -4
  34. package/node_modules/@openclaw/fs-safe/dist/directory-guard.js +20 -5
  35. package/node_modules/@openclaw/fs-safe/dist/directory-mode-node.d.ts +3 -0
  36. package/node_modules/@openclaw/fs-safe/dist/directory-mode-node.js +27 -2
  37. package/node_modules/@openclaw/fs-safe/dist/directory-receipt.d.ts +7 -7
  38. package/node_modules/@openclaw/fs-safe/dist/directory-receipt.js +9 -4
  39. package/node_modules/@openclaw/fs-safe/dist/durability.d.ts +2 -1
  40. package/node_modules/@openclaw/fs-safe/dist/entry-publication.js +2 -2
  41. package/node_modules/@openclaw/fs-safe/dist/exact-identity.d.ts +17 -0
  42. package/node_modules/@openclaw/fs-safe/dist/exact-identity.js +29 -0
  43. package/node_modules/@openclaw/fs-safe/dist/file-handle-transfer.js +4 -2
  44. package/node_modules/@openclaw/fs-safe/dist/file-hash.d.ts +1 -1
  45. package/node_modules/@openclaw/fs-safe/dist/file-hash.js +11 -9
  46. package/node_modules/@openclaw/fs-safe/dist/file-identity.d.ts +7 -1
  47. package/node_modules/@openclaw/fs-safe/dist/file-identity.js +27 -5
  48. package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-admission.js +1 -2
  49. package/node_modules/@openclaw/fs-safe/dist/file-lock-sync.js +8 -1
  50. package/node_modules/@openclaw/fs-safe/dist/file-lock.d.ts +1 -0
  51. package/node_modules/@openclaw/fs-safe/dist/file-lock.js +1 -0
  52. package/node_modules/@openclaw/fs-safe/dist/file-write-lease.d.ts +9 -0
  53. package/node_modules/@openclaw/fs-safe/dist/file-write-lease.js +42 -0
  54. package/node_modules/@openclaw/fs-safe/dist/guarded-mkdir.d.ts +1 -0
  55. package/node_modules/@openclaw/fs-safe/dist/guarded-mkdir.js +22 -15
  56. package/node_modules/@openclaw/fs-safe/dist/guest-native-python.js +3 -0
  57. package/node_modules/@openclaw/fs-safe/dist/hardlink-fallback.d.ts +1 -0
  58. package/node_modules/@openclaw/fs-safe/dist/hardlink-fallback.js +5 -0
  59. package/node_modules/@openclaw/fs-safe/dist/index.d.ts +1 -0
  60. package/node_modules/@openclaw/fs-safe/dist/index.js +1 -0
  61. package/node_modules/@openclaw/fs-safe/dist/json-durable-queue-ownership.js +16 -3
  62. package/node_modules/@openclaw/fs-safe/dist/local-file-descriptor.js +24 -13
  63. package/node_modules/@openclaw/fs-safe/dist/native-binding.d.ts +85 -18
  64. package/node_modules/@openclaw/fs-safe/dist/native-config.d.ts +5 -0
  65. package/node_modules/@openclaw/fs-safe/dist/native-config.js +11 -0
  66. package/node_modules/@openclaw/fs-safe/dist/native-directory-observation.js +6 -4
  67. package/node_modules/@openclaw/fs-safe/dist/native-fallback-warning.d.ts +2 -1
  68. package/node_modules/@openclaw/fs-safe/dist/native-fallback-warning.js +26 -2
  69. package/node_modules/@openclaw/fs-safe/dist/native-guidance.d.ts +3 -0
  70. package/node_modules/@openclaw/fs-safe/dist/native-guidance.js +21 -0
  71. package/node_modules/@openclaw/fs-safe/dist/native-move-fallback.d.ts +1 -1
  72. package/node_modules/@openclaw/fs-safe/dist/native-move-fallback.js +2 -0
  73. package/node_modules/@openclaw/fs-safe/dist/native-operations.js +8 -6
  74. package/node_modules/@openclaw/fs-safe/dist/native-parent-admission.d.ts +44 -4
  75. package/node_modules/@openclaw/fs-safe/dist/native-parent-admission.js +85 -15
  76. package/node_modules/@openclaw/fs-safe/dist/native-pinned-write-windows.d.ts +3 -3
  77. package/node_modules/@openclaw/fs-safe/dist/native-pinned-write-windows.js +33 -6
  78. package/node_modules/@openclaw/fs-safe/dist/native-pinned-write.d.ts +4 -1
  79. package/node_modules/@openclaw/fs-safe/dist/native-pinned-write.js +22 -3
  80. package/node_modules/@openclaw/fs-safe/dist/native-policy-directory-observation.js +4 -3
  81. package/node_modules/@openclaw/fs-safe/dist/native-policy-parent.d.ts +1 -1
  82. package/node_modules/@openclaw/fs-safe/dist/native-policy-parent.js +3 -0
  83. package/node_modules/@openclaw/fs-safe/dist/native-staged-file-metadata.d.ts +3 -0
  84. package/node_modules/@openclaw/fs-safe/dist/native-staged-file-metadata.js +16 -0
  85. package/node_modules/@openclaw/fs-safe/dist/native-staged-file.d.ts +1 -1
  86. package/node_modules/@openclaw/fs-safe/dist/native-staged-file.js +15 -5
  87. package/node_modules/@openclaw/fs-safe/dist/native-stat-identity.d.ts +3 -0
  88. package/node_modules/@openclaw/fs-safe/dist/native-stat-identity.js +7 -0
  89. package/node_modules/@openclaw/fs-safe/dist/native-status.d.ts +17 -0
  90. package/node_modules/@openclaw/fs-safe/dist/native-status.js +30 -0
  91. package/node_modules/@openclaw/fs-safe/dist/native.d.ts +31 -4
  92. package/node_modules/@openclaw/fs-safe/dist/native.js +107 -9
  93. package/node_modules/@openclaw/fs-safe/dist/opened-realpath.js +9 -1
  94. package/node_modules/@openclaw/fs-safe/dist/overwrite-file-handle.js +4 -1
  95. package/node_modules/@openclaw/fs-safe/dist/owner-dacl-batch.js +1 -1
  96. package/node_modules/@openclaw/fs-safe/dist/owner-dacl.js +1 -1
  97. package/node_modules/@openclaw/fs-safe/dist/permission-exec.js +9 -1
  98. package/node_modules/@openclaw/fs-safe/dist/permissions-public.d.ts +1 -0
  99. package/node_modules/@openclaw/fs-safe/dist/permissions-public.js +1 -0
  100. package/node_modules/@openclaw/fs-safe/dist/permissions-windows.js +83 -47
  101. package/node_modules/@openclaw/fs-safe/dist/pinned-open.d.ts +2 -0
  102. package/node_modules/@openclaw/fs-safe/dist/pinned-open.js +8 -3
  103. package/node_modules/@openclaw/fs-safe/dist/pinned-write-input.js +13 -0
  104. package/node_modules/@openclaw/fs-safe/dist/pinned-write-staged.js +10 -8
  105. package/node_modules/@openclaw/fs-safe/dist/pinned-write-types.d.ts +2 -1
  106. package/node_modules/@openclaw/fs-safe/dist/pinned-write.js +7 -5
  107. package/node_modules/@openclaw/fs-safe/dist/pipe.d.ts +9 -0
  108. package/node_modules/@openclaw/fs-safe/dist/pipe.js +26 -0
  109. package/node_modules/@openclaw/fs-safe/dist/private-directory.js +1 -1
  110. package/node_modules/@openclaw/fs-safe/dist/private-producer-handoff.js +2 -5
  111. package/node_modules/@openclaw/fs-safe/dist/private-temp-workspace.d.ts +3 -2
  112. package/node_modules/@openclaw/fs-safe/dist/private-temp-workspace.js +38 -10
  113. package/node_modules/@openclaw/fs-safe/dist/publish-copy-stage.js +92 -10
  114. package/node_modules/@openclaw/fs-safe/dist/publish-file-failure.d.ts +3 -0
  115. package/node_modules/@openclaw/fs-safe/dist/publish-file-failure.js +3 -0
  116. package/node_modules/@openclaw/fs-safe/dist/publish-file.d.ts +5 -3
  117. package/node_modules/@openclaw/fs-safe/dist/publish-file.js +26 -20
  118. package/node_modules/@openclaw/fs-safe/dist/read-opened-file.d.ts +2 -0
  119. package/node_modules/@openclaw/fs-safe/dist/read-opened-file.js +3 -4
  120. package/node_modules/@openclaw/fs-safe/dist/realpath.js +32 -0
  121. package/node_modules/@openclaw/fs-safe/dist/regular-file.d.ts +4 -0
  122. package/node_modules/@openclaw/fs-safe/dist/regular-file.js +11 -15
  123. package/node_modules/@openclaw/fs-safe/dist/replace-file-descriptor.js +3 -2
  124. package/node_modules/@openclaw/fs-safe/dist/root-copy.d.ts +18 -0
  125. package/node_modules/@openclaw/fs-safe/dist/root-copy.js +128 -0
  126. package/node_modules/@openclaw/fs-safe/dist/root-create-native.d.ts +8 -13
  127. package/node_modules/@openclaw/fs-safe/dist/root-create-native.js +252 -27
  128. package/node_modules/@openclaw/fs-safe/dist/root-directory-creation.js +5 -1
  129. package/node_modules/@openclaw/fs-safe/dist/root-directory-list-types.d.ts +11 -8
  130. package/node_modules/@openclaw/fs-safe/dist/root-directory-list.js +35 -13
  131. package/node_modules/@openclaw/fs-safe/dist/root-file-open-batch-sync.d.ts +8 -0
  132. package/node_modules/@openclaw/fs-safe/dist/root-file-open-batch-sync.js +108 -0
  133. package/node_modules/@openclaw/fs-safe/dist/root-file.d.ts +7 -0
  134. package/node_modules/@openclaw/fs-safe/dist/root-file.js +15 -0
  135. package/node_modules/@openclaw/fs-safe/dist/root-impl.d.ts +2 -0
  136. package/node_modules/@openclaw/fs-safe/dist/root-impl.js +154 -175
  137. package/node_modules/@openclaw/fs-safe/dist/root-mkdir-native.d.ts +2 -0
  138. package/node_modules/@openclaw/fs-safe/dist/root-mkdir-native.js +239 -0
  139. package/node_modules/@openclaw/fs-safe/dist/root-move-noreplace.js +37 -15
  140. package/node_modules/@openclaw/fs-safe/dist/root-native-creation-path.d.ts +32 -0
  141. package/node_modules/@openclaw/fs-safe/dist/root-native-creation-path.js +56 -0
  142. package/node_modules/@openclaw/fs-safe/dist/root-options.d.ts +2 -0
  143. package/node_modules/@openclaw/fs-safe/dist/root-public-types.d.ts +1 -0
  144. package/node_modules/@openclaw/fs-safe/dist/root-remove-native.js +95 -16
  145. package/node_modules/@openclaw/fs-safe/dist/root-remove.js +6 -4
  146. package/node_modules/@openclaw/fs-safe/dist/root-walk.js +23 -19
  147. package/node_modules/@openclaw/fs-safe/dist/root-write-admission.js +8 -6
  148. package/node_modules/@openclaw/fs-safe/dist/root-write-publication.js +5 -1
  149. package/node_modules/@openclaw/fs-safe/dist/secure-file-windows.js +3 -1
  150. package/node_modules/@openclaw/fs-safe/dist/secure-file.d.ts +2 -0
  151. package/node_modules/@openclaw/fs-safe/dist/secure-file.js +10 -4
  152. package/node_modules/@openclaw/fs-safe/dist/sibling-staged-file.js +1 -1
  153. package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-reclaim.js +2 -2
  154. package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-retained.js +1 -1
  155. package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-target.js +8 -1
  156. package/node_modules/@openclaw/fs-safe/dist/staged-directory.d.ts +13 -0
  157. package/node_modules/@openclaw/fs-safe/dist/staged-directory.js +29 -0
  158. package/node_modules/@openclaw/fs-safe/dist/temp-workspace-child-admission.js +5 -3
  159. package/node_modules/@openclaw/fs-safe/dist/temp-workspace-descriptor.d.ts +8 -0
  160. package/node_modules/@openclaw/fs-safe/dist/temp-workspace-descriptor.js +13 -2
  161. package/node_modules/@openclaw/fs-safe/dist/temp-workspace-owner.js +71 -3
  162. package/node_modules/@openclaw/fs-safe/dist/temp-workspace-ownership.d.ts +2 -1
  163. package/node_modules/@openclaw/fs-safe/dist/temp-workspace-ownership.js +22 -0
  164. package/node_modules/@openclaw/fs-safe/dist/temp-workspace-types.d.ts +3 -0
  165. package/node_modules/@openclaw/fs-safe/dist/test-hooks.d.ts +1 -0
  166. package/node_modules/@openclaw/fs-safe/dist/test-hooks.js +1 -0
  167. package/node_modules/@openclaw/fs-safe/dist/trash.js +2 -2
  168. package/node_modules/@openclaw/fs-safe/dist/types.d.ts +4 -0
  169. package/node_modules/@openclaw/fs-safe/dist/watch-native.js +2 -2
  170. package/node_modules/@openclaw/fs-safe/dist/windows-appcontainer.d.ts +5 -0
  171. package/node_modules/@openclaw/fs-safe/dist/windows-appcontainer.js +39 -0
  172. package/node_modules/@openclaw/fs-safe/dist/windows-owner.d.ts +1 -0
  173. package/node_modules/@openclaw/fs-safe/dist/windows-owner.js +25 -36
  174. package/node_modules/@openclaw/fs-safe/dist/windows-security-command.js +24 -5
  175. package/node_modules/@openclaw/fs-safe/dist/windows-security-facts.d.ts +2 -1
  176. package/node_modules/@openclaw/fs-safe/dist/windows-security-facts.js +4 -0
  177. package/node_modules/@openclaw/fs-safe/dist/windows-security-helper.d.ts +2 -0
  178. package/node_modules/@openclaw/fs-safe/dist/windows-security-helper.exe +0 -0
  179. package/node_modules/@openclaw/fs-safe/dist/windows-security-helper.js +7 -0
  180. package/node_modules/@openclaw/fs-safe/dist/windows-test-hooks.d.ts +16 -0
  181. package/node_modules/@openclaw/fs-safe/dist/windows-test-hooks.js +51 -0
  182. package/node_modules/@openclaw/fs-safe/dist/write-file-handle.d.ts +1 -0
  183. package/node_modules/@openclaw/fs-safe/dist/write-file-handle.js +12 -10
  184. package/node_modules/@openclaw/fs-safe/docs/advanced.md +2 -1
  185. package/node_modules/@openclaw/fs-safe/docs/archive.md +16 -0
  186. package/node_modules/@openclaw/fs-safe/docs/config.md +71 -1
  187. package/node_modules/@openclaw/fs-safe/docs/contributing.md +45 -5
  188. package/node_modules/@openclaw/fs-safe/docs/copy.md +45 -5
  189. package/node_modules/@openclaw/fs-safe/docs/durability.md +34 -7
  190. package/node_modules/@openclaw/fs-safe/docs/errors.md +8 -0
  191. package/node_modules/@openclaw/fs-safe/docs/guest.md +2 -0
  192. package/node_modules/@openclaw/fs-safe/docs/install.md +47 -27
  193. package/node_modules/@openclaw/fs-safe/docs/migrating-to-0.5.md +6 -6
  194. package/node_modules/@openclaw/fs-safe/docs/migrating-to-0.6.md +1 -1
  195. package/node_modules/@openclaw/fs-safe/docs/native-helper.md +79 -6
  196. package/node_modules/@openclaw/fs-safe/docs/native.md +279 -8
  197. package/node_modules/@openclaw/fs-safe/docs/permissions.md +64 -15
  198. package/node_modules/@openclaw/fs-safe/docs/pipe.md +81 -0
  199. package/node_modules/@openclaw/fs-safe/docs/platform-capabilities.md +63 -0
  200. package/node_modules/@openclaw/fs-safe/docs/public-api.md +10 -0
  201. package/node_modules/@openclaw/fs-safe/docs/queue-link-free-design.md +237 -0
  202. package/node_modules/@openclaw/fs-safe/docs/quickstart.md +23 -3
  203. package/node_modules/@openclaw/fs-safe/docs/reading.md +56 -2
  204. package/node_modules/@openclaw/fs-safe/docs/regular-file.md +2 -0
  205. package/node_modules/@openclaw/fs-safe/docs/secret-file.md +5 -5
  206. package/node_modules/@openclaw/fs-safe/docs/secure-file.md +1 -1
  207. package/node_modules/@openclaw/fs-safe/docs/security-model.md +35 -0
  208. package/node_modules/@openclaw/fs-safe/docs/sidecar-lock.md +31 -13
  209. package/node_modules/@openclaw/fs-safe/docs/staged-file.md +18 -0
  210. package/node_modules/@openclaw/fs-safe/docs/store.md +15 -0
  211. package/node_modules/@openclaw/fs-safe/docs/temp.md +39 -0
  212. package/node_modules/@openclaw/fs-safe/docs/testing.md +13 -0
  213. package/node_modules/@openclaw/fs-safe/docs/types.md +51 -81
  214. package/node_modules/@openclaw/fs-safe/docs/walk.md +4 -1
  215. package/node_modules/@openclaw/fs-safe/docs/watch.md +3 -2
  216. package/node_modules/@openclaw/fs-safe/docs/writing.md +121 -14
  217. package/node_modules/@openclaw/fs-safe/package.json +21 -12
  218. package/node_modules/@openclaw/fs-safe-darwin-arm64/fs-safe-native.node +0 -0
  219. package/node_modules/@openclaw/fs-safe-darwin-arm64/package.json +1 -1
  220. package/node_modules/@openclaw/fs-safe-darwin-x64/fs-safe-native.node +0 -0
  221. package/node_modules/@openclaw/fs-safe-darwin-x64/package.json +1 -1
  222. package/node_modules/@openclaw/fs-safe-freebsd-arm64/fs-safe-native.node +0 -0
  223. package/node_modules/@openclaw/fs-safe-freebsd-arm64/package.json +29 -0
  224. package/node_modules/@openclaw/fs-safe-freebsd-x64/fs-safe-native.node +0 -0
  225. package/node_modules/@openclaw/fs-safe-freebsd-x64/package.json +29 -0
  226. package/node_modules/@openclaw/fs-safe-linux-arm64-gnu/fs-safe-native.node +0 -0
  227. package/node_modules/@openclaw/fs-safe-linux-arm64-gnu/package.json +1 -1
  228. package/node_modules/@openclaw/fs-safe-linux-arm64-musl/fs-safe-native.node +0 -0
  229. package/node_modules/@openclaw/fs-safe-linux-arm64-musl/package.json +1 -1
  230. package/node_modules/@openclaw/fs-safe-linux-x64-gnu/fs-safe-native.node +0 -0
  231. package/node_modules/@openclaw/fs-safe-linux-x64-gnu/package.json +1 -1
  232. package/node_modules/@openclaw/fs-safe-linux-x64-musl/fs-safe-native.node +0 -0
  233. package/node_modules/@openclaw/fs-safe-linux-x64-musl/package.json +1 -1
  234. package/node_modules/@openclaw/fs-safe-win32-arm64-msvc/fs-safe-native.node +0 -0
  235. package/node_modules/@openclaw/fs-safe-win32-arm64-msvc/package.json +29 -0
  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/package.json +5 -5
  239. package/node_modules/@openclaw/fs-safe/dist/windows-security-bridge.cs +0 -336
  240. package/node_modules/@openclaw/fs-safe/dist/windows-security-bridge.ps1 +0 -90
@@ -0,0 +1,63 @@
1
+ # Platform filesystem capabilities
2
+
3
+ These synchronous capabilities load the native helper only when called. All
4
+ three entry points import on every supported platform.
5
+
6
+ ## Linux write leases
7
+
8
+ `tryAcquireWriteLease(fd)` from `@openclaw/fs-safe/file-lock` returns a frozen
9
+ `FileWriteLease`, or `null` when another open descriptor prevents acquisition
10
+ (`EAGAIN` or `EBUSY`). Other failures throw `FsSafeError`; unsupported systems
11
+ throw `unsupported-platform` and unavailable helpers throw `helper-unavailable`.
12
+
13
+ The caller owns the descriptor, must keep it open until release, and must install
14
+ its own process-wide `SIGIO` handler **before** acquiring a lease. Prefer a
15
+ disposable child process when an application cannot own that signal. The API
16
+ does not install a signal handler. A competing open starts the kernel's lease
17
+ break; `isHeld()` then returns false, including during a pending downgrade.
18
+ Release promptly so the competing opener can proceed.
19
+
20
+ `lease.fd` is the borrowed descriptor. `lease.release()` and
21
+ `lease[Symbol.dispose]()` settle once without closing it; `isHeld()` returns
22
+ false after release. Never close or reuse the fd before settling the lease.
23
+ Lease acquisition does not replace the caller's path identity and age checks.
24
+
25
+ ## Darwin ACL inspection
26
+
27
+ `inspectDarwinAcl(path)` from `@openclaw/fs-safe/permissions` opens the final
28
+ component without following symlinks, then inspects a descriptor-bound security
29
+ snapshot. Its `DarwinAclInspection` result is one of:
30
+
31
+ - `{ kind: "none" }` for a proven absent or empty extended ACL.
32
+ - `{ kind: "present", inheritsToFiles, inheritsToDirectories }` for an ACL with
33
+ entries, including when both inheritance flags are false.
34
+ - `{ kind: "unknown", reason }` when opening or inspection fails, the helper
35
+ is unavailable, or the native facts are incomplete. Other platforms return
36
+ `reason: "unsupported-platform"`.
37
+
38
+ Unknown is never evidence of absence. The two inheritance booleans report flag
39
+ presence, not effective permissions or volume ownership enforcement. Intermediate
40
+ path components follow ordinary path resolution; this is inspection, not a Root
41
+ confinement capability.
42
+
43
+ ## Windows test fixtures
44
+
45
+ The following exports from `@openclaw/fs-safe/test-hooks` are for tests, not
46
+ production policy. They require the Windows native helper and reject final
47
+ reparse points. Other platforms throw `unsupported-platform`.
48
+
49
+ - `holdWindowsSharingLock(path)` returns a frozen disposable with idempotent
50
+ `close()`. Its native owner holds a non-inheritable handle with read/write
51
+ sharing, omitting delete sharing, until closed or collected.
52
+ - `setWindowsFileAttributes(path, { readOnly?, hidden?, system? })` updates the
53
+ supplied bits through the opened handle and preserves unspecified attributes.
54
+ - `readWindowsFileExtents(path)` returns `{ vcn, lcn, clusters }[]`, all bigint.
55
+ Empty or resident files may have no extents; `lcn: -1n` denotes sparse clusters.
56
+ Enumeration validates each response and fails on incomplete or invalid data.
57
+ It is an observation, not a snapshot against concurrent file modification.
58
+
59
+ Use Node's `fs.statfsSync(path).bsize` for Windows cluster size. Windows CI
60
+ compares it directly against `GetDiskFreeSpaceW`'s sectors-per-cluster times
61
+ bytes-per-sector on NTFS. CI does not provide ReFS; this does not establish ReFS
62
+ clone sharing. The extent reader reports mappings independently of the clone
63
+ implementation.
@@ -187,3 +187,13 @@ budgets, `persistent`, and lifecycle.
187
187
  Every runtime and type name in `test/public-api.json` must appear somewhere in
188
188
  `README.md` or `docs/`. Documentation examples are also checked so a named
189
189
  import cannot silently move to another package subpath.
190
+
191
+ ## Exact receipt types
192
+
193
+ `ExactFileIdentity` is the immutable bigint `dev`/`ino` pair returned alongside
194
+ legacy numeric metadata. `ExactIdentityReceipt` describes an object with that
195
+ `exactIdentity` companion. `FileIdentityInput` accepts either a traditional
196
+ `FileIdentityStat` pair or an explicit exact-companion receipt.
197
+ `ExactDirectoryReceipt` guarantees the exact companion on a returned directory
198
+ receipt while the caller-constructible `DirectoryReceipt` remains compatible.
199
+ See [the identity round-trip guidance](security-model.md#exact-receipts-and-legacy-numeric-metadata).
@@ -0,0 +1,237 @@
1
+ ---
2
+ title: Link-free queue design
3
+ description: "Unimplemented durable JSON queue v2 proposal, crash recovery, and barriers to mixed-version Android support."
4
+ ---
5
+
6
+ # Link-free durable JSON queue design
7
+
8
+ **Status: design only; not implemented.** The current queue still requires
9
+ hardlinks for claim, quarantine, and retirement recovery. Android/Termux systems
10
+ that refuse those links receive `helper-unavailable`. This document does not
11
+ announce Android support or change any existing API, error code, or disk format.
12
+
13
+ The candidate below moves ownership into one atomic state file in a separate,
14
+ versioned store. It explains how link-free transitions could work for cooperating
15
+ v2 clients. It does **not** solve transparent migration with concurrent legacy
16
+ writers, arbitrary processing paths, or existing failed-file destinations.
17
+ Those gaps prevent implementation as a replacement or fallback for v1.
18
+
19
+ ## Existing guarantees
20
+
21
+ The [durable queue contract](store.md#durable-json-queues) uses plain payloads at
22
+ `<id>.json`, `<id>.processing`, and `<id>.delivered`. A transfer lock serializes
23
+ consumer transitions for the JSON path; producers atomically replace `.json`
24
+ without taking that lock. The lock is not a worker lease. Another load can return
25
+ the existing processing generation, so delivery is at least once.
26
+
27
+ Claim creates a no-replace hardlink from the pending name to the processing
28
+ name. The generation at that syscall is the claimed generation. Source
29
+ retirement moves the then-current pending file into a journal and compares exact
30
+ device/inode identity with the claim. A different generation is restored
31
+ create-only if no newer pending generation exists. Thus acknowledging an older
32
+ claim cannot remove a newer enqueue. Replacing a still-pending generation by a
33
+ later enqueue for the same ID is already permitted; this is not a FIFO of all
34
+ writes to that ID.
35
+
36
+ Quarantine publishes create-only to the failed destination. A shared inode
37
+ distinguishes an interrupted publication from an unrelated destination collision.
38
+ Acknowledgement moves only the claim through the delivered marker. Migration
39
+ retains the opened claim and rejects a stale generation after its callback.
40
+ Completed transitions synchronize affected directories; ambiguous ownership,
41
+ identity, and stale transfer locks fail closed. Existing same-privilege race
42
+ limitations in the [security model](security-model.md) still apply.
43
+
44
+ These are ownership guarantees, not consequences that can be recovered from
45
+ equal JSON bytes. Two independent enqueues may contain identical JSON. A digest,
46
+ timestamp, or rounded numeric inode cannot establish that they are one generation.
47
+
48
+ ## Why an in-place fallback is insufficient
49
+
50
+ | Candidate | Lost guarantee |
51
+ |---|---|
52
+ | Ordinary rename after testing for absence | A competing processing or failed destination can be overwritten. Different queues can share a failed destination without sharing a transfer lock. |
53
+ | Copy into an exclusive destination | A crash can expose partial JSON; equal bytes cannot identify interrupted publication versus unrelated collision evidence. |
54
+ | Exclusive empty placeholder, then record its owner | A crash between creation and recording ownership leaves an empty file indistinguishable from a pre-existing collision. |
55
+ | Add a producer lock or format marker | Old producers ignore both and can replace `.json` during retirement recovery. |
56
+ | Rename a retirement record back to `.json` | An old producer can publish after the absence check; replacing rename then destroys that newer generation. |
57
+
58
+ Native atomic no-replace rename could remove some of these obstacles without
59
+ making old producers cooperate. It is a distinct implementation route, requiring
60
+ qualified Android support and full claim, retirement, and quarantine crash proof.
61
+ An unavailable primitive, cross-device move, or unsupported filesystem must not
62
+ select a replacing rename or a weaker copy fallback.
63
+
64
+ ## Candidate v2 disk format
65
+
66
+ V2 would be an additive, explicitly selected API with a dedicated store root,
67
+ not an option that silently changes the existing helpers. Proposed layout:
68
+
69
+ ```text
70
+ store/
71
+ .fs-safe-queue-v2/
72
+ format.json
73
+ state.json
74
+ objects/
75
+ <object-token>/
76
+ payload.json
77
+ .state-transfer.lock
78
+ ```
79
+
80
+ `format.json` identifies protocol `fs-safe-json-queue`, integer version `2`, and
81
+ a random store ID. `state.json` repeats those values, has a monotonically
82
+ increasing decimal-string revision, and contains queue/ID slots plus a shared
83
+ failed index. Queue names and IDs use validated portable segments; object tokens
84
+ are library-generated and never interpreted as caller-supplied paths.
85
+
86
+ Each queue/ID slot has nullable `pending` and `claimed` references. A reference
87
+ contains a logical delivery-generation token and an object token. Migration can
88
+ replace the object while retaining the delivery generation. Failed slots contain
89
+ the originating queue, ID, delivery generation, and object reference. All queues
90
+ sharing a failed namespace must live in this same store and share its state
91
+ lock. Two separately configured stores cannot advertise one shared failed index.
92
+
93
+ Payload objects are immutable. They remain ordinary JSON, not envelopes exposed
94
+ to the caller. Their unique, exclusively created directories distinguish
95
+ independent equal-byte enqueues. Tokens select protocol records; they never
96
+ replace exact bigint filesystem identity, no-follow admission, or pinned-file
97
+ checks. Reused tokens, references to missing objects, unexpected links, malformed
98
+ state, unknown versions, and inconsistent store IDs fail closed.
99
+
100
+ Opening v2 requires both valid format and state files and an admitted dedicated
101
+ root. Merely finding a directory, an empty file, or a partial initialization is
102
+ not permission to adopt or delete it. Initialization exclusively creates the
103
+ namespace, writes and synchronizes its files and directory edges before success,
104
+ and leaves interrupted/ambiguous initialization for explicit recovery. An
105
+ existing v1 directory remains v1; absence of a marker never means an empty v2
106
+ store. No v2 file uses a top-level legacy queue filename.
107
+
108
+ ## State transitions and persistence ordering
109
+
110
+ All v2 producers, consumers, migrations, and collectors take the same store
111
+ transfer lock. Start with a coarse lock and bounded state/object sizes; splitting
112
+ it requires a separate transaction design. Stale locks require operator recovery
113
+ after proving the owner is gone. No timeout grants ownership automatically.
114
+ Callbacks run outside the lock with their payload descriptor retained.
115
+
116
+ Every state transaction re-admits the root, format, state, and referenced objects,
117
+ including exact identity checks around awaited work. It writes a private sibling
118
+ state temp, synchronizes that file, atomically replaces the admitted state file,
119
+ then strictly synchronizes its parent before reporting success. Replacement has
120
+ the same documented platform confinement limits as existing guarded writes; a
121
+ format token or cooperative lock cannot upgrade those limits.
122
+
123
+ Before publishing a new object reference, synchronize the complete payload file,
124
+ its object directory, and the new edge in `objects/`. Persist store initialization
125
+ edges first. No mutable state may refer to an incomplete object. Before consuming
126
+ a state observed after an interrupted commit, synchronize the state parent under
127
+ the lock. A visible post-rename sync failure is uncertain completion: retain all
128
+ potentially referenced objects, report the error, and retry recovery. Never undo
129
+ the transaction by deleting its payload.
130
+
131
+ | Operation | One atomic state replacement |
132
+ |---|---|
133
+ | Enqueue | Set `pending` to a newly persisted object and delivery generation; retain `claimed`. A later enqueue may supersede this pending reference. |
134
+ | Load without a claim | Move the pending reference to `claimed` and clear `pending`; synchronize before returning bytes. |
135
+ | Load with a claim | Keep the same reference and retry that generation before any pending replacement. |
136
+ | Acknowledge | After processing succeeds, clear only the expected claimed generation. A retry with an absent claim succeeds; a different claim rejects. Never clear pending. |
137
+ | Quarantine | Require an absent failed slot, add its claim reference, and clear that claim in the same state update. An identical origin/generation/object reference is an idempotent retry; any other occupant is a collision. |
138
+ | Migrate | After the callback, require both the delivery generation and original object reference still match the claim; publish a fully persisted replacement object and change only that claim's object reference. |
139
+
140
+ The failed index is terminal inspection evidence, not pending work. It remains
141
+ create-only even across different queues in the store. A loader never scans
142
+ objects to infer pending work. The authoritative state alone determines whether
143
+ an object is pending, claimed, quarantined, superseded, or acknowledged.
144
+
145
+ Garbage collection holds the same lock, first completes parent synchronization,
146
+ and removes only objects unreachable from the committed state. It must preserve
147
+ objects pinned by active readers/migration callbacks; uncertain ownership or
148
+ unverifiable pin lifetime defers collection. Version 2 can initially omit
149
+ collection entirely. Leaked unreferenced objects are preferable to lost entries.
150
+ There is no age-based deletion of potentially referenced data.
151
+
152
+ ## Crash-point table
153
+
154
+ This table is the proposed protocol invariant, **not implementation proof**.
155
+ It assumes cooperative v2 participants, an already initialized store, atomic
156
+ whole-file replacement, a filesystem honoring successful file/directory syncs,
157
+ and recovery after proven-stale lock removal. Filesystem corruption, concurrent
158
+ hostile mutation, and an unavailable durability mechanism remain explicit errors.
159
+ Power-loss behavior on each supported Android filesystem needs qualification.
160
+
161
+ For a durably accepted generation that has not been superseded, acknowledged,
162
+ or quarantined, recovery must find exactly one authoritative pending or claimed
163
+ reference. Physical orphan objects are never redelivered. Quarantine and
164
+ acknowledgement are deliberate terminal states, not exceptions silently counted
165
+ as pending. Processing can repeat if its external side effect precedes durable
166
+ acknowledgement; this is the existing at-least-once boundary.
167
+
168
+ | Crash after / during | Recovery and surviving generation |
169
+ |---|---|
170
+ | Acquiring a lock, before any transaction | Prior state stays authoritative; stale lock blocks until explicit recovery. |
171
+ | Exclusive object directory creation or partial payload write | No state reference exists. Prior pending/claimed state survives; enqueue has not succeeded. Never expose the incomplete object. |
172
+ | Payload file sync, before all object-directory edges sync | Same as above; the object is not yet eligible for publication. |
173
+ | All object syncs, before state replacement | Complete but unreferenced object is retained or later collected; prior state survives. |
174
+ | Partial state-temp write or its file sync | Prior state survives. Temp contents never authorize delivery or cleanup. |
175
+ | State replacement, before parent sync | After restart, admit the complete old or new state and sync its parent before acting. Retain objects needed by either outcome until recovery. No operation has reported durable success. Missing/invalid state is a hard failure, never an empty queue. |
176
+ | Enqueue parent sync, before response | New generation is pending (or subsequently superseded under the ordinary same-ID contract). Producer may retry after an uncertain response; no exactly-once enqueue is promised. |
177
+ | Claim replacement/sync, before returning payload | Old state has pending; new state has the same generation claimed. Recovery retries the claim first. Never both. |
178
+ | Read or callback, before ack | Claim remains; side effects may repeat. A concurrent enqueue changes only pending. |
179
+ | Migration object persistence, before state commit | Original claim survives; new unreferenced object is not delivered. |
180
+ | Migration commit/sync, before response | Old or new object belongs to the same claimed delivery generation; stale callbacks cannot overwrite the new object reference. |
181
+ | Ack replacement, before parent sync | Old state retains the claim and may redeliver; new state has an acknowledged generation. A newer pending reference survives both. |
182
+ | Ack parent sync, before response | Acknowledged generation is absent; retry never clears a different claim. Objects left behind cannot resurrect it. |
183
+ | Quarantine replacement, before parent sync | Old state retains the claim; new state has only terminal failed evidence for it. No intermediate state removes both references or exposes both as deliverable. |
184
+ | Quarantine parent sync, before response | Failed reference remains; identical retry succeeds, different-origin collision preserves both generations. |
185
+ | Collection unlink or directory sync | Only unreferenced, unpinned garbage is affected; pending/claimed/failed objects stay intact. |
186
+
187
+ ## Mixed-version behavior and cutover
188
+
189
+ An old writer cannot be made to honor a new marker or lock by updating the
190
+ consumer. For example: read legacy generation A, copy it into v2, then let an
191
+ old writer replace `.json` with B. Removing `.json` can now delete B; leaving
192
+ it can deliver A twice through independent protocols. Equality of bytes does
193
+ not distinguish these events. Watching for a quiet interval cannot prove the
194
+ old writer will not publish later.
195
+
196
+ | Participants | Permitted behavior |
197
+ |---|---|
198
+ | Old and upgraded clients on v1 | Keep the current v1 protocol and error codes. Upgraded clients do not auto-convert or copy around refused links. |
199
+ | All cooperating clients on a new v2 store | Candidate state protocol above; requires an explicit new API and validation before implementation. |
200
+ | Old v1 writers alongside an isolated v2 store | Namespace coexistence only: separate backlogs. V2 must neither delete nor adopt v1 files. Old writers' work is not silently included in v2 success/drain results. |
201
+ | Old writers with a requested single merged backlog on link-refusing storage | Unsupported. Report the capability/migration failure and preserve legacy evidence; do not claim the queue was drained. |
202
+ | Quiesced legacy queue | A future explicit importer could retain migration receipts and copy under exclusive ownership. No importer is specified or authorized by this proposal. |
203
+
204
+ Old batch loaders do not recurse into the v2 namespace, but that is only format
205
+ separation, not a guarantee that an application shares a logical queue safely.
206
+ Applications must stop and fence old producers/consumers before any destructive
207
+ cutover. A directory scan, PID timeout, or absence of a current lock is not that
208
+ fence. Deployments unable to stop old writers need a qualified native primitive
209
+ route that continues the v1 ownership protocol.
210
+
211
+ ## Implementation gates and decision
212
+
213
+ Do not retrofit this candidate into `writeJsonDurableQueueEntry()` or the
214
+ existing path-based lifecycle helpers. A separate proposal must settle the
215
+ public API, callback path semantics, expected-generation receipts, bounded state
216
+ growth, active-reader collection, and fail-closed initialization recovery.
217
+ Arbitrary `processingPath`/`deliveredPath`, shared external `failed/<id>.json`,
218
+ and cross-device destinations cannot be represented by this single-store
219
+ transaction without another protocol. They must retain v1 behavior, not acquire
220
+ a silently weaker fallback. V2 is not an implementation of those contracts.
221
+
222
+ Before implementing an approved scope, require deterministic crashes before and
223
+ after every mutation and sync above, subprocess termination/restart tests,
224
+ identical-payload distinct-generation tests, stale migration/ack tests, failed
225
+ index collisions across queues, invalid-format/identity tests, and initialization
226
+ and garbage-collection failures. Run a link-refusal matrix for `EACCES`, `EPERM`,
227
+ `EXDEV`, `ENOTSUP`, `EOPNOTSUPP`, and `ENOSYS` in `off`, `auto`, and `require`.
228
+ V1 must preserve its typed failures; v2 must make no ownership link attempt.
229
+ Use real Linux seccomp denial of `link`/`linkat` and Android/Termux filesystem
230
+ crash/durability qualification as separate evidence, not substitutes for one
231
+ another. Native-capable operations must retain their native security mechanism;
232
+ any performance change needs measured native/off comparison with an A/A control.
233
+
234
+ **Decision: retain the existing fail-closed implementation and land this design
235
+ only.** Transparent link-free v1 support with concurrent old writers has not
236
+ been established. A separately scoped v2 API or qualified native no-replace
237
+ implementation remains follow-up work; neither is required for 0.26.3.
@@ -1,6 +1,8 @@
1
1
  # Quickstart
2
2
 
3
- Five minutes. By the end you will have a working `root()` and know how to read, write, atomically replace, and unpack an archive — without your code being able to escape the workspace.
3
+ Build a `root()` and use it to read, write, atomically replace, and unpack an
4
+ archive under a trusted workspace. Confinement strength and concurrent-mutation
5
+ limits depend on the operation and platform; see the [security model](security-model.md).
4
6
 
5
7
  If you have used Go's `os.Root` / `OpenInRoot` or Rust's [`cap-std`](https://github.com/bytecodealliance/cap-std), this is the same shape: a capability-style handle that carries the boundary across every operation. The first thing to internalize is that you stop reasoning about *paths* and start reasoning about *the handle*.
6
8
 
@@ -31,7 +33,9 @@ await fs.write("notes/today.txt", "hello\n");
31
33
  const text = await fs.readText("notes/today.txt");
32
34
  ```
33
35
 
34
- Writes use a sibling temp file plus `rename`, so a partial write never appears at the destination. Reads open with `O_NOFOLLOW` where available and verify the opened fd matches the path identity before returning the buffer.
36
+ Replacement writes use a sibling temp file plus `rename`, so a partial write
37
+ never appears at the destination. Reads open with `O_NOFOLLOW` where available
38
+ and verify the opened fd matches the path identity before returning the buffer.
35
39
 
36
40
  `create()` is the don't-clobber variant of `write()` and throws `already-exists` when the target is already there:
37
41
 
@@ -39,6 +43,13 @@ Writes use a sibling temp file plus `rename`, so a partial write never appears a
39
43
  await fs.create("notes/README.md", "seed\n"); // throws if it already exists
40
44
  ```
41
45
 
46
+ Default create-only writes can expose incomplete content on the JavaScript
47
+ fallback. Pass `{ atomic: true }` to stage complete content before publication.
48
+ When hardlinks are refused, staged publication can still expose an empty
49
+ destination placeholder before replacing it with the completed file. See
50
+ [atomic creation](writing.md#atomic-buffered-creation) and
51
+ [publication without hardlinks](writing.md#publication-without-hardlinks).
52
+
42
53
  ## 3. JSON, with parsing
43
54
 
44
55
  ```ts
@@ -60,7 +71,16 @@ await fs.move("notes/today.txt", "notes/archive/today.txt", { overwrite: true })
60
71
  await fs.remove("notes/archive/today.txt");
61
72
  ```
62
73
 
63
- `move()` defaults to no clobber. That mode requires the native helper so a concurrent target cannot be replaced between an absence check and the rename; without it, the call fails with `helper-unavailable`. Pass `{ overwrite: true }` when replacing the target is intentional. `remove()` removes files and empty directories by default. To remove a non-empty directory, pass `{ recursive: true }`; use `maxEntries`, `maxDepth`, and `signal` to bound the work. See [`root()`](root.md) for removal ordering, limits, and partial-removal semantics.
74
+ `move()` defaults to no clobber and requires native support. Linux `auto` can
75
+ fall back when no-replace rename is unsupported; its directory fallback can
76
+ replace a concurrently created empty directory. Use native `require` when
77
+ that must fail closed. See the [move contract](writing.md#fs-move-from-to-options).
78
+ Pass `{ overwrite: true }` when replacement is intended.
79
+
80
+ `remove()` removes files and empty directories by default. For a non-empty
81
+ directory, pass `{ recursive: true }`; use `maxEntries`, `maxDepth`, and `signal`
82
+ to bound the work. See [`root()`](root.md) for removal ordering, limits, and
83
+ partial-removal semantics.
64
84
 
65
85
  ## 5. Inspect
66
86
 
@@ -81,6 +81,12 @@ For tighter control over malformed-or-missing JSON, use the standalone helpers i
81
81
 
82
82
  Returns a `FileHandle` plus `containment: "best-effort"`, the verified `realPath`, and `stat`. Use this for streaming or partial reads, and **always close the handle**:
83
83
 
84
+ `open()` accepts `RootOpenOptions`: the read symlink and hardlink policies,
85
+ without `maxBytes`. It does not consume bytes or apply the Root's read-size
86
+ budget to later handle or stream operations. Bound that I/O in the caller, or
87
+ use `read()` / `readBytes()` when the library should enforce the whole-file cap.
88
+ The result also supports `await using` for automatic handle cleanup.
89
+
84
90
  ```ts
85
91
  const opened = await fs.open("large.log");
86
92
  try {
@@ -103,9 +109,9 @@ type RootReadOptions = {
103
109
  };
104
110
  ```
105
111
 
106
- `maxBytes` is enforced eagerly: the library reads up to `maxBytes + 1` and throws `too-large` if there is more, so a hostile target cannot silently exhaust memory. Values must be non-negative safe integers or positive `Infinity`; zero is an active cap, while `Infinity` disables it. Explicitly forwarding `undefined` preserves the Root default.
112
+ For the byte-consuming read methods, `maxBytes` is enforced eagerly: the library reads up to `maxBytes + 1` and throws `too-large` if there is more, so a hostile target cannot silently exhaust memory. Values must be non-negative safe integers or positive `Infinity`; zero is an active cap, while `Infinity` disables it. Explicitly forwarding `undefined` preserves the Root default.
107
113
 
108
- Safe reads always add the platform's nonblocking open flag where available so a raced FIFO cannot pin a worker indefinitely; regular-file descriptor reads retain normal Node behavior.
114
+ Safe reads always add the platform's nonblocking open flag where available so a raced FIFO cannot pin a worker indefinitely. Whole-file reads continue after short chunks, including on WSL 9p mounts; a short read alone is not treated as EOF. This also applies when the byte cap is disabled.
109
115
 
110
116
  ## `readAbsolute()` and `reader()`
111
117
 
@@ -175,6 +181,44 @@ try {
175
181
  }
176
182
  ```
177
183
 
184
+ ## Synchronous open batches
185
+
186
+ `createRootFileOpenBatchSync()` from `@openclaw/fs-safe/advanced` returns an
187
+ explicitly scoped batch for repeated `openRootFileSync` calls:
188
+
189
+ ```ts
190
+ import fs from "node:fs";
191
+ import { createRootFileOpenBatchSync } from "@openclaw/fs-safe/advanced";
192
+
193
+ using batch = createRootFileOpenBatchSync();
194
+ for (const absolutePath of sourceFiles) {
195
+ const opened = batch.openFile({ rootPath: sourceRoot, absolutePath, boundaryLabel: "source" });
196
+ if (!opened.ok) throw opened.error;
197
+ try {
198
+ consume(fs.readFileSync(opened.fd));
199
+ } finally {
200
+ fs.closeSync(opened.fd);
201
+ }
202
+ }
203
+ ```
204
+
205
+ `openFile()` accepts `OpenRootFileSyncParams` and returns the same
206
+ `RootFileOpenResult` as `openRootFileSync()`. On Windows, canonical drive paths
207
+ with the default symlink rejection reuse a native root descriptor within the
208
+ batch. Native opens reject every reparse component beneath that root. Each open
209
+ still checks the root and current leaf identities, applies its hardlink and
210
+ byte-limit policy, and runs the final root/file/root confinement fence. Replacing
211
+ an admitted root rejects later native opens; file contents and file identities
212
+ are never cached. Other platforms, unavailable native support, other path
213
+ spellings, explicit canonical-root hints, custom filesystem adapters, and
214
+ symlink-following policies retain the ordinary per-open admission path.
215
+
216
+ Successful opens transfer descriptor ownership to the caller. `close()` and
217
+ `Symbol.dispose` release retained root descriptors, are idempotent, and do not close
218
+ previously returned descriptors. Opening after close returns a validation
219
+ failure with code `invalid-path`. Keep batches scoped to one operation rather
220
+ than retaining them as a process-wide filesystem cache.
221
+
178
222
  ## Common errors
179
223
 
180
224
  - **`outside-workspace`** — relative path escaped the root, or `readAbsolute` got an absolute path outside.
@@ -193,3 +237,13 @@ See [Errors](errors.md) for the full list.
193
237
  - [Writing](writing.md) — companion verbs for produce-side I/O.
194
238
  - [JSON files](json.md) — standalone strict/lenient JSON helpers.
195
239
  - [Secure file reads](secure-file.md) — pinned absolute file reads with permission checks.
240
+
241
+ ## Exact identity metadata
242
+
243
+ `OpenResult`, `WritableOpenResult`, `ReadResult`, and advanced `openRootFile()` /
244
+ `openRootFileSync()` results include immutable bigint `exactIdentity` alongside
245
+ `stat: Stats`. The legacy stat and companion derive from the same descriptor
246
+ observation. Use the companion for later identity checks, especially on Windows;
247
+ retain it rather than re-statting a pathname after closing the handle.
248
+ `readRegularFile()` / `readRegularFileSync()`, `statRegularFile()` /
249
+ `statRegularFileSync()`, and `readSecureFile()` follow the same rule.
@@ -72,6 +72,8 @@ re-inspection without reopening; persistent unknowns or replacements throw
72
72
 
73
73
  Throws `FsSafeError` with code `too-large` if the file exceeds `maxBytes`. Other I/O errors propagate as `NodeJS.ErrnoException`.
74
74
 
75
+ Reads continue after short filesystem chunks even when `maxBytes` is omitted. Nonblocking opens remain enabled to reject raced FIFOs without hanging.
76
+
75
77
  ### `readRegularFileSync(params)`
76
78
 
77
79
  Synchronous variant. Same shape; the only required field is `filePath`. `maxBytes` is optional.
@@ -27,11 +27,11 @@ import {
27
27
 
28
28
  ## Constants
29
29
 
30
- ```ts
31
- DEFAULT_SECRET_FILE_MAX_BYTES = 16 * 1024; // 16 KiB
32
- PRIVATE_SECRET_DIR_MODE = 0o700;
33
- PRIVATE_SECRET_FILE_MODE = 0o600;
34
- ```
30
+ | Exported constant | Value |
31
+ |---|---|
32
+ | `DEFAULT_SECRET_FILE_MAX_BYTES` | `16 * 1024` (16 KiB) |
33
+ | `PRIVATE_SECRET_DIR_MODE` | `0o700` |
34
+ | `PRIVATE_SECRET_FILE_MODE` | `0o600` |
35
35
 
36
36
  The 16 KiB cap is intentionally aggressive — credentials should be small. If you need bigger, pass `maxBytes` explicitly.
37
37
 
@@ -29,7 +29,7 @@ The helper:
29
29
 
30
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 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.
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, prebuilt .NET Framework helper to inspect the borrowed file handle. This route requires the [Windows security fallback prerequisites](install.md#windows-security-fallback), including permission to run the executable 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 starts a fresh helper process for each inspection.
33
33
 
34
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
35
 
@@ -91,6 +91,18 @@ publication, and cleanup. File writers preserve the raw suffix. Root-relative
91
91
  APIs and caller-constructed relative directory receipts continue to reject
92
92
  drive designators.
93
93
 
94
+ ### Portable Windows security transport
95
+
96
+ The portable fallback runs a prebuilt .NET Framework helper for each security
97
+ check. Node passes the borrowed fd as the child's stdin; C# inspects that exact
98
+ handle using `GetStdHandle(-10)`. Replacement descriptors and every verification
99
+ across callbacks, publication, link-count changes, and chmod retain the same
100
+ ordering and ownership boundaries as the native fallback protocol. No facts are
101
+ cached between calls. A persistent helper cannot inherit later-opened fds, and
102
+ portable Node does not expose the OS HANDLE or `DuplicateHandle` needed to send
103
+ them. See the [reproducible build](native.md#windows-helper-build) and
104
+ [execution policy requirements](install.md#windows-security-fallback).
105
+
94
106
  ### Exclusive creation on Windows
95
107
 
96
108
  Windows Node exclusive creation can follow a dangling file symlink before an
@@ -382,3 +394,26 @@ public result types and containment labels are unchanged.
382
394
  ## Reporting issues
383
395
 
384
396
  Suspected security issues belong in private disclosure first. See [`SECURITY.md`](https://github.com/openclaw/fs-safe/blob/main/SECURITY.md) in the repo for the current contact path.
397
+
398
+ ## Exact receipts and legacy numeric metadata
399
+
400
+ Identity comparisons require bigint or safe-integer components. Unsafe Number
401
+ IDs remain rejected: converting a rounded Number to bigint cannot recover the
402
+ lost bits. This matters on NTFS, whose file IDs can exceed Number precision.
403
+
404
+ Public open/read results, standalone publication results, directory durability
405
+ receipts, and temp-workspace receipts carry `exactIdentity`. Publication failures
406
+ carry `exactTargetIdentity` when the created target is known. Retain and pass
407
+ those exact companions back to identity-checking APIs. Existing numeric fields
408
+ keep their types and serve as legacy metadata views. Both views come from the
409
+ same admitted observation, never an extra pathname stat to recover identity.
410
+ `sameFileIdentity()` and standalone publication prefer an explicit
411
+ `exactIdentity` on a whole receipt. They do not guess between nested source,
412
+ target, directory, or staged identities.
413
+
414
+ `Root.stat()`, typed `list()`, and `entries()` expose a non-enumerable
415
+ `exactIdentity`, preserving their established numeric JSON representation.
416
+ Object spread and JSON serialization omit that field: retain the returned object
417
+ or copy its exact companion explicitly. Other bigint receipt fields also require
418
+ an application-defined encoding for JSON. Exact receipts record identity at an
419
+ observation; they add no atomic rename, deletion, ownership, or durability guarantee.
@@ -1,5 +1,7 @@
1
1
  # File lock
2
2
 
3
+ For [Linux kernel write leases](platform-capabilities.md#linux-write-leases), `FileWriteLease.isHeld() === false` means “do not act on the file,” including while the kernel is still processing a lease break. The caller’s `SIGIO` handler still owns break notification.
4
+
3
5
  `acquireFileLock()` and `withFileLock()` provide a cross-process file lock with retry and process-exit cleanup. The lock is implemented as a sidecar file (e.g. `state.json` ↔ `state.json.lock`) — only one acquirer can create the sidecar with `O_CREAT | O_EXCL` at a time.
4
6
 
5
7
  On Windows, both the target and an explicitly supplied `lockPath` reject NTFS
@@ -72,21 +74,29 @@ The raw sidecar bytes are not a canonical JSON representation: tools that trim o
72
74
  ## API
73
75
 
74
76
  ```ts
75
- function acquireFileLock<TPayload>(
77
+ import type {
78
+ FileLockAcquireOptions,
79
+ FileLockHandle,
80
+ FileLockManager,
81
+ FileLockSyncAcquireOptions,
82
+ FileLockSyncHandle,
83
+ } from "@openclaw/fs-safe/file-lock";
84
+
85
+ declare function acquireFileLock<TPayload extends Record<string, unknown>>(
76
86
  targetPath: string,
77
87
  options: FileLockAcquireOptions<TPayload>,
78
88
  ): Promise<FileLockHandle>;
79
89
 
80
- function withFileLock<T, TPayload>(
90
+ declare function withFileLock<T, TPayload extends Record<string, unknown>>(
81
91
  targetPath: string,
82
92
  options: FileLockAcquireOptions<TPayload>,
83
93
  fn: () => Promise<T>,
84
94
  ): Promise<T>;
85
95
 
86
- function createFileLockManager(key: string): FileLockManager;
96
+ declare function createFileLockManager(key: string): FileLockManager;
87
97
 
88
- function acquireFileLockSync<TPayload>(targetPath: string, options: FileLockSyncAcquireOptions<TPayload>): FileLockSyncHandle;
89
- function withFileLockSync<T, TPayload>(targetPath: string, options: FileLockSyncAcquireOptions<TPayload>, fn: () => T): T;
98
+ declare function acquireFileLockSync<TPayload extends Record<string, unknown>>(targetPath: string, options: FileLockSyncAcquireOptions<TPayload>): FileLockSyncHandle;
99
+ declare function withFileLockSync<T, TPayload extends Record<string, unknown>>(targetPath: string, options: FileLockSyncAcquireOptions<TPayload>, fn: () => T): T;
90
100
  ```
91
101
 
92
102
  `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.
@@ -141,7 +151,7 @@ type FileLockAcquireOptions<TPayload extends Record<string, unknown>> = {
141
151
  shouldReclaim?: (params: {
142
152
  lockPath: string;
143
153
  normalizedTargetPath: string;
144
- payload: Record<string, unknown> | null;
154
+ payload: unknown;
145
155
  staleMs: number;
146
156
  nowMs: number;
147
157
  heldByThisProcess: boolean;
@@ -150,7 +160,7 @@ type FileLockAcquireOptions<TPayload extends Record<string, unknown>> = {
150
160
  lockPath: string;
151
161
  normalizedTargetPath: string;
152
162
  raw: string;
153
- payload: Record<string, unknown> | null;
163
+ payload: unknown;
154
164
  }) => boolean | Promise<boolean>;
155
165
  metadata?: Record<string, unknown>; // attached to heldEntries() output for diagnostics
156
166
  parsePayload?: (raw: string) => unknown;
@@ -542,24 +552,32 @@ holder still alive":
542
552
 
543
553
  ```ts
544
554
  import { kill } from "node:process";
555
+ import { acquireFileLock } from "@openclaw/fs-safe/file-lock";
545
556
 
546
557
  const handle = await acquireFileLock(targetPath, {
547
558
  staleMs: 60_000,
548
559
  payload: () => ({ pid: process.pid }),
549
- shouldReclaim: ({ payload, nowMs, staleMs }) => {
550
- if (!payload) return true;
551
- const pid = Number(payload.pid);
552
- if (!Number.isFinite(pid)) return true;
560
+ shouldReclaim: ({ payload }) => {
561
+ if (!payload || typeof payload !== "object" || !("pid" in payload)) return true;
562
+ const pid = payload.pid;
563
+ if (typeof pid !== "number" || !Number.isSafeInteger(pid) || pid <= 0) return true;
553
564
  try {
554
565
  kill(pid, 0);
555
566
  return false; // process still alive — keep waiting
556
- } catch {
557
- return true; // process gone — fail closed for recovery
567
+ } catch (error) {
568
+ return error !== null && typeof error === "object" &&
569
+ "code" in error && error.code === "ESRCH"; // only a missing process is stale
558
570
  }
559
571
  },
560
572
  });
561
573
  ```
562
574
 
575
+ The observed payload is `unknown`, even when this caller writes a typed payload.
576
+ Validate it before reading fields. Permission failures such as `EPERM` do not
577
+ prove that the holder exited. PID checks assume a shared local PID namespace;
578
+ PID reuse can keep a stale lock waiting. This callback classifies staleness,
579
+ not ownership or permission to remove another holder's lock.
580
+
563
581
  `heldByThisProcess` is true when this manager already holds the lock. A `true` result marks the observed sidecar as stale; `staleRecovery` then decides whether acquisition fails closed or attempts caller-approved removal.
564
582
 
565
583
  ## Stale recovery: guarded `remove-if-unchanged`