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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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
@@ -5,6 +5,17 @@ description: "The platform-specific native bindings, fd-relative beneath model,
5
5
 
6
6
  # Native architecture
7
7
 
8
+ Windows native realpath calls and handle-bound canonical-name checks share one
9
+ native resolver. It keeps DOS-name resolution first and uses normalized NT names
10
+ on the same handle only after `ERROR_ACCESS_DENIED` in an AppContainer. Ordinary
11
+ process errors retain their original code and identity. AppContainer support
12
+ requires the addon; native-off or unavailable membership leaves Node EPERM
13
+ unchanged without a subprocess probe. Drive-root translation is
14
+ uncached and fail-closed; see the [AppContainer limits](permissions.md#advanced-windows-acl-helpers).
15
+
16
+ See [platform filesystem capabilities](platform-capabilities.md) for Linux
17
+ write leases, public Darwin ACL facts, and Windows test fixtures.
18
+
8
19
  `@openclaw/fs-safe` uses native bindings that supply mechanisms Node does
9
20
  not expose directly. The Rust layer is deliberately not a second policy engine.
10
21
  TypeScript owns trusted-root selection, path validation, archive filtering,
@@ -14,12 +25,46 @@ platform syscall sequence that can preserve the boundary.
14
25
 
15
26
  Every operation that has an equivalent safe Node implementation keeps that
16
27
  guarded JavaScript path. Native loading is lazy; installs do not compile Rust,
17
- run postinstall code, or fetch binaries at runtime. Seven exact-version optional
28
+ run postinstall code, or fetch binaries at runtime. Exact-version optional
18
29
  packages are filtered by OS, CPU, and Linux libc, so an installation receives
19
- only its matching prebuilt binding.
30
+ only its matching prebuilt binding. Windows ships both x64 and ARM64 MSVC
31
+ bindings; selection follows the Node process architecture. An x64 Node process
32
+ on Windows ARM64 uses the x64 binding under emulation.
20
33
  Native-only operations fail explicitly
21
34
  instead of substituting a weaker implementation.
22
35
 
36
+ ## FreeBSD 14.4 and newer
37
+
38
+ The FreeBSD x64 and arm64 packages provide native anonymous pipes, descriptor
39
+ close and POSIX canonicalization. The larger Linux/macOS/Windows filesystem backend is excluded
40
+ from these binaries: its Unix gates contain platform-specific resolver, clone,
41
+ copy, observation and cleanup implementations. FreeBSD does not export stubs for
42
+ those operations. In `auto`, each operation selects its existing guarded JavaScript
43
+ or WASM backend; in `require`, a missing required capability fails with
44
+ `helper-unavailable`. Pure JavaScript APIs retain their ordinary behavior.
45
+
46
+ | Native capability | FreeBSD binding |
47
+ |---|---|
48
+ | `createPipe`, `closeOwnedFd`, `canonicalizePath` | Implemented; real pipe, descriptor and pathname syscalls |
49
+ | Beneath open/create/mkdir/link, replace/no-replace rename and identity-fenced rename | Absent; guarded fallback where supported |
50
+ | Directory observations, retained files/symlinks, entry publication, owned-tree and root removal | Absent; guarded fallback or explicit native-only rejection |
51
+ | File/tree clone, native copy/hash, archive inspection/extraction and buffer readers | Absent; JavaScript/WASM fallback where supported |
52
+ | Native watch | Absent; polling fallback in `auto` |
53
+ | Darwin ACL and Windows security/identity methods | Not applicable |
54
+
55
+ The release workflow builds inside FreeBSD 14.4 VMs and checks installed-package
56
+ behavior on the main thread and a real Worker. ARM64 uses QEMU CPU emulation
57
+ with a real FreeBSD kernel. This is not a Linux cross-build runtime test. Build
58
+ locally on FreeBSD with `pnpm native:build:freebsd`; the JavaScript/WASM distribution
59
+ can be prepared separately with `pnpm build`. There is no qualified FreeBSD Bun
60
+ artifact in the runtime matrix. FreeBSD x64 installed-package proof covers npm
61
+ and pnpm. FreeBSD arm64 covers npm only because pnpm does not publish a native
62
+ binary for that target. CI prepares the ARM64 proof harness dependencies on Linux
63
+ and syncs them into the VM; consumer installations and runtime proof run inside
64
+ FreeBSD, without running pnpm there.
65
+ Reopening pipe descriptors through `/dev/fd` requires the standard `fdescfs`
66
+ mount there; the VM proof mounts it before testing reopen, inheritance and EOF.
67
+
23
68
  ## The beneath model
24
69
 
25
70
  A trusted directory descriptor is the capability. Native operations accept
@@ -41,9 +86,23 @@ validated Root handle
41
86
  The TypeScript layer validates and decides. The native layer never decides
42
87
  whether a path, archive entry, mode, owner, or cleanup policy is acceptable.
43
88
 
89
+ Ordinary recursive `Root.mkdir()` calls retain the exact existing parent and
90
+ every opened missing component for the duration of one synchronous native walk.
91
+ Each creation is descriptor-relative, followed by fresh Root, retained-parent,
92
+ child identity and canonical-name checks before the next component. Explicit
93
+ mutation policies, callbacks, private permissions, aliases and bindings without
94
+ the required child-open capability retain their existing admission paths.
95
+
44
96
  - Linux uses `openat2(RESOLVE_BENEATH | RESOLVE_NO_MAGICLINKS)`, fd-relative
45
97
  `mkdirat`/`linkat`/`renameat`/`renameat2`, `FICLONE`, and `copy_file_range`.
46
98
  Without `openat2`, beneath opens use the [guarded fallback](#linux-without-openat2).
99
+ Regular-file copies with `clone: "auto"` also recover when `FICLONE` returns
100
+ `EAGAIN` or `EIO`, as can happen for dirty extents on ZFS. Recovery requires
101
+ successful identity-checked removal of the unpublished exclusive clone target
102
+ before the native ordinary-copy path creates a new stage. `clone: "always"`
103
+ still fails when cloning fails. Ordinary-copy I/O errors, clone normalization
104
+ errors, and cleanup failures remain terminal; generic `EIO` is never a retry
105
+ signal.
47
106
  - macOS 15.4 and newer first use `openat(O_RESOLVE_BENEATH)`; older kernels walk
48
107
  components with `openat(O_NOFOLLOW)` and restart in-root symlinks from the
49
108
  pinned root. Both routes apply an `F_GETPATH` post-open containment detector,
@@ -71,10 +130,21 @@ whether a path, archive entry, mode, owner, or cleanup policy is acceptable.
71
130
  use `GetSecurityInfo`; private directories receive their protected DACL in
72
131
  an exclusive, handle-relative `NtCreateFile` call. Their created handles remain
73
132
  open through ACL and pathname-association checks and own any failure cleanup.
133
+ Admitted, resolved absolute paths use extended-length Win32 spelling for
134
+ pathname calls, including internal staging and public-name verification;
135
+ relative child names remain relative to retained handles. Long paths retain
136
+ the same alias validation, ACL checks, identity fences and cleanup ownership.
137
+ Already-admitted namespace spellings pass through unchanged, including
138
+ filesystem-drive paths using `\\.\`; the encoder does not admit or reject paths.
74
139
  N-API descriptors cross into and out of
75
140
  this layer only through the host executable's paired libuv descriptor bridge;
76
141
  missing or partial exports fail with `ENOTSUP` instead of trying a raw HANDLE
77
142
  or add-on CRT descriptor namespace.
143
+ `Root.copyIn` uses the same ReFS integrity, sparse-file, and cluster-aligned
144
+ block-clone operations as tree cloning, through retained source and destination
145
+ parent handles. The exclusive stage enters the existing guarded publication
146
+ path; transfer failures retain its cleanup receipt. It does not inherit the
147
+ tree API's source metadata policy.
78
148
 
79
149
  The internal macOS `inspectDarwinAcl(fd)` capability reports `absent`, `empty`,
80
150
  or `present` for the opened object's extended ACL. It synchronously borrows the
@@ -269,7 +339,8 @@ not bypass the byte limit.
269
339
  ## Mode semantics
270
340
 
271
341
  See [Native helper policy](native-helper.md#modes) for the `auto`, `require`, and
272
- `off` contracts and cached loader behavior.
342
+ `off` contracts and cached loader behavior. See [What needs the native binding](#what-needs-the-native-binding)
343
+ for the exceptions to fallback availability.
273
344
 
274
345
  `sha256FileSync()` is a synchronous Node implementation in all three modes and
275
346
  does not load the binding. Use asynchronous `sha256File()` for native hashing
@@ -281,8 +352,76 @@ Features without a safe fallback, including no-clobber
281
352
  when native support is absent or off. Staging is currently Linux/macOS only and
282
353
  rejects Windows with `unsupported-platform`.
283
354
 
355
+ For an ordinary direct child, native `Root.mkdir()` retains the already-admitted
356
+ Root directly instead of running a parent-creation walk. A missing portable
357
+ single-component name also skips general alias resolution after a fresh Root
358
+ identity check and no-follow missing-entry observation; retained Root and child
359
+ identity checks still surround creation. Its short descriptor
360
+ open, creation, identity checks, and close are synchronous; the public method
361
+ remains asynchronous. Root and child pathname/canonical identities are checked
362
+ before success. Ordinary portable paths with existing canonical parents retain the admitted
363
+ parent directly. A fresh exact parent snapshot must match its beneath-opened
364
+ descriptor; parent, child and Root name/identity checks still surround creation.
365
+ Ordinary missing parents use the retained descriptor walk described above when
366
+ the binding can create and open a child together. Aliases, private creation,
367
+ mutation policies, callbacks and older bindings retain their existing admission
368
+ paths. Policy-free `ensureRoot()` verifies the
369
+ retained native Root directly. In `off` and `auto`, ordinary `Root.mkdir()` keeps
370
+ its metadata-only creation syscall in the synchronous guard sequence; supplied
371
+ mutation callbacks, mutation policies, and private creation retain their existing
372
+ asynchronous path. Both public methods continue to return promises.
373
+
374
+ Native removal and moves also retain Root synchronously and reuse that descriptor
375
+ when Root is an immediate parent. Nonrecursive removal keeps its own leaf path,
376
+ including fresh policy, identity and post-removal checks; recursive traversal
377
+ retains its directory, mount, budget and cancellation checks. Neither optimization
378
+ changes the descriptor-relative mutation or atomic no-replace mechanism.
379
+
380
+ Ordinary `Root.ensureRoot()` in `require` can combine retained-root opening,
381
+ exact identity/metadata/canonical observations, the final named Root fence and
382
+ checked close in one native call. The initial JavaScript Root identity fence
383
+ remains before native lookup. Verification and close failures retain both errors;
384
+ an unavailable native observer hands its still-open descriptor to the existing
385
+ guarded observation route. Private or policy-bound calls keep their admission.
386
+
387
+ Writable opens without an explicit mutation policy attempt the existing-file
388
+ handle path before creating parents. Policy-bound opens first prepare the parent
389
+ receipt needed to select the file.
390
+ In `require`, missing files and parents still use native confined creation;
391
+ existing files keep their guarded Node handle path, identity fences and hardlink
392
+ checks. Required creation capabilities are checked even when the file exists.
393
+
394
+ For ordinary portable missing-file paths with an existing canonical parent,
395
+ native writable creation retains that parent's exact identity through beneath
396
+ admission. An absence observation selects exclusive native creation; an entry
397
+ that appears concurrently returns to the checked existing-file open. The public
398
+ FileHandle handoff and all post-open identity, link-count and canonical checks
399
+ remain in place. Explicit policies, authority callbacks, aliases and missing
400
+ parents retain their full admission path.
401
+
402
+ Recursive removal in both implementations retains exact directory identities
403
+ at admission. Repeated fences use ordinary numeric stats only when both captured
404
+ identity components are safely representable; larger identities keep bigint
405
+ observations, and unknown Windows components receive the existing bounded exact
406
+ retry. Every directory identity and canonical-path observation remains in place.
407
+
408
+ Native publication retains Root through synchronous descriptor open/close and
409
+ checks canonical staging directories against their retained descriptors. Older
410
+ bindings keep the existing guarded directory verification. Owned-descriptor
411
+ staged buffers up to 64 KiB can write synchronously when no mutation assertion is
412
+ configured; direct-publication buffers, larger buffers, streams, and assertion-bearing writes keep their
413
+ asynchronous path. Publication, permission, and post-operation identity checks
414
+ remain unchanged.
415
+
416
+ Asynchronous temp workspaces create their exclusive child relative to the
417
+ retained cleanup parent when the binding provides that operation. The returned
418
+ descriptor remains owned through the existing mode, parent, ancestry and final
419
+ pathname/descriptor checks. Collisions retry with a fresh random name and never
420
+ adopt or remove the existing directory. Capability probing still completes before
421
+ creation. Synchronous creation and older bindings retain their existing routes.
422
+
284
423
  Windows owner/DACL inspection, private-directory creation, and secure-file
285
- inspection support [PowerShell fallbacks](install.md#windows-security-fallback)
424
+ inspection support [Windows helper fallbacks](install.md#windows-security-fallback)
286
425
  in `auto` and `off`. `require` rejects missing capabilities, and native operation
287
426
  failures are terminal.
288
427
 
@@ -312,6 +451,78 @@ read metadata behavior are unchanged. Missing Windows pathname identity still
312
451
  requires a guarded path reopen and comparison with the original retained file;
313
452
  that fallback does not apply to POSIX no-read modes.
314
453
 
454
+ ### Windows buffered-write cost
455
+
456
+ `Root.write(..., { durable: false })` skips durability synchronization, but still
457
+ stages and atomically replaces the file, admits the existing destination, and
458
+ checks the root, parent, published pathname and retained descriptor identities.
459
+ Raw Node writes truncate the destination in place and are not equivalent work.
460
+
461
+ A [Windows x64 measurement](https://github.com/openclaw/fs-safe/actions/runs/38335844371)
462
+ of source revision `fe216e22` used Node 24.21.0, a 16-logical-processor hosted
463
+ runner, and 23-byte replacements. Across 21 samples of 100 writes, uninstrumented
464
+ median sample means were 1.912 ms in `off` and 1.233 ms in `require`; their raw
465
+ Node controls were 0.246 ms and 0.255 ms. Paired raw/raw A/A ratios were 1.011
466
+ and 1.009. These are observations on that runner, not latency guarantees or a
467
+ same-process comparison between native modes.
468
+
469
+ Separate instrumentation recorded every Node filesystem and native binding call
470
+ over 101 warmed 22-byte replacements. The following times are median cumulative
471
+ microseconds per write for each call family; instrumentation adds overhead and
472
+ these rows must not be substituted for the uninstrumented timings above.
473
+
474
+ | Call family | `off` calls / µs | `require` calls / µs |
475
+ |---|---:|---:|
476
+ | Canonicalize pathname (`realpathSync.native`) | 14 / 797 | 4 / 265 |
477
+ | Exact file and directory metadata (`lstat`, `stat`, `fstat`) | 41 / 365 | 22 / 166 |
478
+ | Node asynchronous open | 2 / 272 | 1 / 74 |
479
+ | Native beneath open: parent, stage, published file | 0 / 0 | 3 / 174 |
480
+ | Native parent observation | 0 / 0 | 1 / 45 |
481
+ | Write payload | 1 / 95 | 1 / 50 |
482
+ | Replace by rename | 1 / 199 | 1 / 173 |
483
+
484
+ The native stage creation accounted for about 141 µs of its beneath-open total;
485
+ the published-file reopen accounted for about 22 µs. Native rename uses
486
+ handle-relative `NtSetInformationFile`, not `MoveFileEx`. Neither mode invoked
487
+ the security helper, queried ACLs, changed mode, or flushed a file in these
488
+ non-private, non-durable writes. Path and identity observations explain much of
489
+ the remaining cost; a hidden helper process or durability flush does not.
490
+
491
+ These measurements did not isolate antivirus/indexer time inside filesystem
492
+ calls and do not establish a universal NTFS lower bound. Storage, pathname depth,
493
+ Windows version and security software can change the result. Repeated identity
494
+ observations fence different admission and publication boundaries; their count
495
+ alone does not establish that an observation can safely be removed.
496
+
497
+ ## What needs the native binding
498
+
499
+ In `off` and missing-binding `auto`, these capabilities are unavailable. Other
500
+ APIs keep their guarded fallback; the [delta table](#javascript-fallback-guarantees-and-delta)
501
+ describes its strength. Installing a binding does not add support on an excluded
502
+ platform or filesystem.
503
+
504
+ | API or option | Binding requirement without a fallback |
505
+ |---|---|
506
+ | `Root.move()` with `overwrite: false` (the default); `replaceDirectoryAtomic()` | `helper-unavailable`; atomic no-replace publication cannot use check-then-rename. |
507
+ | `stageFileInDirectory()`, `retainSymlinkInDirectory()` | Linux/macOS: `helper-unavailable`; other platforms: `unsupported-platform`. |
508
+ | `retainEntryForPublication()` | Linux/macOS/Windows: `helper-unavailable`; other platforms: `unsupported-platform`. |
509
+ | `retainFileInDirectory()` | Windows retention only; returns an unsupported admission with `helper-unavailable` rather than throwing. |
510
+ | `createPipe()` | Linux/macOS/FreeBSD: `helper-unavailable`; other platforms: `unsupported-platform`. |
511
+ | `tryAcquireWriteLease()` | Linux: `helper-unavailable`; other platforms: `unsupported-platform`. |
512
+ | `createCloneSource()`; `copyTree()` or `Root.copyIn()` with `clone: "always"` | `helper-unavailable`; synchronous `copyRootFileSync()` / batch copying with `clone: "always"` reports `unsupported-platform`. Ordinary automatic byte copying remains available. |
513
+ | `readCloneFileMetadata()`; `inspectDarwinAcl()` | macOS: metadata reading throws `helper-unavailable`; ACL inspection returns `{ kind: "unknown", reason: "helper-unavailable" }`. Elsewhere, they return unsupported/unknown facts. |
514
+ | `Root.copyIn()` with `sourceSymlinks: "copy-link"` | macOS/Windows: `helper-unavailable`; Linux/FreeBSD retain the guarded literal-link path. |
515
+ | `watch()` with `mode: "events"` | `helper-unavailable`; `mode: "auto"` uses polling. |
516
+ | Temp workspace/file helpers with `cleanupSafety: "require-bounded"` | `helper-unavailable`; the default `"compatible"` cleanup remains available. |
517
+ | Private creation on macOS | `helper-unavailable` for descriptor ACL admission: `createDirectory[Sync]()` / `createFileSync()` and Root creation with `private: true`, `Root.createJson()` and `Root.ensureRoot()` with `private: true`. |
518
+ | Windows test hooks `holdWindowsSharingLock()`, `setWindowsFileAttributes()`, `readWindowsFileExtents()` | Windows: `helper-unavailable`; other platforms: `unsupported-platform`. |
519
+
520
+ `preserveMetadata: true` does **not** require native support in `auto` or `off`:
521
+ see [copy metadata and source links](writing.md#copy-metadata-and-source-links)
522
+ for timestamp precision and Windows attribute differences. Archive fallbacks
523
+ include bundled WASM, and Windows security fallbacks use the packaged
524
+ prebuilt .NET Framework helper; neither requires the optional native binding.
525
+
315
526
  ## JavaScript fallback guarantees and delta
316
527
 
317
528
  Policy-bound parent creation can refresh exact directory facts through the
@@ -345,20 +556,70 @@ See [Root containment guarantees](security-model.md#containment-guarantees-by-pl
345
556
  | 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. |
346
557
  | 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. |
347
558
  | `rename-noreplace` | Atomic platform no-replace rename; Linux `auto` file publication can use identity-checked, non-crash-atomic link/unlink when unsupported. | Unsupported; no emulation by check-then-rename. |
348
- | Windows DACL read | Direct `GetSecurityInfo`; the public facts API exposes ordered basic allow/deny ACE SIDs, masks, and decoded flags without trust policy. Secure-file reads query the borrowed open descriptor and compare its 32-bit volume serial and 64-bit file-index projection with Node's bigint receipt. | The packaged PowerShell/C# bridge preserves raw facts and inspects the borrowed descriptor for secure reads, with the same Node identity comparison; its command failures reject. Structured .NET pathname reporting retains its separate compatibility query. |
349
- | Windows private directory | Creation-time protected DACL. | The packaged PowerShell/C# bridge applies the protected DACL at creation and retains exact handles through identity validation and failure cleanup. Command failures reject. |
559
+ | Windows DACL read | Direct `GetSecurityInfo`; the public facts API exposes ordered basic allow/deny ACE SIDs, masks, and decoded flags without trust policy. Secure-file reads query the borrowed open descriptor and compare its 32-bit volume serial and 64-bit file-index projection with Node's bigint receipt. | The packaged prebuilt .NET Framework helper preserves raw facts and inspects the borrowed descriptor for secure reads, with the same Node identity comparison; its command failures reject. Structured .NET pathname reporting retains its separate compatibility query. |
560
+ | Windows private directory | Creation-time protected DACL. | The packaged prebuilt .NET Framework helper applies the protected DACL at creation and retains exact handles through identity validation and failure cleanup. Command failures reject. |
350
561
 
351
562
  Use `off` in CI to keep the fallback contract exercised. Use `require` when a
352
563
  deployment depends on the stronger mechanism or a native-only feature; do not
353
564
  infer native loading from timing.
354
565
 
566
+ ## Windows helper build
567
+
568
+ `pnpm build:windows-helper` builds the portable security executable with the
569
+ .NET SDK pinned by `global.json`, Roslyn deterministic compilation, a fixed
570
+ source path map, no debug symbols, AnyCPU and `Prefer32Bit=false`. The project
571
+ under `native/windows-security-helper/` targets .NET Framework 4.6.2 using
572
+ Microsoft's `Microsoft.NETFramework.ReferenceAssemblies` 1.0.3 and its committed
573
+ NuGet lock file; builds restore in locked mode. CI compares Ubuntu and Windows
574
+ SHA-256 hashes to enforce byte-identical **unsigned** output. Release builds
575
+ repeat this comparison before Authenticode signing on an x64 Windows runner.
576
+ The signing manifest records that the helper's pre-signing SHA-256 equals both
577
+ reproducible build hashes; its signed SHA-256 is checked against the exe inside
578
+ the final npm tarball. Timestamped signatures change the distributed bytes, so
579
+ rebuilding source is expected to reproduce the unsigned hash, not the signed
580
+ file. Both Windows addons and the helper must have a valid, timestamped embedded
581
+ signature from OpenClaw Foundation before signed x64/ARM64 runtime tests and
582
+ release collection. See [release prerequisites](https://github.com/openclaw/fs-safe/blob/main/RELEASE-PREREQS.md#windows-authenticode-signing-one-time-maintainer-setup)
583
+ for setup and the publication-free dry run. The main package includes only
584
+ the exe; the three C# source files remain available in the repository. The separate
585
+ `windows-owner-query.cs` compatibility query preserves the former PowerShell
586
+ query's .NET API sequence, normalization, link behavior, and locality rules;
587
+ the raw security bridge remains independent.
588
+
589
+ The 4.6.2 target selects modern `System.IO` path handling on 4.6.2+ runtimes,
590
+ matching Windows PowerShell 5.1. An executable targeting an earlier framework
591
+ gets legacy handling that rejects extended-length `\\?\` paths with
592
+ "Illegal characters in path." The helper still uses only APIs available in
593
+ .NET Framework 4.0 and the same CLR v4.0.30319; it needs no `.exe.config` sidecar.
594
+ Locked restore pins the `Microsoft.NETFramework.ReferenceAssemblies.net462`
595
+ package to 1.0.3 with its content hash. The raw bridge keeps extended paths out
596
+ of .NET normalization and uses `CreateFileW`; its security checks are unchanged.
597
+
598
+ Build output is copied beside the source modules for Vitest and beside the
599
+ compiled modules in `dist/`; both use the same module-relative filename.
600
+ `pnpm build` can skip compilation on non-Windows developer machines without the
601
+ SDK, retaining a previously built source-side copy when present. Packing still
602
+ requires the executable. CI and release assembly install the pinned SDK and
603
+ build it, including package smoke tests on other operating systems.
604
+
605
+ Each invocation still inspects the inherited borrowed handle with
606
+ `GetStdHandle(-10)` and exits. A persistent helper cannot receive replacement
607
+ fds opened later: portable Node exposes neither the underlying OS HANDLE nor
608
+ `DuplicateHandle`. Per-call processes preserve descriptor ownership and all
609
+ existing checks across callbacks, publication, link-count changes, and chmod.
610
+ There is no security-fact cache or pathname substitution for descriptor checks.
611
+
355
612
  ## Loader security
356
613
 
357
614
  Native initialization runs in the async context captured when fs-safe's loader
358
615
  module is evaluated. Import fs-safe during application startup, outside request
359
616
  or lease scopes, so the addon's process-lifetime housekeeping cannot retain the
360
617
  first operation's `AsyncLocalStorage` stores. Importing still does not load the
361
- addon: only the first native operation pays the registration cost. Subsequent
618
+ addon: only the first native operation or explicit
619
+ [`await getFsSafeNativeStatus({ probe: true })`](config.md#getfssafenativestatus-options)
620
+ call pays the registration cost. The `config` subpath defers the internal loader
621
+ until status is requested and shares that loader's cache with native operations.
622
+ Subsequent
362
623
  operations and their callbacks retain their own caller context. Dynamically
363
624
  importing fs-safe for the first time inside a request scope captures that import
364
625
  scope instead; this boundary does not clear an already active import context.
@@ -371,9 +632,19 @@ loader conservatively attempts the glibc package and lets normal module loading
371
632
  fail into `auto` fallback. The loader requires only the package selected from
372
633
  the detected target; it never probes unrelated packages, downloads code, or
373
634
  runs a postinstall step. A missing or incompatible binary
374
- silently selects the JavaScript fallback in `auto`, throws typed
635
+ selects the guarded fallback in `auto`, throws typed
375
636
  `helper-unavailable` in `require`, and is never inspected in `off`. Tests reject
376
637
  `child_process`, `exec`, or `spawn` usage in the loader.
638
+ Windows security/private-creation fallbacks emit an actionable, path-free
639
+ warning; other fallbacks remain silent. See
640
+ [fallback troubleshooting](native-helper.md#troubleshooting-fallback-warnings).
641
+
642
+ ## Anonymous pipes
643
+
644
+ The lean [`pipe` subpath](pipe.md) creates native-owned anonymous pipes. Linux and
645
+ FreeBSD use atomic close-on-exec creation; Darwin reports its non-atomic flag
646
+ setup explicitly. Each end must close through its native owner, including when
647
+ a Node stream uses it in a Worker.
377
648
 
378
649
  ## Related pages
379
650
 
@@ -42,10 +42,51 @@ POSIX remediation strings shell-quote paths with whitespace or metacharacters
42
42
  and protect option-like paths with `--`, so they can be presented as commands
43
43
  without letting the inspected pathname add shell syntax.
44
44
 
45
- `inspectPathPermissions()` follows symlink targets for the effective mode but tells you whether the original path was a symlink. On POSIX it reports owner/group/world bits. On Windows it delegates to the ACL helpers below and also reports `ownerSid` plus `ownerTrusted` when ownership can be verified. `ownerTrusted` is true only for a local volume owned by the current user, LocalSystem, or built-in Administrators; remote filesystems fail closed. This remains a pathname reporting API with the fallbacks described below. `readSecureFile()` obtains descriptor-bound owner/DACL facts for the exact handle it reads, using native support or the packaged PowerShell/C# bridge in `auto` and `off` modes.
45
+ `inspectPathPermissions()` follows symlink targets for the effective mode but tells you whether the original path was a symlink. On POSIX it reports owner/group/world bits. On Windows it delegates to the ACL helpers below and also reports `ownerSid` plus `ownerTrusted` when ownership can be verified. `ownerTrusted` is true only for a local volume owned by the current user, LocalSystem, or built-in Administrators; remote filesystems fail closed. This remains a pathname reporting API with the fallbacks described below. `readSecureFile()` obtains descriptor-bound owner/DACL facts for the exact handle it reads, using native support or the packaged prebuilt .NET Framework helper in `auto` and `off` modes.
46
46
 
47
47
  ## Advanced Windows ACL helpers
48
48
 
49
+ Private Windows DACLs grant full access to the process user, SYSTEM, and
50
+ Administrators. Inside an AppContainer they additionally grant that process
51
+ token's own container SID, with inheritance on directories. Other containers
52
+ and ALL APPLICATION PACKAGES receive no grant. Creation and security inspection
53
+ use the same token identity; a token-query failure rejects the operation.
54
+ Outside an AppContainer the existing descriptors are unchanged. The native
55
+ addon and packaged .NET security helper both enforce this boundary.
56
+
57
+ When an AppContainer cannot obtain DOS volume names, guarded path operations
58
+ require the native addon. fs-safe resolves the same open handle's normalized NT
59
+ name, then verifies its drive-root prefix before returning the DOS spelling.
60
+ UNC names are translated from the NT network namespace. This preserves long-name
61
+ canonicalization and the existing identity and containment checks without
62
+ granting access to other containers. Drive mappings are queried on each operation.
63
+ Canonicalization resolves relative inputs against the process working directory
64
+ before opening, and UNC inputs accept either Windows path separator spelling.
65
+ APIs that require absolute paths, including `readSecureFile()`, retain that requirement. The current
66
+ container SID is trusted during ACL inspection independently of the object's owner.
67
+ Folder-mounted volumes, volume-GUID inputs, and unmatched drive prefixes fail
68
+ closed with `FsSafeError("path-mismatch")` when DOS resolution is denied.
69
+ AppContainer support requires the native addon. In native mode `off`, or without
70
+ native membership support, a denied DOS-name query retains Node's original
71
+ `EPERM` error unchanged. The JavaScript realpath resolver is not a fallback.
72
+ Outside a sandbox, successful DOS resolution and ordinary errors retain their
73
+ existing behavior, including in native mode `off`.
74
+
75
+ Only a denied DOS-name query triggers the canonicalizer's in-process token probe.
76
+ The addon caches immutable AppContainer membership across Workers. Canonicalization
77
+ never starts a helper process. In a container, failure to open the path or query
78
+ its normalized NT name also preserves the original error, so existing contention
79
+ handling remains effective. Token-probe failures are typed.
80
+
81
+ Inside an AppContainer, advanced ACL inspection uses native facts for inherited
82
+ entries and unique full-control allow entries with uniform inheritance flags,
83
+ including fs-safe's private DACL. Operations that require an external helper
84
+ instead fail with `helper-unavailable` when native membership identifies the
85
+ container. Current Node/libuv child-process pipe creation can fail or hang inside
86
+ AppContainers ([nodejs/node#66374](https://github.com/nodejs/node/issues/66374),
87
+ [libuv/libuv#5178](https://github.com/libuv/libuv/issues/5178)); fs-safe must not
88
+ start the .NET helper or a Node helper child there.
89
+
49
90
  The low-level Windows ACL parser and `icacls` command builders live in `@openclaw/fs-safe/advanced`:
50
91
 
51
92
  ```ts
@@ -69,15 +110,18 @@ resolveWindowsUserPrincipal(env);
69
110
  ```
70
111
 
71
112
  The fallback Windows inspector reads the owner and DACL together through one
72
- built-in Windows PowerShell/.NET query. The query addresses its JSON command by
73
- module name and limits module discovery to PowerShell's bundled system modules.
113
+ built-in prebuilt .NET Framework helper query. It runs the packaged
114
+ `windows-security-helper.exe` with arguments `["owner", targetPath]`, without
115
+ starting PowerShell. The query preserves .NET's named-path security lookup and
116
+ ACL normalization, including leaf-link behavior.
74
117
  It returns canonical SIDs and numeric access masks, so Unicode paths and account
75
118
  names do not pass through lossy console display text. `inspectWindowsAcl()` uses native descriptor facts for
76
119
  complete local ACLs with nonzero inherited ACEs (or empty/null DACLs) when the
77
120
  optional Windows binding is available. It applies
78
121
  the same classifier to native facts and the fallback query, returning canonical
79
- SIDs in its `principal` fields with normalized rights tokens. Explicit `env` or
80
- `exec` options retain the query path. Disabled or unavailable native helpers,
122
+ SIDs in its `principal` fields with normalized rights tokens. An explicit
123
+ `exec` option retains the query path; `env` no longer selects a query executable
124
+ or bypasses native inspection. Disabled or unavailable native helpers,
81
125
  remote or incomplete descriptors, leaf symbolic links, and native query errors
82
126
  use the fallback. Explicit ACEs and zero-mask entries also retain the query so
83
127
  .NET continues to own its ACE ordering and normalization.
@@ -89,6 +133,9 @@ result. The query returns SIDs directly; no translation cache is needed.
89
133
  Injected executors must return the same structured success JSON as the built-in
90
134
  query: valid `ownerSid` and `currentUserSid` strings, an explicit boolean
91
135
  `remote`, and complete DACL facts (`complete`, `daclPresent`, and `aces`).
136
+ Pass-through executors can run the supplied helper command and arguments
137
+ directly; canned-JSON executors use the same parser. `env` remains available for
138
+ principal classification and remediation command construction.
92
139
  Missing or nonboolean locality leaves both inspectors unverified; only
93
140
  `remote: false` establishes locality for owner trust. An explicit `remote: true`
94
141
  retains the ACL report but never grants trusted ownership.
@@ -178,7 +225,7 @@ Object-specific and other ACE layouts are not guessed: they are omitted,
178
225
  `unsupportedAceTypes`, allowing a security-sensitive caller to fail closed.
179
226
  Non-Windows systems return `{ status: "unsupported-platform", platform }`.
180
227
  Windows prefers the native binding. In native `auto` or `off` mode, a missing
181
- binding or capability uses the packaged PowerShell/C# bridge with
228
+ binding or capability uses the packaged prebuilt .NET Framework helper with
182
229
  the same raw ACE projection. Native `require` rejects either absence with
183
230
  `FsSafeError("helper-unavailable")` and starts no command. An available native
184
231
  query's failure is terminal. The existing coarse `inspectPathPermissions()` API
@@ -220,13 +267,13 @@ An available native capability runs all queries in one isolated process using
220
267
  the current runtime executable. Its resolved native mode is forwarded explicitly;
221
268
  Node preload and module-search environment overrides are not inherited. An
222
269
  available native query's failure is terminal. In `auto` when the binding or
223
- capability is absent, or in `off`, one packaged PowerShell process reads the
224
- path array from stdin and compiles the existing C# bridge once. Native `require`
270
+ capability is absent, or in `off`, one packaged .NET Framework helper process reads the
271
+ path array from stdin. Native `require`
225
272
  rejects missing support before launching a process. No route launches one
226
273
  process per path or wraps synchronous parent-process queries in promises.
227
274
 
228
275
  `timeoutMs` defaults to 60,000 and applies to the whole process, including
229
- startup and fallback compilation. It must be an integer from 1 through
276
+ helper startup. It must be an integer from 1 through
230
277
  2,147,483,647. Combined stdout and stderr are limited to 16 MiB. Success waits
231
278
  for process exit and both output pipes to close. Timeout and transport failure
232
279
  request termination, then retain the existing one-second settlement grace.
@@ -236,7 +283,7 @@ unconfirmed child can still be reading after rejection; the operation returns
236
283
  no facts and owns no output files or artifact cleanup. Neither timing out nor
237
284
  receiving a successful kill request is reported as confirmed exit.
238
285
 
239
- The PowerShell fallback encodes and budgets each result while collecting it,
286
+ The helper fallback encodes and budgets each result while collecting it,
240
287
  instead of retaining every descriptor graph until the entire batch finishes.
241
288
  An encoded response that would exceed 16 MiB rejects with `too-large` before
242
289
  inspecting later paths. A query failure observed earlier retains its original
@@ -266,7 +313,7 @@ On Windows, this creates the directory and applies a
266
313
  protected owner + LocalSystem + Administrators full-control DACL directly with
267
314
  an atomic security descriptor. The native route launches no command. When its
268
315
  binding or capability is unavailable, native `auto` and `off` modes use the
269
- packaged PowerShell/C# bridge. Both routes retain the parent and exact created-directory handles
316
+ packaged prebuilt .NET Framework helper. Both routes retain the parent and exact created-directory handles
270
317
  through ACL and final pathname validation. If validation fails, it attempts only
271
318
  nonrecursive deletion through the created handle, preserving any pathname
272
319
  replacement. If cleanup also fails, the error retains the original failure and
@@ -300,10 +347,9 @@ pathname-only compatibility shim. Existing Windows permission inspection still
300
347
  retains its structured .NET compatibility fallback.
301
348
 
302
349
  The raw owner/DACL and private-directory fallbacks each emit one path-free
303
- `FS_SAFE_NATIVE_FALLBACK` warning per process. PowerShell startup and C#
304
- compilation add overhead to each call; install the native package for frequent
305
- operations. These routes run the package's readable, fixed scripts under normal
306
- system PowerShell policy; see the [Windows security fallback prerequisites](install.md#windows-security-fallback).
350
+ `FS_SAFE_NATIVE_FALLBACK` warning per process. A fresh prebuilt helper process
351
+ adds startup overhead to each call; install the native package for frequent
352
+ operations. These routes run the packaged executable under normal system policy; see the [Windows security fallback prerequisites](install.md#windows-security-fallback).
307
353
  If command support is unavailable, disallowed, or fails, the operation rejects.
308
354
  Private-directory creation never falls back to inherited permissions.
309
355
  The asynchronous creation command has a 30-second deadline. After a timeout or
@@ -318,6 +364,9 @@ pathname, establish that the earlier operation stopped and verify any existing
318
364
  directory's security. The library does not attempt pathname-based cleanup.
319
365
 
320
366
  Use `createIcaclsResetCommand()` when you need a structured command and argv pair. Use `formatIcaclsResetCommand()` when you only need a remediation string for a user-facing message.
367
+ These builders and `formatPermissionRemediation()` throw `helper-unavailable` in
368
+ a known AppContainer: removing inherited grants could revoke the container's
369
+ own access. Ordinary-process commands remain unchanged.
321
370
 
322
371
  ## Types
323
372
 
@@ -0,0 +1,81 @@
1
+ # Anonymous pipes
2
+
3
+ `@openclaw/fs-safe/pipe` exposes a synchronous, native anonymous pipe for
4
+ one-shot input to a child process. Both ends are reopenable through
5
+ `/proc/self/fd/N` on Linux and `/dev/fd/N` on Darwin and FreeBSD. This supports
6
+ programs that open an inherited descriptor as a pathname.
7
+
8
+ On FreeBSD, reopening through `/dev/fd/N` requires `fdescfs` mounted at `/dev/fd`.
9
+ An administrator can mount it with `mount -t fdescfs fdesc /dev/fd`; pipe creation
10
+ itself does not require that mount.
11
+
12
+ ```ts
13
+ import { createPipe, type OwnedPipeSync } from "@openclaw/fs-safe/pipe";
14
+
15
+ const pipe: OwnedPipeSync = createPipe();
16
+ try {
17
+ // Pass pipe.reader.fd as a numeric child_process spawn stdio entry.
18
+ // Keep both owners alive until their respective users finish.
19
+ } finally {
20
+ pipe.reader.close();
21
+ pipe.writer.close();
22
+ }
23
+ ```
24
+
25
+ The frozen result has `reader` and `writer` members of the existing
26
+ `OwnedFileDescriptorSync` type (`readonly fd: number`, `close(): void`, and
27
+ `[Symbol.dispose](): void`) and `readonly atomicCloseOnExec: boolean`.
28
+ Each end closes idempotently through its native owner. A close attempt consumes
29
+ ownership even if it throws; do not retry with a raw close or use the fd again.
30
+ The pair itself does not have a `close` or disposal method.
31
+
32
+ Linux and FreeBSD create both ends atomically with close-on-exec set.
33
+ Darwin sets the flag on both ends immediately after creation and reports
34
+ `atomicCloseOnExec: false`: a concurrent native fork/exec can inherit an end
35
+ in that short window. No JavaScript async work intervenes, but this is not an
36
+ atomic inheritance guarantee. Partial setup failure closes both ends.
37
+
38
+ Importing the subpath is lazy and does not load the addon or the secret-file
39
+ modules. Calling `createPipe()` requires a compatible native helper; disabled
40
+ or missing helpers throw `FsSafeError` with `helper-unavailable`. Windows and
41
+ other unsupported platforms throw `unsupported-platform`. Native creation
42
+ failures throw `helper-failed` with the original error as `cause`.
43
+ The cause's `code` preserves the native errno, including `EMFILE` (the process
44
+ descriptor limit) and `ENFILE` (the system-wide limit).
45
+
46
+ ## Handing the writer to a Node stream
47
+
48
+ A native-created fd must close through its owner, including in Workers.
49
+ Provide the stream's custom close hook; do not let Node close it directly.
50
+ This is the writer handoff contract: retain the owner in the hook rather than
51
+ detaching its descriptor.
52
+
53
+ ```ts
54
+ import { createWriteStream, write, writev } from "node:fs";
55
+ import { createPipe } from "@openclaw/fs-safe/pipe";
56
+
57
+ const pipe = createPipe();
58
+ const stream = createWriteStream("", {
59
+ fd: pipe.writer.fd,
60
+ fs: {
61
+ write,
62
+ writev,
63
+ close(_fd, callback) {
64
+ try {
65
+ pipe.writer.close();
66
+ callback(null);
67
+ } catch (error) {
68
+ callback(error instanceof Error ? error : new Error(String(error)));
69
+ }
70
+ },
71
+ },
72
+ });
73
+ // After spawning with pipe.reader.fd in the child's stdio:
74
+ pipe.reader.close();
75
+ stream.end("one-shot input");
76
+ ```
77
+
78
+ Once the stream is created, its lifetime owns use of the writer: keep an error
79
+ listener, destroy it on cancellation, and wait for `close` before considering
80
+ the fd released. If stream construction fails, close both owners. Closing the
81
+ last writer delivers EOF; unread bytes remain available to the reader.