@openclaw/fs-safe 0.5.6 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (289) hide show
  1. package/CHANGELOG.md +89 -0
  2. package/README.md +23 -10
  3. package/dist/advanced.d.ts +2 -1
  4. package/dist/advanced.d.ts.map +1 -1
  5. package/dist/advanced.js +1 -0
  6. package/dist/archive-crc32.d.ts +2 -0
  7. package/dist/archive-crc32.d.ts.map +1 -0
  8. package/dist/archive-crc32.js +14 -0
  9. package/dist/archive-deadline.d.ts +3 -0
  10. package/dist/archive-deadline.d.ts.map +1 -1
  11. package/dist/archive-deadline.js +44 -8
  12. package/dist/archive-entry.d.ts.map +1 -1
  13. package/dist/archive-entry.js +1 -0
  14. package/dist/archive-errors.d.ts +1 -0
  15. package/dist/archive-errors.d.ts.map +1 -1
  16. package/dist/archive-errors.js +3 -0
  17. package/dist/archive-input.d.ts.map +1 -1
  18. package/dist/archive-input.js +26 -16
  19. package/dist/archive-kind.js +2 -2
  20. package/dist/archive-limits.d.ts +11 -3
  21. package/dist/archive-limits.d.ts.map +1 -1
  22. package/dist/archive-limits.js +24 -0
  23. package/dist/archive-native.d.ts +3 -2
  24. package/dist/archive-native.d.ts.map +1 -1
  25. package/dist/archive-native.js +31 -9
  26. package/dist/archive-policy.d.ts +2 -0
  27. package/dist/archive-policy.d.ts.map +1 -1
  28. package/dist/archive-policy.js +9 -1
  29. package/dist/archive-read.d.ts.map +1 -1
  30. package/dist/archive-read.js +88 -41
  31. package/dist/archive-staging.d.ts +3 -0
  32. package/dist/archive-staging.d.ts.map +1 -1
  33. package/dist/archive-staging.js +91 -43
  34. package/dist/archive-tar-admission.d.ts +7 -0
  35. package/dist/archive-tar-admission.d.ts.map +1 -0
  36. package/dist/archive-tar-admission.js +43 -0
  37. package/dist/archive-tar-gnu.d.ts +2 -0
  38. package/dist/archive-tar-gnu.d.ts.map +1 -0
  39. package/dist/archive-tar-gnu.js +20 -0
  40. package/dist/archive-tar-header.d.ts +8 -0
  41. package/dist/archive-tar-header.d.ts.map +1 -0
  42. package/dist/archive-tar-header.js +47 -0
  43. package/dist/archive-tar-meta.d.ts +29 -1
  44. package/dist/archive-tar-meta.d.ts.map +1 -1
  45. package/dist/archive-tar-meta.js +135 -19
  46. package/dist/archive-tar-pax.d.ts +8 -0
  47. package/dist/archive-tar-pax.d.ts.map +1 -0
  48. package/dist/archive-tar-pax.js +100 -0
  49. package/dist/archive-tar-runtime.d.ts +4 -0
  50. package/dist/archive-tar-runtime.d.ts.map +1 -1
  51. package/dist/archive-tar-runtime.js +3 -0
  52. package/dist/archive-tar.d.ts.map +1 -1
  53. package/dist/archive-tar.js +8 -2
  54. package/dist/archive-zip-admission.d.ts +7 -0
  55. package/dist/archive-zip-admission.d.ts.map +1 -0
  56. package/dist/archive-zip-admission.js +60 -0
  57. package/dist/archive-zip-count.d.ts +2 -0
  58. package/dist/archive-zip-count.d.ts.map +1 -0
  59. package/dist/archive-zip-count.js +140 -0
  60. package/dist/archive-zip-directory.d.ts +8 -0
  61. package/dist/archive-zip-directory.d.ts.map +1 -0
  62. package/dist/archive-zip-directory.js +224 -0
  63. package/dist/archive-zip-integrity.d.ts.map +1 -1
  64. package/dist/archive-zip-integrity.js +1 -14
  65. package/dist/archive-zip-names.d.ts +12 -0
  66. package/dist/archive-zip-names.d.ts.map +1 -0
  67. package/dist/archive-zip-names.js +99 -0
  68. package/dist/archive-zip-preflight.d.ts +1 -1
  69. package/dist/archive-zip-preflight.d.ts.map +1 -1
  70. package/dist/archive-zip-preflight.js +8 -145
  71. package/dist/archive.d.ts.map +1 -1
  72. package/dist/archive.js +63 -42
  73. package/dist/atomic.d.ts +1 -1
  74. package/dist/atomic.d.ts.map +1 -1
  75. package/dist/bounded-read-stream.d.ts.map +1 -1
  76. package/dist/bounded-read-stream.js +2 -4
  77. package/dist/bounded-read.d.ts.map +1 -1
  78. package/dist/bounded-read.js +3 -10
  79. package/dist/byte-budget.d.ts +5 -0
  80. package/dist/byte-budget.d.ts.map +1 -0
  81. package/dist/byte-budget.js +9 -0
  82. package/dist/file-hash.d.ts.map +1 -1
  83. package/dist/file-hash.js +25 -19
  84. package/dist/file-lock-sync.d.ts.map +1 -1
  85. package/dist/file-lock-sync.js +26 -9
  86. package/dist/file-store-boundary.d.ts.map +1 -1
  87. package/dist/file-store-boundary.js +9 -5
  88. package/dist/file-store-limit.d.ts +2 -0
  89. package/dist/file-store-limit.d.ts.map +1 -0
  90. package/dist/file-store-limit.js +8 -0
  91. package/dist/file-store-sync-write.d.ts.map +1 -1
  92. package/dist/file-store-sync-write.js +38 -8
  93. package/dist/file-store.d.ts.map +1 -1
  94. package/dist/file-store.js +26 -26
  95. package/dist/json-durable-queue-directory.d.ts +2 -0
  96. package/dist/json-durable-queue-directory.d.ts.map +1 -0
  97. package/dist/json-durable-queue-directory.js +20 -0
  98. package/dist/json-durable-queue-ownership.d.ts +14 -0
  99. package/dist/json-durable-queue-ownership.d.ts.map +1 -0
  100. package/dist/json-durable-queue-ownership.js +168 -0
  101. package/dist/json-durable-queue-retirement.d.ts +9 -0
  102. package/dist/json-durable-queue-retirement.d.ts.map +1 -0
  103. package/dist/json-durable-queue-retirement.js +126 -0
  104. package/dist/json-durable-queue-transfer-lock.d.ts +2 -0
  105. package/dist/json-durable-queue-transfer-lock.d.ts.map +1 -0
  106. package/dist/json-durable-queue-transfer-lock.js +19 -0
  107. package/dist/json-durable-queue.d.ts +1 -0
  108. package/dist/json-durable-queue.d.ts.map +1 -1
  109. package/dist/json-durable-queue.js +90 -57
  110. package/dist/json.d.ts.map +1 -1
  111. package/dist/json.js +27 -8
  112. package/dist/local-roots.d.ts.map +1 -1
  113. package/dist/local-roots.js +4 -2
  114. package/dist/native-binding.d.ts +15 -3
  115. package/dist/native-binding.d.ts.map +1 -1
  116. package/dist/native-operations.d.ts +4 -1
  117. package/dist/native-operations.d.ts.map +1 -1
  118. package/dist/native-operations.js +22 -6
  119. package/dist/native-pinned-write-windows.d.ts +8 -0
  120. package/dist/native-pinned-write-windows.d.ts.map +1 -0
  121. package/dist/native-pinned-write-windows.js +92 -0
  122. package/dist/native-pinned-write.d.ts.map +1 -1
  123. package/dist/native-pinned-write.js +136 -127
  124. package/dist/native-staged-file.d.ts +24 -0
  125. package/dist/native-staged-file.d.ts.map +1 -0
  126. package/dist/native-staged-file.js +337 -0
  127. package/dist/native.d.ts.map +1 -1
  128. package/dist/native.js +4 -4
  129. package/dist/opened-realpath.d.ts +2 -0
  130. package/dist/opened-realpath.d.ts.map +1 -1
  131. package/dist/opened-realpath.js +12 -7
  132. package/dist/output-sibling.d.ts.map +1 -1
  133. package/dist/output-sibling.js +11 -110
  134. package/dist/output.d.ts.map +1 -1
  135. package/dist/output.js +4 -2
  136. package/dist/owner-dacl.d.ts.map +1 -1
  137. package/dist/owner-dacl.js +2 -1
  138. package/dist/permission-exec.d.ts +19 -0
  139. package/dist/permission-exec.d.ts.map +1 -1
  140. package/dist/permission-exec.js +57 -11
  141. package/dist/permissions-public.d.ts +1 -1
  142. package/dist/permissions-public.d.ts.map +1 -1
  143. package/dist/permissions-windows.d.ts +3 -0
  144. package/dist/permissions-windows.d.ts.map +1 -1
  145. package/dist/permissions-windows.js +16 -5
  146. package/dist/permissions.d.ts +5 -0
  147. package/dist/permissions.d.ts.map +1 -1
  148. package/dist/pinned-open.d.ts.map +1 -1
  149. package/dist/pinned-open.js +27 -48
  150. package/dist/pinned-write.d.ts +6 -0
  151. package/dist/pinned-write.d.ts.map +1 -1
  152. package/dist/pinned-write.js +26 -37
  153. package/dist/private-directory.d.ts.map +1 -1
  154. package/dist/private-directory.js +3 -2
  155. package/dist/private-temp-workspace.d.ts +3 -1
  156. package/dist/private-temp-workspace.d.ts.map +1 -1
  157. package/dist/private-temp-workspace.js +81 -56
  158. package/dist/publish-file.d.ts.map +1 -1
  159. package/dist/publish-file.js +2 -4
  160. package/dist/read-opened-file.d.ts.map +1 -1
  161. package/dist/read-opened-file.js +6 -4
  162. package/dist/regular-file.d.ts.map +1 -1
  163. package/dist/regular-file.js +136 -82
  164. package/dist/replace-file-copy-fallback.d.ts +3 -1
  165. package/dist/replace-file-copy-fallback.d.ts.map +1 -1
  166. package/dist/replace-file-copy-fallback.js +21 -36
  167. package/dist/replace-file-copy-source.d.ts +21 -0
  168. package/dist/replace-file-copy-source.d.ts.map +1 -0
  169. package/dist/replace-file-copy-source.js +112 -0
  170. package/dist/replace-file-descriptor.d.ts +13 -3
  171. package/dist/replace-file-descriptor.d.ts.map +1 -1
  172. package/dist/replace-file-descriptor.js +32 -7
  173. package/dist/replace-file-rename-policy.d.ts +7 -0
  174. package/dist/replace-file-rename-policy.d.ts.map +1 -0
  175. package/dist/replace-file-rename-policy.js +30 -0
  176. package/dist/replace-file-temp-owner.d.ts +46 -0
  177. package/dist/replace-file-temp-owner.d.ts.map +1 -0
  178. package/dist/replace-file-temp-owner.js +346 -0
  179. package/dist/replace-file.d.ts +6 -1
  180. package/dist/replace-file.d.ts.map +1 -1
  181. package/dist/replace-file.js +72 -58
  182. package/dist/root-impl.d.ts.map +1 -1
  183. package/dist/root-impl.js +118 -98
  184. package/dist/root-paths.d.ts +11 -14
  185. package/dist/root-paths.d.ts.map +1 -1
  186. package/dist/root-paths.js +36 -27
  187. package/dist/root-write-verification.d.ts +11 -0
  188. package/dist/root-write-verification.d.ts.map +1 -0
  189. package/dist/root-write-verification.js +91 -0
  190. package/dist/secret-file.d.ts +1 -6
  191. package/dist/secret-file.d.ts.map +1 -1
  192. package/dist/secret-file.js +49 -120
  193. package/dist/secret-read-async.d.ts +1 -1
  194. package/dist/secret-read-async.d.ts.map +1 -1
  195. package/dist/secret-read-async.js +51 -72
  196. package/dist/secret-read-policy.d.ts +13 -0
  197. package/dist/secret-read-policy.d.ts.map +1 -0
  198. package/dist/secret-read-policy.js +28 -0
  199. package/dist/secret.d.ts +2 -1
  200. package/dist/secret.d.ts.map +1 -1
  201. package/dist/secret.js +2 -1
  202. package/dist/secure-file.d.ts.map +1 -1
  203. package/dist/secure-file.js +42 -29
  204. package/dist/sibling-staged-file.d.ts +15 -0
  205. package/dist/sibling-staged-file.d.ts.map +1 -0
  206. package/dist/sibling-staged-file.js +148 -0
  207. package/dist/sibling-temp.d.ts +3 -0
  208. package/dist/sibling-temp.d.ts.map +1 -1
  209. package/dist/sibling-temp.js +30 -74
  210. package/dist/sidecar-lock-acquire.d.ts.map +1 -1
  211. package/dist/sidecar-lock-acquire.js +48 -27
  212. package/dist/sidecar-lock-handle.d.ts +6 -2
  213. package/dist/sidecar-lock-handle.d.ts.map +1 -1
  214. package/dist/sidecar-lock-handle.js +17 -3
  215. package/dist/sidecar-lock-policy.d.ts +2 -0
  216. package/dist/sidecar-lock-policy.d.ts.map +1 -1
  217. package/dist/sidecar-lock-policy.js +29 -0
  218. package/dist/sidecar-lock-reclaim.d.ts.map +1 -1
  219. package/dist/sidecar-lock-reclaim.js +29 -7
  220. package/dist/sidecar-lock.d.ts.map +1 -1
  221. package/dist/sidecar-lock.js +28 -16
  222. package/dist/staged-directory.d.ts +16 -0
  223. package/dist/staged-directory.d.ts.map +1 -0
  224. package/dist/staged-directory.js +60 -0
  225. package/dist/staged-file-types.d.ts +56 -0
  226. package/dist/staged-file-types.d.ts.map +1 -0
  227. package/dist/staged-file-types.js +1 -0
  228. package/dist/staged-file.d.ts +10 -0
  229. package/dist/staged-file.d.ts.map +1 -0
  230. package/dist/staged-file.js +15 -0
  231. package/dist/strict-file-identity.d.ts +6 -0
  232. package/dist/strict-file-identity.d.ts.map +1 -0
  233. package/dist/strict-file-identity.js +48 -0
  234. package/dist/suppressed-error.d.ts +6 -0
  235. package/dist/suppressed-error.d.ts.map +1 -0
  236. package/dist/suppressed-error.js +15 -0
  237. package/dist/temp-cleanup.d.ts +2 -0
  238. package/dist/temp-cleanup.d.ts.map +1 -1
  239. package/dist/temp-cleanup.js +25 -10
  240. package/dist/temp-workspace-owner.d.ts +23 -0
  241. package/dist/temp-workspace-owner.d.ts.map +1 -0
  242. package/dist/temp-workspace-owner.js +320 -0
  243. package/dist/temp.d.ts +1 -1
  244. package/dist/temp.d.ts.map +1 -1
  245. package/dist/test-hooks.d.ts +5 -0
  246. package/dist/test-hooks.d.ts.map +1 -1
  247. package/dist/windows-owner.d.ts +3 -0
  248. package/dist/windows-owner.d.ts.map +1 -1
  249. package/dist/windows-owner.js +10 -2
  250. package/docs/advanced.md +19 -2
  251. package/docs/archive.md +250 -35
  252. package/docs/atomic.md +11 -2
  253. package/docs/config.md +7 -0
  254. package/docs/contributing.md +45 -7
  255. package/docs/durability.md +18 -5
  256. package/docs/errors.md +16 -1
  257. package/docs/file-store.md +2 -0
  258. package/docs/index.md +3 -1
  259. package/docs/install.md +20 -8
  260. package/docs/json.md +8 -4
  261. package/docs/migrating-to-0.5.md +7 -7
  262. package/docs/migrating-to-0.6.md +43 -0
  263. package/docs/native-helper.md +30 -8
  264. package/docs/native.md +73 -17
  265. package/docs/output.md +10 -0
  266. package/docs/path-scope.md +28 -2
  267. package/docs/permissions.md +13 -2
  268. package/docs/public-api.md +6 -3
  269. package/docs/quickstart.md +1 -1
  270. package/docs/reading.md +1 -1
  271. package/docs/regular-file.md +9 -2
  272. package/docs/root.md +3 -1
  273. package/docs/secret-file.md +12 -0
  274. package/docs/secure-file.md +21 -3
  275. package/docs/security-model.md +33 -1
  276. package/docs/sidecar-lock.md +15 -1
  277. package/docs/staged-file.md +178 -0
  278. package/docs/store.md +13 -0
  279. package/docs/temp.md +128 -14
  280. package/docs/testing.md +1 -1
  281. package/docs/writing.md +19 -0
  282. package/package.json +16 -9
  283. package/dist/native/darwin-arm64/fs-safe-native.node +0 -0
  284. package/dist/native/darwin-x64/fs-safe-native.node +0 -0
  285. package/dist/native/linux-arm64-gnu/fs-safe-native.node +0 -0
  286. package/dist/native/linux-arm64-musl/fs-safe-native.node +0 -0
  287. package/dist/native/linux-x64-gnu/fs-safe-native.node +0 -0
  288. package/dist/native/linux-x64-musl/fs-safe-native.node +0 -0
  289. package/dist/native/win32-x64-msvc/fs-safe-native.node +0 -0
