@openclaw/matrix 2026.10.1-beta.2 → 2026.10.2

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 (68) hide show
  1. package/node_modules/@openclaw/fs-safe/CHANGELOG.md +56 -0
  2. package/node_modules/@openclaw/fs-safe/dist/advanced.d.ts +1 -0
  3. package/node_modules/@openclaw/fs-safe/dist/advanced.js +1 -0
  4. package/node_modules/@openclaw/fs-safe/dist/archive-entry.js +7 -3
  5. package/node_modules/@openclaw/fs-safe/dist/copy-root-file-sync.d.ts +35 -0
  6. package/node_modules/@openclaw/fs-safe/dist/copy-root-file-sync.js +218 -0
  7. package/node_modules/@openclaw/fs-safe/dist/create.d.ts +1 -0
  8. package/node_modules/@openclaw/fs-safe/dist/create.js +4 -1
  9. package/node_modules/@openclaw/fs-safe/dist/entry-publication.js +3 -1
  10. package/node_modules/@openclaw/fs-safe/dist/errors.d.ts +2 -0
  11. package/node_modules/@openclaw/fs-safe/dist/errors.js +5 -0
  12. package/node_modules/@openclaw/fs-safe/dist/index.d.ts +1 -1
  13. package/node_modules/@openclaw/fs-safe/dist/index.js +1 -1
  14. package/node_modules/@openclaw/fs-safe/dist/native-binding.d.ts +3 -0
  15. package/node_modules/@openclaw/fs-safe/dist/native-move-fallback.d.ts +3 -0
  16. package/node_modules/@openclaw/fs-safe/dist/native-move-fallback.js +54 -0
  17. package/node_modules/@openclaw/fs-safe/dist/native-noreplace.d.ts +8 -0
  18. package/node_modules/@openclaw/fs-safe/dist/native-noreplace.js +55 -0
  19. package/node_modules/@openclaw/fs-safe/dist/native-pinned-write.d.ts +1 -1
  20. package/node_modules/@openclaw/fs-safe/dist/native-pinned-write.js +12 -2
  21. package/node_modules/@openclaw/fs-safe/dist/native-rename-outcome.js +3 -1
  22. package/node_modules/@openclaw/fs-safe/dist/native-staged-file.d.ts +1 -1
  23. package/node_modules/@openclaw/fs-safe/dist/native-staged-file.js +48 -5
  24. package/node_modules/@openclaw/fs-safe/dist/pinned-write-input.d.ts +2 -0
  25. package/node_modules/@openclaw/fs-safe/dist/pinned-write-input.js +17 -0
  26. package/node_modules/@openclaw/fs-safe/dist/pinned-write.js +1 -1
  27. package/node_modules/@openclaw/fs-safe/dist/publish-file-failure.d.ts +5 -0
  28. package/node_modules/@openclaw/fs-safe/dist/publish-file-failure.js +1 -0
  29. package/node_modules/@openclaw/fs-safe/dist/publish-file-native.d.ts +4 -0
  30. package/node_modules/@openclaw/fs-safe/dist/publish-file-native.js +35 -0
  31. package/node_modules/@openclaw/fs-safe/dist/publish-file.d.ts +2 -0
  32. package/node_modules/@openclaw/fs-safe/dist/publish-file.js +8 -2
  33. package/node_modules/@openclaw/fs-safe/dist/replace-directory.js +6 -5
  34. package/node_modules/@openclaw/fs-safe/dist/root-errors.js +25 -4
  35. package/node_modules/@openclaw/fs-safe/dist/root-impl.js +3 -3
  36. package/node_modules/@openclaw/fs-safe/dist/root-move-noreplace.d.ts +4 -2
  37. package/node_modules/@openclaw/fs-safe/dist/root-move-noreplace.js +13 -17
  38. package/node_modules/@openclaw/fs-safe/dist/sibling-staged-file.js +18 -3
  39. package/node_modules/@openclaw/fs-safe/dist/staged-file-settlement.js +4 -1
  40. package/node_modules/@openclaw/fs-safe/dist/temp-workspace-owner.d.ts +1 -0
  41. package/node_modules/@openclaw/fs-safe/dist/temp-workspace-owner.js +27 -4
  42. package/node_modules/@openclaw/fs-safe/docs/advanced.md +1 -0
  43. package/node_modules/@openclaw/fs-safe/docs/archive.md +2 -1
  44. package/node_modules/@openclaw/fs-safe/docs/copy.md +61 -1
  45. package/node_modules/@openclaw/fs-safe/docs/durability.md +50 -5
  46. package/node_modules/@openclaw/fs-safe/docs/errors.md +37 -0
  47. package/node_modules/@openclaw/fs-safe/docs/native-helper.md +5 -3
  48. package/node_modules/@openclaw/fs-safe/docs/native.md +39 -6
  49. package/node_modules/@openclaw/fs-safe/docs/public-api.md +3 -1
  50. package/node_modules/@openclaw/fs-safe/docs/staged-file.md +10 -3
  51. package/node_modules/@openclaw/fs-safe/docs/watch.md +3 -1
  52. package/node_modules/@openclaw/fs-safe/docs/writing.md +40 -11
  53. package/node_modules/@openclaw/fs-safe/package.json +11 -11
  54. package/node_modules/@openclaw/fs-safe-darwin-arm64/fs-safe-native.node +0 -0
  55. package/node_modules/@openclaw/fs-safe-darwin-arm64/package.json +1 -1
  56. package/node_modules/@openclaw/fs-safe-darwin-x64/fs-safe-native.node +0 -0
  57. package/node_modules/@openclaw/fs-safe-darwin-x64/package.json +1 -1
  58. package/node_modules/@openclaw/fs-safe-linux-arm64-gnu/fs-safe-native.node +0 -0
  59. package/node_modules/@openclaw/fs-safe-linux-arm64-gnu/package.json +1 -1
  60. package/node_modules/@openclaw/fs-safe-linux-arm64-musl/fs-safe-native.node +0 -0
  61. package/node_modules/@openclaw/fs-safe-linux-arm64-musl/package.json +1 -1
  62. package/node_modules/@openclaw/fs-safe-linux-x64-gnu/fs-safe-native.node +0 -0
  63. package/node_modules/@openclaw/fs-safe-linux-x64-gnu/package.json +1 -1
  64. package/node_modules/@openclaw/fs-safe-linux-x64-musl/fs-safe-native.node +0 -0
  65. package/node_modules/@openclaw/fs-safe-linux-x64-musl/package.json +1 -1
  66. package/node_modules/@openclaw/fs-safe-win32-x64-msvc/fs-safe-native.node +0 -0
  67. package/node_modules/@openclaw/fs-safe-win32-x64-msvc/package.json +1 -1
  68. package/package.json +5 -5
