@openclaw/openshell-sandbox 2026.10.6 → 2026.10.7

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 (122) hide show
  1. package/node_modules/@openclaw/fs-safe/CHANGELOG.md +48 -0
  2. package/node_modules/@openclaw/fs-safe/README.md +14 -4
  3. package/node_modules/@openclaw/fs-safe/dist/archive-merge.js +28 -3
  4. package/node_modules/@openclaw/fs-safe/dist/archive-parser.wasm +0 -0
  5. package/node_modules/@openclaw/fs-safe/dist/archive-read.js +11 -5
  6. package/node_modules/@openclaw/fs-safe/dist/archive-zip-loader.js +4 -2
  7. package/node_modules/@openclaw/fs-safe/dist/archive.js +2 -2
  8. package/node_modules/@openclaw/fs-safe/dist/clone-metadata.js +1 -1
  9. package/node_modules/@openclaw/fs-safe/dist/copy-file-input.d.ts +2 -0
  10. package/node_modules/@openclaw/fs-safe/dist/copy-file-input.js +4 -3
  11. package/node_modules/@openclaw/fs-safe/dist/copy-link.d.ts +26 -0
  12. package/node_modules/@openclaw/fs-safe/dist/copy-link.js +169 -0
  13. package/node_modules/@openclaw/fs-safe/dist/copy-metadata.d.ts +9 -0
  14. package/node_modules/@openclaw/fs-safe/dist/copy-metadata.js +39 -0
  15. package/node_modules/@openclaw/fs-safe/dist/copy-root-file-sync.js +1 -1
  16. package/node_modules/@openclaw/fs-safe/dist/copy-tree-portable.js +3 -6
  17. package/node_modules/@openclaw/fs-safe/dist/copy.js +3 -3
  18. package/node_modules/@openclaw/fs-safe/dist/darwin-acl.d.ts +12 -0
  19. package/node_modules/@openclaw/fs-safe/dist/darwin-acl.js +29 -0
  20. package/node_modules/@openclaw/fs-safe/dist/directory-mode-node.d.ts +3 -0
  21. package/node_modules/@openclaw/fs-safe/dist/directory-mode-node.js +27 -2
  22. package/node_modules/@openclaw/fs-safe/dist/entry-publication.js +1 -1
  23. package/node_modules/@openclaw/fs-safe/dist/file-handle-transfer.js +4 -2
  24. package/node_modules/@openclaw/fs-safe/dist/file-hash.js +4 -3
  25. package/node_modules/@openclaw/fs-safe/dist/file-lock.d.ts +1 -0
  26. package/node_modules/@openclaw/fs-safe/dist/file-lock.js +1 -0
  27. package/node_modules/@openclaw/fs-safe/dist/file-write-lease.d.ts +9 -0
  28. package/node_modules/@openclaw/fs-safe/dist/file-write-lease.js +42 -0
  29. package/node_modules/@openclaw/fs-safe/dist/guest-native-python.js +3 -0
  30. package/node_modules/@openclaw/fs-safe/dist/hardlink-fallback.d.ts +1 -0
  31. package/node_modules/@openclaw/fs-safe/dist/hardlink-fallback.js +5 -0
  32. package/node_modules/@openclaw/fs-safe/dist/json-durable-queue-ownership.js +16 -3
  33. package/node_modules/@openclaw/fs-safe/dist/native-binding.d.ts +57 -18
  34. package/node_modules/@openclaw/fs-safe/dist/native-directory-observation.js +1 -1
  35. package/node_modules/@openclaw/fs-safe/dist/native-move-fallback.d.ts +1 -1
  36. package/node_modules/@openclaw/fs-safe/dist/native-move-fallback.js +2 -0
  37. package/node_modules/@openclaw/fs-safe/dist/native-operations.js +1 -1
  38. package/node_modules/@openclaw/fs-safe/dist/native-parent-admission.d.ts +1 -1
  39. package/node_modules/@openclaw/fs-safe/dist/native-parent-admission.js +2 -0
  40. package/node_modules/@openclaw/fs-safe/dist/native-pinned-write-windows.d.ts +1 -1
  41. package/node_modules/@openclaw/fs-safe/dist/native-pinned-write-windows.js +19 -1
  42. package/node_modules/@openclaw/fs-safe/dist/native-pinned-write.d.ts +4 -1
  43. package/node_modules/@openclaw/fs-safe/dist/native-pinned-write.js +18 -0
  44. package/node_modules/@openclaw/fs-safe/dist/native-policy-parent.d.ts +1 -1
  45. package/node_modules/@openclaw/fs-safe/dist/native-policy-parent.js +3 -0
  46. package/node_modules/@openclaw/fs-safe/dist/native-staged-file.d.ts +1 -1
  47. package/node_modules/@openclaw/fs-safe/dist/native-staged-file.js +3 -0
  48. package/node_modules/@openclaw/fs-safe/dist/native.d.ts +5 -3
  49. package/node_modules/@openclaw/fs-safe/dist/native.js +25 -6
  50. package/node_modules/@openclaw/fs-safe/dist/opened-realpath.js +9 -1
  51. package/node_modules/@openclaw/fs-safe/dist/overwrite-file-handle.js +4 -1
  52. package/node_modules/@openclaw/fs-safe/dist/permissions-public.d.ts +1 -0
  53. package/node_modules/@openclaw/fs-safe/dist/permissions-public.js +1 -0
  54. package/node_modules/@openclaw/fs-safe/dist/permissions-windows.js +36 -47
  55. package/node_modules/@openclaw/fs-safe/dist/pinned-write-input.js +2 -0
  56. package/node_modules/@openclaw/fs-safe/dist/pinned-write-staged.js +3 -0
  57. package/node_modules/@openclaw/fs-safe/dist/pinned-write-types.d.ts +2 -1
  58. package/node_modules/@openclaw/fs-safe/dist/pinned-write.js +5 -3
  59. package/node_modules/@openclaw/fs-safe/dist/pipe.d.ts +9 -0
  60. package/node_modules/@openclaw/fs-safe/dist/pipe.js +26 -0
  61. package/node_modules/@openclaw/fs-safe/dist/private-producer-handoff.js +2 -5
  62. package/node_modules/@openclaw/fs-safe/dist/publish-copy-stage.js +92 -10
  63. package/node_modules/@openclaw/fs-safe/dist/publish-file.d.ts +1 -1
  64. package/node_modules/@openclaw/fs-safe/dist/publish-file.js +12 -11
  65. package/node_modules/@openclaw/fs-safe/dist/root-copy.d.ts +18 -0
  66. package/node_modules/@openclaw/fs-safe/dist/root-copy.js +128 -0
  67. package/node_modules/@openclaw/fs-safe/dist/root-create-native.js +12 -4
  68. package/node_modules/@openclaw/fs-safe/dist/root-impl.js +6 -92
  69. package/node_modules/@openclaw/fs-safe/dist/root-move-noreplace.js +1 -1
  70. package/node_modules/@openclaw/fs-safe/dist/root-options.d.ts +2 -0
  71. package/node_modules/@openclaw/fs-safe/dist/root-remove-native.js +8 -1
  72. package/node_modules/@openclaw/fs-safe/dist/sibling-staged-file.js +1 -1
  73. package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-retained.js +1 -1
  74. package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-target.js +8 -1
  75. package/node_modules/@openclaw/fs-safe/dist/test-hooks.d.ts +1 -0
  76. package/node_modules/@openclaw/fs-safe/dist/test-hooks.js +1 -0
  77. package/node_modules/@openclaw/fs-safe/dist/watch-native.js +2 -2
  78. package/node_modules/@openclaw/fs-safe/dist/windows-security-bridge.cs +11 -5
  79. package/node_modules/@openclaw/fs-safe/dist/windows-security-command.js +9 -1
  80. package/node_modules/@openclaw/fs-safe/dist/windows-test-hooks.d.ts +16 -0
  81. package/node_modules/@openclaw/fs-safe/dist/windows-test-hooks.js +51 -0
  82. package/node_modules/@openclaw/fs-safe/docs/archive.md +16 -0
  83. package/node_modules/@openclaw/fs-safe/docs/contributing.md +35 -5
  84. package/node_modules/@openclaw/fs-safe/docs/copy.md +22 -0
  85. package/node_modules/@openclaw/fs-safe/docs/durability.md +4 -2
  86. package/node_modules/@openclaw/fs-safe/docs/errors.md +8 -0
  87. package/node_modules/@openclaw/fs-safe/docs/guest.md +2 -0
  88. package/node_modules/@openclaw/fs-safe/docs/install.md +13 -5
  89. package/node_modules/@openclaw/fs-safe/docs/migrating-to-0.5.md +5 -5
  90. package/node_modules/@openclaw/fs-safe/docs/native-helper.md +15 -5
  91. package/node_modules/@openclaw/fs-safe/docs/native.md +51 -2
  92. package/node_modules/@openclaw/fs-safe/docs/pipe.md +81 -0
  93. package/node_modules/@openclaw/fs-safe/docs/platform-capabilities.md +63 -0
  94. package/node_modules/@openclaw/fs-safe/docs/quickstart.md +23 -3
  95. package/node_modules/@openclaw/fs-safe/docs/reading.md +7 -1
  96. package/node_modules/@openclaw/fs-safe/docs/secret-file.md +5 -5
  97. package/node_modules/@openclaw/fs-safe/docs/sidecar-lock.md +31 -13
  98. package/node_modules/@openclaw/fs-safe/docs/store.md +11 -0
  99. package/node_modules/@openclaw/fs-safe/docs/types.md +45 -80
  100. package/node_modules/@openclaw/fs-safe/docs/writing.md +121 -14
  101. package/node_modules/@openclaw/fs-safe/package.json +18 -10
  102. package/node_modules/@openclaw/fs-safe-darwin-arm64/fs-safe-native.node +0 -0
  103. package/node_modules/@openclaw/fs-safe-darwin-arm64/package.json +1 -1
  104. package/node_modules/@openclaw/fs-safe-darwin-x64/fs-safe-native.node +0 -0
  105. package/node_modules/@openclaw/fs-safe-darwin-x64/package.json +1 -1
  106. package/node_modules/@openclaw/fs-safe-freebsd-arm64/fs-safe-native.node +0 -0
  107. package/node_modules/@openclaw/fs-safe-freebsd-arm64/package.json +29 -0
  108. package/node_modules/@openclaw/fs-safe-freebsd-x64/fs-safe-native.node +0 -0
  109. package/node_modules/@openclaw/fs-safe-freebsd-x64/package.json +29 -0
  110. package/node_modules/@openclaw/fs-safe-linux-arm64-gnu/fs-safe-native.node +0 -0
  111. package/node_modules/@openclaw/fs-safe-linux-arm64-gnu/package.json +1 -1
  112. package/node_modules/@openclaw/fs-safe-linux-arm64-musl/fs-safe-native.node +0 -0
  113. package/node_modules/@openclaw/fs-safe-linux-arm64-musl/package.json +1 -1
  114. package/node_modules/@openclaw/fs-safe-linux-x64-gnu/fs-safe-native.node +0 -0
  115. package/node_modules/@openclaw/fs-safe-linux-x64-gnu/package.json +1 -1
  116. package/node_modules/@openclaw/fs-safe-linux-x64-musl/fs-safe-native.node +0 -0
  117. package/node_modules/@openclaw/fs-safe-linux-x64-musl/package.json +1 -1
  118. package/node_modules/@openclaw/fs-safe-win32-arm64-msvc/fs-safe-native.node +0 -0
  119. package/node_modules/@openclaw/fs-safe-win32-arm64-msvc/package.json +29 -0
  120. package/node_modules/@openclaw/fs-safe-win32-x64-msvc/fs-safe-native.node +0 -0
  121. package/node_modules/@openclaw/fs-safe-win32-x64-msvc/package.json +1 -1
  122. package/package.json +5 -5