package/docs/install.md CHANGED
@@ -85,19 +85,27 @@ Use the main entry for the common surface, or the focused subpaths when you want
85
85
 
86
86
  ## Runtime dependencies
87
87
 
88
- `@openclaw/fs-safe` lists `jszip` and `tar` as optional dependencies for [archive extraction](archive.md). They are loaded lazily and only required when ZIP/TAR helpers run. Installs that omit optional dependencies can still import and use every non-archive subpath; archive calls fail with a clear missing-optional-dependency message.
88
+ `@openclaw/fs-safe` lists `jszip` and `tar` as optional dependencies for JavaScript ZIP/TAR [archive extraction](archive.md). They are loaded lazily; the JavaScript archive fallback requires the corresponding codec and reports a missing-optional-dependency error without it. Public subpaths remain safe to import with all optional dependencies omitted, but imports do not prove native availability.
89
89
 
90
- There are no peer dependencies. The single npm package bundles all seven native binaries, so consumers do not run a native build, download platform code, or execute a postinstall step. Shipping every target increases the tarball size compared with per-platform packages, intentionally trading bandwidth for deterministic installation.
90
+ There are no peer dependencies. Exact-version optional packages carry the seven
91
+ native targets and npm-compatible OS, CPU, and Linux libc filters install only
92
+ the matching binary. Consumers do not run a native build, download code at
93
+ runtime, or execute a postinstall step. Omitting optional dependencies keeps
94
+ non-archive fallback-capable operations working in `auto` or `off`. Native-only
95
+ features, including strict owned-tree temp cleanup, retained-directory staging,
96
+ atomic `rename-noreplace`, zstd/bzip2 TAR handling, and Windows private-directory
97
+ creation, remain unavailable. Operations needing the binding in `require` mode fail with
98
+ `helper-unavailable` when the matching package is absent or incompatible.
91
99
 
