@openclaw/fs-safe 0.9.0 → 0.11.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 (326) hide show
  1. package/CHANGELOG.md +134 -0
  2. package/LICENSE +1 -0
  3. package/README.md +52 -4
  4. package/dist/absolute-path.d.ts.map +1 -1
  5. package/dist/absolute-path.js +3 -2
  6. package/dist/advanced.d.ts +5 -0
  7. package/dist/advanced.d.ts.map +1 -1
  8. package/dist/advanced.js +5 -0
  9. package/dist/archive-crc32.d.ts.map +1 -1
  10. package/dist/archive-crc32.js +6 -1
  11. package/dist/archive-deadline.d.ts.map +1 -1
  12. package/dist/archive-deadline.js +3 -4
  13. package/dist/archive-durability.d.ts +6 -6
  14. package/dist/archive-durability.d.ts.map +1 -1
  15. package/dist/archive-durability.js +1 -1
  16. package/dist/archive-entry.d.ts.map +1 -1
  17. package/dist/archive-entry.js +4 -5
  18. package/dist/archive-gzip-tail.d.ts +3 -0
  19. package/dist/archive-gzip-tail.d.ts.map +1 -1
  20. package/dist/archive-gzip-tail.js +23 -3
  21. package/dist/archive-input.d.ts.map +1 -1
  22. package/dist/archive-input.js +4 -2
  23. package/dist/archive-merge.d.ts +5 -1
  24. package/dist/archive-merge.d.ts.map +1 -1
  25. package/dist/archive-merge.js +16 -13
  26. package/dist/archive-native.d.ts.map +1 -1
  27. package/dist/archive-native.js +7 -6
  28. package/dist/archive-parser.wasm +0 -0
  29. package/dist/archive-read.d.ts.map +1 -1
  30. package/dist/archive-read.js +83 -74
  31. package/dist/archive-staging.d.ts +6 -3
  32. package/dist/archive-staging.d.ts.map +1 -1
  33. package/dist/archive-staging.js +42 -22
  34. package/dist/archive-tar-stream.d.ts +11 -4
  35. package/dist/archive-tar-stream.d.ts.map +1 -1
  36. package/dist/archive-tar-stream.js +23 -10
  37. package/dist/archive-tar-wasm.d.ts.map +1 -1
  38. package/dist/archive-tar-wasm.js +18 -14
  39. package/dist/archive-zip-admission.d.ts +1 -1
  40. package/dist/archive-zip-admission.d.ts.map +1 -1
  41. package/dist/archive-zip-admission.js +48 -12
  42. package/dist/archive-zip-loader.d.ts +6 -0
  43. package/dist/archive-zip-loader.d.ts.map +1 -0
  44. package/dist/archive-zip-loader.js +38 -0
  45. package/dist/archive-zip-names.d.ts.map +1 -1
  46. package/dist/archive-zip-names.js +13 -8
  47. package/dist/archive-zip-preflight.d.ts +2 -3
  48. package/dist/archive-zip-preflight.d.ts.map +1 -1
  49. package/dist/archive-zip-preflight.js +2 -34
  50. package/dist/archive.d.ts.map +1 -1
  51. package/dist/archive.js +12 -10
  52. package/dist/bounded-read.d.ts +12 -0
  53. package/dist/bounded-read.d.ts.map +1 -1
  54. package/dist/bounded-read.js +82 -45
  55. package/dist/clone-metadata.d.ts +19 -0
  56. package/dist/clone-metadata.d.ts.map +1 -0
  57. package/dist/clone-metadata.js +32 -0
  58. package/dist/copy-file-input.d.ts +22 -0
  59. package/dist/copy-file-input.d.ts.map +1 -0
  60. package/dist/copy-file-input.js +69 -0
  61. package/dist/copy-policy.d.ts +3 -0
  62. package/dist/copy-policy.d.ts.map +1 -0
  63. package/dist/copy-policy.js +8 -0
  64. package/dist/copy-publication.d.ts +10 -1
  65. package/dist/copy-publication.d.ts.map +1 -1
  66. package/dist/copy-publication.js +27 -0
  67. package/dist/copy-tree-portable.d.ts +9 -0
  68. package/dist/copy-tree-portable.d.ts.map +1 -0
  69. package/dist/copy-tree-portable.js +222 -0
  70. package/dist/copy.d.ts +16 -0
  71. package/dist/copy.d.ts.map +1 -0
  72. package/dist/copy.js +125 -0
  73. package/dist/directory-durability.d.ts.map +1 -1
  74. package/dist/directory-durability.js +5 -4
  75. package/dist/directory-guard.d.ts +11 -1
  76. package/dist/directory-guard.d.ts.map +1 -1
  77. package/dist/directory-guard.js +53 -11
  78. package/dist/durability.d.ts +1 -1
  79. package/dist/durability.d.ts.map +1 -1
  80. package/dist/durability.js +1 -1
  81. package/dist/error-detail.d.ts.map +1 -1
  82. package/dist/error-detail.js +4 -1
  83. package/dist/file-handle-transfer.d.ts +14 -0
  84. package/dist/file-handle-transfer.d.ts.map +1 -0
  85. package/dist/file-handle-transfer.js +64 -0
  86. package/dist/file-hash.d.ts +9 -2
  87. package/dist/file-hash.d.ts.map +1 -1
  88. package/dist/file-hash.js +135 -39
  89. package/dist/file-lock-sync.d.ts.map +1 -1
  90. package/dist/file-lock-sync.js +8 -4
  91. package/dist/file-store-boundary.d.ts.map +1 -1
  92. package/dist/file-store-boundary.js +7 -5
  93. package/dist/file-store-path.d.ts +3 -0
  94. package/dist/file-store-path.d.ts.map +1 -0
  95. package/dist/file-store-path.js +27 -0
  96. package/dist/file-store-prune.d.ts.map +1 -1
  97. package/dist/file-store-prune.js +6 -4
  98. package/dist/file-store-sync-write.d.ts.map +1 -1
  99. package/dist/file-store-sync-write.js +56 -44
  100. package/dist/file-store.d.ts.map +1 -1
  101. package/dist/file-store.js +2 -18
  102. package/dist/filename.d.ts.map +1 -1
  103. package/dist/filename.js +4 -13
  104. package/dist/guarded-mkdir.d.ts +1 -0
  105. package/dist/guarded-mkdir.d.ts.map +1 -1
  106. package/dist/guarded-mkdir.js +4 -2
  107. package/dist/guarded-mutation.d.ts +2 -0
  108. package/dist/guarded-mutation.d.ts.map +1 -1
  109. package/dist/guarded-mutation.js +8 -4
  110. package/dist/guest-dispatch-python.d.ts +2 -0
  111. package/dist/guest-dispatch-python.d.ts.map +1 -0
  112. package/dist/guest-dispatch-python.js +117 -0
  113. package/dist/guest-native-python.d.ts +4 -0
  114. package/dist/guest-native-python.d.ts.map +1 -0
  115. package/dist/guest-native-python.js +135 -0
  116. package/dist/guest.d.ts +9 -0
  117. package/dist/guest.d.ts.map +1 -0
  118. package/dist/guest.js +413 -0
  119. package/dist/home-dir.d.ts.map +1 -1
  120. package/dist/home-dir.js +9 -7
  121. package/dist/index.d.ts +1 -1
  122. package/dist/index.d.ts.map +1 -1
  123. package/dist/install-path.d.ts.map +1 -1
  124. package/dist/install-path.js +5 -8
  125. package/dist/json-durable-queue-directory.js +3 -3
  126. package/dist/json-durable-queue.d.ts.map +1 -1
  127. package/dist/json-durable-queue.js +19 -18
  128. package/dist/json.d.ts.map +1 -1
  129. package/dist/json.js +2 -1
  130. package/dist/local-roots.d.ts.map +1 -1
  131. package/dist/local-roots.js +24 -25
  132. package/dist/move-path-stage.d.ts +8 -0
  133. package/dist/move-path-stage.d.ts.map +1 -0
  134. package/dist/move-path-stage.js +56 -0
  135. package/dist/move-path.d.ts.map +1 -1
  136. package/dist/move-path.js +30 -34
  137. package/dist/mutation-authority.d.ts +9 -0
  138. package/dist/mutation-authority.d.ts.map +1 -0
  139. package/dist/mutation-authority.js +36 -0
  140. package/dist/native-binding.d.ts +29 -1
  141. package/dist/native-binding.d.ts.map +1 -1
  142. package/dist/native-operations.d.ts +1 -1
  143. package/dist/native-operations.d.ts.map +1 -1
  144. package/dist/native-operations.js +11 -5
  145. package/dist/native-pinned-write-windows.d.ts.map +1 -1
  146. package/dist/native-pinned-write-windows.js +19 -7
  147. package/dist/native-pinned-write.d.ts.map +1 -1
  148. package/dist/native-pinned-write.js +3 -1
  149. package/dist/native-staged-file.d.ts +3 -3
  150. package/dist/native-staged-file.d.ts.map +1 -1
  151. package/dist/native-staged-file.js +35 -8
  152. package/dist/opened-realpath.d.ts.map +1 -1
  153. package/dist/opened-realpath.js +5 -4
  154. package/dist/output.d.ts +2 -0
  155. package/dist/output.d.ts.map +1 -1
  156. package/dist/output.js +2 -0
  157. package/dist/overwrite-file-handle.d.ts +8 -0
  158. package/dist/overwrite-file-handle.d.ts.map +1 -0
  159. package/dist/overwrite-file-handle.js +42 -0
  160. package/dist/path-case.d.ts +7 -0
  161. package/dist/path-case.d.ts.map +1 -0
  162. package/dist/path-case.js +136 -0
  163. package/dist/path.d.ts.map +1 -1
  164. package/dist/path.js +5 -2
  165. package/dist/permissions-windows.d.ts +1 -1
  166. package/dist/permissions-windows.d.ts.map +1 -1
  167. package/dist/permissions-windows.js +85 -53
  168. package/dist/pinned-open.d.ts.map +1 -1
  169. package/dist/pinned-open.js +3 -1
  170. package/dist/pinned-operation.js +1 -1
  171. package/dist/pinned-write.d.ts +7 -3
  172. package/dist/pinned-write.d.ts.map +1 -1
  173. package/dist/pinned-write.js +51 -30
  174. package/dist/positional-read.d.ts +9 -0
  175. package/dist/positional-read.d.ts.map +1 -0
  176. package/dist/positional-read.js +36 -0
  177. package/dist/private-temp-workspace.d.ts.map +1 -1
  178. package/dist/private-temp-workspace.js +6 -4
  179. package/dist/publish-copy-stage.d.ts +13 -0
  180. package/dist/publish-copy-stage.d.ts.map +1 -0
  181. package/dist/publish-copy-stage.js +47 -0
  182. package/dist/publish-file.d.ts.map +1 -1
  183. package/dist/publish-file.js +6 -5
  184. package/dist/realpath.d.ts +4 -0
  185. package/dist/realpath.d.ts.map +1 -0
  186. package/dist/realpath.js +43 -0
  187. package/dist/recursive-mkdir-path.d.ts +3 -0
  188. package/dist/recursive-mkdir-path.d.ts.map +1 -0
  189. package/dist/recursive-mkdir-path.js +8 -0
  190. package/dist/replace-directory.d.ts.map +1 -1
  191. package/dist/replace-directory.js +2 -1
  192. package/dist/replace-file-copy-fallback.d.ts.map +1 -1
  193. package/dist/replace-file-copy-fallback.js +23 -31
  194. package/dist/replace-file-copy-source.d.ts.map +1 -1
  195. package/dist/replace-file-copy-source.js +7 -12
  196. package/dist/replace-file-mode.d.ts +3 -0
  197. package/dist/replace-file-mode.d.ts.map +1 -0
  198. package/dist/replace-file-mode.js +10 -0
  199. package/dist/replace-file-temp-owner.d.ts.map +1 -1
  200. package/dist/replace-file-temp-owner.js +6 -2
  201. package/dist/replace-file.d.ts +1 -0
  202. package/dist/replace-file.d.ts.map +1 -1
  203. package/dist/replace-file.js +14 -8
  204. package/dist/root-context.d.ts +3 -0
  205. package/dist/root-context.d.ts.map +1 -1
  206. package/dist/root-context.js +53 -28
  207. package/dist/root-create-input.d.ts +10 -0
  208. package/dist/root-create-input.d.ts.map +1 -0
  209. package/dist/root-create-input.js +80 -0
  210. package/dist/root-directory-list.d.ts +26 -0
  211. package/dist/root-directory-list.d.ts.map +1 -0
  212. package/dist/root-directory-list.js +219 -0
  213. package/dist/root-entries.d.ts +11 -0
  214. package/dist/root-entries.d.ts.map +1 -0
  215. package/dist/root-entries.js +61 -0
  216. package/dist/root-errors.d.ts +6 -5
  217. package/dist/root-errors.d.ts.map +1 -1
  218. package/dist/root-errors.js +16 -12
  219. package/dist/root-file.d.ts +2 -0
  220. package/dist/root-file.d.ts.map +1 -1
  221. package/dist/root-file.js +12 -4
  222. package/dist/root-impl.d.ts +17 -51
  223. package/dist/root-impl.d.ts.map +1 -1
  224. package/dist/root-impl.js +358 -234
  225. package/dist/root-options.d.ts +77 -0
  226. package/dist/root-options.d.ts.map +1 -0
  227. package/dist/root-options.js +18 -0
  228. package/dist/root-path-existing.d.ts +5 -0
  229. package/dist/root-path-existing.d.ts.map +1 -1
  230. package/dist/root-path-existing.js +59 -3
  231. package/dist/root-path-symlink.d.ts.map +1 -1
  232. package/dist/root-path-symlink.js +3 -2
  233. package/dist/root-path.d.ts +1 -0
  234. package/dist/root-path.d.ts.map +1 -1
  235. package/dist/root-path.js +74 -162
  236. package/dist/root-paths.d.ts.map +1 -1
  237. package/dist/root-paths.js +13 -9
  238. package/dist/root-remove.d.ts +5 -0
  239. package/dist/root-remove.d.ts.map +1 -0
  240. package/dist/root-remove.js +286 -0
  241. package/dist/root-symlink-policy.d.ts +14 -0
  242. package/dist/root-symlink-policy.d.ts.map +1 -0
  243. package/dist/root-symlink-policy.js +34 -0
  244. package/dist/root-walk.d.ts +5 -4
  245. package/dist/root-walk.d.ts.map +1 -1
  246. package/dist/root-walk.js +99 -50
  247. package/dist/root-write-mode.d.ts.map +1 -1
  248. package/dist/root-write-mode.js +3 -5
  249. package/dist/root.d.ts +4 -1
  250. package/dist/root.d.ts.map +1 -1
  251. package/dist/secret-file.d.ts.map +1 -1
  252. package/dist/secret-file.js +2 -1
  253. package/dist/secret-read-async.d.ts.map +1 -1
  254. package/dist/secret-read-async.js +2 -1
  255. package/dist/secure-file.d.ts.map +1 -1
  256. package/dist/secure-file.js +25 -8
  257. package/dist/secure-temp-dir.d.ts.map +1 -1
  258. package/dist/secure-temp-dir.js +2 -1
  259. package/dist/sibling-staged-file.d.ts +1 -0
  260. package/dist/sibling-staged-file.d.ts.map +1 -1
  261. package/dist/sibling-staged-file.js +42 -8
  262. package/dist/sibling-temp.d.ts +2 -0
  263. package/dist/sibling-temp.d.ts.map +1 -1
  264. package/dist/sibling-temp.js +6 -4
  265. package/dist/sidecar-lock-acquire.d.ts.map +1 -1
  266. package/dist/sidecar-lock-acquire.js +17 -5
  267. package/dist/sidecar-lock-policy.d.ts +2 -0
  268. package/dist/sidecar-lock-policy.d.ts.map +1 -1
  269. package/dist/sidecar-lock-policy.js +20 -1
  270. package/dist/staged-directory.d.ts.map +1 -1
  271. package/dist/staged-directory.js +4 -3
  272. package/dist/temp-cleanup.d.ts.map +1 -1
  273. package/dist/temp-cleanup.js +3 -2
  274. package/dist/temp-target.d.ts +14 -12
  275. package/dist/temp-target.d.ts.map +1 -1
  276. package/dist/temp-target.js +12 -6
  277. package/dist/timing.d.ts +1 -0
  278. package/dist/timing.d.ts.map +1 -1
  279. package/dist/timing.js +25 -6
  280. package/dist/trash.d.ts.map +1 -1
  281. package/dist/trash.js +9 -7
  282. package/dist/unicode-path.d.ts +3 -0
  283. package/dist/unicode-path.d.ts.map +1 -0
  284. package/dist/unicode-path.js +13 -0
  285. package/dist/walk.d.ts.map +1 -1
  286. package/dist/walk.js +9 -28
  287. package/dist/windows-owner.d.ts +9 -12
  288. package/dist/windows-owner.d.ts.map +1 -1
  289. package/dist/windows-owner.js +24 -58
  290. package/dist/write-file-handle.d.ts +7 -0
  291. package/dist/write-file-handle.d.ts.map +1 -0
  292. package/dist/write-file-handle.js +26 -0
  293. package/docs/advanced.md +22 -4
  294. package/docs/archive.md +44 -9
  295. package/docs/atomic.md +24 -1
  296. package/docs/config.md +1 -0
  297. package/docs/contributing.md +36 -1
  298. package/docs/copy.md +155 -0
  299. package/docs/directory-identity.md +85 -0
  300. package/docs/durability.md +53 -3
  301. package/docs/entries.md +109 -0
  302. package/docs/errors.md +4 -4
  303. package/docs/file-store.md +15 -0
  304. package/docs/guest.md +141 -0
  305. package/docs/in-place-write.md +81 -0
  306. package/docs/index.md +2 -0
  307. package/docs/install.md +31 -0
  308. package/docs/local-roots.md +8 -1
  309. package/docs/native-helper.md +10 -3
  310. package/docs/native.md +18 -1
  311. package/docs/output.md +32 -6
  312. package/docs/path-case.md +64 -0
  313. package/docs/path-scope.md +1 -1
  314. package/docs/permissions.md +29 -10
  315. package/docs/positional-read.md +63 -0
  316. package/docs/public-api.md +31 -2
  317. package/docs/root.md +196 -6
  318. package/docs/secure-file.md +2 -0
  319. package/docs/security-model.md +14 -0
  320. package/docs/sidecar-lock.md +12 -3
  321. package/docs/temp.md +35 -6
  322. package/docs/timing.md +2 -0
  323. package/docs/types.md +19 -12
  324. package/docs/walk.md +54 -5
  325. package/docs/writing.md +173 -5
  326. package/package.json +19 -8