@@ -0,0 +1,51 @@
1
+ import path from "node:path";
2
+ import { FsSafeError } from "./errors.js";
3
+ import { requireNativeBinding } from "./native.js";
4
+ function call(operation) {
5
+ if (process.platform !== "win32") {
6
+ throw new FsSafeError("unsupported-platform", "Windows test fixtures require Windows");
7
+ }
8
+ try {
9
+ return operation();
10
+ }
11
+ catch (cause) {
12
+ if (cause instanceof FsSafeError)
13
+ throw cause;
14
+ throw new FsSafeError("helper-failed", "Windows test fixture operation failed", { cause });
15
+ }
16
+ }
17
+ /** Test-only handle that omits delete sharing. */
18
+ export function holdWindowsSharingLock(filePath) {
19
+ return call(() => {
20
+ const native = requireNativeBinding();
21
+ if (!native.holdWindowsSharingLock)
22
+ throw new FsSafeError("helper-unavailable", "Windows sharing fixture unavailable");
23
+ const owner = native.holdWindowsSharingLock(path.toNamespacedPath(filePath));
24
+ let closed = false;
25
+ const close = () => {
26
+ if (closed)
27
+ return;
28
+ closed = true;
29
+ call(() => owner.close());
30
+ };
31
+ return Object.freeze({ close, [Symbol.dispose]: close });
32
+ });
33
+ }
34
+ /** Test-only attribute updates preserve unspecified bits on the opened object. */
35
+ export function setWindowsFileAttributes(filePath, attrs) {
36
+ call(() => {
37
+ const native = requireNativeBinding();
38
+ if (!native.setWindowsFileAttributes)
39
+ throw new FsSafeError("helper-unavailable", "Windows attribute fixture unavailable");
40
+ native.setWindowsFileAttributes(path.toNamespacedPath(filePath), attrs);
41
+ });
42
+ }
43
+ /** Test-only physical mapping reader; lcn -1 represents sparse clusters. */
44
+ export function readWindowsFileExtents(filePath) {
45
+ return call(() => {
46
+ const native = requireNativeBinding();
47
+ if (!native.readWindowsFileExtents)
48
+ throw new FsSafeError("helper-unavailable", "Windows extent fixture unavailable");
49
+ return native.readWindowsFileExtents(path.toNamespacedPath(filePath));
50
+ });
51
+ }
@@ -185,6 +185,15 @@ policy to the complete archive, including entries that extraction would strip
185
185
  or skip. Known UTF-8 and Unicode Path names are checked during physical admission;