92
- Upgrading an existing consumer? Follow [Migrating to 0.5](migrating-to-0.5.md)
93
- before choosing a native mode or accepting the new archive clamp default.
100
+ Upgrading an existing 0.5 consumer? Follow [Migrating to 0.6](migrating-to-0.6.md)
101
+ before deploying with native mode `require` or native-only features.
94
102
 
95
103
  ## Native helper policy
96
104
 
97
- The bundled native binaries provide fd-relative open/link/mkdir primitives,
105
+ The platform native binaries provide fd-relative open/link/mkdir primitives,
98
106
  atomic no-replace rename, and file identity checks. The default is `auto`: use
99
107
  the matching binary when it loads, otherwise silently keep the guarded
100
- JavaScript path. Platforms without one of the seven bundled targets therefore
108
+ JavaScript path. Platforms without one of the seven published targets therefore
101
109
  continue through the documented fallback in `auto` mode.
102
110
 
103
111
  ```ts
@@ -116,10 +124,14 @@ FS_SAFE_NATIVE_MODE=off # auto | off | require
116
124
 
117
125
  `OPENCLAW_FS_SAFE_NATIVE_MODE` is also accepted.
118
126
 
119
- Disabling native loading keeps the public API working through Node path
127
+ Disabling native loading keeps fallback-capable operations working through Node path
120
128
  operations guarded by lexical and canonical checks plus identity verification.
121
129
  Use `require` when native-backed operations must fail instead of falling back.
122
- The exact boundary is documented in [native helper policy](native-helper.md).
130
+ Temp workspaces retain compatible JavaScript quarantine cleanup in `auto` and
131
+ `off`. Set `cleanupSafety: "require-bounded"` to reject before child creation
132
+ unless native no-replace quarantine and descriptor-bounded tree removal are
133
+ available. See the [temp workspace contract](temp.md#private-temp-workspaces). The exact boundary
134
+ for other operations is documented in [native helper policy](native-helper.md).
123
135
 
124
136
  ## Verify the install
125
137
 
package/docs/json.md CHANGED
@@ -135,10 +135,14 @@ where lower latency matters more than crash-durability.
135
135
 
136
136
  Synchronous variant. It pretty-prints with two spaces, appends a newline,
137
137
  creates parents at `0o700`, writes at `0o600`, and synchronizes the parent
138
- directory best-effort. It has no options bag. On `EPERM`/`EEXIST`, its legacy
139
- compatibility path may replace by copy rather than atomic rename; use the async
140
- `writeJson()`/`replaceFileAtomic()` surfaces when fallback policy must be
141
- explicit.
138
+ directory best-effort. File-mode tightening carries the staged bigint identity
139
+ through rename and applies `fchmod` only when the reopened descriptor and current
140
+ pathname still name that same single-link regular file; a swap is preserved and
141
+ skips this best-effort step. It has no options bag. On `EPERM`/`EEXIST`, its legacy
142
+ compatibility path removes the existing destination and retries the staged-file
143
+ rename, so that fallback is temporarily non-atomic while retaining the staged
144
+ file's `0600` mode; use the async `writeJson()`/`replaceFileAtomic()` surfaces
145
+ when fallback policy must be explicit.
142
146
 
143
147
  ```ts
