@openclaw/openshell-sandbox 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.
- package/node_modules/@openclaw/fs-safe/CHANGELOG.md +56 -0
- package/node_modules/@openclaw/fs-safe/dist/advanced.d.ts +1 -0
- package/node_modules/@openclaw/fs-safe/dist/advanced.js +1 -0
- package/node_modules/@openclaw/fs-safe/dist/archive-entry.js +7 -3
- package/node_modules/@openclaw/fs-safe/dist/copy-root-file-sync.d.ts +35 -0
- package/node_modules/@openclaw/fs-safe/dist/copy-root-file-sync.js +218 -0
- package/node_modules/@openclaw/fs-safe/dist/create.d.ts +1 -0
- package/node_modules/@openclaw/fs-safe/dist/create.js +4 -1
- package/node_modules/@openclaw/fs-safe/dist/entry-publication.js +3 -1
- package/node_modules/@openclaw/fs-safe/dist/errors.d.ts +2 -0
- package/node_modules/@openclaw/fs-safe/dist/errors.js +5 -0
- package/node_modules/@openclaw/fs-safe/dist/index.d.ts +1 -1
- package/node_modules/@openclaw/fs-safe/dist/index.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/native-binding.d.ts +3 -0
- package/node_modules/@openclaw/fs-safe/dist/native-move-fallback.d.ts +3 -0
- package/node_modules/@openclaw/fs-safe/dist/native-move-fallback.js +54 -0
- package/node_modules/@openclaw/fs-safe/dist/native-noreplace.d.ts +8 -0
- package/node_modules/@openclaw/fs-safe/dist/native-noreplace.js +55 -0
- package/node_modules/@openclaw/fs-safe/dist/native-pinned-write.d.ts +1 -1
- package/node_modules/@openclaw/fs-safe/dist/native-pinned-write.js +12 -2
- package/node_modules/@openclaw/fs-safe/dist/native-rename-outcome.js +3 -1
- package/node_modules/@openclaw/fs-safe/dist/native-staged-file.d.ts +1 -1
- package/node_modules/@openclaw/fs-safe/dist/native-staged-file.js +48 -5
- package/node_modules/@openclaw/fs-safe/dist/pinned-write-input.d.ts +2 -0
- package/node_modules/@openclaw/fs-safe/dist/pinned-write-input.js +17 -0
- package/node_modules/@openclaw/fs-safe/dist/pinned-write.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/publish-file-failure.d.ts +5 -0
- package/node_modules/@openclaw/fs-safe/dist/publish-file-failure.js +1 -0
- package/node_modules/@openclaw/fs-safe/dist/publish-file-native.d.ts +4 -0
- package/node_modules/@openclaw/fs-safe/dist/publish-file-native.js +35 -0
- package/node_modules/@openclaw/fs-safe/dist/publish-file.d.ts +2 -0
- package/node_modules/@openclaw/fs-safe/dist/publish-file.js +8 -2
- package/node_modules/@openclaw/fs-safe/dist/replace-directory.js +6 -5
- package/node_modules/@openclaw/fs-safe/dist/root-errors.js +25 -4
- package/node_modules/@openclaw/fs-safe/dist/root-impl.js +3 -3
- package/node_modules/@openclaw/fs-safe/dist/root-move-noreplace.d.ts +4 -2
- package/node_modules/@openclaw/fs-safe/dist/root-move-noreplace.js +13 -17
- package/node_modules/@openclaw/fs-safe/dist/sibling-staged-file.js +18 -3
- package/node_modules/@openclaw/fs-safe/dist/staged-file-settlement.js +4 -1
- package/node_modules/@openclaw/fs-safe/dist/temp-workspace-owner.d.ts +1 -0
- package/node_modules/@openclaw/fs-safe/dist/temp-workspace-owner.js +27 -4
- package/node_modules/@openclaw/fs-safe/docs/advanced.md +1 -0
- package/node_modules/@openclaw/fs-safe/docs/archive.md +2 -1
- package/node_modules/@openclaw/fs-safe/docs/copy.md +61 -1
- package/node_modules/@openclaw/fs-safe/docs/durability.md +50 -5
- package/node_modules/@openclaw/fs-safe/docs/errors.md +37 -0
- package/node_modules/@openclaw/fs-safe/docs/native-helper.md +5 -3
- package/node_modules/@openclaw/fs-safe/docs/native.md +39 -6
- package/node_modules/@openclaw/fs-safe/docs/public-api.md +3 -1
- package/node_modules/@openclaw/fs-safe/docs/staged-file.md +10 -3
- package/node_modules/@openclaw/fs-safe/docs/watch.md +3 -1
- package/node_modules/@openclaw/fs-safe/docs/writing.md +40 -11
- package/node_modules/@openclaw/fs-safe/package.json +11 -11
- package/node_modules/@openclaw/fs-safe-darwin-arm64/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-darwin-arm64/package.json +1 -1
- package/node_modules/@openclaw/fs-safe-darwin-x64/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-darwin-x64/package.json +1 -1
- package/node_modules/@openclaw/fs-safe-linux-arm64-gnu/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-linux-arm64-gnu/package.json +1 -1
- package/node_modules/@openclaw/fs-safe-linux-arm64-musl/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-linux-arm64-musl/package.json +1 -1
- package/node_modules/@openclaw/fs-safe-linux-x64-gnu/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-linux-x64-gnu/package.json +1 -1
- package/node_modules/@openclaw/fs-safe-linux-x64-musl/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-linux-x64-musl/package.json +1 -1
- package/node_modules/@openclaw/fs-safe-win32-x64-msvc/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-win32-x64-msvc/package.json +1 -1
- 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` |
|
|
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
|
|
397
|
-
source to the target without replacement
|
|
398
|
-
|
|
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()`
|
|
98
|
-
|
|
99
|
-
|
|
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()`
|
|
131
|
-
`renameat2(RENAME_NOREPLACE)`.
|
|
132
|
-
|
|
133
|
-
the
|
|
134
|
-
|
|
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
|
|
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
|
|
117
|
+
without explicit non-publication provenance therefore report `indeterminate`:
|
|
118
118
|
cleanup preserves names and closes descriptors, and further publication rejects.
|
|
119
|
-
|
|
120
|
-
|
|
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.
|
|
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.
|
|
420
|
-
|
|
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
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
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
|
|
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.
|
|
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.
|
|
177
|
-
"@openclaw/fs-safe-darwin-x64": "0.
|
|
178
|
-
"@openclaw/fs-safe-linux-arm64-gnu": "0.
|
|
179
|
-
"@openclaw/fs-safe-linux-arm64-musl": "0.
|
|
180
|
-
"@openclaw/fs-safe-linux-x64-gnu": "0.
|
|
181
|
-
"@openclaw/fs-safe-linux-x64-musl": "0.
|
|
182
|
-
"@openclaw/fs-safe-win32-x64-msvc": "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.
|
|
187
|
+
"@napi-rs/cli": "3.10.6",
|
|
188
188
|
"@types/node": "^26.6.1",
|
|
189
|
-
"@vitest/coverage-v8": "5.0.
|
|
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.
|
|
197
|
+
"vite": "8.3.2",
|
|
198
198
|
"vitest": "^5.0.1"
|
|
199
199
|
},
|
|
200
200
|
"engines": {
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openclaw/openshell-sandbox",
|
|
3
|
-
"version": "2026.10.
|
|
3
|
+
"version": "2026.10.2",
|
|
4
4
|
"description": "OpenClaw sandbox backend for the NVIDIA OpenShell CLI with mirrored local workspaces and SSH command execution.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
},
|
|
9
9
|
"type": "module",
|
|
10
10
|
"dependencies": {
|
|
11
|
-
"@openclaw/fs-safe": "0.
|
|
11
|
+
"@openclaw/fs-safe": "0.24.2",
|
|
12
12
|
"p-limit": "7.3.3",
|
|
13
13
|
"zod": "4.6.5"
|
|
14
14
|
},
|
|
@@ -23,10 +23,10 @@
|
|
|
23
23
|
"minHostVersion": ">=2026.5.12-beta.1"
|
|
24
24
|
},
|
|
25
25
|
"compat": {
|
|
26
|
-
"pluginApi": ">=2026.10.
|
|
26
|
+
"pluginApi": ">=2026.10.2"
|
|
27
27
|
},
|
|
28
28
|
"build": {
|
|
29
|
-
"openclawVersion": "2026.10.
|
|
29
|
+
"openclawVersion": "2026.10.2",
|
|
30
30
|
"bundledDist": false
|
|
31
31
|
},
|
|
32
32
|
"release": {
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"assets/activity.svg"
|
|
46
46
|
],
|
|
47
47
|
"peerDependencies": {
|
|
48
|
-
"openclaw": ">=2026.10.
|
|
48
|
+
"openclaw": ">=2026.10.2"
|
|
49
49
|
},
|
|
50
50
|
"peerDependenciesMeta": {
|
|
51
51
|
"openclaw": {
|