@@ -30,10 +30,17 @@ The equivalent environment variables are `FS_SAFE_NATIVE_MODE` and `OPENCLAW_FS_
30
30
  | `require` | Throw `FsSafeError("helper-unavailable")` instead of falling back when an operation needs the native binding and it cannot load. |
31
31
 
32
32
  TAR/gzip in the guarded JavaScript path uses a bundled, import-free WASM build
33
- of the same Rust parser used by native. `off` still disables native filesystem
34
- code; it does not disable this portable parser. ZIP fallback still requires
33
+ of the same Rust parser used by native. `off` still disables the optional native
34
+ filesystem helper; it does not disable this portable parser. ZIP fallback still requires
35
35
  optional `jszip`, and zstd/bzip2 remain native-only.
36
36
 
37
+ On Bun macOS/Linux, the [runtime path adapter](install.md#bun-runtime) uses the
38
+ same Rust addon for system canonicalization in `auto` and `require`. No JIT is
39
+ needed. With `off` or a missing addon in `auto`, Bun's own resolver retains its
40
+ path and permission limitations. Canonicalization in `require` fails with
41
+ `helper-unavailable` if the addon or its canonicalizer is missing, including
42
+ when admitting a temp workspace. Containment and identity checks stay intact.
43
+
37
44
  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.
38
45
 
39
46
  [`tempWorkspace()` and its scoped/sync variants](temp.md#private-temp-workspaces)
@@ -57,7 +64,7 @@ change the mode policy of existing fallback-capable APIs.
57
64
 
58
65
  The native layer exposes policy-free filesystem mechanisms: beneath-root
59
66
  open/mkdir/link, replace and no-replace rename, identity reads, archive decode/execution,
60
- clone/copy/hash workers, and Windows security descriptor calls. The TypeScript
67
+ clone/copy/hash workers, POSIX canonicalization, and Windows security descriptor calls. The TypeScript
61
68
  layer owns policy, retries, filters, budgets, modes, cleanup, error
62
69
  normalization, and the decision to fall back.
63
70
 
package/docs/native.md CHANGED
@@ -59,6 +59,19 @@ whether a path, archive entry, mode, owner, or cleanup policy is acceptable.
59
59
 
60
60
  ## Archives
61
61
 
62
+ Native ZIP and TAR entry reads retain a private, unpooled input Buffer in
63
+ the async reader. ZIP reuses its parsed directory; TAR retains admitted member
64
+ offsets. TypeScript validates and selects the requested member before reading. The internal binding borrows that allocation: it must never be
65
+ mutated or detached while the reader or a read task exists. The public API only
66
+ accepts a pathname and owns this buffer exclusively. An N-API reference keeps
67
+ the bytes alive; a mutex serializes access to the retained ZIP cursor. Plain
68
+ TAR copies only the selected range after complete admission, while gzip, zstd,
69
+ and bzip2 replay bounded decompression and validate the full physical stream.
70
+ Concurrent TAR reads share immutable input and own separate decoder state.
71
+ Each output owns a new vector, which N-API transfers to Node without a second
72
+ payload copy on runtimes supporting external buffers. No entry-read path
73
+ requires temporary-file staging. Extraction retains its private staged input.
74
+
62
75
  Rust streams ZIP and TAR payloads, including gzip, zstd, and bzip2. It first
63
76
  returns a bounded manifest. TypeScript applies the shared path, filter, strip,
64
77
  mode, and byte policies and returns an index-bound extraction plan. Rust then
@@ -120,6 +133,10 @@ workers rather than the JavaScript event loop.
120
133
  | `require` | Try once, cache the result | Throw `FsSafeError("helper-unavailable")` |
121
134
  | `off` | Never attempt a binding load | Always use guarded JavaScript |
122
135
 
136
+ `sha256FileSync()` is a synchronous Node implementation in all three modes and
137
+ does not load the binding. Use asynchronous `sha256File()` for native hashing
138
+ and cancellation that can respond while JavaScript callbacks run.
139
+
123
140
  Features without a safe JavaScript implementation, including zstd/bzip2 TAR,
124
141
  Windows private-directory creation, and [retained-directory staging](staged-file.md),
125
142
  fail with `helper-unavailable` when native support is absent or off. Staging
@@ -165,7 +182,7 @@ remain TypeScript-owned. What changes is the syscall strength or availability:
165
182
  | Zstd/bzip2 TAR | Supported. | Unsupported; typed `helper-unavailable`. |
166
183
  | 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. |
167
184
  | `rename-noreplace` | Atomic platform no-replace rename. | Unsupported; no emulation by check-then-rename. |
168
- | Windows DACL read | Direct `GetSecurityInfo`; the public facts API exposes ordered basic allow/deny ACE SIDs, masks, and decoded flags without trust policy. | Established .NET/`icacls` inspection fallback for coarse permission checks; raw ACE facts are native-only. |
185
+ | Windows DACL read | Direct `GetSecurityInfo`; the public facts API exposes ordered basic allow/deny ACE SIDs, masks, and decoded flags without trust policy. | Structured .NET owner/DACL inspection for coarse permission checks; the public raw ACE facts API remains native-only. |
169
186
  | Windows private directory | Creation-time protected DACL. | Unsupported; no weaker pathname-only substitute. |
170
187
 
171
188
  Use `off` in CI to keep the fallback contract exercised. Use `require` when a
package/docs/output.md CHANGED
@@ -35,6 +35,7 @@ type ExternalFileWriteOptions<T = void> = {
35
35
  maxBytes?: number;
36
36
  mode?: number;
37
37
  staging?: "workspace" | "sibling"; // default: "workspace"
38
+ producerIsolation?: "private-directory"; // opt-in for sibling staging
38
39
  fallbackFileName?: string; // safe staged-name fallback
39
40
  };
40
41
 
@@ -60,7 +61,7 @@ hazards but does not trim Windows-normalized trailing dots or spaces; reject or
60
61
  rewrite those when cross-platform filename uniqueness matters.
61
62
  `staging: "workspace"` passes the sanitized basename to the producer.
62
63
  `staging: "sibling"` embeds that basename in its randomized temporary name.
63
- When the complete temporary component would exceed 255 bytes under NFC or NFD,
64
+ When the complete temporary component would exceed 255 bytes as written or under NFC or NFD,
64
65
  only the embedded tail is shortened, preserving its extension when possible;
65
66
  short callback paths remain unchanged. The final target and returned `path` use
66
67
  the destination basename, sanitized
@@ -76,8 +77,8 @@ the temp and destination filesystems may differ, or when an externally produced
76
77
  partial file must never appear in the destination directory. The final target
77
78
  still appears only after guarded finalization.
78
79
 
79
- `staging: "sibling"` gives the producer a randomized temp path in the target
80
- directory. Choose it only when that directory itself is the approved writable
80
+ By default, `staging: "sibling"` gives the producer a randomized temp path in
81
+ the target directory. Choose it only when that directory itself is the approved writable
81
82
  boundary and same-filesystem atomic replacement is required. After the callback
82
83
  returns, fs-safe pins and validates the staged regular file, rejects hardlinks
83
84
  and size-limit violations, applies `mode`, fsyncs it, and atomically renames it
@@ -91,11 +92,36 @@ cleanup retry.
91
92
  Sibling staging shares the [callback sibling owner](temp.md#sibling-temp-writes):
92
93
  it checks exact pre-open, descriptor, and current-path identities, retains the
93
94
  descriptor through publication, and never chmods or reads a replacement by path.
94
- Cleanup preserves unverified paths, including partial output when the callback
95
- throws before admission. Native-off and Windows operation remain supported with
96
- the platform limits and non-atomic rename/unlink identity checks described there.
95
+ Without producer isolation, cleanup preserves unverified paths, including partial
96
+ output when the callback throws before admission. Native-off and Windows
97
+ operation remain supported with the platform limits and non-atomic
98
+ rename/unlink identity checks described there.
97
99
  When `mode` is omitted, output-sibling staging preserves the producer's mode.
98
100
 
101
+ Add `producerIsolation: "private-directory"` to sibling staging when the
102
+ producer can leave partial output before throwing. It receives an initially
103
+ absent file path inside a private child workspace under the target parent, on
104
+ the target filesystem. Directory cleanup ownership is captured before the
105
+ callback. A callback exception triggers owned workspace cleanup, including
106
+ partial output, subject to directory identity checks and I/O failures.
107
+ After success, `Root.move` checks source aliases and moves the output to the
108
+ ordinary sibling path; an escaping symlink can fail with `path-alias` here.
109
+ Rejected output still inside the workspace follows its cleanup contract. Once
110
+ output moves to the sibling path, the existing unadmitted-file retention and
111
+ single-link regular-file admission, mode, file sync, and final rename rules apply.
112
+
113
+ Exact bigint parent and workspace identities are rechecked before moving
114
+ output to the sibling path to reject observed replacements. Cleanup uses the
115
+ existing [`withTempFile` ownership contract](temp.md#withtempfile). A moved or replaced parent or workspace can
116
+ leave original or replacement paths behind; the option does not promise
117
+ cleanup through a retained directory after a rename. The existing Windows,
118
+ native-off, and JavaScript guard limitations remain, with no additional
119
+ permissions or durability guarantee. See the [producer-isolation contract](temp.md#sibling-temp-writes)
120
+ for cleanup and pathname-race details. The option affects only `staging: "sibling"`;
121
+ with `staging: "workspace"`, it is redundant and harmless because the producer
122
+ already uses a private workspace. Omitting it leaves both staging defaults
123
+ unchanged.
124
+
99
125
  ## Why not pass the final path to the library?
100
126
 
101
127
  If a target parent can be swapped after validation, handing an external library
@@ -0,0 +1,64 @@
1
+ # Path case probing
2
+
3
+ `probePathCaseInsensitiveSync()` observes whether a path's lookup location
4
+ folds ASCII case. It returns `true`, `false`, or `undefined` when the observation
5
+ cannot establish an answer. It does not infer a filesystem property from the
6
+ operating system or cache its result.
7
+
8
+ ```ts
9
+ import { probePathCaseInsensitiveSync } from "@openclaw/fs-safe/advanced";
10
+
11
+ const insensitive = probePathCaseInsensitiveSync("/srv/data/future.json", {
12
+ allowTemporaryProbe: false,
13
+ });
14
+ if (insensitive === undefined) {
15
+ // The application decides how to handle an unavailable observation.
16
+ }
17
+ ```
18
+
19
+ ## Lookup location
20
+
21
+ The input is resolved with Node's `path.resolve()`. Existing targets, including
22
+ directories and final symlinks, are first compared by basename in their parent.
23
+ The probe does not follow a final symlink to decide the target's case behavior.
24
+ Parent aliases are followed. For a missing path, it walks to the nearest existing
25
+ directory whose metadata can be read, without creating the missing directories.
26
+ An unreadable directory listing returns `undefined`.
27
+
28
+ The probe checks existing directory entries before considering a temporary
29
+ file. Separately listed case variants count as distinct entries even when they
30
+ are hardlinks to the same inode. Identity comparisons retain bigint precision;
31
+ unknown Windows identities do not count as matches. The original entry is
32
+ rechecked after looking up its case variant, so its disappearance or replacement
33
+ invalidates the observation. A detected directory replacement also returns
34
+ `undefined`.
35
+
36
+ ## Temporary probes and cleanup
37
+
38
+ `allowTemporaryProbe` defaults to `true`. When existing entries give no answer,
39
+ the helper exclusively creates one empty `.fs-safe-case-probe-*` file in the
40
+ selected directory at mode `0o600`. It uses the existing temporary-file owner
41
+ to retain the descriptor and exact cleanup identity. Successful ordinary
42
+ completion removes the probe and closes the descriptor.
43
+
44
+ Set `allowTemporaryProbe: false` for strictly read-only observation. In that
45
+ mode an empty directory, or one with no useful ASCII-case names, returns
46
+ `undefined` without creating a temporary file. Temporary probing can change
47
+ directory timestamps and trigger filesystem watchers even when cleanup succeeds.
48
+
49
+ Operational failures, unverified identities, changed entries, and cleanup
50
+ failures return `undefined`. A substituted or hardlinked temporary entry is
51
+ preserved. When cleanup fails operationally, the existing owner retains its
52
+ identity-bound process-exit retry. A creation whose identity cannot be obtained
53
+ may leave an empty file; the helper never guesses cleanup ownership. Therefore
54
+ `undefined` does not promise that no temporary artifact remains.
55
+
56
+ ## Limits
57
+
58
+ This is a local ASCII-case observation, not a Unicode-normalization test,
59
+ filesystem-wide guarantee, lock, or authorization receipt. Directory enumeration
60
+ and metadata lookups are separate operations. Concurrent changes can invalidate
61
+ or immediately stale a result, and the final identity check and unlink are not
62
+ an atomic conditional deletion. Use temporary probing only where creating a
63
+ temporary file is permitted. The caller retains any admission, serialization,
64
+ fallback, or later mutation policy.
@@ -1,6 +1,6 @@
1
1
  # pathScope()
2
2
 
3
- `pathScope()` is an advanced helper with the same boundary semantics as `root()`, but it operates on **absolute paths** the caller already trusts and returns plain `{ ok, path }` results instead of throwing. Use it when you want the boundary check up front before handing an absolute path to another library.
3
+ `pathScope()` prepares absolute paths and returns plain `{ ok, path }` results. `resolve()` and `resolveAll()` check lexical containment without touching the filesystem; `existing()`, `files()`, and `writable()` add the filesystem checks described below. Use it to prepare paths before handing them to another library, whose file-opening and mutation behavior still applies.
4
4
 
5
5
  ```ts