144
148
  writeJsonSync("./prefs.json", { theme: "dark" });
@@ -8,8 +8,8 @@ description: "Ordered checklist for moving a 0.4 consumer from the Python helper
8
8
  Use this checklist from top to bottom. Version 0.5 replaces the Python worker,
9
9
  changes the default archive mode policy, and adds explicit contracts for
10
10
  publication, walking, locks, secrets, and native-only features. Nothing in this
11
- guide requires a Rust toolchain: all supported native binaries are prebuilt and
12
- bundled in `@openclaw/fs-safe`.
11
+ guide requires a Rust toolchain: all supported native binaries are prebuilt.
12
+ Current releases install the matching exact-version optional platform package.
13
13
 
14
14
  ## 1. Update the package and runtime
15
15
 
@@ -17,10 +17,10 @@ bundled in `@openclaw/fs-safe`.
17
17
  - Update `@openclaw/fs-safe` and regenerate every lock or shrinkwrap file your
18
18
  deployment consumes.
19
19
  - Keep optional dependencies enabled if you want JavaScript ZIP/TAR support.
20
- Native loading no longer depends on optional packages because all seven
21
- binaries ship in `@openclaw/fs-safe`. An install that omits optional packages
22
- can still import fs-safe, but missing JS archive decoders fail with actionable
23
- errors.
20
+ Native loading no longer depends on Python. Keep optional dependencies enabled
21
+ for the matching native package and JavaScript ZIP/TAR decoders. An install
22
+ that omits them can still import fs-safe, but native-only features and missing
23
+ JS archive decoders fail with actionable errors.
24
24
 
25
25
  If you call `resolveRootPath()` or `resolveRootPathSync()` directly, upgrade to
26
26
  0.5: versions through 0.4.7 could approve an in-root symlink traversal that
@@ -42,7 +42,7 @@ configureFsSafeNative({ mode: "auto" });
42
42
  | `configureFsSafePython({ mode })` | `configureFsSafeNative({ mode })` |
43
43
  | `FS_SAFE_PYTHON_MODE` | `FS_SAFE_NATIVE_MODE` |
44
44
  | `OPENCLAW_FS_SAFE_PYTHON_MODE` | `OPENCLAW_FS_SAFE_NATIVE_MODE` |
45
- | `pythonPath`, `FS_SAFE_PYTHON`, pinned-Python aliases | Nothing; bundled native binaries do not use an interpreter |
45
+ | `pythonPath`, `FS_SAFE_PYTHON`, pinned-Python aliases | Nothing; prebuilt native binaries do not use an interpreter |
46
46
 
47
47
  The old names warn once and map `auto`, `off`, or `require` so a shipped 0.4
48
48
  deployment does not silently change policy. Interpreter paths are ignored and
@@ -0,0 +1,43 @@
1
+ ---
2
+ title: Migrating to 0.6
3
+ description: "Upgrade checklist for the platform-native package split."
4
+ ---
5
+
6
+ # Migrating from 0.5 to 0.6
7
+
8
+ Version 0.6 moves native bindings out of the root `@openclaw/fs-safe` tarball
9
+ and into exact-version platform packages. This removes unrelated operating
10
+ system and architecture binaries from each installation.
11
+
12
+ ## Keep optional dependencies enabled for native mode
13
+
14
+ Package managers select one binding by OS, CPU, and Linux libc. A normal
15
+ install needs no command change:
16
+
17
+ ```bash
18
+ pnpm add @openclaw/fs-safe
19
+ ```
20
+
21
+ If a deployment currently installs 0.5 with `--omit=optional`, `--no-optional`,
22
+ or an equivalent lockfile policy, change that policy before upgrading when it
23
+ uses native mode `require` or any native-only feature. Version 0.5 kept its
24
+ binding in the root tarball; version 0.6 intentionally does not.
25
+
26
+ Omitting optional dependencies remains supported for fallback-capable APIs in
27
+ `auto` mode. It disables native-only features such as zstd/bzip2 TAR handling,
28
+ retained-directory staging, atomic `rename-noreplace`, and Windows private
29
+ directory creation. Native mode `require` reports `helper-unavailable` when the
30
+ matching package is absent or incompatible.
31
+
32
+ ## Deployment checklist
33
+
34
+ 1. Remove any option or policy that omits optional dependencies when native
35
+ support is required.
36
+ 2. Regenerate every lockfile or shrinkwrap file consumed by deployment.
37
+ 3. Verify that the lock contains the matching `@openclaw/fs-safe-*` package.
38
+ 4. Run a native-required operation on every deployed OS/libc target.
39
+ 5. Keep an `FS_SAFE_NATIVE_MODE=off` lane when the guarded JavaScript fallback
40
+ is part of the application contract.
41
+
42
+ No Rust toolchain, postinstall build, or runtime download is introduced. See
43
+ [Native helper policy](native-helper.md) for the exact fallback boundary.
@@ -1,11 +1,15 @@
1
1
  ---
2
2
  title: Native helper policy
3
- description: "How fs-safe loads its bundled native filesystem primitives and how auto, require, and off affect guarded fallbacks."
3
+ description: "How fs-safe loads its platform-specific native filesystem primitives and how auto, require, and off affect guarded fallbacks."
4
4
  ---
5
5
 
6
6
  # Native helper policy
7
7
 