186
186
  legacy names retain backend-selected decoding and undergo the same collision
187
187
  check before callbacks or selected bytes are returned.
188
+ Native legacy decoding is CP437 when neither the UTF-8 flag nor a valid Unicode
189
+ Path override declares UTF-8, even if the raw bytes also happen to form valid
190
+ UTF-8. Invalid flagged UTF-8 and invalid Unicode Path CRC/encoding fail closed;
191
+ the native metadata boundary enforces these checks before decoder normalization.
192
+ Native ZIP inspection also retains rejection of malformed Unicode Comment
193
+ checksums and UTF-8, including in entries unrelated to a requested payload.
194
+ Comment CRCs follow the native decoder's historical decoded-comment semantics;
195
+ valid sequential Unicode overrides retain their decoder order. Public physical
196
+ admission continues to reject duplicate critical Unicode Path fields.
188
197
 
189
198
  ZIP end-record admission searches the bounded comment window for signatures
190
199
  while retaining complete comment-length and ambiguity checks. Dense signature
@@ -244,6 +253,9 @@ remaining components are skipped before the filter callback, but still count
244
253
  toward `maxEntries` and undergo traversal validation. JavaScript TAR extraction
245
254
  copies the admitted payload range to this accepted output path, so depth checks,
246
255
  collision checks, writes, and mode application agree.
256
+ ZIP directories naming the relative archive root (for example `./`) follow the
257
+ same skip rule on both backends. Portable preflight retains their relative `./`
258
+ spelling; a literal absolute `/` entry is still rejected.
247
259
 
248
260
  An `entryFilter` sees the validated **canonical effective archive path before
249
261
  stripping**, entry kind, and declared size. On every JavaScript and native
@@ -348,6 +360,10 @@ A failure before publication preserves a pre-existing file, and rejection does
348
360
  not grant authority to delete a substituted file or alias. Failed extraction does not
349
361
  restore overwritten contents. Active destination mutations and their guarded
350
362
  cleanup still finish before rejection; no later destination mutation begins.
363
+ On supported native POSIX paths, each active destination ancestor uses its retained
364
+ descriptor plus fresh named-identity and physical-path observations at every guard
365
+ boundary. These checks are never cached or reduced to the deepest directory alone.
366
+ Other paths retain the full pathname checks.
351
367
  Portable ZIP output is not eligible for publication until its stream closes or
352
368
  the defensive `FileHandle` close succeeds. If that fallback close rejects,
353
369
  `extractArchive()` propagates the error and publishes no entry from the staged
@@ -100,6 +100,29 @@ On Linux x64 or arm64 with Docker, run `pnpm build`, copy the matching GNU artif
100
100
  artifact and run native security and no-replace move tests in Rocky Linux 8
101
101
  (glibc 2.28). Both GNU architectures execute this load test on matching runners.
102
102
 
103
+ ### Windows ARM64 builds
104
+
105
+ `windows-11-arm` builds and runs the ARM64 MSVC addon using native ARM64 Node
106
+ and Rust, and separately tests x64 Node and Rust under Windows emulation. Both
107
+ lanes run the native suite, main-thread and Worker private-creation/descriptor
108
+ proof, and root-only npm/pnpm install smoke. PE inspection matches the loaded
109
+ consumer binary's hash, architecture and required NT imports. Omitted-optionals
110
+ creation probes in the x64-emulated cell have a 600-second deadline: three
111
+ complete measurements had a 183.18-second median, with 140 PowerShell launches
112
+ accounting for 98–99% of wall time. This is over three times that median, rounded
113
+ up to a whole minute. Native ARM64 and x64 cells, and every other consumer probe,
114
+ retain their 120-second deadlines. Release builds
115
+ also run the ARM64 native security and Worker tests before uploading the binding.
116
+
117
+ The only linked C codec is `zstd-sys`; `cargo test --workspace --locked` and the
118
+ release addon build compile it for `aarch64-pc-windows-msvc`. Bzip2 uses
119
+ `libbz2-rs-sys`, and deflate uses `miniz_oxide`, both Rust implementations.
120
+ The portable WASM build uses the checksum-pinned x64 WASI SDK under Windows
121
+ emulation because that SDK has no Windows ARM64 host archive. Its compiler and
122
+ archiver produce wasm32 only; the addon still uses MSVC for the selected target.
123
+ Bun 1.4.2 has no Windows ARM64 artifact; the existing Windows x64 Bun lanes remain
124
+ the Bun runtime proof. Native Windows ARM64 support is qualified with Node.
125
+
103
126
  ## Test
104
127
 