6
6
  import { pathScope } from "@openclaw/fs-safe/advanced";
@@ -68,11 +68,27 @@ createIcaclsResetCommand(targetPath, { isDir, env });
68
68
  resolveWindowsUserPrincipal(env);
69
69
  ```
70
70
 
71
- The fallback Windows inspector calls `icacls.exe <path>` using its supported
72
- path-only inspection syntax and classifies principals as trusted, world, or
73
- group. Trusted defaults include the current user, SYSTEM, and Administrators.
74
- Built-in PowerShell, `icacls.exe`, and `whoami.exe` invocations have a fixed
75
- 30-second per-process deadline. A command failure or timeout returns an
71
+ The fallback Windows inspector reads the owner and DACL together through one
72
+ built-in Windows PowerShell/.NET query. It returns canonical SIDs and numeric
73
+ access masks, so Unicode paths and account names do not pass through lossy
74
+ console display text. `inspectWindowsAcl()` uses native descriptor facts for
75
+ complete local ACLs with nonzero inherited ACEs (or empty/null DACLs) when the
76
+ optional Windows binding is available. It applies
77
+ the same classifier to native facts and the fallback query, returning canonical
78
+ SIDs in its `principal` fields with normalized rights tokens. Explicit `env` or
79
+ `exec` options retain the query path. Disabled or unavailable native helpers,
80
+ remote or incomplete descriptors, leaf symbolic links, and native query errors
81
+ use the fallback. Explicit ACEs and zero-mask entries also retain the query so
82
+ .NET continues to own its ACE ordering and normalization.
83
+ Structured ACLs containing only canonical SIDs are classified directly from
84
+ the current-user SID without requiring a separate account-name lookup.
85
+ The advanced options retain `currentUserSid` as an explicit classification
86
+ override and `principalTranslationFailed: true` as an immediate unverified
87
+ result. The optional `principalSids` translation cache is still accepted but
88
+ is no longer needed because the query returns SIDs directly.
89
+ The existing classifier assigns principals to trusted, world, or group;
90
+ trusted defaults include the current user, SYSTEM, and Administrators.
91
+ The built-in query has a fixed 30-second process deadline. A command failure or timeout returns an
76
92
  unverified result (`source: "unknown"`) so callers fail closed. Advanced callers
77
93
  that inject a custom `exec` implementation own that executor's deadline.
78
94
  Failed owner and ACL inspections retain `error` text and an optional
@@ -86,15 +102,18 @@ characters, including a trailing `…` when truncated. Diagnostics do not copy
86
102
  stdout or read target file contents. The separate `errorCause` retains the
87
103
  original exception for restricted local diagnosis; do not serialize or expose
88
104
  it as display text.
89
- The parser is on the advanced surface so tests and CLIs can process captured
90
- `icacls` output without spawning a process.
105
+ The parser and remediation command builders remain on the advanced surface for
106
+ CLIs processing captured `icacls` output or presenting an explicit repair.
107
+ Runtime inspection does not parse that display text. A null DACL reports
108
+ unrestricted access; an empty DACL grants nothing. Inherit-only ACEs do not
109
+ apply to the inspected object, and deny ACEs never subtract coarse grants or
110
+ claim effective-access evaluation. Unsupported ACE layouts remain unverified.
91
111
 
92
112
  When the native binding is available, `inspectPathPermissions()`
93
113
  reads the owner and DACL directly with Windows security APIs. It classifies the
94
114
  current user, LocalSystem, and built-in Administrators as trusted and reports
95
115
  the world/group read/write facts consumed by secure reads. Descriptor forms it
96
- cannot classify equivalently fall back to the established owner/.NET and
97
- `icacls` path; `mode: "off"` exercises that fallback deterministically.
116
+ cannot classify equivalently fall back to the structured .NET query; `mode: "off"` exercises that fallback deterministically.
98
117
 
99
118
  ## Policy-free owner and DACL facts
100
119
 
@@ -164,7 +183,7 @@ This API is Windows-only and native-only; it fails closed with
164
183
  or when the binding is unavailable. POSIX callers should create private
165
184
  directories through their existing trusted-root creation policy rather than a
166
185
  pathname-only compatibility shim. Existing Windows permission inspection still
167
- retains its .NET/`icacls` compatibility fallback.
186
+ retains its structured .NET compatibility fallback.
168
187
 
169
188
  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.
170
189
 
@@ -0,0 +1,63 @@
1
+ # Positional reads
2
+
3
+ Use `readFileWindowFully()` and `readFileWindowFullySync()` to read a bounded
4
+ window from an already-open file. They fill a caller-owned `Buffer`, completing
5
+ short reads until the buffer is full or the file reaches EOF, and return the
6
+ number of bytes read. They never allocate a payload buffer, close the descriptor,
7
+ or change its current offset.
8
+
9
+ ```ts
10
+ import { root } from "@openclaw/fs-safe";
11
+ import { readFileWindowFully } from "@openclaw/fs-safe/advanced";
12
+
13
+ const workspace = await root("/srv/workspace");
14
+ await using opened = await workspace.open("large.log");
15
+ const buffer = Buffer.allocUnsafe(4096);
16
+ const bytesRead = await readFileWindowFully(opened.handle, buffer, 8192);
17
+ const window = buffer.subarray(0, bytesRead);
18
+ ```
19
+
20
+ ## Signatures
21
+
22
+ ```ts
23
+ type ReadFileWindowOptions = { signal?: AbortSignal };
24
+
25
+ function readFileWindowFully(
26
+ handle: import("node:fs/promises").FileHandle,
27
+ buffer: Buffer,
28
+ position: number,
29
+ options?: ReadFileWindowOptions,
30
+ ): Promise<number>;
31
+
32
+ function readFileWindowFullySync(
33
+ fd: number,
34
+ buffer: Buffer,
35
+ position: number,
36
+ ): number;
37
+ ```
38
+
39
+ `position` and the exclusive window end (`position + buffer.length`) must be
40
+ non-negative safe integers. Invalid ranges throw `RangeError` before reading.
41
+ A zero-length buffer returns zero without I/O. Reading at or beyond EOF also
42
+ returns zero. If EOF occurs within the window, only the returned prefix is
43
+ written; the remaining buffer bytes stay unchanged. Always slice by the returned
44
+ count before using an unsafe-allocated buffer.
45
+
46
+ These helpers use the caller's open descriptor directly. They do not establish
47
+ path containment, file identity, file-type admission, or a snapshot of concurrently
48
+ modified contents. Use [`Root.open()`](root.md#reads) to admit untrusted paths,
49
+ and keep the handle open and the buffer available until the operation settles.
50
+ Underlying I/O errors propagate unchanged.
51
+
52
+ ## Cancellation
53
+
54
+ The async variant accepts `signal`. A pre-aborted signal rejects before I/O.
55
+ In-flight cancellation is checked after the pending read settles and before
56
+ another read starts, preserving the signal's reason. Bytes already read remain
57
+ in the buffer; cancellation does not roll them back. Once the promise settles,
58
+ the caller can reuse the buffer or close its handle without a hidden read still
59
+ running.
60
+
61
+ For a whole-file read that rejects files exceeding a byte limit, use the
62
+ [bounded descriptor readers](advanced.md#files-and-identity) instead. Positional
63
+ reads stop successfully at the requested window and do not probe for extra data.
@@ -38,6 +38,33 @@ The advanced root-file primitive exports `OpenRootFileParams`,
38
38
  `RootFileOpenFailureReason`. These are composition types for callers building
39
39
  their own pinned-open flow, not substitutes for the higher-level `Root` verbs.
40
40
 
41
+ `copyFileHandle` and `CopyFileHandleOptions` transfer bytes between already-open
42
+ regular files without taking over their cursors, lifetime, or publication.
43
+ See [borrowed-handle transfers](copy.md#borrowed-filehandle-transfers).
44
+
45
+ `readDirectoryIdentity`, `assertDirectoryIdentitySync`, and `DirectoryIdentity`
46
+ provide exact directory observations without owning a descriptor or a mutation.
47
+ The assertion accepts an observed path and optional expected canonical path;
48
+ see [directory identity](directory-identity.md).
49
+
50
+ `overwriteFileHandle` and `OverwriteFileHandleOptions` provide in-place byte
51
+ replacement through a borrowed regular-file handle. Its once-only `beforeWrite`
52
+ callback admits the complete write and any required best-effort rollback after
53
+ prefix preparation. See [in-place writes](in-place-write.md).
54
+
55
+ `probePathCaseInsensitiveSync` and `ProbePathCaseOptions` are advanced exports
56
+ for local ASCII-case observations. An unavailable answer remains `undefined`;
57
+ the caller selects any fallback. See [path case probing](path-case.md).
58
+
59
+ ## Guest source
60
+
61
+ `@openclaw/fs-safe/guest` exports `GUEST_FILESYSTEM_PYTHON`,
62
+ `GUEST_FILESYSTEM_CREATE_EXISTS_EXIT_CODE`,
63
+ `GUEST_FILESYSTEM_READ_NOT_FOUND_EXIT_CODE`, and
64
+ `GUEST_FILESYSTEM_RENAME_NO_REPLACE_PYTHON`. These are source and protocol
65
+ constants; the caller launches the Python guest and owns authorization and
66
+ transport lifetime. See the [guest protocol](guest.md).
67
+
41
68
  ## `json` and `store`
42
69
 
43
70
  Standalone structured reads use `ReadJsonOptions`, `ReadRootJsonSyncOptions`,
@@ -89,8 +116,10 @@ The durability surface also exports the synchronous strict
89
116
  `EnsureDurableDirectoryOptions`, `PublishFileExclusiveResult`,
90
117
  `PublishFileExclusiveStrategy`, `PublishFileExclusiveCleanup`,
91
118
  `PublishFileExclusiveFailurePhase`,
92
- `PublishFileExclusiveDirectorySyncFailure`, `Sha256FileInput`, and
93
- `Sha256FileResult`.
119
+ `PublishFileExclusiveDirectorySyncFailure`, `Sha256FileInput`,
120
+ `Sha256FileSyncInput`, `Sha256FileOptions`, and `Sha256FileResult`.
121
+ `sha256FileSync()` provides synchronous pathname or borrowed-descriptor hashing
122
+ with the same byte-budget and digest-result contracts as `sha256File()`.
94
123
 
95
124
  ## Archives
96
125