8
- `@openclaw/fs-safe` itself contains seven prebuilt binaries for Linux x64/arm64 (glibc or musl), macOS x64/arm64, and Windows x64. The loader selects `dist/native/<target>/fs-safe-native.node` without optional platform packages, downloads, postinstall scripts, or a consumer Rust build. Carrying every target makes the npm tarball larger, but every installation receives the same complete artifact.
8
+ `@openclaw/fs-safe` declares seven exact-version optional packages for Linux
9
+ x64/arm64 (glibc or musl), macOS x64/arm64, and Windows x64. Package-manager
10
+ OS, CPU, and libc filters install only the matching package. The loader requires
11
+ that package lazily, without runtime downloads, postinstall scripts, or a
12
+ consumer Rust build.
9
13
 
10
14
  ```ts
11
15
  import { configureFsSafeNative } from "@openclaw/fs-safe/config";
@@ -21,23 +25,40 @@ The equivalent environment variables are `FS_SAFE_NATIVE_MODE` and `OPENCLAW_FS_
21
25
 
22
26
  | Mode | Behavior |
23
27
  |---|---|
24
- | `auto` | Prefer native primitives when the current bundled binary loads; otherwise silently use the guarded JavaScript path. |
25
- | `off` | Do not load a bundled binary. Use the guarded JavaScript path deterministically. |
28
+ | `auto` | Prefer native primitives when the current platform package loads; otherwise silently use the guarded JavaScript path. |
29
+ | `off` | Do not load a native package. Use the guarded JavaScript path deterministically. |
26
30
  | `require` | Throw `FsSafeError("helper-unavailable")` instead of falling back when an operation needs the native binding and it cannot load. |
27
31
 
28
32
  Configure the mode once during startup. Loading is lazy and cached; changing from `auto` to `require` after a failed load changes failure policy but does not repeatedly probe the binary.
29
33
 
