@openclaw/fs-safe 0.10.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 (259) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/LICENSE +1 -0
  3. package/README.md +36 -5
  4. package/dist/absolute-path.d.ts.map +1 -1
  5. package/dist/absolute-path.js +3 -2
  6. package/dist/advanced.d.ts +4 -0
  7. package/dist/advanced.d.ts.map +1 -1
  8. package/dist/advanced.js +4 -0
  9. package/dist/archive-durability.d.ts +6 -6
  10. package/dist/archive-durability.d.ts.map +1 -1
  11. package/dist/archive-durability.js +1 -1
  12. package/dist/archive-entry.d.ts.map +1 -1
  13. package/dist/archive-entry.js +4 -5
  14. package/dist/archive-gzip-tail.d.ts +1 -0
  15. package/dist/archive-gzip-tail.d.ts.map +1 -1
  16. package/dist/archive-gzip-tail.js +3 -0
  17. package/dist/archive-input.d.ts.map +1 -1
  18. package/dist/archive-input.js +4 -2
  19. package/dist/archive-merge.d.ts +5 -1
  20. package/dist/archive-merge.d.ts.map +1 -1
  21. package/dist/archive-merge.js +15 -12
  22. package/dist/archive-native.js +4 -4
  23. package/dist/archive-parser.wasm +0 -0
  24. package/dist/archive-read.d.ts.map +1 -1
  25. package/dist/archive-read.js +17 -9
  26. package/dist/archive-staging.d.ts +6 -3
  27. package/dist/archive-staging.d.ts.map +1 -1
  28. package/dist/archive-staging.js +42 -22
  29. package/dist/archive-tar-stream.d.ts.map +1 -1
  30. package/dist/archive-tar-stream.js +7 -6
  31. package/dist/archive-tar-wasm.d.ts.map +1 -1
  32. package/dist/archive-tar-wasm.js +16 -13
  33. package/dist/archive-zip-admission.d.ts +1 -1
  34. package/dist/archive-zip-admission.d.ts.map +1 -1
  35. package/dist/archive-zip-admission.js +48 -12
  36. package/dist/archive-zip-loader.d.ts +6 -0
  37. package/dist/archive-zip-loader.d.ts.map +1 -0
  38. package/dist/archive-zip-loader.js +38 -0
  39. package/dist/archive-zip-names.d.ts.map +1 -1
  40. package/dist/archive-zip-names.js +13 -8
  41. package/dist/archive-zip-preflight.d.ts +2 -3
  42. package/dist/archive-zip-preflight.d.ts.map +1 -1
  43. package/dist/archive-zip-preflight.js +2 -34
  44. package/dist/archive.d.ts.map +1 -1
  45. package/dist/archive.js +12 -10
  46. package/dist/bounded-read.d.ts +5 -0
  47. package/dist/bounded-read.d.ts.map +1 -1
  48. package/dist/bounded-read.js +16 -9
  49. package/dist/copy-file-input.d.ts +0 -1
  50. package/dist/copy-file-input.d.ts.map +1 -1
  51. package/dist/copy-file-input.js +4 -26
  52. package/dist/copy-tree-portable.d.ts.map +1 -1
  53. package/dist/copy-tree-portable.js +57 -26
  54. package/dist/copy.d.ts +1 -1
  55. package/dist/copy.d.ts.map +1 -1
  56. package/dist/copy.js +3 -1
  57. package/dist/directory-durability.d.ts.map +1 -1
  58. package/dist/directory-durability.js +5 -4
  59. package/dist/directory-guard.d.ts +11 -1
  60. package/dist/directory-guard.d.ts.map +1 -1
  61. package/dist/directory-guard.js +53 -11
  62. package/dist/durability.d.ts +1 -1
  63. package/dist/durability.d.ts.map +1 -1
  64. package/dist/durability.js +1 -1
  65. package/dist/file-handle-transfer.d.ts +14 -0
  66. package/dist/file-handle-transfer.d.ts.map +1 -0
  67. package/dist/file-handle-transfer.js +64 -0
  68. package/dist/file-hash.d.ts +3 -0
  69. package/dist/file-hash.d.ts.map +1 -1
  70. package/dist/file-hash.js +91 -31
  71. package/dist/file-lock-sync.d.ts.map +1 -1
  72. package/dist/file-lock-sync.js +8 -4
  73. package/dist/file-store-boundary.d.ts.map +1 -1
  74. package/dist/file-store-boundary.js +7 -5
  75. package/dist/file-store-path.d.ts +3 -0
  76. package/dist/file-store-path.d.ts.map +1 -0
  77. package/dist/file-store-path.js +27 -0
  78. package/dist/file-store-prune.d.ts.map +1 -1
  79. package/dist/file-store-prune.js +6 -4
  80. package/dist/file-store-sync-write.d.ts.map +1 -1
  81. package/dist/file-store-sync-write.js +56 -44
  82. package/dist/file-store.d.ts.map +1 -1
  83. package/dist/file-store.js +2 -18
  84. package/dist/filename.d.ts.map +1 -1
  85. package/dist/filename.js +2 -1
  86. package/dist/guarded-mkdir.d.ts.map +1 -1
  87. package/dist/guarded-mkdir.js +3 -2
  88. package/dist/guest-dispatch-python.d.ts +2 -0
  89. package/dist/guest-dispatch-python.d.ts.map +1 -0
  90. package/dist/guest-dispatch-python.js +117 -0
  91. package/dist/guest-native-python.d.ts +4 -0
  92. package/dist/guest-native-python.d.ts.map +1 -0
  93. package/dist/guest-native-python.js +135 -0
  94. package/dist/guest.d.ts +9 -0
  95. package/dist/guest.d.ts.map +1 -0
  96. package/dist/guest.js +413 -0
  97. package/dist/index.d.ts +1 -1
  98. package/dist/index.d.ts.map +1 -1
  99. package/dist/install-path.d.ts.map +1 -1
  100. package/dist/install-path.js +3 -2
  101. package/dist/json-durable-queue-directory.js +3 -3
  102. package/dist/json-durable-queue.d.ts.map +1 -1
  103. package/dist/json-durable-queue.js +6 -4
  104. package/dist/json.d.ts.map +1 -1
  105. package/dist/json.js +2 -1
  106. package/dist/local-roots.d.ts.map +1 -1
  107. package/dist/local-roots.js +2 -1
  108. package/dist/move-path-stage.d.ts.map +1 -1
  109. package/dist/move-path-stage.js +2 -1
  110. package/dist/move-path.d.ts.map +1 -1
  111. package/dist/move-path.js +4 -3
  112. package/dist/mutation-authority.d.ts +1 -0
  113. package/dist/mutation-authority.d.ts.map +1 -1
  114. package/dist/mutation-authority.js +4 -4
  115. package/dist/native-binding.d.ts +7 -2
  116. package/dist/native-binding.d.ts.map +1 -1
  117. package/dist/native-pinned-write-windows.d.ts.map +1 -1
  118. package/dist/native-pinned-write-windows.js +3 -2
  119. package/dist/native-pinned-write.d.ts.map +1 -1
  120. package/dist/native-pinned-write.js +2 -1
  121. package/dist/opened-realpath.d.ts.map +1 -1
  122. package/dist/opened-realpath.js +5 -4
  123. package/dist/output.d.ts +2 -0
  124. package/dist/output.d.ts.map +1 -1
  125. package/dist/output.js +2 -0
  126. package/dist/overwrite-file-handle.d.ts +8 -0
  127. package/dist/overwrite-file-handle.d.ts.map +1 -0
  128. package/dist/overwrite-file-handle.js +42 -0
  129. package/dist/path-case.d.ts +7 -0
  130. package/dist/path-case.d.ts.map +1 -0
  131. package/dist/path-case.js +136 -0
  132. package/dist/path.d.ts.map +1 -1
  133. package/dist/path.js +2 -1
  134. package/dist/permissions-windows.d.ts +1 -1
  135. package/dist/permissions-windows.d.ts.map +1 -1
  136. package/dist/permissions-windows.js +48 -6
  137. package/dist/pinned-open.d.ts.map +1 -1
  138. package/dist/pinned-open.js +3 -1
  139. package/dist/pinned-write.d.ts +2 -2
  140. package/dist/pinned-write.d.ts.map +1 -1
  141. package/dist/pinned-write.js +2 -1
  142. package/dist/private-temp-workspace.d.ts.map +1 -1
  143. package/dist/private-temp-workspace.js +6 -4
  144. package/dist/realpath.d.ts +4 -0
  145. package/dist/realpath.d.ts.map +1 -0
  146. package/dist/realpath.js +43 -0
  147. package/dist/recursive-mkdir-path.d.ts +3 -0
  148. package/dist/recursive-mkdir-path.d.ts.map +1 -0
  149. package/dist/recursive-mkdir-path.js +8 -0
  150. package/dist/replace-directory.d.ts.map +1 -1
  151. package/dist/replace-directory.js +2 -1
  152. package/dist/replace-file-copy-fallback.d.ts.map +1 -1
  153. package/dist/replace-file-copy-fallback.js +23 -31
  154. package/dist/replace-file-copy-source.d.ts.map +1 -1
  155. package/dist/replace-file-copy-source.js +7 -12
  156. package/dist/replace-file-mode.d.ts +3 -0
  157. package/dist/replace-file-mode.d.ts.map +1 -0
  158. package/dist/replace-file-mode.js +10 -0
  159. package/dist/replace-file.d.ts +1 -0
  160. package/dist/replace-file.d.ts.map +1 -1
  161. package/dist/replace-file.js +14 -8
  162. package/dist/root-context.d.ts.map +1 -1
  163. package/dist/root-context.js +8 -8
  164. package/dist/root-create-input.d.ts +10 -0
  165. package/dist/root-create-input.d.ts.map +1 -0
  166. package/dist/root-create-input.js +80 -0
  167. package/dist/root-directory-list.d.ts +3 -1
  168. package/dist/root-directory-list.d.ts.map +1 -1
  169. package/dist/root-directory-list.js +21 -3
  170. package/dist/root-entries.d.ts +11 -0
  171. package/dist/root-entries.d.ts.map +1 -0
  172. package/dist/root-entries.js +61 -0
  173. package/dist/root-errors.d.ts +5 -5
  174. package/dist/root-errors.d.ts.map +1 -1
  175. package/dist/root-errors.js +13 -12
  176. package/dist/root-impl.d.ts +13 -3
  177. package/dist/root-impl.d.ts.map +1 -1
  178. package/dist/root-impl.js +40 -36
  179. package/dist/root-options.d.ts +12 -1
  180. package/dist/root-options.d.ts.map +1 -1
  181. package/dist/root-path-existing.d.ts.map +1 -1
  182. package/dist/root-path-existing.js +4 -3
  183. package/dist/root-path-symlink.d.ts.map +1 -1
  184. package/dist/root-path-symlink.js +3 -2
  185. package/dist/root-paths.d.ts.map +1 -1
  186. package/dist/root-paths.js +13 -9
  187. package/dist/root-remove.d.ts +5 -0
  188. package/dist/root-remove.d.ts.map +1 -0
  189. package/dist/root-remove.js +286 -0
  190. package/dist/root-symlink-policy.d.ts +2 -1
  191. package/dist/root-symlink-policy.d.ts.map +1 -1
  192. package/dist/root-symlink-policy.js +2 -2
  193. package/dist/root-write-mode.d.ts.map +1 -1
  194. package/dist/root-write-mode.js +2 -1
  195. package/dist/root.d.ts +2 -1
  196. package/dist/root.d.ts.map +1 -1
  197. package/dist/secret-file.d.ts.map +1 -1
  198. package/dist/secret-file.js +2 -1
  199. package/dist/secret-read-async.d.ts.map +1 -1
  200. package/dist/secret-read-async.js +2 -1
  201. package/dist/secure-file.d.ts.map +1 -1
  202. package/dist/secure-file.js +21 -4
  203. package/dist/secure-temp-dir.d.ts.map +1 -1
  204. package/dist/secure-temp-dir.js +2 -1
  205. package/dist/sibling-staged-file.d.ts +1 -0
  206. package/dist/sibling-staged-file.d.ts.map +1 -1
  207. package/dist/sibling-staged-file.js +42 -8
  208. package/dist/sibling-temp.d.ts +2 -0
  209. package/dist/sibling-temp.d.ts.map +1 -1
  210. package/dist/sibling-temp.js +6 -4
  211. package/dist/sidecar-lock-acquire.d.ts.map +1 -1
  212. package/dist/sidecar-lock-acquire.js +15 -4
  213. package/dist/sidecar-lock-policy.d.ts +2 -0
  214. package/dist/sidecar-lock-policy.d.ts.map +1 -1
  215. package/dist/sidecar-lock-policy.js +17 -0
  216. package/dist/staged-directory.d.ts.map +1 -1
  217. package/dist/staged-directory.js +4 -3
  218. package/dist/temp-target.d.ts +14 -12
  219. package/dist/temp-target.d.ts.map +1 -1
  220. package/dist/temp-target.js +12 -6
  221. package/dist/trash.d.ts.map +1 -1
  222. package/dist/trash.js +7 -5
  223. package/dist/unicode-path.d.ts +3 -0
  224. package/dist/unicode-path.d.ts.map +1 -0
  225. package/dist/unicode-path.js +13 -0
  226. package/dist/walk.d.ts.map +1 -1
  227. package/dist/walk.js +3 -2
  228. package/dist/write-file-handle.d.ts +1 -0
  229. package/dist/write-file-handle.d.ts.map +1 -1
  230. package/dist/write-file-handle.js +3 -2
  231. package/docs/advanced.md +4 -0
  232. package/docs/archive.md +25 -5
  233. package/docs/atomic.md +17 -1
  234. package/docs/config.md +1 -0
  235. package/docs/contributing.md +29 -1
  236. package/docs/copy.md +75 -6
  237. package/docs/directory-identity.md +85 -0
  238. package/docs/durability.md +36 -1
  239. package/docs/entries.md +109 -0
  240. package/docs/errors.md +3 -3
  241. package/docs/file-store.md +15 -0
  242. package/docs/guest.md +141 -0
  243. package/docs/in-place-write.md +81 -0
  244. package/docs/index.md +2 -0
  245. package/docs/install.md +31 -0
  246. package/docs/native-helper.md +10 -3
  247. package/docs/native.md +4 -0
  248. package/docs/output.md +32 -6
  249. package/docs/path-case.md +64 -0
  250. package/docs/path-scope.md +1 -1
  251. package/docs/permissions.md +11 -2
  252. package/docs/public-api.md +31 -2
  253. package/docs/root.md +30 -2
  254. package/docs/secure-file.md +2 -0
  255. package/docs/sidecar-lock.md +12 -3
  256. package/docs/temp.md +35 -6
  257. package/docs/types.md +1 -1
  258. package/docs/writing.md +153 -3
  259. package/package.json +14 -8