105
128
  ```bash
@@ -262,14 +285,14 @@ OS/CPU/libc selection, a native-required SHA-256 operation, and fresh-process
262
285
  optionals. Omitted-optionals installs also verify that all public subpaths can
263
286
  be imported, without implying every operation remains available.
264
287
 
265
- Host-only smoke supplies the six foreign packages using their unchanged real
288
+ Host-only smoke supplies the seven foreign packages using their unchanged real
266
289
  manifests and clearly marked synthetic, non-executable payloads. Every foreign
267
290
  metadata/tarball endpoint is checked before installation, so a missing fixture
268
291
  cannot masquerade as successful platform filtering. These temporary fixtures
269
292
  never enter `packages/`, release artifacts, or the publish manifest. They prove
270
293
  installer filtering, not foreign native compilation or execution. Full release
271
- collection uses the actual seven collected native tarballs instead. Run it with
272
- `pnpm package:collect` after assembling all seven real bindings; missing targets
294
+ collection uses the actual eight collected native tarballs instead. Run it with
295
+ `pnpm package:collect` after assembling all eight real bindings; missing targets
273
296
  fail collection. `pnpm package:collect --allow-host-only` exercises the same
274
297
  lifecycle boundary locally but proves only the host. Both collection commands
275
298
  require the pnpm lifecycle CLI path; JavaScript CLIs run through Node and standalone
@@ -277,7 +300,7 @@ require the pnpm lifecycle CLI path; JavaScript CLIs run through Node and standa
277
300
  direct `node` invocation without lifecycle metadata is unsupported. Archive
278
301
  codecs and their dependencies are packed from the installed dependency graph.
279
302
 
280
- PR CI builds and executes all seven shipped bindings. The existing check names
303
+ PR CI builds and executes all eight shipped bindings. The existing check names
281
304
  stay stable; extra runner/runtime combinations add checks without changing the
282
305
  repository ruleset. Native lanes run Node 24; the GNU host lanes also exercise
283
306
  Bun 1.4.2 (as do macOS and Windows).
@@ -294,6 +317,7 @@ Bun 1.4.2 (as do macOS and Windows).
294
317
  | Native check | `macos-15-intel` | x64 | Node 24, Bun 1.4.2 |
295
318
  | Native check | `fs-safe-windows-16core` (`windows-latest`) | x64 | Node 24, Bun 1.4.2 |
296
319
  | Native check | `windows-2022` (standard hosted) | x64 | Node 24, Bun 1.4.2 |
320
+ | Windows ARM64 host | `windows-11-arm` | arm64 native and x64 emulated | Node 24 |
297
321
  | Native check (Alpine 3.24) | `ubuntu-latest` | x64 / musl | Node 24 |
298
322
  | Native check (Alpine 3.24) | `ubuntu-24.04-arm` | arm64 / musl | Node 24 |
299
323
  | GNU glibc 2.28 build + Rocky Linux 8 load | `ubuntu-latest` | x64 / glibc | Node 24 |
@@ -302,11 +326,17 @@ Bun 1.4.2 (as do macOS and Windows).
302
326
  | Coverage | `ubuntu-latest`, `macos-15`, `fs-safe-windows-16core` | host | Node 22 |
303
327
 
304
328
  Both musl lanes also run root-only package smoke with the real host binding.
305
- The seven-target source build matrix still runs on release tags. The smoke writes manager versions,
329
+ The eight-target source build matrix still runs on release tags. The smoke writes manager versions,
306
330
  cases, and synthetic-fixture scope to `release-artifacts/consumer-proof.json`.
307
331
 
308
332
  ## Docs
309
333
 
334
+ `pnpm docs:check` runs examples against the built package and compiles every
335
+ TypeScript block in `README.md`, the lock API signatures, and the custom stale-lock
336
+ policy example against its declarations. Run `pnpm build` first.
337
+ The compiler supplies typed application context for excerpted variables such as
338
+ `download`; package API calls and callback values remain fully type-checked.
339
+
310
340
  The docs site is rendered recursively from Markdown files under `docs/` by `scripts/build-docs-site.mjs`. Build locally to preview:
311
341
 
312
342
  ```bash
@@ -68,12 +68,24 @@ On Linux, automatic byte copying also uses the native binding when available. It
68
68
 
69
69
  Tree-copy cleanup attempts every acquired close once, even when another close fails. Portable file handles close output before input; completed directories close their pinned source before target; the public wrapper then closes its source before the destination parent. A copy, identity, metadata, native-clone, or cancellation failure already observed at one of those scopes remains the reported value instead of being replaced by cleanup. If that scope otherwise succeeded, its first close failure is reported unchanged. Concurrent siblings have no global structural close order: the first observed sibling failure stops new work while admitted work settles. Portable copying records caller cancellation when it occurs, so a later abort during cleanup cannot replace an earlier copy failure.
70
70
 