34
+ [`tempWorkspace()` and its scoped/sync variants](temp.md#private-temp-workspaces)
35
+ remain available in every mode. Their default compatible cleanup uses guarded
36
+ JavaScript quarantine when owned native tree removal is unavailable.
37
+ `cleanupSafety: "require-bounded"` instead rejects before child creation unless
38
+ no-replace quarantine plus descriptor-relative owned-tree removal are available.
39
+ On Linux, admission probes the exact `openat2` child-directory flags, including
40
+ `RESOLVE_NO_XDEV`, at runtime. An unavailable or denied probe selects compatible
41
+ JavaScript cleanup even in global `require` mode; `require-bounded` rejects before
42
+ child creation.
43
+ Already-created strict workspaces retain their binding
44
+ and descriptors across later mode changes.
45
+
46
+ [`stageFileInDirectory()`](staged-file.md) always requires native support on
47
+ Linux/macOS and rejects before creation when off, unavailable, or missing the
48
+ required capability. Windows is unsupported for this lifecycle. This does not
49
+ change the mode policy of existing fallback-capable APIs.
50
+
30
51
  ## Native boundary
31
52
 
32
- The bundled native layer exposes policy-free filesystem mechanisms: beneath-root
53
+ The native layer exposes policy-free filesystem mechanisms: beneath-root
33
54
  open/mkdir/link, replace and no-replace rename, identity reads, archive decode/execution,
34
55
  clone/copy/hash workers, and Windows security descriptor calls. The TypeScript
35
56
  layer owns policy, retries, filters, budgets, modes, cleanup, error
36
57
  normalization, and the decision to fall back.
37
58
 
38
- - Linux uses `openat2` with `RESOLVE_BENEATH | RESOLVE_NO_MAGICLINKS`, `renameat`, and `renameat2(RENAME_NOREPLACE)`.
39
- - macOS 15.4 and newer prefer `O_RESOLVE_BENEATH`; older kernels resolve components with `O_NOFOLLOW` and restart in-root symlinks from the pinned root descriptor. Both routes use an `F_GETPATH` post-open escape detector and report `best-effort` because directory rename races are not atomic with that check. Publication uses `renameat` for replacement and `renameatx_np(RENAME_EXCL)` for no-replace.
40
- - Windows uses handle-relative `NtCreateFile`, rejects reparse points, and uses `FileRenameInfoEx` with replacement selected explicitly by the TypeScript policy layer.
59
+ - Linux uses `openat2` with `RESOLVE_BENEATH | RESOLVE_NO_MAGICLINKS`, `renameat`, and `renameat2(RENAME_NOREPLACE)`. Owned-tree cleanup enumerates and unlinks through retained directory descriptors and rejects device crossings.
60
+ - macOS 15.4 and newer prefer `O_RESOLVE_BENEATH`; older kernels resolve components with `O_NOFOLLOW` and restart in-root symlinks from the pinned root descriptor. Both routes use an `F_GETPATH` post-open escape detector and report `best-effort` because directory rename races are not atomic with that check. Publication uses `renameat` for replacement and `renameatx_np(RENAME_EXCL)` for no-replace; owned-tree cleanup uses descriptor-relative `openat`/`unlinkat`.
61
+ - Windows uses handle-relative `NtCreateFile`, rejects reparse points during root-bounded traversal, uses `FileRenameInfoEx` with replacement selected explicitly by the TypeScript policy layer, and deletes owned trees through exact opened handles with `FileDispositionInfoEx`; symlink/reparse entries in owned trees are removed as leaves and never traversed.
41
62
 
42
63
  Native primitives back create-only and replacing pinned writes, async sidecar creation,
43
64
  guarded publication, archive acceleration, and direct Windows ACL operations.
@@ -89,3 +110,4 @@ consumer performs its 0.5 upgrade.
89
110
  - [File locks](sidecar-lock.md)
90
111
  - [Durability](durability.md)
91
112
  - [Migrating to 0.5](migrating-to-0.5.md)
113
+ - [Migrating to 0.6](migrating-to-0.6.md)
package/docs/native.md CHANGED
@@ -1,11 +1,11 @@
1
1
  ---
2
2
  title: Native architecture
3
- description: "The bundled native bindings, fd-relative beneath model, platform mechanisms, loader security, and JavaScript fallback contract."
3
+ description: "The platform-specific native bindings, fd-relative beneath model, platform mechanisms, loader security, and JavaScript fallback contract."
4
4
  ---
5
5
 
6
6
  # Native architecture
7
7
 
8
- `@openclaw/fs-safe` bundles native bindings that supply mechanisms Node does
8
+ `@openclaw/fs-safe` uses native bindings that supply mechanisms Node does
9
9
  not expose directly. The Rust layer is deliberately not a second policy engine.
10
10
  TypeScript owns trusted-root selection, path validation, archive filtering,
11
11
  budgets, modes, identity fencing, cleanup decisions, and error normalization.
@@ -14,8 +14,9 @@ platform syscall sequence that can preserve the boundary.
14
14
 
15
15
  Every operation that has an equivalent safe Node implementation keeps that
16
16
  guarded JavaScript path. Native loading is lazy; installs do not compile Rust,
17
- run postinstall code, or fetch binaries. The npm tarball carries all seven
18
- supported targets, so it is larger than a per-platform package by design.
17
+ run postinstall code, or fetch binaries at runtime. Seven exact-version optional
18
+ packages are filtered by OS, CPU, and Linux libc, so an installation receives
19
+ only its matching prebuilt binding.
19
20
  Native-only formats and creation-time Windows DACL guarantees fail explicitly
20
21
  instead of substituting a weaker implementation.
21
22
 
@@ -63,12 +64,39 @@ returns a bounded manifest. TypeScript applies the shared path, filter, strip,
63
64
  mode, and byte policies and returns an index-bound extraction plan. Rust then
64
65
  creates only those planned entries beneath a private staging descriptor.
65
66
 
66
- A fixed-512-byte pass-through meter sits between decompression and the TAR
67
- crate. It reads only header type and octal/base-256 size fields. It never parses
68
- metadata content. Oversized GNU long-name/link metadata is rejected before
69
- buffering; PAX size overrides and GNU sparse entries are rejected as
70
- unmeterable rather than guessed. The JavaScript node-tar path receives the same
71
- `maxMetaEntryBytes` value and a matching fixed-header preflight.
67
+ A raw meter sits between decompression and the TAR crate, with matching
68
+ TypeScript admission before node-tar. It parses 512-byte headers and bounded
69
+ local PAX `x` metadata, using supported effective sizes to locate the following
70
+ member body. GNU long-name/link `L`/`K` payloads remain
71
+ supported. `maxMetaEntryBytes` bounds each metadata body before allocation;
72
+ unsupported global/old metadata and sparse forms fail closed rather than being
73
+ interpreted as ordinary members. See [bounded local PAX support](archive.md#bounded-local-pax-support).
74
+
75
+ Every raw pass receives only TypeScript's resolved `maxEntries`,
76
+ `maxMetaEntryBytes`, and `maxDecodedBytes`. Shared resolution caps metadata and
77
+ decoded byte fields at JavaScript's safe-integer maximum and entry counts at
78
+ `2^32 - 1` before backend selection. Large finite limits remain accepted;
79
+ native conversion mirrors those caps and rejects malformed non-finite or
80
+ negative direct-call values before casting. Logical member headers count
81
+ before filtering/stripping; metadata records do not. `maxEntryBytes` and
82
+ `maxExtractedBytes` remain exclusively in TypeScript's accepted-plan builder,
83
+ after strip/filter policy, and are absent from the raw meter's interface.
84
+ Bounded reads use the default count/metadata/decoded bounds; public `maxBytes`
85
+ bounds only the requested output. TypeScript derives the internal decoded cap
86
+ by safely adding `maxExtractedBytes` and `maxArchiveBytes`, clamped to the safe
87
+ integer maximum. Every native pass receives that same cap and charges headers,
88
+ metadata, bodies, padding, EOF blocks, and trailing zeros. It rejects overflow
89
+ with `archive-decoded-size-exceeds-limit`; no ratio policy is implied.
90
+ Extraction and entry reads drain the metered reader through physical EOF after
91
+ TAR iteration. Trailing framing or decoded-limit failures propagate before
92
+ directory modes are finalized, staging is published, or selected bytes return.
93
+ Native reads stop at framing boundaries so a rejected header does not request
94
+ its body from the decoder; codec buffering can still read ahead internally.
95
+ Inspection finishes the complete bounded framing pass before parsing. Directory
96
+ and link bodies, missing two-block EOF, and nonzero trailers reject on both
97
+ backends, as detailed in [raw TAR framing](archive.md#raw-tar-framing). Raw and
98
+ padded sizes above JavaScript's safe-integer maximum reject as invalid framing
99
+ before applying member budgets, including when local PAX overrides the size.
72
100
 
73
101
  ## Publication and hashing
74
102
 
@@ -86,9 +114,36 @@ workers rather than the JavaScript event loop.
86
114
  | `require` | Try once, cache the result | Throw `FsSafeError("helper-unavailable")` |
87
115
  | `off` | Never attempt a binding load | Always use guarded JavaScript |
88
116
 
89
- The one exception is functionality with no safe JavaScript implementation:
90
- zstd/bzip2 TAR and Windows private-directory creation fail with
91
- `helper-unavailable` when native support is absent or off.
117
+ Features without a safe JavaScript implementation, including zstd/bzip2 TAR,
118
+ Windows private-directory creation, and [retained-directory staging](staged-file.md),
119
+ fail with `helper-unavailable` when native support is absent or off. Staging
120
+ is currently Linux/macOS only and rejects Windows with `unsupported-platform`.
121
+
122
+ The staged-file owner also serves POSIX native pinned writes, including streaming.
123
+ Unpublished files remain at `0600`; requested modes are applied through the
124
+ owned file descriptor only after rename and published-entry identity validation.
125
+ Post-rename chmod or sync failures retain the publication receipt and final name.
126
+ Its direct-child exclusive `openat` hands off the descriptor before any fallible
127
+ post-open checks; non-following `statat` compares against that descriptor with
128
+ exact native identities, and cleanup uses `unlinkat` in the retained parent.
129
+ The separate checks and unlink are not atomic conditional deletion. Windows
130
+ pinned writes and other fallback-capable APIs retain their existing mechanisms.
131
+ Native writers share root and parent admission, but keep their platform identity
132
+ checks and leaf ownership. POSIX coordinator disposal uses `SuppressedError` to
133
+ retain both an operation failure and a disposal failure, including their receipts;
134
+ stage preparation and cleanup keep their documented error mappings.
135
+ Root replacement verification borrows the published descriptor after final mode
136
+ application, while a private coordinator retains the staged owner until the
137
+ asynchronous check finishes. The owner never escapes that coordinator; public
138
+ staging methods and receipts expose no descriptor or verification callback.
139
+ Verification failures preserve the published name, and disposal still retains
140
+ both verification and cleanup errors when both fail.
141
+ The private verification channel carries exact bigint identity from the original
142
+ owned descriptor (or the content-accepted FUSE descriptor). Root compares it
143
+ against exact fd and pathname metadata; legacy helper return facts and public
144
+ read metadata behavior are unchanged. Missing Windows pathname identity still
145
+ requires a guarded path reopen and comparison with the original retained file;
146
+ that fallback does not apply to POSIX no-read modes.
92
147
 
93
148
  ## JavaScript fallback guarantees and delta
94
149
 
@@ -116,10 +171,10 @@ infer native loading from timing.
116
171
  Importing fs-safe never executes a child process. Linux libc selection uses
117
172
  the Node process report, conventional musl library filenames, and the ELF
118
173
  `PT_INTERP` field of `process.execPath`. If all probes are inconclusive, the
119
- loader conservatively attempts the bundled glibc binary and lets normal module
120
- loading fail into `auto` fallback. The loader requires only
121
- `dist/native/<target>/fs-safe-native.node`; it never probes optional packages,
122
- downloads code, or runs a postinstall step. A missing or incompatible binary
174
+ loader conservatively attempts the glibc package and lets normal module loading
175
+ fail into `auto` fallback. The loader requires only the package selected from
176
+ the detected target; it never probes unrelated packages, downloads code, or
177
+ runs a postinstall step. A missing or incompatible binary
123
178
  silently selects the JavaScript fallback in `auto`, throws typed
124
179
  `helper-unavailable` in `require`, and is never inspected in `off`. Tests reject
125
180
  `child_process`, `exec`, or `spawn` usage in the loader.
@@ -132,3 +187,4 @@ silently selects the JavaScript fallback in `auto`, throws typed
132
187
  - [Durability](durability.md)
133
188
  - [Permissions](permissions.md)
134
189
  - [Migrating to 0.5](migrating-to-0.5.md)
190
+ - [Migrating to 0.6](migrating-to-0.6.md)
package/docs/output.md CHANGED
@@ -48,6 +48,8 @@ The requested `path` must name a file. Missing destination parents are created
48
48
  by the helper because the operation is "produce this output file under the
49
49
  root"; callers should choose the filename before calling this API.
50
50
 
51
+ `maxBytes` must be a non-negative safe integer or positive `Infinity`; zero is an active cap and `Infinity` disables it. Invalid values reject before the producer or filesystem staging runs.
52
+
51
53
  Use `maxBytes` when the external producer can create arbitrarily large files,
52
54
  and `mode` when the finalized file needs a specific POSIX mode. Both staging
53
55
  modes enforce them after the producer returns and before committing the target.
@@ -80,6 +82,14 @@ rename. If an error leaves the sibling temp in place and immediate cleanup
80
82
  fails, its verified identity remains registered for a best-effort process-exit
81
83
  cleanup retry.
82
84
 
85
+ Sibling staging shares the [callback sibling owner](temp.md#sibling-temp-writes):
86
+ it checks exact pre-open, descriptor, and current-path identities, retains the
87
+ descriptor through publication, and never chmods or reads a replacement by path.
88
+ Cleanup preserves unverified paths, including partial output when the callback
89
+ throws before admission. Native-off and Windows operation remain supported with
90
+ the platform limits and non-atomic rename/unlink identity checks described there.
91
+ When `mode` is omitted, output-sibling staging preserves the producer's mode.
92
+
83
93
  ## Why not pass the final path to the library?
84
94
 
85
95
  If a target parent can be swapped after validation, handing an external library
@@ -24,6 +24,8 @@ await sharp(photo.path).resize(800).toFile(/* … */);
24
24
  ## Signature
25
25
 
26
26
  ```ts
27
+ import type { FsSafeError } from "@openclaw/fs-safe/errors";
28
+
27
29
  function pathScope(rootDir: string, options: PathScopeOptions): PathScope;
28
30
 
29
31
  type PathScopeOptions = {
@@ -36,6 +38,10 @@ type PathScopeResolveOptions = {
36
38
 
37
39
  type PathResult = { ok: true; path: string } | { ok: false; error: string };
38
40
  type PathsResult = { ok: true; paths: string[] } | { ok: false; error: string };
41
+ // Illustrative result alias, not a named package export.
42
+ type DirectoryResult =
43
+ | { ok: true; path: string }
44
+ | { ok: false; error: string; diagnostic?: FsSafeError };
39
45
 
40
46
  type PathScope = {
41
47
  rootDir: string;
@@ -45,7 +51,7 @@ type PathScope = {
45
51
  existing(requestedPaths: string[]): Promise<PathsResult>;
46
52
  files(requestedPaths: string[]): Promise<PathsResult>;
47
53
  writable(requestedPath: string, options?: PathScopeResolveOptions): Promise<PathResult>;
48
- ensureDir(requestedPath: string, options?: PathScopeResolveOptions & { mode?: number }): Promise<PathResult>;
54
+ ensureDir(requestedPath: string, options?: PathScopeResolveOptions & { mode?: number }): Promise<DirectoryResult>;
49
55
  };
50
56
  ```
51
57
 
@@ -92,9 +98,29 @@ await fs.writeFile(t.path, body);
92
98
 
93
99
  Async. `mkdir -p` inside the scope. Walks each segment, refuses any symlink in the path, creates missing directories. Optional `mode` sets the directory mode.
94
100
 
101
+ Failures remain nonthrowing results with `error: string`. Policy rejections
102
+ (including traversal, NUL input, symlinks and non-directory segments) omit
103
+ `diagnostic`. Operational failures from `lstat`, `realpath` or `mkdir` include
104
+ an `FsSafeError` with `code: "helper-failed"`, `category: "operational"`, and
105
+ the exact original error in `cause`, including its native `code`, `errno` and
106
+ `syscall` when available. Only `ensureDir()` adds this diagnostic; other scope
107
+ methods retain their existing result shapes.
108
+
109
+ `error` equals `diagnostic.message` for operational failures, for example
110
+ `"Could not prepare uploads directory: ENAMETOOLONG during lstat"`. Display
111
+ text bounds and escapes the label and native code/syscall, without copying
112
+ the requested path or native error message. Treat the raw cause as sensitive
113
+ local diagnostic data, not as text to return to an untrusted caller. A failure
114
+ can leave already-created parent directories; it does not roll them back or
115
+ retry through an unchecked filesystem path.
116
+
95
117
  ```ts
96
118
  const dir = await uploads.ensureDir("inbox", { mode: 0o755 });
97
- if (!dir.ok) return reply(500, dir.error);
119
+ if (!dir.ok) {
120
+ // Pass the cause only to an appropriately restricted local diagnostic sink.
121
+ if (dir.diagnostic) recordLocalFailure(dir.diagnostic.cause);
122
+ return reply(dir.diagnostic ? 500 : 400, dir.error);
123
+ }
98
124
  ```
99
125
 
100
126
  ## Result type vs throwing
@@ -75,10 +75,21 @@ Built-in PowerShell, `icacls.exe`, and `whoami.exe` invocations have a fixed
75
75
  30-second per-process deadline. A command failure or timeout returns an
76
76
  unverified result (`source: "unknown"`) so callers fail closed. Advanced callers
77
77
  that inject a custom `exec` implementation own that executor's deadline.
78
+ Failed owner and ACL inspections retain `error` text and an optional
79
+ `errorDetail: PermissionCommandFailure` with `command`, integer `durationMs`,
80
+ `timedOut`, `exitCode`, `signal`, and `stderr`. The type is exported from both
81
+ `@openclaw/fs-safe/permissions` and `@openclaw/fs-safe/advanced`. Built-in
82
+ execution measures elapsed time; injected execFile-shaped failures receive
83
+ best-effort command diagnostics. Plain errors have no `errorDetail`.
84
+ Display reasons and stderr escape control characters and are limited to 400
85
+ characters, including a trailing `…` when truncated. Diagnostics do not copy
86
+ stdout or read target file contents. The separate `errorCause` retains the
87
+ original exception for restricted local diagnosis; do not serialize or expose
88
+ it as display text.
78
89
  The parser is on the advanced surface so tests and CLIs can process captured
79
90
  `icacls` output without spawning a process.
80
91
 
81
- When the bundled native binding is available, `inspectPathPermissions()`
92
+ When the native binding is available, `inspectPathPermissions()`
82
93
  reads the owner and DACL directly with Windows security APIs. It classifies the
83
94
  current user, LocalSystem, and built-in Administrators as trusted and reports
84
95
  the world/group read/write facts consumed by secure reads. Descriptor forms it
@@ -128,7 +139,7 @@ Object-specific and other ACE layouts are not guessed: they are omitted,
128
139
  `complete` becomes false, and their numeric types appear in
129
140
  `unsupportedAceTypes`, allowing a security-sensitive caller to fail closed.
130
141
  Non-Windows systems return `{ status: "unsupported-platform", platform }`.
131
- Windows requires the bundled native binding; if it is unavailable or forced
142
+ Windows requires the native binding; if it is unavailable or forced
132
143
  off, the call throws `FsSafeError("helper-unavailable")`. The existing coarse
133
144
  `inspectPathPermissions()` API still owns its compatibility fallback and trust
134
145
  classification.
@@ -69,9 +69,12 @@ The file-lock diagnostics surface includes `FileLockHeldEntry`,
69
69
  manager controls; production code should not use them as lock recovery.
70
70
 
71
71
  Standalone walkers use the `WalkEntryKind` and `WalkSymlinkPolicy` unions.
72
- Private workspaces expose `TempPathIdentityReceipt` and the
73
- `TempWorkspaceCleanupResult` union so callers can distinguish removal,
74
- absence, and identity mismatch.
72
+ Private workspaces expose `TempPathIdentityReceipt`,
73
+ `TempWorkspaceCleanupResult`, and `TempWorkspaceCleanupSafety` so callers can
74
+ distinguish outcomes and select compatible or required bounded-tree cleanup.
75
+ Strict workspace creation requires native no-replace quarantine plus retained
76
+ parent/workspace descriptors; see the
77
+ [creation and cleanup contract](temp.md#private-temp-workspaces).
75
78
 
76
79
  ## Atomic replacement and durability
77
80
 
@@ -145,7 +145,7 @@ await withTempWorkspace({ rootDir: "/srv/jobs/tmp", prefix: "build-" }, async (w
145
145
  });
146
146
  ```
147
147
 
148
- The directory is mode `0700`, sits under a per-user secure temp root, and is removed when the callback returns or throws. See [Temp workspaces](temp.md).
148
+ The directory is mode `0700` under the caller-provided root, and cleanup runs when the callback returns or throws. Compatible cleanup remains available without native support; pass `cleanupSafety: "require-bounded"` to require native no-replace quarantine and descriptor-bounded tree removal before creating a child. Cleanup preserves raced or ambiguous entries under a private quarantine name. See the [temp workspace compatibility and security contract](temp.md#private-temp-workspaces).
149
149
 
150
150
  ## Where to next
151
151
 
package/docs/reading.md CHANGED
@@ -89,7 +89,7 @@ type RootReadOptions = {
89
89
  };
90
90
  ```
91
91
 
92
- `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.
92
+ `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.
93
93
 
94
94
  `nonBlockingRead` remains as a compatibility hint. 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.
95
95
 
@@ -64,7 +64,11 @@ processLog(result.buffer);
64
64
  ```
65
65
 
66
66
  The result is `{ buffer, stat }`. Missing files preserve the normal `ENOENT`
67
- shape; non-regular targets throw.
67
+ shape; non-regular targets throw. Reads compare exact bigint identities from the
68
+ preview, opened descriptor, and current path before consuming bytes; the returned
69
+ `stat` remains numeric Node `Stats`. Unknown Windows identities receive one
70
+ re-inspection without reopening; persistent unknowns or replacements throw
71
+ `FsSafeError("path-mismatch")`.
68
72
 
69
73
  Throws `FsSafeError` with code `too-large` if the file exceeds `maxBytes`. Other I/O errors propagate as `NodeJS.ErrnoException`.
70
74
 
@@ -101,7 +105,10 @@ type AppendRegularFileOptions = {
101
105
  };
102
106
  ```
103
107
 
104
- The helper refuses symlink and hardlinked final targets. With
108
+ The helper refuses symlink and hardlinked final targets. Preview, opened
109
+ descriptor, and current pathname identities remain exact bigints through the
110
+ append boundary; rounded-equal replacements and persistent unknown Windows
111
+ identities reject before chmod or writing bytes. With
105
112
  `rejectSymlinkParents: true`, it also rejects symlinked ancestor directories.
106
113
 
107
114
  ### `appendRegularFileSync(options)`
package/docs/root.md CHANGED
@@ -38,6 +38,8 @@ type DenyMutationPolicy = {
38
38
 
39
39
  `defaults` apply to every method on the returned handle. Per-call options on individual methods override the defaults for that call only, except `denyMutations`: root and per-call deny entries are merged so a call cannot clear a root-level deny.
40
40
 
41
+ Every `maxBytes` value must be a non-negative safe integer or positive `Infinity`. Zero is an active zero-byte cap; `Infinity` explicitly disables the cap. An omitted or explicitly `undefined` per-call value preserves the configured Root default instead of clearing it.
42
+
41
43
  ## The `Root` interface
42
44
 
43
45
  Every method on the returned handle accepts paths relative to the root and rejects anything that would escape it.
@@ -140,7 +142,7 @@ new destination name is subject to the portable guard.
140
142
 
141
143
  ## Native helper mode
142
144
 
143
- Create-only writes prefer the bundled native helper for fd-relative opens and
145
+ Create-only writes prefer the platform native helper for fd-relative opens and
144
146
  atomic no-replace rename. Operations without native wiring retain their guarded
145
147
  JavaScript implementations.
146
148
 
@@ -73,6 +73,8 @@ type SecretFileReadOptions = {
73
73
  };
74
74
  ```
75
75
 
76
+ `maxBytes` must be a non-negative safe integer or positive `Infinity`; zero is an active cap, `Infinity` disables it, and omitted or explicitly `undefined` values preserve the 16 KiB default.
77
+
76
78
  The reader trims the file content and rejects empty results. Symlink paths are
77
79
  followed and pinned by default; set `rejectSymlink: true` when the pathname
78
80
  itself must not be an alias. Hardlinks are rejected by default so another
@@ -89,6 +91,16 @@ credential must also fail on broad permissions or unexpected ownership.
89
91
  the same pinned-handle validation, byte cap, trimming, error codes, and strict
90
92
  versus missing-is-undefined naming semantics.
91
93
 
94
+ Both sync and async readers compare lossless bigint identities from the preview,
95
+ opened descriptor, resolved target, and current input path before reading. POSIX
96
+ opens are nonblocking, so a raced FIFO is rejected by descriptor type instead of
97
+ waiting for a writer. An
98
+ allowed symlink must still point to the opened file. On Windows, a zero device
99
+ or inode is unverified: that inspection is retried once without reopening the
100
+ file, preserving known identity components and link checks. Definite mismatches
101
+ and persistent ambiguity fail with `path-mismatch`; optional reads do not treat
102
+ these failures as missing files.
103
+
92
104
  If an already validated descriptor fails while reading, both readers throw an
93
105
  operational `FsSafeError` with `code: "read-failed"`; inspect `cause` for the
94
106
  underlying Node filesystem code such as `EIO`.