package/docs/guest.md ADDED
@@ -0,0 +1,141 @@
1
+ # Guest filesystem source
2
+
3
+ `@openclaw/fs-safe/guest` exports Python 3 source for filesystem operations in
4
+ Linux and macOS guests that do not have Node installed. The host imports a
5
+ string and passes it to its existing container, SSH, or process transport.
6
+ Importing this subpath performs no I/O and launches no process.
7
+
8
+ This is the filesystem engine extracted from OpenClaw's sandbox bridge. The
9
+ caller still owns root admission, mount selection, read-only policy, canonical
10
+ path authorization, live authority, argument framing, and process lifetime.
11
+ Use [Root](root.md) for ordinary filesystem operations in the Node process.
12
+ This source artifact is separate from the retired host-side Python worker
13
+ described in [Migrating to 0.5](migrating-to-0.5.md).
14
+
15
+ ## Exports and requirements
16
+
17
+ ```ts
18
+ import {
19
+ GUEST_FILESYSTEM_PYTHON,
20
+ GUEST_FILESYSTEM_CREATE_EXISTS_EXIT_CODE,
21
+ GUEST_FILESYSTEM_READ_NOT_FOUND_EXIT_CODE,
22
+ GUEST_FILESYSTEM_RENAME_NO_REPLACE_PYTHON,
23
+ } from "@openclaw/fs-safe/guest";
24
+ ```
25
+
26
+ `GUEST_FILESYSTEM_PYTHON` is the complete single-invocation program. The two
27
+ exit constants are `17` for an exclusive-create collision and `2` for a
28
+ missing read parent or leaf after the root opens. Other nonzero statuses are
29
+ errors; stderr is diagnostic text, not a structured error protocol.
30
+
31
+ The guest needs Python 3 with `ctypes`, descriptor-relative POSIX operations,
32
+ `O_DIRECTORY`, and `O_NOFOLLOW`. Linux and macOS are the supported guest
33
+ platforms. The guest does not need fs-safe's Node package, native addon, Rust,
34
+ or WASM. Native mode configuration in the host does not configure this program.
35
+
36
+ `GUEST_FILESYSTEM_RENAME_NO_REPLACE_PYTHON` contains just the
37
+ `rename_no_replace(src_parent_fd, src_basename, dst_parent_fd, dst_basename)`
38
+ definition. OpenClaw's workspace bootstrap consumes this same fragment when
39
+ publishing a prepared workspace; exporting it avoids a second source owner.
40
+ Its embedding program must import `ctypes`, `errno`, `os`, and `sys`, admit
41
+ the descriptors and single-component names, and own cleanup and syncing.
42
+ The fragment has no argument preflight of its own.
43
+
44
+ On Linux it uses `renameat2(RENAME_NOREPLACE)`; on macOS it uses
45
+ `renameatx_np(RENAME_EXCL)`. If Linux lacks that function or reports it as
46
+ unsupported, it falls back to `link(..., follow_symlinks=False)`, leaving the
47
+ source entry for caller cleanup. That fallback supports files, not directory
48
+ publication. Existing destinations are never replaced by this fragment.
49
+
50
+ ## Invocation protocol
51
+
52
+ Pass the source through `python3 -c` and arguments as literal argv elements.
53
+ Do not concatenate untrusted values into a shell command. Transport adapters
54
+ that require a shell must apply their existing quoting rules to every element.
55
+ The following example uses an already admitted guest root and parent path:
56
+
57
+ ```ts
58
+ import { spawnSync } from "node:child_process";
59
+ import { GUEST_FILESYSTEM_PYTHON } from "@openclaw/fs-safe/guest";
60
+
61
+ const result = spawnSync("python3", [
62
+ "-c", GUEST_FILESYSTEM_PYTHON,
63
+ "write", "/srv/admitted-workspace", "notes", "today.txt", "1",
64
+ ], { input: Buffer.from("hello\n"), timeout: 10_000 });
65
+ if (result.error) throw result.error;
66
+ if (result.status !== 0) throw new Error(result.stderr.toString());
67
+ ```
68
+
69
+ All frames below start at `sys.argv[1]`. `root` is an admitted guest directory;
70
+ `parent` and `directory` are paths relative to that root. An empty relative
71
+ path selects the root itself. Flags are strings: `"1"` enables and `"0"`
72
+ disables. Do not omit required fields.
73
+
74
+ | Operation | Positional frame | Input / output |
75
+ |---|---|---|
76
+ | Read | `read root parent basename [maxBytes]` | Raw bytes on stdout; optional nonnegative integer byte limit. |
77
+ | Write | `write root parent basename mkdir` | Raw stdin; atomically replaces a file entry. |
78
+ | Create | `create root parent basename mkdir` | Raw stdin; exclusively publishes a completed file. |
79
+ | Copy | `copy srcRoot srcParent srcBasename dstRoot dstParent dstBasename mkdir` | Regular-file copy, atomically replaces destination. |
80
+ | Rename | `rename srcRoot srcParent srcBasename dstRoot dstParent dstBasename mkdir` | Rename with cross-device copy/delete fallback. |
81
+ | Remove | `remove root parent basename recursive force` | Removes the leaf, or recursively removes its tree. |
82
+ | Make directories | `mkdirp root directory` | Creates missing relative directory components. |
83
+ | List directory | `readdir root directory` | JSON array of `{ name, isDirectory }`; no sorting guarantee. |
84
+
85
+ The complete program rejects empty, `.`, `..`, slash-containing, and NUL
86
+ basenames before opening roots or creating parents. Both leaf operands of copy
87
+ and rename are checked. Backslashes, colons, quotes, and newlines remain legal
88
+ POSIX basename characters. Relative directory traversal rejects `..`; empty
89
+ and `.` components are skipped. Supply admitted relative paths, not arbitrary
90
+ absolute paths whose spelling happens to pass that component walk.
91
+
92
+ ## Boundary and operation behavior
93
+
94
+ The supplied root spelling is trusted. Opening it does not prove the root is
95
+ the mount or inode previously authorized by the caller. Basename syntax checks
96
+ are not authorization. Callers must admit their roots and paths, preserve
97
+ read-only shadows and live authority, and run the program inside the intended
98
+ OS isolation boundary.
99
+
100
+ Parent traversal and operation bodies use directory descriptors. Reads and
101
+ copies refuse final symlinks, hardlinked files, and nonregular files. Bounded
102
+ reads check both admitted size and consumed bytes; a growing file can produce
103
+ partial stdout before rejection. Consumers must discard read output when the
104
+ exit status indicates failure. Removal unlinks a final symlink without
105
+ following it; rename moves symlink entries. Write and copy replace destination
106
+ entries, including symlinks, without following them. A force removal tolerates
107
+ a missing leaf but does not suppress failure to open its parent.
108
+
109
+ Write preserves an existing regular file's mode and otherwise creates private
110
+ files. Copy preserves the source mode. Exclusive create publishes a mode-0600
111
+ file from a private staging directory. Staging names retain the `.openclaw-*`
112
+ prefixes and use short random suffixes independent of the destination basename,
113
+ so legal names near the filesystem's component limit also work for writes and
114
+ cross-device moves.
115
+
116
+ Cross-device directory moves build a copy manifest and check it during source
117
+ cleanup. Source changes can leave the published destination and some or all
118
+ of the source. Regular-file and symlink move fallbacks unlink the source
119
+ pathname after publication; they do not perform the directory manifest's
120
+ identity checks. Directory cleanup also has check-to-unlink race windows.
121
+ These mechanics do not promise content integrity against same-UID peers.
122
+
123
+ ## Failure, cancellation, and budgets
124
+
125
+ Nonzero exit is not proof that nothing was published. A post-publication
126
+ identity or sync failure, or cross-device source-cleanup failure, can leave a
127
+ destination. The caller owns reconciliation and retry policy; do not assume
128
+ it is safe to remove that destination or replay a mutation blindly.
129
+
130
+ Ordinary Python failures run `finally` cleanup. There is no cancellation frame
131
+ or signal handler. Process death closes descriptors, but forced termination
132
+ can leave staging names. The transport owns termination, waiting for children
133
+ to settle, and any application-specific recovery.
134
+
135
+ Read byte limits are optional. Copy, write, recursive removal, directory
136
+ listing, and cross-device tree moves have no byte, entry, or depth budget in
137
+ this protocol. Recursive traversal and listing collect entries eagerly.
138
+ Use caller-owned isolation and resource limits appropriate to the operation.
139
+ The existing fsync sequence is preserved; this is not a recursive transaction,
140
+ rollback facility, or a stronger durability guarantee than the underlying
141
+ filesystem provides.
@@ -0,0 +1,81 @@
1
+ ---
2
+ title: In-place writes
3
+ description: "Replace bytes through a borrowed file handle while preserving its inode and attempting rollback on failure."
4
+ ---
5
+
6
+ # In-place writes
7
+
8
+ `overwriteFileHandle()` replaces a regular file's contents through a handle the
9
+ caller already owns. Use it when replacing the inode would break hardlinked
10
+ aliases, or when an existing writable file lives in a directory where the caller
11
+ cannot create a sibling temporary file.
12
+
13
+ ```ts
14
+ import { open } from "node:fs/promises";
15
+ import { overwriteFileHandle } from "@openclaw/fs-safe/advanced";
16
+
17
+ const handle = await open(filePath, "r+");
18
+ try {
19
+ await overwriteFileHandle(handle, Buffer.from(nextContents, "utf8"), {
20
+ beforeWrite: () => assertCurrentOwner(),
21
+ });
22
+ } finally {
23
+ await handle.close();
24
+ }
25
+ ```
26
+
27
+ ## Contract
28
+
29
+ ```ts
30
+ overwriteFileHandle(
31
+ handle: FileHandle,
32
+ data: Uint8Array,
33
+ options?: { beforeWrite?: () => void },
34
+ ): Promise<void>;
35
+ ```
36
+
37
+ The handle must be readable and writable, refer to a regular file, and have been
38
+ opened **without append mode**. Node cannot portably inspect a handle's append
39
+ flag, and some operating systems ignore positioned-write offsets for append
40
+ handles. Opening, path admission, symlink and hardlink policy, and closing remain
41
+ the caller's responsibility. The helper never reopens or replaces the inode,
42
+ changes the handle's current position, or closes it. Every hardlinked alias sees
43
+ the in-place changes.
44
+
45
+ The payload is borrowed, including its byte offset and length. Keep it unchanged,
46
+ attached, and accessible until the returned promise settles. Do not close the
47
+ handle or run concurrent I/O against the file, including through other aliases,
48
+ during preparation, writing, or rollback. This helper does not acquire a lock.
49
+
50
+ ## Preparation, ordering, and failure
51
+
52
+ The helper checks the file type and original size, then saves only the prefix
53
+ that will be overwritten: `min(data.byteLength, originalSize)` bytes. A short
54
+ prefix read fails with `FsSafeError("read-failed")` before writing. Memory for the
55
+ backup is bounded by that prefix length; a short replacement of a large file does
56
+ not read or buffer the untouched tail.
57
+
58
+ After preparation, `beforeWrite` runs synchronously once. A thrown value is
59
+ propagated unchanged without any file mutation. A Promise or thenable return is
60
+ rejected with `TypeError`; asynchronous callbacks cannot admit a write. Once
61
+ admitted, the operation finishes the write or its failure recovery without
62
+ calling `beforeWrite` again. This callback is a whole-operation admission point,
63
+ not the per-mutation `assertBeforeMutation` callback used by Root operations.
64
+ It must not change the file, handle, or payload. There is no cancellation option.
65
+
66
+ For growth, additional tail bytes are written before the existing prefix is
67
+ touched. The prefix is then overwritten, completing partial writes. For shrinkage,
68
+ truncation is last. If a write or truncation fails, the helper attempts to restore
69
+ the saved prefix, if touched, and the original length. It waits for both recovery
70
+ attempts and then rethrows the original error, even when recovery also fails.
71
+ Zero-progress writes fail with `FsSafeError("helper-failed")`.
72
+
73
+ Recovery is best effort, not atomic publication or a crash-recovery guarantee.
74
+ Other observers can see intermediate contents; failed recovery can leave partial
75
+ bytes. The helper does not chmod or synchronize the file or its directory.
76
+ Normal filesystem effects such as timestamp updates or clearing special mode
77
+ bits can still occur. Callers own any durability or broader transaction policy.
78
+
79
+ The implementation uses Node positional reads and writes in every native mode;
80
+ it does not load a native binding. Prefer [`Root.write`](writing.md) when atomic
81
+ replacement and root-based path admission are the intended contract.
package/docs/index.md CHANGED
@@ -52,6 +52,7 @@ await fs.remove("notes/archive/today.txt");
52
52
  |---|---|