71
+ For guarded per-file copying, [`Root.copyIn`](writing.md#copy-metadata-and-source-links) can preserve timestamps and Windows non-permission attributes with `preserveMetadata`, and copy literal links with `sourceSymlinks: "copy-link"`. Permission selection remains separate. Windows reparse payload handling is shared with this tree copier.
72
+
71
73
  Clones preserve file contents, empty directories, timestamps, executable modes where supported, and literal symbolic links. Editing a clone does not modify its source. Unsupported filesystem operations fail; callers may choose their own copy or checkout fallback after the failed operation has settled.
72
74
 
73
75
  Native Windows byte copies can store large zero-filled chunks as sparse ranges when the destination is initially empty and its filesystem supports sparse files. This still reads every source byte and creates an independent copy.
74
76
 
75
77
  The ReFS backend rejects files with alternate data streams and unsupported reparse-point types instead of silently losing their contents. Symbolic links and junctions are preserved.
76
78
 
79
+ For successful native ReFS clones through `copyTree` or `Root.copyIn`, all full
80
+ clusters are block-cloned; a final partial cluster may be copied. Both preserve exact bytes and logical file size.
81
+ The shared implementation keeps EOF at the source size and rounds only the final
82
+ clone request to the allocation-cluster boundary. Microsoft's
83
+ [block-cloning rules](https://learn.microsoft.com/en-us/windows/win32/fileio/block-cloning#restrictions-and-remarks)
84
+ require cluster-aligned ranges and setting destination EOF before cloning;
85
+ rounding a request does not promise physical sharing of a partial final cluster.
86
+ Windows x64 and ARM64 tests compare every full-cluster LCN for both APIs,
87
+ including the 4 MiB shared prefix of a 4 MiB + 1 byte file.
88
+
77
89
  Failed ReFS clones attempt to remove their partial output through the retained directory handles. On Windows versions that reject the ignore-readonly deletion flag, clone rollback retries without that flag so ordinary output can be removed. It never clears readonly attributes: readonly output can remain, and the original clone error includes the cleanup failure. Other processes retaining output handles can delay deletion beyond settlement; a failed call does not guarantee an absent destination.
78
90
 
79
91
  XFS and ZFS preserve regular-file and directory modes, timestamps, extended attributes, and ACLs. They reject special files, symlink extended attributes, non-UTF-8 names, and directory nesting deeper than 128 levels. Hardlinked source files become independent reflinked files. Portable byte copying preserves file contents, empty directories, modes where supported, file and directory timestamps, and literal symbolic links; it does not promise ownership, ACL, extended-attribute, alternate-stream, or sparse-layout preservation. On Windows, byte copying rejects unresolved symbolic links because Node does not expose their file/directory link type; resolved links keep their literal target and source type. POSIX dangling links are preserved. Choose a copying policy that meets the caller's metadata requirements; automatic copying can select either path.
@@ -192,6 +204,16 @@ Byte copying retains fractional file and directory access/modification timestamp
192
204
 
193
205
  After building the host native binding, run `pnpm test test/clone.test.ts test/copy-tree.test.ts`. APFS tests can use the normal macOS temporary directory. For Btrfs, ReFS, XFS, or ZFS, set `FS_SAFE_CLONE_TEST_ROOT` to an existing writable directory on that filesystem. The test creates and cleans only its own temporary children. An explicitly configured unsupported directory fails the test rather than silently skipping platform proof. XFS and ZFS metadata tests require the `attr` and `acl` utilities.
194
206
 
207
+ The `ReFS copyIn` CI workflow creates a disposable 2 GiB expanding VHD on
208
+ Windows Server 2025 x64 and Windows 11 ARM64. It formats the latter as a Dev
209
+ Drive, verifies that the mounted filesystem is ReFS, and sets
210
+ `FS_SAFE_CLONE_TEST_ROOT`. `test/root-copy-refs.test.ts` runs the built package on
211
+ the main thread and in a real Node Worker, checks shared physical extents with
212
+ `readWindowsFileExtents`, independent writes, exclusive publication, byte limits,
213
+ cancellation cleanup, and revocation between file copies. Both jobs also run the
214
+ tree clone suite, then detach and delete the VHD even after failure. A failed
215
+ volume setup fails the job; it never substitutes NTFS or skips the ReFS proof.
216
+
195
217
  Run `node scripts/clone-xfs-proof.mjs MOUNT` on a real XFS volume to verify the public API, hashes, independent writes, and shared physical extents. It requires `filefrag` from `e2fsprogs`. Add `no-reflink` for an XFS fixture formatted with reflinks disabled; strict copying must fail and automatic copying must succeed through byte copying.
196
218
 
197
219
  Run `node scripts/clone-zfs-proof.mjs MOUNT POOL` on a dedicated, otherwise idle Linux ZFS pool with compression and deduplication disabled. It verifies both `copyTree` and `Root.copyIn` through hashes and changes in the documented `bclonesaved` pool counter. It requires `zfs`, `zpool`, and `findmnt`, including permission to run `zpool sync`. Add `no-reflink` for a pool without block cloning to verify strict refusal and automatic byte fallback. The script creates and removes only its temporary directory; it does not create pools or change their properties.
@@ -224,9 +224,11 @@ count limits need not mean the entire filesystem lacks hard-link support.
224
224
  `link-required` and `link-or-copy` do not use `renameat2` and are unchanged.
225
225
 
226
226
  `"link-required"` propagates an unsupported hardlink failure.
227
- `"link-or-copy"` falls back only for `EPERM`, `EXDEV`, `ENOTSUP`,
227
+ `"link-or-copy"` falls back only for `EACCES`, `EPERM`, `EXDEV`, `ENOTSUP`,
228
228
  `EOPNOTSUPP`, or `ENOSYS`; `isHardlinkFallbackError()` exposes that exact
229
- classifier. The fallback copies from the pinned source into a `wx` target,
229
+ classifier, including Android/Termux hardlink permission refusals. A genuine
230
+ permission denial still fails the fallback's exclusive target creation; `EEXIST`
231
+ never permits fallback. The fallback copies from the pinned source into a `wx` target,
230
232
  fsyncs it, and fences source and target identity and content before reporting
231
233
  success. `parentReceipt`, when supplied, must name the target's direct parent.
232
234
 
@@ -38,6 +38,14 @@ class FsSafeError extends Error {
38
38
 
39
39
  `cause` is available through the standard `Error` `cause` property when the failure was triggered by a `NodeJS.ErrnoException` (e.g. a wrapped `EACCES`). Inspect it for the original `code` / `errno` / `syscall` if you need finer-grained reporting.
40
40
 
41
+ Native Windows failures, including private directory and file creation failures,
42
+ retain the original numeric Win32 error in `errno` (for example, 183 for
43
+ `ERROR_ALREADY_EXISTS` or 5 for `ERROR_ACCESS_DENIED`). This also applies to the
44
+ Windows security command fallback. A wrapped failure retains that number on its
45
+ original `cause`; the semantic `FsSafeError.code` remains the contract. Policy
46
+ checks and preflight rejections that do not execute a failing Windows operation
47
+ have no Win32 error number. Node-managed filesystem errors retain Node's own errno.
48
+
41
49
  Guarded write preparation describes permission, read-only filesystem, and disk-space
42
50
  failures with messages such as `permission denied (EACCES)` or
43
51
  `no space left on device (ENOSPC)`. Other errno failures include their code in
@@ -56,6 +56,8 @@ can replace only an **empty directory created concurrently**; non-empty
56
56
  directories and non-directory targets are refused as `EEXIST`. An existing
57
57
  empty directory observed by the absence check is also refused. No empty claim
58
58
  directory is exposed before publishing the completed source.
59
+ An `ENOTDIR` rename failure rechecks the source before reporting a destination
60
+ collision; a detected source substitution reports `ESTALE` instead of `EEXIST`.
59
61
 
60
62
  Identity checks and mutation remain separate, not source-name compare-and-swap.
61
63
  The fallback verifies the published identity afterward. A file fallback failure
@@ -106,6 +106,7 @@ Use the main entry for the common surface, or the focused subpaths when you want
106
106
  | `@openclaw/fs-safe/json` | `tryReadJson`, `readJson`, `readJsonIfExists`, `writeJson`, sync variants. |
107
107
  | `@openclaw/fs-safe/store` | `fileStore()`, `fileStoreSync()`, and `jsonStore<T>()`. |
108
108
  | `@openclaw/fs-safe/secret` | Secret file read/write helpers. |
109
+ | `@openclaw/fs-safe/pipe` | Native-owned, reopenable anonymous pipes for one-shot child input. |
109
110
  | `@openclaw/fs-safe/atomic` | `replaceFileAtomic`, `writeTextAtomic`, `replaceDirectoryAtomic`, `movePathWithCopyFallback`. |
110
111
  | `@openclaw/fs-safe/durability` | Pinned directories, strict sync, durable directory creation, exclusive publication, and streaming SHA-256. |
111
112
  | `@openclaw/fs-safe/temp` | `tempWorkspace`, `withTempWorkspace`, sync variants, `resolveSecureTempRoot`. |
@@ -125,9 +126,11 @@ Use the main entry for the common surface, or the focused subpaths when you want
125
126
 
126
127
  `@openclaw/fs-safe` bundles an import-free WASM build of its Rust TAR parser and zstd/bzip2 codecs for guarded [archive extraction and bounded entry reads](archive.md). Plain TAR, gzip, zstd, and bzip2 work in `off` and missing-native `auto`, including installs with all optional dependencies omitted; gzip uses Node's built-in decoder. These archive fallbacks need no runtime interpreter, download, or consumer compiler. ZIP fallback uses lazily loaded optional `jszip` and reports a missing-dependency error without it. Public subpaths remain safe to import with all optional dependencies omitted, but imports do not prove native availability. Native `require` remains strict, and an available native operation's failure never triggers a WASM retry.
127
128
 
128
- There are no peer dependencies. Exact-version optional packages carry the seven
129
+ There are no peer dependencies. Exact-version optional packages carry the
129
130
  native targets and npm-compatible OS, CPU, and Linux libc filters install only
130
- the matching binary. Consumers do not run a Rust build, download code at
131
+ the matching binary. Windows ARM64 Node selects
132
+ `@openclaw/fs-safe-win32-arm64-msvc`; x64 Node on the same host selects the x64
133
+ package. Consumers do not run a Rust build, download code at
131
134
  runtime, or execute a postinstall step. Omitting optional dependencies keeps
132
135
  fallback-capable operations working in `auto` or `off`. Native-only
133
136
  features, including strict owned-tree temp cleanup, retained-directory staging,
@@ -145,8 +148,11 @@ before deploying with native mode `require` or native-only features.
145
148
  The native addon supports kernels without `openat2` (before Linux 5.6) and
146
149
  containers returning `ENOSYS` or probe-time `EPERM` for that syscall. Beneath
147
150
  opens use a guarded no-follow component walk and report `best-effort`.
148
- Nested no-clobber moves retain atomic `renameat2(RENAME_NOREPLACE)` and exact
149
- identity checks; keep native mode enabled. Strict bounded cleanup still
151
+ Nested no-clobber moves retain native parent admission and exact identity checks;
152
+ keep native mode enabled. If `renameat2(RENAME_NOREPLACE)` is also unsupported,
153
+ Linux `auto` uses [move fallbacks](writing.md#fs-move-from-to-options), including
154
+ a directory rename that can replace a concurrently created empty directory.
155
+ Native `require` refuses these fallbacks. Strict bounded cleanup still
150
156
  requires `openat2` with `RESOLVE_NO_XDEV` and reports `helper-unavailable`
151
157
  without it. See [Linux capability behavior and limits](native.md#linux-without-openat2).
152
158
 
@@ -180,7 +186,9 @@ available native operation's error never triggers a command retry. See
180
186
  ### Supported native platforms
181
187
 
182
188
  Prebuilt bindings cover Linux x64/arm64 (GNU glibc **2.28 or newer**, or musl),
183
- macOS x64/arm64, and Windows x64. The GNU baseline includes RHEL 8, Rocky Linux 8,
189
+ macOS x64/arm64, Windows x64/arm64, and FreeBSD 14.4+ x64/arm64. FreeBSD provides the
190
+ [documented subset](native.md#freebsd-14-4-and-newer); its other operations retain
191
+ guarded fallbacks in `auto`. The GNU baseline includes RHEL 8, Rocky Linux 8,
184
192
  and AlmaLinux 8. A compatible Node 22+ runtime and the kernel/filesystem features
185
193
  required by each operation are still necessary; a loadable addon alone does not
186
194
  guarantee every native capability. Systems older than glibc 2.28 are outside the
@@ -16,11 +16,11 @@ Current releases install the matching exact-version optional platform package.
16
16
  - Run on Node.js 22 or newer.
17
17
  - Update `@openclaw/fs-safe` and regenerate every lock or shrinkwrap file your
18
18
  deployment consumes.
19
- - Keep optional dependencies enabled if you want JavaScript ZIP/TAR support.
20
- Native loading no longer depends on Python. Keep optional dependencies enabled
21
- for the matching native package and JavaScript ZIP/TAR decoders. An install
22
- that omits them can still import fs-safe, but native-only features and missing
23
- JS archive decoders fail with actionable errors.
19
+ - Keep optional dependencies enabled for the matching native package and the
20
+ JSZip fallback. Current TAR/gzip/zstd/bzip2 fallbacks are bundled and work
21
+ with optional dependencies omitted. Native loading no longer depends on
22
+ Python. See [runtime dependencies](install.md#runtime-dependencies) for the
23
+ current installation contract.
24
24
 
25
25
  If you call `resolveRootPath()` or `resolveRootPathSync()` directly, upgrade to
26
26
  0.5: versions through 0.4.7 could approve an in-root symlink traversal that
@@ -5,8 +5,10 @@ description: "How fs-safe loads its platform-specific native filesystem primitiv
5
5
 
6
6
  # Native helper policy
7
7
 
8
- `@openclaw/fs-safe` declares seven exact-version optional packages for Linux
9
- x64/arm64 (glibc or musl), macOS x64/arm64, and Windows x64. Package-manager
8
+ `@openclaw/fs-safe` declares exact-version optional packages for Linux
9
+ x64/arm64 (glibc or musl), macOS x64/arm64, Windows x64/arm64, and FreeBSD x64/arm64.
10
+ The FreeBSD 14.4+ bindings expose the [qualified subset](native.md#freebsd-14-4-and-newer).
11
+ Package-manager
10
12
  OS, CPU, and libc filters install only the matching package. The loader requires
11
13
  that package lazily, without runtime downloads, postinstall scripts, or a
12
14
  consumer Rust build.
@@ -33,6 +35,12 @@ See [Archive extraction](archive.md) for native, bundled WASM, and ZIP backends.
33
35
  Native archive-operation failures are terminal; `auto` does not retry them through a
34
36
  fallback. `require` rejects missing bindings or required capabilities.
35
37
 
38
+ A loaded binding may provide only a subset of native operations. Each operation
39
+ checks its required capabilities before native dispatch: missing capabilities
40
+ select the existing guarded fallback in `auto`, or report `helper-unavailable`
41
+ in `require`. A failure from an available native method is still an operation
42
+ failure; it is not treated as a missing capability.
43
+
36
44
  Windows owner/DACL inspection, private-directory creation, and secure reads can
37
45
  use [PowerShell fallbacks](install.md#windows-security-fallback) in `auto` and
38
46
  `off`; `require` stays strict, and native operation failures remain terminal.
@@ -109,10 +117,12 @@ is authoritative for operation and platform guarantees.
109
117
 
110
118
  ## Migration from the Python helper
111
119
 
112
- The deprecated configuration bridge maps Python settings to native modes and
113
- warns once; it never executes Python. Follow the
120
+ The Python configuration bridge has been removed. Old Python environment
121
+ settings are ignored without a warning, so an old `require` or `off` setting
122
+ does not select the corresponding native policy. Configure
123
+ `FS_SAFE_NATIVE_MODE` or `configureFsSafeNative()` explicitly. Follow the
114
124
  [0.5 migration checklist](migrating-to-0.5.md#2-replace-python-helper-configuration)
115
- for aliases, warning behavior, and precedence.
125
+ for the removed settings and current precedence.
116
126
 
117
127
  ## Related pages
118
128
 
@@ -5,6 +5,9 @@ description: "The platform-specific native bindings, fd-relative beneath model,
5
5
 
6
6
  # Native architecture
7
7
 
8
+ See [platform filesystem capabilities](platform-capabilities.md) for Linux
9
+ write leases, public Darwin ACL facts, and Windows test fixtures.
10
+
8
11
  `@openclaw/fs-safe` uses native bindings that supply mechanisms Node does
9
12
  not expose directly. The Rust layer is deliberately not a second policy engine.
10
13
  TypeScript owns trusted-root selection, path validation, archive filtering,
@@ -14,12 +17,46 @@ platform syscall sequence that can preserve the boundary.
14
17
 
15
18
  Every operation that has an equivalent safe Node implementation keeps that
16
19
  guarded JavaScript path. Native loading is lazy; installs do not compile Rust,
17
- run postinstall code, or fetch binaries at runtime. Seven exact-version optional
20
+ run postinstall code, or fetch binaries at runtime. Exact-version optional
18
21
  packages are filtered by OS, CPU, and Linux libc, so an installation receives
19
- only its matching prebuilt binding.
22
+ only its matching prebuilt binding. Windows ships both x64 and ARM64 MSVC
23
+ bindings; selection follows the Node process architecture. An x64 Node process
24
+ on Windows ARM64 uses the x64 binding under emulation.
20
25
  Native-only operations fail explicitly
21
26
  instead of substituting a weaker implementation.
22
27
 
28
+ ## FreeBSD 14.4 and newer
29
+
30
+ The FreeBSD x64 and arm64 packages provide native anonymous pipes, descriptor
31
+ close and POSIX canonicalization. The larger Linux/macOS/Windows filesystem backend is excluded
32
+ from these binaries: its Unix gates contain platform-specific resolver, clone,
33
+ copy, observation and cleanup implementations. FreeBSD does not export stubs for
34
+ those operations. In `auto`, each operation selects its existing guarded JavaScript
35
+ or WASM backend; in `require`, a missing required capability fails with
36
+ `helper-unavailable`. Pure JavaScript APIs retain their ordinary behavior.
37
+
38
+ | Native capability | FreeBSD binding |
39
+ |---|---|
40
+ | `createPipe`, `closeOwnedFd`, `canonicalizePath` | Implemented; real pipe, descriptor and pathname syscalls |
41
+ | Beneath open/create/mkdir/link, replace/no-replace rename and identity-fenced rename | Absent; guarded fallback where supported |
42
+ | Directory observations, retained files/symlinks, entry publication, owned-tree and root removal | Absent; guarded fallback or explicit native-only rejection |
43
+ | File/tree clone, native copy/hash, archive inspection/extraction and buffer readers | Absent; JavaScript/WASM fallback where supported |
44
+ | Native watch | Absent; polling fallback in `auto` |
45
+ | Darwin ACL and Windows security/identity methods | Not applicable |
46
+
47
+ The release workflow builds inside FreeBSD 14.4 VMs and checks installed-package
48
+ behavior on the main thread and a real Worker. ARM64 uses QEMU CPU emulation
49
+ with a real FreeBSD kernel. This is not a Linux cross-build runtime test. Build
50
+ locally on FreeBSD with `pnpm native:build:freebsd`; the JavaScript/WASM distribution
51
+ can be prepared separately with `pnpm build`. There is no qualified FreeBSD Bun
52
+ artifact in the runtime matrix. FreeBSD x64 installed-package proof covers npm
53
+ and pnpm. FreeBSD arm64 covers npm only because pnpm does not publish a native
54
+ binary for that target. CI prepares the ARM64 proof harness dependencies on Linux
55
+ and syncs them into the VM; consumer installations and runtime proof run inside
56
+ FreeBSD, without running pnpm there.
57
+ Reopening pipe descriptors through `/dev/fd` requires the standard `fdescfs`
58
+ mount there; the VM proof mounts it before testing reopen, inheritance and EOF.
59
+
23
60
  ## The beneath model
24
61
 
25
62
  A trusted directory descriptor is the capability. Native operations accept
@@ -75,6 +112,11 @@ whether a path, archive entry, mode, owner, or cleanup policy is acceptable.
75
112
  this layer only through the host executable's paired libuv descriptor bridge;
76
113
  missing or partial exports fail with `ENOTSUP` instead of trying a raw HANDLE
77
114
  or add-on CRT descriptor namespace.
115
+ `Root.copyIn` uses the same ReFS integrity, sparse-file, and cluster-aligned
116
+ block-clone operations as tree cloning, through retained source and destination
117
+ parent handles. The exclusive stage enters the existing guarded publication
118
+ path; transfer failures retain its cleanup receipt. It does not inherit the
119
+ tree API's source metadata policy.
78
120
 
79
121
  The internal macOS `inspectDarwinAcl(fd)` capability reports `absent`, `empty`,
80
122
  or `present` for the opened object's extended ACL. It synchronously borrows the
@@ -375,6 +417,13 @@ silently selects the JavaScript fallback in `auto`, throws typed
375
417
  `helper-unavailable` in `require`, and is never inspected in `off`. Tests reject
376
418
  `child_process`, `exec`, or `spawn` usage in the loader.
377
419
 
420
+ ## Anonymous pipes
421
+
422
+ The lean [`pipe` subpath](pipe.md) creates native-owned anonymous pipes. Linux and
423
+ FreeBSD use atomic close-on-exec creation; Darwin reports its non-atomic flag
424
+ setup explicitly. Each end must close through its native owner, including when
425
+ a Node stream uses it in a Worker.
426
+
378
427
  ## Related pages
379
428
 
380
429
  - [Native helper policy](native-helper.md)
@@ -0,0 +1,81 @@
1
+ # Anonymous pipes
2
+
3
+ `@openclaw/fs-safe/pipe` exposes a synchronous, native anonymous pipe for
4
+ one-shot input to a child process. Both ends are reopenable through
5
+ `/proc/self/fd/N` on Linux and `/dev/fd/N` on Darwin and FreeBSD. This supports
6
+ programs that open an inherited descriptor as a pathname.
7
+
8
+ On FreeBSD, reopening through `/dev/fd/N` requires `fdescfs` mounted at `/dev/fd`.
9
+ An administrator can mount it with `mount -t fdescfs fdesc /dev/fd`; pipe creation
10
+ itself does not require that mount.
11
+
12
+ ```ts
13
+ import { createPipe, type OwnedPipeSync } from "@openclaw/fs-safe/pipe";
14
+
15
+ const pipe: OwnedPipeSync = createPipe();
16
+ try {
17
+ // Pass pipe.reader.fd as a numeric child_process spawn stdio entry.
18
+ // Keep both owners alive until their respective users finish.
19
+ } finally {
20
+ pipe.reader.close();
21
+ pipe.writer.close();
22
+ }
23
+ ```
24
+
25
+ The frozen result has `reader` and `writer` members of the existing
26
+ `OwnedFileDescriptorSync` type (`readonly fd: number`, `close(): void`, and
27
+ `[Symbol.dispose](): void`) and `readonly atomicCloseOnExec: boolean`.
28
+ Each end closes idempotently through its native owner. A close attempt consumes
29
+ ownership even if it throws; do not retry with a raw close or use the fd again.
30
+ The pair itself does not have a `close` or disposal method.
31
+
32
+ Linux and FreeBSD create both ends atomically with close-on-exec set.
33
+ Darwin sets the flag on both ends immediately after creation and reports
34
+ `atomicCloseOnExec: false`: a concurrent native fork/exec can inherit an end
35
+ in that short window. No JavaScript async work intervenes, but this is not an
36
+ atomic inheritance guarantee. Partial setup failure closes both ends.
37
+
38
+ Importing the subpath is lazy and does not load the addon or the secret-file
39
+ modules. Calling `createPipe()` requires a compatible native helper; disabled
40
+ or missing helpers throw `FsSafeError` with `helper-unavailable`. Windows and
41
+ other unsupported platforms throw `unsupported-platform`. Native creation
42
+ failures throw `helper-failed` with the original error as `cause`.
43
+ The cause's `code` preserves the native errno, including `EMFILE` (the process
44
+ descriptor limit) and `ENFILE` (the system-wide limit).
45
+
46
+ ## Handing the writer to a Node stream
47
+
48
+ A native-created fd must close through its owner, including in Workers.
49
+ Provide the stream's custom close hook; do not let Node close it directly.
50
+ This is the writer handoff contract: retain the owner in the hook rather than
51
+ detaching its descriptor.
52
+
53
+ ```ts
54
+ import { createWriteStream, write, writev } from "node:fs";
55
+ import { createPipe } from "@openclaw/fs-safe/pipe";
56
+
57
+ const pipe = createPipe();
58
+ const stream = createWriteStream("", {
59
+ fd: pipe.writer.fd,
60
+ fs: {
61
+ write,
62
+ writev,
63
+ close(_fd, callback) {
64
+ try {
65
+ pipe.writer.close();
66
+ callback(null);
67
+ } catch (error) {
68
+ callback(error instanceof Error ? error : new Error(String(error)));
69
+ }
70
+ },
71
+ },
72
+ });
73
+ // After spawning with pipe.reader.fd in the child's stdio:
74
+ pipe.reader.close();
75
+ stream.end("one-shot input");
76
+ ```
77
+
78
+ Once the stream is created, its lifetime owns use of the writer: keep an error
79
+ listener, destroy it on cancellation, and wait for `close` before considering
80
+ the fd released. If stream construction fails, close both owners. Closing the
81
+ last writer delivers EOF; unread bytes remain available to the reader.
@@ -0,0 +1,63 @@
1
+ # Platform filesystem capabilities
2
+
3
+ These synchronous capabilities load the native helper only when called. All
4
+ three entry points import on every supported platform.
5
+
6
+ ## Linux write leases
7
+
8
+ `tryAcquireWriteLease(fd)` from `@openclaw/fs-safe/file-lock` returns a frozen
9
+ `FileWriteLease`, or `null` when another open descriptor prevents acquisition
10
+ (`EAGAIN` or `EBUSY`). Other failures throw `FsSafeError`; unsupported systems
11
+ throw `unsupported-platform` and unavailable helpers throw `helper-unavailable`.
12
+
13
+ The caller owns the descriptor, must keep it open until release, and must install
14
+ its own process-wide `SIGIO` handler **before** acquiring a lease. Prefer a
15
+ disposable child process when an application cannot own that signal. The API
16
+ does not install a signal handler. A competing open starts the kernel's lease
17
+ break; `isHeld()` then returns false, including during a pending downgrade.
18
+ Release promptly so the competing opener can proceed.
19
+
20
+ `lease.fd` is the borrowed descriptor. `lease.release()` and
21
+ `lease[Symbol.dispose]()` settle once without closing it; `isHeld()` returns
22
+ false after release. Never close or reuse the fd before settling the lease.
23
+ Lease acquisition does not replace the caller's path identity and age checks.
24
+
25
+ ## Darwin ACL inspection
26
+
27
+ `inspectDarwinAcl(path)` from `@openclaw/fs-safe/permissions` opens the final
28
+ component without following symlinks, then inspects a descriptor-bound security
29
+ snapshot. Its `DarwinAclInspection` result is one of:
30
+
31
+ - `{ kind: "none" }` for a proven absent or empty extended ACL.
32
+ - `{ kind: "present", inheritsToFiles, inheritsToDirectories }` for an ACL with
33
+ entries, including when both inheritance flags are false.
34
+ - `{ kind: "unknown", reason }` when opening or inspection fails, the helper
35
+ is unavailable, or the native facts are incomplete. Other platforms return
36
+ `reason: "unsupported-platform"`.
37
+
38
+ Unknown is never evidence of absence. The two inheritance booleans report flag
39
+ presence, not effective permissions or volume ownership enforcement. Intermediate
40
+ path components follow ordinary path resolution; this is inspection, not a Root
41
+ confinement capability.
42
+
43
+ ## Windows test fixtures
44
+
45
+ The following exports from `@openclaw/fs-safe/test-hooks` are for tests, not
46
+ production policy. They require the Windows native helper and reject final
47
+ reparse points. Other platforms throw `unsupported-platform`.
48
+
49
+ - `holdWindowsSharingLock(path)` returns a frozen disposable with idempotent
50
+ `close()`. Its native owner holds a non-inheritable handle with read/write
51
+ sharing, omitting delete sharing, until closed or collected.
52
+ - `setWindowsFileAttributes(path, { readOnly?, hidden?, system? })` updates the
53
+ supplied bits through the opened handle and preserves unspecified attributes.
54
+ - `readWindowsFileExtents(path)` returns `{ vcn, lcn, clusters }[]`, all bigint.
55
+ Empty or resident files may have no extents; `lcn: -1n` denotes sparse clusters.
56
+ Enumeration validates each response and fails on incomplete or invalid data.
57
+ It is an observation, not a snapshot against concurrent file modification.
58
+
59
+ Use Node's `fs.statfsSync(path).bsize` for Windows cluster size. Windows CI
60
+ compares it directly against `GetDiskFreeSpaceW`'s sectors-per-cluster times
61
+ bytes-per-sector on NTFS. CI does not provide ReFS; this does not establish ReFS
62
+ clone sharing. The extent reader reports mappings independently of the clone
63
+ implementation.