@@ -58,7 +58,7 @@ Apple [strongly discourages general directory cloning](https://github.com/apple-
58
58
  | `"always"` | Require native cloning. Unsupported operations fail without a byte-copy fallback. |
59
59
  | `"never"` | Copy regular file bytes using reads and writes. No native cloning or copy-offload calls. Works without a native binding. |
60
60
 
61
- Automatic copying does not recover from permission errors, I/O errors, cancellation, or rejected source contents such as ReFS named streams. A failed clone must leave the destination absent before fallback can create it; otherwise copying fails rather than merging into a partial tree.
61
+ On Linux XFS and ZFS, `FICLONE` capability failures (including an ioctl denied by container seccomp or LSM policy with `EPERM`) select byte copying in `"auto"` mode; `"always"` reports `unsupported-platform`. Automatic copying does not recover from ordinary file-access permission errors, I/O errors, cancellation, or rejected source contents such as ReFS named streams. Native reflink workers settle and remove partial output before returning a clone failure. A failed clone must leave the destination absent before fallback can create it; otherwise copying fails rather than merging into a partial tree.
62
62
 
63
63
  `concurrency` accepts integers from 1 through 32 and bounds active file copies. ReFS, XFS, and ZFS cloning default to 16 workers. Byte copying defaults to four concurrent files on Windows and one elsewhere. Btrfs uses its bulk operation. APFS uses a bulk clone followed by native directory-entry enumeration to restore directory timestamps; known regular files and symbolic links need no additional stat or open.
64
64
 
@@ -197,3 +197,63 @@ Run `node scripts/clone-xfs-proof.mjs MOUNT` on a real XFS volume to verify the
197
197
  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.
198
198
 
199
199
  Run `node benchmarks/clone.mjs SOURCE DESTINATION_PARENT` after `pnpm build` to compare one, four, and 16 workers on the same immutable source. Add `3 auto` or `3 never` to measure three samples of ordinary copying, including NTFS destinations. It records copying time separately from fixture preparation and full file-hash verification, and retains its uniquely named output directory for inspection. Prepare Btrfs sources with `createCloneSource` first.
200
+
201
+ ## Synchronous guarded file copies
202
+
203
+ `copyRootFileSync` from `@openclaw/fs-safe/advanced` is for synchronous module
204
+ hooks and capture owners. Both roots and file paths are absolute; destination
205
+ parents must exist. The destination must be absent, and is created exclusively.
206
+
207
+ ```ts
208
+ import { copyRootFileSync } from "@openclaw/fs-safe/advanced";
209
+
210
+ using copied = copyRootFileSync({
211
+ source: { rootPath: "/srv/plugins", absolutePath: "/srv/plugins/plugin.js" },
212
+ destination: { rootPath: "/srv/capture", absolutePath: "/srv/capture/plugin.js" },
213
+ clone: "auto",
214
+ maxBytes: 16 * 1024 * 1024,
215
+ mode: 0o600,
216
+ });
217
+ // Hash or read copied.fd: this is the owned destination, never a reopened path.
218
+ console.log(copied.bytes, copied.method, copied.identity, copied.sourceIdentity);
219
+ ```
220
+
221
+ `CopyRootFileSyncOptions` selects the source and destination, `clone`, `maxBytes`,
222
+ `mode`, `preserveSourceMode`, `sourceHardlinks`, and optional `expectedSourceIdentity`.
223
+ `CopiedRootFileSync` owns a
224
+ readable/writable `fd`, with idempotent `close()` and `Symbol.dispose`, and returns
225
+ `path`, `bytes`, `method` (`"clone"`, `"copy-file-range"`, or `"copy"`), and exact
226
+ bigint destination `identity` and `sourceIdentity` (`dev`, `ino`). Both identities
227
+ are readonly and frozen. Closing releases the descriptor, not the file.
228
+
229
+ Callers that already pinned a source can pass `expectedSourceIdentity: { dev, ino }`
230
+ from `fs.fstatSync(pinnedSourceFd, { bigint: true })`. fs-safe still admits and
231
+ opens its own source descriptor, and compares its exact bigint identity before
232
+ creating any destination. A mismatch throws `FsSafeError("path-mismatch")`.
233
+ Keep the caller's descriptor open across the call; it remains caller-owned and
234
+ its cursor is unchanged. Do not derive the expectation from numeric stat fields,
235
+ which may have lost identity bits. Omitting the option preserves ordinary source
236
+ admission. Every successful copy method returns `sourceIdentity` for the source
237
+ actually copied, verified current again at the end of the operation. This binds
238
+ identity only; keep any existing source fingerprint/content and destination
239
+ receipt checks for concurrent edits and later replacements.
240
+
241
+ Clone policy matches `Root.copyIn`: the default is `"never"`; `"auto"` tries
242
+ native cloning, then bounded copy offload and byte copying on capability failure;
243
+ `"always"` requires cloning or throws `unsupported-platform`. APFS cloning
244
+ creates the guarded destination name from the admitted source descriptor. Linux
245
+ uses FICLONE and copy_file_range. Windows and native-disabled operation copy
246
+ bytes; they cannot satisfy `"always"`.
247
+
248
+ The source descriptor remains open through before/after source identity checks.
249
+ Symlink components are rejected; source hardlinks are rejected unless
250
+ `sourceHardlinks: "allow"` is selected. The byte limit is an EOF bound, not a
251
+ prefix length: overflow throws `too-large`. Omitted limits are unlimited. An
252
+ explicit mode wins over `preserveSourceMode`; otherwise new files use `0o600`
253
+ subject to the process umask, consistently with `copyIn`.
254
+
255
+ Failures attempt identity-checked removal of the owned destination and preserve
256
+ replacement entries. This helper creates directly at the destination name, so
257
+ other processes may observe partial bytes until it returns. It does not promise
258
+ crash durability or a coherent snapshot of concurrent in-place source edits.
259
+ Use a private capture directory and keep sources unchanged while capturing.
@@ -191,7 +191,37 @@ try {
191
191
  |---|---|---|
192
192
  | `link-required` | Create a same-filesystem hardlink or propagate the failure. | No; guarded JS `link` fallback remains. |
193
193
  | `link-or-copy` | Try hardlink, then clone, Linux `copy_file_range`, then the JS byte loop for classified unsupported errors. | No; acceleration is optional. |
194
- | `rename-noreplace` | Atomically move the source without replacing an existing target. Success consumes `sourcePath`. | Yes. |
194
+ | `rename-noreplace` | Move the source without replacing an existing target; Linux `auto` can use identity-checked link/unlink when no-replace rename is unsupported. Success consumes `sourcePath`. | Yes. |
195
+
196
+ `rename-noreplace` normally uses atomic native rename. On Linux in native `auto`
197
+ mode, a definitively unsupported `RENAME_NOREPLACE` result (`EINVAL`, `ENOSYS`,
198
+ or unsupported-operation errno) permits the existing native file-move fallback.
199
+ It requires a regular source with one link, retains both parents, creates the
200
+ target with `linkat` without following symlinks, verifies both names against the
201
+ staged identity with exactly two links, then unlinks the source and verifies the
202
+ target has one link. Fallback collisions report `already-exists` without clobbering;
203
+ the atomic rename path retains its existing `EEXIST` error.
204
+ Success still returns `method: "rename-noreplace"`, with the additive
205
+ `fallback: "link-unlink"` field identifying this weaker mechanism. Atomic rename
206
+ and the other strategies omit `fallback`.
207
+
208
+ **Link/unlink is not crash-atomic.** A crash between those syscalls can leave both
209
+ names on the same complete inode. Callers must recognize that interrupted pair
210
+ as incomplete evidence or recover it under their own identity/ownership rules
211
+ before accepting a sealed artifact. The source parent is synchronized after
212
+ unlink, and the target parent is synchronized through the existing pinned
213
+ directory/`parentReceipt` path before success. These directory barriers do not
214
+ flush file contents: callers must finish and synchronize staged contents first.
215
+ Identity checks and syscalls remain separate, not source-name compare-and-swap;
216
+ detected substitutions refuse completion and preserve evidence.
217
+
218
+ Native `require` remains fail-closed with `helper-unavailable` and
219
+ `details.capability === "rename-noreplace"`. Missing helpers, native `off`, and
220
+ non-Linux platforms gain no fallback. If `linkat` returns `EPERM`, `EOPNOTSUPP`,
221
+ `EMLINK`, `EXDEV`, or `ENOSYS`, publication also fails closed, naming both
222
+ `rename-noreplace` and `details.fallbackCapability: "linkat"`. Permission or link
223
+ count limits need not mean the entire filesystem lacks hard-link support.
224
+ `link-required` and `link-or-copy` do not use `renameat2` and are unchanged.
195
225
 
196
226
  `"link-required"` propagates an unsupported hardlink failure.
197
227
  `"link-or-copy"` falls back only for `EPERM`, `EXDEV`, `ENOTSUP`,
@@ -339,6 +369,9 @@ type PublishFileExclusiveFailureDetails = {
339
369
  targetIdentity?: { dev: number | bigint; ino: number | bigint };
340
370
  cleanup: "removed" | "preserved" | "unknown";
341
371
  directorySync?: { status: "failed"; code?: string };
372
+ fallback?: "link-unlink";
373
+ publication?: "published";
374
+ sourceRemoval?: "not-attempted" | "still-linked" | "unverified" | "removed";
342
375
  };
343
376
  ```
344
377
 
@@ -352,6 +385,16 @@ this receipt instead of inferring ownership from path existence. The original
352
385
  failure remains available as `cause`. Failures before target creation retain
353
386
  their existing error shape and do not claim a cleanup result.
354
387
 
388
+ After a fallback link succeeds, failure always preserves the target, irrespective
389
+ of `onSyncFailure`. The receipt reports `fallback: "link-unlink"`,
390
+ `publication: "published"`, and the source-removal state. `still-linked` means
391
+ unlink failed and both names were reverified as the exact two-link pair;
392
+ `not-attempted` means pair verification failed before unlink; `unverified` means
393
+ unlink failed and the pair could no longer be verified; `removed` means unlink
394
+ completed but a later verification or sync failed. Publication describes the
395
+ completed link operation, not continuing authority over the target pathname.
396
+ Unverified targets do not receive a claimed `targetIdentity`.
397
+
355
398
  Source and target identities are checked again after successful or unsupported
356
399
  directory synchronization, while their descriptors remain owned. A late
357
400
  verification failure retains its strategy's verification phase and is not a
@@ -389,13 +432,15 @@ can make a failed directory sync succeed: rollback deletion is also not proven
389
432
  durable, and a preserved name may disappear after a crash. Always use the
390
433
  typed receipt rather than inferring ownership from `exists()`.
391
434
 
392
- `rename-noreplace` always preserves its target after a successful rename,
435
+ `rename-noreplace` always preserves its target after a successful rename or fallback link,
393
436
  because removing it would discard the source's only remaining name; its typed
394
437
  failure receipt makes that explicit regardless of `onSyncFailure`.
395
438
 
396
- `"rename-noreplace"` requires the native helper and atomically moves the
397
- source to the target without replacement. A collision is reported as
398
- `EEXIST`, both files remain unchanged, and a successful call returns
439
+ `"rename-noreplace"` requires the native helper and moves the
440
+ source to the target without replacement, with the Linux `auto` fallback limits
441
+ described above. A collision is reported as `EEXIST` for atomic rename or
442
+ `already-exists` for its Linux fallback, both files remain
443
+ unchanged, and a successful call returns
399
444
  `method: "rename-noreplace"` after synchronizing the source and target parent
400
445
  directories. Unlike the link/copy strategies, success consumes `sourcePath`.
401
446
 
@@ -45,6 +45,11 @@ failures with messages such as `permission denied (EACCES)` or
45
45
  code and `policy` category for compatibility, along with the original `cause`;
46
46
  they do not expose native message text or paths. Already-classified `FsSafeError`
47
47
  instances and missing-path errors keep their existing classification.
48
+ Disk-space diagnostics also survive combined publication and disposal failures,
49
+ including `SuppressedError` and `AggregateError`. They retain the original cause
50
+ graph and cleanup/publication receipts, without changing the compatibility code
51
+ or category. An uncertain native rename still reports indeterminate publication
52
+ and a preserved stage; `ENOSPC` does not prove that publication never happened.
48
53
 
49
54
  Descriptor exhaustion during writes uses `helper-failed` / `operational` and
50
55
  names `EMFILE` (process descriptor limit) or `ENFILE` (system descriptor limit)
@@ -254,3 +259,35 @@ Two reasons it isn't a richer hierarchy of subclasses:
254
259
  - [Reading](reading.md) — read-path codes.
255
260
  - [Writing](writing.md) — write-path codes.
256
261
  - [Archive extraction](archive.md) — `ArchiveLimitError` and `ArchiveSecurityError`.
262
+
263
+ ## Unsupported no-replace moves
264
+
265
+ `isNoReplaceUnsupported(error)` is exported from `@openclaw/fs-safe` and
266
+ `@openclaw/fs-safe/errors`. It recognizes a `helper-unavailable` `FsSafeError`
267
+ with `details.capability === "rename-noreplace"`. This includes strict native
268
+ publication refusals and a Root file move whose link fallback is also unavailable
269
+ (`details.fallback === "link-unlink"`, `details.fallbackCapability === "linkat"`).
270
+ It does not classify a missing native addon, unrelated permission errors, or
271
+ post-publication failures. Once no-replace rename has been rejected, a denied
272
+ or unsupported `linkat` fallback (`EPERM`, `EOPNOTSUPP`, `EMLINK`, `EXDEV`,
273
+ or `ENOSYS`) also fails with this classification. `EPERM` can reflect source
274
+ permissions or inode flags rather than a filesystem-wide lack of hard links;
275
+ the fallback is unavailable for this move. No link-capability cache is populated.
276
+ Consumers need not inspect internal native cause codes.
277
+ The classifier is diagnostic; it does not authorize a replacing retry.
278
+
279
+ When a Root file move's link/unlink fallback fails after creating the target,
280
+ `details.operation` is `"move"`, `details.fallback` is `"link-unlink"`, and
281
+ `details.publication` is `"published"` (the link was created, not a guarantee
282
+ that a concurrent actor has left it unchanged). `details.sourceRemoval` is:
283
+
284
+ | Value | Observation |
285
+ | --- | --- |
286
+ | `"not-attempted"` | Identity or link-pair verification failed; source unlink was refused (`path-mismatch`). |
287
+ | `"still-linked"` | Unlink failed and both original inode links were reverified (`helper-failed`). |
288
+ | `"unverified"` | Unlink failed and the current pair could not be verified (`helper-failed`). |
289
+ | `"removed"` | Source unlink succeeded but final target verification failed (`helper-failed`). |
290
+
291
+ The target is never deleted to roll back these failures. These are observations,
292
+ not locks; inspect the names and identities before recovery. Interrupted processes
293
+ can likewise leave both links, and default hardlink rejection remains enabled.
@@ -94,9 +94,11 @@ through the retained parents without another receipt, duplicate, reopen, or
94
94
  macOS `F_GETPATH`; the documented final source-name substitution window remains
95
95
  there. Deeper names retain guarded parent traversal.
96
96
 
97
- No-clobber `Root.move()` fails with `helper-unavailable` if descriptor-relative
98
- parent admission or atomic no-replace rename is unavailable; it never substitutes
99
- a check followed by a replacing rename. See the
97
+ No-clobber `Root.move()` requires descriptor-relative parent admission. On Linux,
98
+ `auto` can use identity-checked link/unlink for files or plain descriptor-relative
99
+ rename for directories when `RENAME_NOREPLACE` is unsupported. Directory fallback
100
+ can replace an empty directory created concurrently; `require` stays strict.
101
+ See the [move contract](writing.md#fs-move-from-to-options) and the
100
102
  [fallback contract](native.md#javascript-fallback-guarantees-and-delta).
101
103
 
102
104
  Guarded JavaScript mutations can have out-of-root effects before a post-check
@@ -127,11 +127,44 @@ is the same documented containment class as the macOS and JavaScript paths;
127
127
  applications requiring atomic beneath resolution must check the result or use
128
128
  OS isolation. Rejection after a mutating open does not promise rollback.
129
129
 
130
- Nested no-clobber `Root.move()` still admits both parents and uses
131
- `renameat2(RENAME_NOREPLACE)`. Existing destinations are never overwritten.
132
- That separate syscall/filesystem capability remains required; if unavailable,
133
- the operation fails with `helper-unavailable`. Turning native mode `off` still
134
- disables no-clobber moves because Node has no equivalent atomic rename API.
130
+ Nested no-clobber `Root.move()` admits both parents and first uses
131
+ `renameat2(RENAME_NOREPLACE)`. Linux `auto` can fall back through those same
132
+ descriptors to identity-checked file link/unlink or directory `renameat`;
133
+ the directory fallback can replace an empty directory created concurrently.
134
+ See the [move contract](writing.md#fs-move-from-to-options) for identity windows
135
+ and partial-state receipts. `require` rejects an unsupported capability.
136
+ Turning native mode `off` still disables no-clobber moves.
137
+
138
+ For regular-file writes with no-clobber publication (including `Root.create()`
139
+ and archive file extraction), `auto` removes its identity-checked unpublished
140
+ stage and retries through the same guarded JavaScript path as native mode `off`
141
+ when Linux rejects `RENAME_NOREPLACE` with `EINVAL`, `ENOSYS`, or unsupported-operation
142
+ errno. This is a definite non-publication, not an uncertain remote rename.
143
+ Completed streams are replayed from the retained stage descriptor, without
144
+ calling their producer again. Unsupported capability is cached by the retained
145
+ parent's exact device number in a process-local cache bounded to 128 devices.
146
+ Only distinct sibling publication/quarantine/move failures populate that cache; generic
147
+ cross-parent moves can also reject directory ancestry after a source swap.
148
+ Other devices and replacement writes retain their native route. `require`
149
+ cleans the stage and reports `helper-unavailable`, naming the operation and
150
+ `renameat2 RENAME_NOREPLACE`. Failed cleanup prevents retry.
151
+ Copies explicitly requiring `clone: "always"` also fail closed: the JavaScript
152
+ byte-copy path cannot satisfy that requirement.
153
+
154
+ Private producer workspace promotion likewise uses its existing guarded
155
+ JavaScript handoff in `auto`, preserving the completed producer output.
156
+ Explicit native-only staged, retained-entry, symlink and directory-replacement
157
+ contracts fail closed with the same capability
158
+ diagnostic. Compatible temp-workspace cleanup can use its existing guarded
159
+ JavaScript quarantine/removal path; `require-bounded` and native `require` refuse
160
+ that downgrade. These publication/cleanup owners keep their own recovery
161
+ contracts. Regular-file `publishFileExclusive` with `rename-noreplace` also opts
162
+ into the retained-parent file-move fallback in Linux `auto`, reporting
163
+ `fallback: "link-unlink"`; `require` still refuses. This fallback is not
164
+ crash-atomic: see [publication strategies](durability.md#strategies) and partial
165
+ failure receipts. The standalone
166
+ `movePathWithCopyFallback()` uses guarded replacing rename and its existing
167
+ cross-device copy policy, so unsupported no-replace flags do not affect it.
135
168
 
136
169
  Bounded owned-tree cleanup deliberately has no `openat` fallback:
137
170
  `RESOLVE_NO_XDEV` rejects bind mounts even when device numbers match, which
@@ -311,7 +344,7 @@ See [Root containment guarantees](security-model.md#containment-guarantees-by-pl
311
344
  | ZIP/TAR/gzip | Rust streaming decode and fd-relative output creation. | Optional JSZip or bundled WASM TAR into guarded private staging, then the same guarded merge policy. |
312
345
  | Zstd/bzip2 TAR | Rust streaming decode and fd-relative output creation. | Bundled WASM codecs feed the shared Rust TAR parser, then guarded private staging and the same merge policy; no optional codec dependency. |
313
346
  | Publication copy | Clone, Linux `copy_file_range`, async native SHA-256. | Exclusive `wx` byte loop and Node SHA-256 with the same content/identity fences. |
314
- | `rename-noreplace` | Atomic platform no-replace rename. | Unsupported; no emulation by check-then-rename. |
347
+ | `rename-noreplace` | Atomic platform no-replace rename; Linux `auto` file publication can use identity-checked, non-crash-atomic link/unlink when unsupported. | Unsupported; no emulation by check-then-rename. |
315
348
  | Windows DACL read | Direct `GetSecurityInfo`; the public facts API exposes ordered basic allow/deny ACE SIDs, masks, and decoded flags without trust policy. Secure-file reads query the borrowed open descriptor and compare its 32-bit volume serial and 64-bit file-index projection with Node's bigint receipt. | The packaged PowerShell/C# bridge preserves raw facts and inspects the borrowed descriptor for secure reads, with the same Node identity comparison; its command failures reject. Structured .NET pathname reporting retains its separate compatibility query. |
316
349
  | Windows private directory | Creation-time protected DACL. | The packaged PowerShell/C# bridge applies the protected DACL at creation and retains exact handles through identity validation and failure cleanup. Command failures reject. |
317
350
 
@@ -24,7 +24,9 @@ The handle resolver verifies exact descriptor and pathname identities, with one
24
24
  bounded retry for unknown Windows observations. It borrows the handle without
25
25
  reading, reopening, closing it, or changing its cursor.
26
26
 
27
- The error helpers are `categorizeFsSafeError` and `FsSafeErrorDetails`.
27
+ The error helpers are `categorizeFsSafeError`, `isNoReplaceUnsupported`, and
28
+ `FsSafeErrorDetails`. The no-replace classifier recognizes the documented
29
+ capability refusal without matching internal cause codes.
28
30
 
29
31
  ## `path` and `advanced`
30
32
 
@@ -114,10 +114,17 @@ drive-relative spellings, and the stage's own name are rejected.
114
114
  With `overwrite: false`, publication is genuine kernel no-replace rename.
115
115
  A native collision raises `FsSafeError("already-exists")`, but ordinary errno
116
116
  does not prove that a remote rename never committed. Native rename failures
117
- without explicit pre-dispatch provenance therefore report `indeterminate`:
117
+ without explicit non-publication provenance therefore report `indeterminate`:
118
118
  cleanup preserves names and closes descriptors, and further publication rejects.
119
- Only rejection before rename dispatch leaves the stage eligible for cleanup or
120
- publication under another name. With
119
+ Rejection before rename dispatch, or an unsupported no-replace capability,
120
+ leaves the stage eligible for identity-checked cleanup. Linux `EINVAL` for the
121
+ fixed `RENAME_NOREPLACE` flag on a regular file renamed to a distinct sibling,
122
+ `ENOSYS`, and unsupported-operation errno prove that nothing was published
123
+ ([rename(2)](https://man7.org/linux/man-pages/man2/rename.2.html),
124
+ [VFS rename contract](https://www.kernel.org/doc/html/latest/filesystems/vfs.html)).
125
+ This native-only API reports `helper-unavailable` with the capability name;
126
+ it does not retry through JavaScript. `Root.create()` in native `auto` mode owns
127
+ that separate compatibility retry. With
121
128
  `overwrite: true`, publication is plain atomic replacement. Neither route
122
129
  copies. Both source and destination resolve through the retained original
123
130
  parent, with checks immediately before rename and after publication.
@@ -131,7 +131,9 @@ and, when present, the entry itself. Parent activity requests a guarded scan
131
131
  without supplying filenames. The entry descriptor covers content and attribute
132
132
  changes and is replaced when a guarded scan admits a new identity. Missing paths
133
133
  use their nearest admitted ancestor. Descriptors are opened without following
134
- symlinks; a symlink entry uses `O_SYMLINK` to observe the link itself. Every
134
+ symlinks; a symlink entry uses `O_SYMLINK` to observe the link itself. All
135
+ non-directory entries are opened nonblocking so a FIFO cannot stall the shared
136
+ watch hub. Every
135
137
  descriptor's device/inode must match the guarded observation. There are at most
136
138
  two retained descriptors per entry scope (128 scopes maximum), counted in health
137
139
  `directories`; descriptor exhaustion fails registration with `EMFILE`. Removal
@@ -367,6 +367,11 @@ For high-volume logging, consider [`openWritable`](#openwritable-for-streaming)
367
367
 
368
368
  ### `fs.copyIn(rel, sourceAbsPath, options?)`
369
369
 
370
+ Synchronous module hooks can use [`copyRootFileSync`](copy.md#synchronous-guarded-file-copies)
371
+ from `@openclaw/fs-safe/advanced` to create an absent destination and retain its
372
+ descriptor for hashing. Root itself remains asynchronous.
373
+
374
+
370
375
  `copyIn` accepts a `RootCopySource`: a trusted absolute source path or a file
371
376
  within another Root. The guarded form supplies `root` with only its `open` and
372
377
  `stat` read capabilities, plus `relativePath`:
@@ -416,8 +421,11 @@ default to `"auto"`:
416
421
  | `auto` | Try native file cloning, then copy offload or ordinary byte copying when cloning is unavailable. |
417
422
  | `always` | Require native cloning; fail when the binding or filesystem cannot provide it. |
418
423
 
419
- Native file cloning supports APFS and supported Linux filesystems. Windows
420
- currently uses byte copying for `never` and `auto`; `always` fails. Clone choice
424
+ Native file cloning supports APFS and supported Linux filesystems. On Linux, a
425
+ `FICLONE` request denied with `EPERM`, as container seccomp profiles commonly
426
+ do, counts as unavailable cloning: `auto` copies bytes and `always` fails with
427
+ `unsupported-platform`. Windows currently uses byte copying for `never` and
428
+ `auto`; `always` fails. Clone choice
421
429
  does not change modes, durability, root confinement, or source and publication
422
430
  identity checks. The shared strategy does not replace Root's guarded regular-file
423
431
  contract with `copyTree`'s caller-owned immutable-tree and metadata contract.
@@ -460,17 +468,38 @@ Mutation policy is captured at call start; changes to caller-owned denial arrays
460
468
  apply to later moves. For live cancellation or revocation, throw from
461
469
  `assertBeforeMutation` immediately before dispatch.
462
470
 
463
- The default no-clobber mode requires the native helper. It admits both parent
464
- directory descriptors and performs a descriptor-relative no-replace rename, so
465
- a competitor that creates the target first is preserved and the source remains
466
- in place. If the helper or safe parent admission is unavailable, the call fails
467
- with `helper-unavailable`; it never falls back to a check followed by a
468
- replacing rename. After dispatch it rechecks both parent identities, so a
469
- post-operation rejection can mean the no-replace rename completed. Directory
470
- moves continue to require `overwrite: true`.
471
+ The default no-clobber mode supports regular files and directories and requires
472
+ the native helper. It admits both parent directory descriptors and first uses
473
+ descriptor-relative no-replace rename. If the helper or safe parent admission
474
+ is unavailable, the call fails with `helper-unavailable`. After dispatch it
475
+ rechecks both parent identities; rejection does not imply rollback.
476
+
477
+ On Linux filesystems that reject `renameat2(RENAME_NOREPLACE)`, `auto` uses the
478
+ same retained parents and rechecks the source identity before falling back:
479
+
480
+ - Regular files use `linkat` without following symlinks, verify the exact
481
+ source/target inode pair with two links, then recheck the source before
482
+ `unlinkat`. Existing targets cannot be overwritten. Only this intermediate
483
+ pair permits two links; admission and completed publication require one.
484
+ Denied or unsupported hard links fail closed, naming both capabilities.
485
+ - Directories recheck that the target is absent and use plain `renameat`.
486
+ A concurrent non-empty directory or non-directory target rejects as
487
+ `already-exists`. **A concurrently created empty directory can be replaced.**
488
+ A trailing slash on the direct-child source requires a directory at syscall
489
+ time, even after a file or symlink substitution. Source identity checks and
490
+ rename/unlink are separate syscalls,
491
+ not source-name compare-and-swap; use exclusive namespace ownership or OS
492
+ isolation against hostile actors in the final check-to-syscall window.
493
+
494
+ `require` does not take these fallbacks. Use
495
+ [`isNoReplaceUnsupported(error)`](errors.md#unsupported-no-replace-moves) to
496
+ classify the capability refusal. File fallback failures after link publication
497
+ leave the target intact and report `details.publication: "published"` plus
498
+ `details.sourceRemoval`; callers must inspect that partial state before retrying.
499
+ There is no automatic rollback or crash-durability guarantee.
471
500
 
472
501
  Linux without `openat2` uses the [guarded native parent walk](native.md#linux-without-openat2).
473
- The move still uses `renameat2(RENAME_NOREPLACE)` and preserves collisions;
502
+ The move uses `renameat2(RENAME_NOREPLACE)` or the Linux auto fallbacks above;
474
503
  parent resolution reports the documented `best-effort` containment class.
475
504
  Disabling the addon still makes no-clobber moves unavailable.
476
505
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/fs-safe",
3
- "version": "0.23.0",
3
+ "version": "0.24.2",
4
4
  "description": "Capability-style filesystem roots for Node.js apps that handle untrusted relative paths.",
5
5
  "keywords": [
6
6
  "filesystem",
@@ -173,20 +173,20 @@
173
173
  "archive:producer-smoke": "node scripts/archive-producer-smoke.mjs"
174
174
  },
175
175
  "optionalDependencies": {
176
- "@openclaw/fs-safe-darwin-arm64": "0.23.0",
177
- "@openclaw/fs-safe-darwin-x64": "0.23.0",
178
- "@openclaw/fs-safe-linux-arm64-gnu": "0.23.0",
179
- "@openclaw/fs-safe-linux-arm64-musl": "0.23.0",
180
- "@openclaw/fs-safe-linux-x64-gnu": "0.23.0",
181
- "@openclaw/fs-safe-linux-x64-musl": "0.23.0",
182
- "@openclaw/fs-safe-win32-x64-msvc": "0.23.0",
176
+ "@openclaw/fs-safe-darwin-arm64": "0.24.2",
177
+ "@openclaw/fs-safe-darwin-x64": "0.24.2",
178
+ "@openclaw/fs-safe-linux-arm64-gnu": "0.24.2",
179
+ "@openclaw/fs-safe-linux-arm64-musl": "0.24.2",
180
+ "@openclaw/fs-safe-linux-x64-gnu": "0.24.2",
181
+ "@openclaw/fs-safe-linux-x64-musl": "0.24.2",
182
+ "@openclaw/fs-safe-win32-x64-msvc": "0.24.2",
183
183
  "jszip": "^3.10.2"
184
184
  },
185
185
  "devDependencies": {
186
186
  "@emnapi/runtime": "2.0.0-alpha.5",
187
- "@napi-rs/cli": "3.10.5",
187
+ "@napi-rs/cli": "3.10.6",
188
188
  "@types/node": "^26.6.1",
189
- "@vitest/coverage-v8": "5.0.2",
189
+ "@vitest/coverage-v8": "5.0.3",
190
190
  "fast-check": "^4.10.1",
191
191
  "istanbul-lib-coverage": "3.2.2",
192
192
  "istanbul-lib-report": "3.0.1",
@@ -194,7 +194,7 @@
194
194
  "sigstore": "5.0.0",
195
195
  "tar": "7.5.22",
196
196
  "typescript": "^7.0.2",
197
- "vite": "8.3.1",
197
+ "vite": "8.3.2",
198
198
  "vitest": "^5.0.1"
199
199
  },
200
200
  "engines": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/fs-safe-darwin-arm64",
3
- "version": "0.23.0",
3
+ "version": "0.24.2",
4
4
  "description": "macOS arm64 native binding for @openclaw/fs-safe.",
5
5
  "license": "MIT",
6
6
  "author": "OpenClaw Team <dev@openclaw.ai>",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/fs-safe-darwin-x64",
3
- "version": "0.23.0",
3
+ "version": "0.24.2",
4
4
  "description": "macOS x64 native binding for @openclaw/fs-safe.",
5
5
  "license": "MIT",
6
6
  "author": "OpenClaw Team <dev@openclaw.ai>",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/fs-safe-linux-arm64-gnu",
3
- "version": "0.23.0",
3
+ "version": "0.24.2",
4
4
  "description": "Linux arm64 glibc native binding for @openclaw/fs-safe.",
5
5
  "license": "MIT",
6
6
  "author": "OpenClaw Team <dev@openclaw.ai>",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/fs-safe-linux-arm64-musl",
3
- "version": "0.23.0",
3
+ "version": "0.24.2",
4
4
  "description": "Linux arm64 musl native binding for @openclaw/fs-safe.",
5
5
  "license": "MIT",
6
6
  "author": "OpenClaw Team <dev@openclaw.ai>",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/fs-safe-linux-x64-gnu",
3
- "version": "0.23.0",
3
+ "version": "0.24.2",
4
4
  "description": "Linux x64 glibc native binding for @openclaw/fs-safe.",
5
5
  "license": "MIT",
6
6
  "author": "OpenClaw Team <dev@openclaw.ai>",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/fs-safe-linux-x64-musl",
3
- "version": "0.23.0",
3
+ "version": "0.24.2",
4
4
  "description": "Linux x64 musl native binding for @openclaw/fs-safe.",
5
5
  "license": "MIT",
6
6
  "author": "OpenClaw Team <dev@openclaw.ai>",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/fs-safe-win32-x64-msvc",
3
- "version": "0.23.0",
3
+ "version": "0.24.2",
4
4
  "description": "Windows x64 MSVC native binding for @openclaw/fs-safe.",
5
5
  "license": "MIT",
6
6
  "author": "OpenClaw Team <dev@openclaw.ai>",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/matrix",
3
- "version": "2026.10.1-beta.2",
3
+ "version": "2026.10.2",
4
4
  "description": "OpenClaw Matrix channel plugin for rooms and direct messages.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -10,7 +10,7 @@
10
10
  "dependencies": {
11
11
  "@matrix-org/matrix-sdk-crypto-nodejs": "0.6.6",
12
12
  "@matrix-org/matrix-sdk-crypto-wasm": "18.8.0",
13
- "@openclaw/fs-safe": "0.23.0",
13
+ "@openclaw/fs-safe": "0.24.2",
14
14
  "fake-indexeddb": "6.2.5",
15
15
  "markdown-it": "15.0.2",
16
16
  "matrix-js-sdk": "42.4.0",
@@ -19,7 +19,7 @@
19
19
  "zod": "4.6.5"
20
20
  },
21
21
  "peerDependencies": {
22
- "openclaw": ">=2026.10.1-beta.2"
22
+ "openclaw": ">=2026.10.2"
23
23
  },
24
24
  "peerDependenciesMeta": {
25
25
  "openclaw": {
@@ -169,11 +169,11 @@
169
169
  "allowInvalidConfigRecovery": true
170
170
  },
171
171
  "compat": {
172
- "pluginApi": ">=2026.10.1-beta.2"
172
+ "pluginApi": ">=2026.10.2"
173
173
  },
174
174
  "build": {
175
175
  "bundledDist": false,
176
- "openclawVersion": "2026.10.1-beta.2"
176
+ "openclawVersion": "2026.10.2"
177
177
  },
178
178
  "release": {
179
179
  "publishToClawHub": true,