53
53
  | [`root()`](root.md) | One boundary for read/write/move/remove and bounded recursive walking inside a trusted directory. |
54
54
  | [`@openclaw/fs-safe/config`](config.md) | Process-global native helper and lock-option defaults. |
55
+ | [`@openclaw/fs-safe/guest`](guest.md) | Python filesystem source for caller-owned guest transports, with admitted roots and descriptor-relative operations. |
55
56
  | [Native helper policy](native-helper.md) | Choose `auto`, `off`, or `require` for platform-native primitives. |
56
57
  | [Native architecture](native.md) | Understand the thin syscall layer, beneath model, platform mechanisms, and fallback boundary. |
57
58
  | [`replaceFileAtomic`](atomic.md) | Sibling-temp + rename, fsync hooks, mode preservation, copy fallback. |
@@ -65,6 +66,7 @@ await fs.remove("notes/archive/today.txt");
65
66
  | [`tempWorkspace`](temp.md) | 0700 scratch dir with auto-cleanup. |
66
67
  | [`readSecureFile`](secure-file.md) | Absolute file reads with fd pinning, permissions, owner, size, and timeout checks. |
67
68
  | [`walkDirectory` / `Root.walk`](walk.md) | Standalone inventories plus root-bounded pruning, budgets, and partial-error reporting. |
69
+ | [`Root.entries`](entries.md) | Guarded nonrecursive entries, bounded name collection, and caller-owned symlink validation. |
68
70
  | [`extractArchive`](archive.md) | Policy-driven ZIP/TAR extraction with clamp/filter, metadata/path-depth, link, count, and byte limits. |
69
71
  | [Secret files](secret-file.md) | Mode-0600 credentials with size and TOCTOU defense. |
70
72
  | [Permissions](permissions.md) | POSIX mode helpers plus Windows ACL inspection, raw owner/ACE facts, remediation, and private-directory creation. |
package/docs/install.md CHANGED
@@ -31,6 +31,37 @@ node --version
31
31
  # v22.0.0 or newer
32
32
  ```
33
33
 
34
+ ## Bun runtime
35
+
36
+ Bun 1.4.2 can run the same public APIs and load the matching native package.
37
+ On macOS and Linux, fs-safe uses its Rust N-API addon to call the system
38
+ `realpath` implementation for native resolution. Ordinary resolution follows
39
+ Node's component walk, including lexical normalization of expanded symlink
40
+ targets, in Rust, with a 1,024-link expansion limit that returns `ELOOP` for
41
+ excessive or cyclic expansion. This works around Bun path-resolution defects that
42
+ otherwise reject restrictive permissions and confuse literal POSIX backslashes
43
+ with directory separators. The OS still resolves symlinks and canonical file
44
+ names; fs-safe retains its confinement and file-identity checks.
45
+
46
+ This works with `bun --jitless` and needs no runtime FFI or JIT. Use native mode
47
+ `auto` or `require` with the matching addon installed. `FS_SAFE_NATIVE_MODE=off`
48
+ still disables all addon loading; `auto` without the addon falls back to Bun's
49
+ resolver. Those configurations retain Bun 1.4.2's limitations with restrictive
50
+ permissions, sockets, literal backslashes, and symlink/parent traversal. Use
51
+ Node if you need full compatibility without the addon. On Bun POSIX, `require`
52
+ also rejects canonicalization when the addon or its canonicalizer is unavailable.
53
+
54
+ Node and Windows use their existing runtime canonicalizers. On Windows, Bun's
55
+ recursive directory creation receives an absolute spelling that preserves raw
56
+ path components, working around its rejection of existing relative `.` and `..`
57
+ directories. Public paths and caller-supplied filesystem adapters remain unchanged.
58
+
59
+ The upstream fix is tracked in [Bun #42374](https://github.com/oven-sh/bun/pull/42374).
60
+ The adapter can be removed when the supported Bun baseline includes that fix.
61
+ Run native compatibility checks with `pnpm test:bun:native` after building the
62
+ package and addon. See [contributing](contributing.md) for the Node/pnpm toolchain
63
+ and the broader diagnostic suite.
64
+
34
65
  ## TypeScript
35
66
 
36
67
  Types ship with the package — no `@types/openclaw__fs-safe` needed. The `exports` map in `package.json` provides typed entries for every subpath:
@@ -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
@@ -133,6 +133,10 @@ workers rather than the JavaScript event loop.
133
133
  | `require` | Try once, cache the result | Throw `FsSafeError("helper-unavailable")` |
134
134
  | `off` | Never attempt a binding load | Always use guarded JavaScript |
135
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
+
136
140
  Features without a safe JavaScript implementation, including zstd/bzip2 TAR,
137
141
  Windows private-directory creation, and [retained-directory staging](staged-file.md),
138
142
  fail with `helper-unavailable` when native support is absent or off. Staging
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";
@@ -71,8 +71,17 @@ resolveWindowsUserPrincipal(env);
71
71
  The fallback Windows inspector reads the owner and DACL together through one
72
72
  built-in Windows PowerShell/.NET query. It returns canonical SIDs and numeric
73
73
  access masks, so Unicode paths and account names do not pass through lossy
74
- console display text. `inspectWindowsAcl()` uses the same query and returns
75
- canonical SIDs in its `principal` fields, with normalized rights tokens.
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.
76
85
  The advanced options retain `currentUserSid` as an explicit classification
77
86
  override and `principalTranslationFailed: true` as an immediate unverified
78
87
  result. The optional `principalSids` translation cache is still accepted but
@@ -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
 
package/docs/root.md CHANGED
@@ -113,13 +113,21 @@ fs.append(rel, data, options?) // append text/buffer; syncs before clo
113
113
  fs.copyIn(rel, sourceAbsPath, options?) // copy from outside the root, atomically, with size cap
114
114
  fs.openWritable(rel, options?) // FileHandle for streaming writes; supports await using
115
115
  fs.move(from, to, options?) // rename within the root; defaults to no clobber
116
- fs.remove(rel, options?) // unlink file or rmdir empty directory
116
+ fs.remove(rel, options?) // unlink file, rmdir, or bounded recursive removal
117
117
  fs.mkdir(rel, options?) // mkdir -p (creates missing parents)
118
118
  fs.ensureRoot(options?) // accepts "" / "." as the root itself
119
119
  ```
120
120
 
121
121
  `write`, `create`, `append`, `writeJson`, and `createJson` accept `mode?: number`; use `0o600` for credentials and other private state. `writeJson` also accepts the same options as `JSON.stringify` plus `trailingNewline?: boolean` (defaults `true` so the file ends in `\n`).
122
122
 
123
+ `create` also accepts `AsyncIterable<Uint8Array>` with `RootCreateStreamOptions`:
124
+ the same path, authority, mode, and durability options, plus `maxBytes` and
125
+ `signal`, without `encoding` or `renameIdentity`. It consumes one chunk at a
126
+ time and publishes the completed file exclusively. The byte cap inherits an
127
+ explicit `Root.defaults.maxBytes`; without either cap, consumption is unlimited.
128
+ See [streamed creation](writing.md#streamed-creation) for cancellation,
129
+ cleanup, and filesystem requirements.
130
+
123
131
  `append` accepts `prependNewlineIfNeeded: true` to separate text from existing
124
132
  content when neither side supplies a newline. String data uses its `encoding`
125
133
  for the newline check, including UTF-16LE; Buffer data uses a single LF byte.
@@ -213,6 +221,18 @@ basename first.
213
221
 
214
222
  `openWritable` opens a writable file with options `mode?: number` and `writeMode?: "replace" | "append" | "update"`. `replace` truncates existing files and is the default; `update` keeps existing contents. Use it for streaming output. Prefer `await using` for cleanup.
215
223
 
224
+ `remove` leaves non-empty directories unchanged unless `recursive: true` is
225
+ provided. Recursive removal defaults to streaming entries in filesystem order;
226
+ `order: "sorted"` processes each directory's children lexicographically. The
227
+ `maxEntries` (100,000 by default) and `maxDepth` (64 by default) budgets accept
228
+ explicit `Infinity` when the caller needs unlimited traversal. It never
229
+ follows discovered symlinks; an explicit `mutationSymlinks` policy rejects them,
230
+ while the omitted policy unlinks them. `force: true` ignores missing targets,
231
+ and `signal` stops further work after admitted I/O and resource cleanup settle.
232
+ Removal is not transactional: a budget, cancellation, policy, or identity
233
+ failure can leave a partially removed tree. See [removal](writing.md)
234
+ for the full counting and failure contract.
235
+
216
236
  ### Live mutation authority
217
237
 
218
238
  All mutation methods accept `assertBeforeMutation?: () => void`. Use it when a
@@ -273,14 +293,22 @@ fs.exists(rel) // boolean
273
293
  fs.stat(rel) // PathStat
274
294
  fs.list(rel) // string[]
275
295
  fs.list(rel, { withFileTypes }) // DirEntry[]
296
+ fs.entries(rel, options?) // nonrecursive AsyncIterable<DirEntry>, including symlinks
276
297
  fs.resolve(rel) // absolute path inside the root, after canonicalization
277
298
  ```
278
299
 
279
300
  These do not pin a later operation. They are safe to expose to UIs and decision points; for the actual read or write, use the verb methods so the operation pins identity at the point of use.
280
301
 
302
+ `entries()` streams immediate children in filesystem order by default. It
303
+ supports cancellation, a physical-entry limit that throws on overflow, and
304
+ bounded sorted-name collection. It reports child symlinks without following
305
+ them; its `symlinks` option applies only to the selected directory path.
306
+ See [Directory entries](entries.md) for ordering, identity, and partial-result
307
+ semantics.
308
+
281
309
  `resolve()` is the exception to the existing-object rule: because it selects a
282
310
  location for later use, it rejects a leading drive-relative spelling. Reads,
283
- `stat`, `exists`, `list`, `walk`, `remove`, and the source argument of `move`
311
+ `stat`, `exists`, `list`, `entries`, `walk`, `remove`, and the source argument of `move`
284
312
  accept an existing POSIX filename such as `c:notes.txt`. For `move`, only the
285
313
  new destination name is subject to the portable guard.
286
314
 
@@ -21,6 +21,7 @@ The helper:
21
21
  - rejects every non-regular preview and, by default, symlink paths
22
22
  - opens POSIX paths no-follow and nonblocking before reading, then verifies the opened fd still matches the path and realpath; a FIFO swap cannot block before `timeoutMs` owns the byte read
23
23
  - optionally requires the real path to live under one of `trust.trustedDirs`
24
+ - rejects hardlink aliases using descriptor, pathname, and realpath link counts, then rechecks the descriptor after reading before returning bytes
24
25
  - rejects hard-to-verify or unsafe permissions unless `permissions.allowInsecure` is set
25
26
  - rejects files owned by another POSIX uid
26
27
  - enforces `maxBytes` before and after reading
@@ -73,6 +74,7 @@ type SecureFileReadOptions = {
73
74
  | `not-found` | The path could not be stat'd before open. |
74
75
  | `not-file` | The opened target is not a regular file. |
75
76
  | `symlink` | The path is a symlink and `trust.allowSymlink` is false. |
77
+ | `hardlink` | The descriptor, pathname, or realpath has more than one link. |
76
78
  | `path-mismatch` | The path or realpath changed between open and verification, or filesystem identity could not be verified after bounded re-inspection. |
77
79
  | `outside-workspace` | `realPath` is outside `trust.trustedDirs`. |
78
80
  | `permission-unverified` | Required mode/ACL checks could not be completed. |