@openclaw/openshell-sandbox 2026.10.1-beta.2 → 2026.10.5-beta.1
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/dist/index.js +69 -111
- package/node_modules/@openclaw/fs-safe/CHANGELOG.md +91 -0
- package/node_modules/@openclaw/fs-safe/README.md +1 -1
- 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-durability.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/archive-entry.js +7 -3
- package/node_modules/@openclaw/fs-safe/dist/archive-merge.js +4 -4
- package/node_modules/@openclaw/fs-safe/dist/archive-parser.wasm +0 -0
- package/node_modules/@openclaw/fs-safe/dist/archive-read.js +5 -4
- package/node_modules/@openclaw/fs-safe/dist/archive-staging.d.ts +1 -1
- package/node_modules/@openclaw/fs-safe/dist/archive-staging.js +22 -38
- package/node_modules/@openclaw/fs-safe/dist/copy-path-admission.d.ts +17 -0
- package/node_modules/@openclaw/fs-safe/dist/copy-path-admission.js +117 -0
- package/node_modules/@openclaw/fs-safe/dist/copy-root-file-sync.d.ts +38 -0
- package/node_modules/@openclaw/fs-safe/dist/copy-root-file-sync.js +195 -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/file-lock-sync-root-acquire.d.ts +3 -0
- package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root-acquire.js +57 -11
- package/node_modules/@openclaw/fs-safe/dist/guarded-mutation.js +5 -5
- package/node_modules/@openclaw/fs-safe/dist/guest-native-python.js +52 -5
- package/node_modules/@openclaw/fs-safe/dist/guest.js +1 -1
- 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 +4 -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-operations.d.ts +0 -1
- package/node_modules/@openclaw/fs-safe/dist/native-operations.js +22 -26
- package/node_modules/@openclaw/fs-safe/dist/native-pinned-write-windows.js +5 -1
- 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 +52 -6
- package/node_modules/@openclaw/fs-safe/dist/permission-exec.js +2 -1
- 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-types.d.ts +6 -1
- package/node_modules/@openclaw/fs-safe/dist/pinned-write.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/private-temp-workspace.js +2 -2
- 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.d.ts +2 -0
- package/node_modules/@openclaw/fs-safe/dist/publish-file.js +35 -2
- package/node_modules/@openclaw/fs-safe/dist/replace-directory.js +6 -5
- package/node_modules/@openclaw/fs-safe/dist/replace-file-copy-fallback.js +1 -4
- package/node_modules/@openclaw/fs-safe/dist/replace-file-descriptor.js +6 -0
- package/node_modules/@openclaw/fs-safe/dist/root-create-input.d.ts +15 -1
- package/node_modules/@openclaw/fs-safe/dist/root-create-input.js +4 -0
- package/node_modules/@openclaw/fs-safe/dist/root-directory-entry.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/root-directory-list.d.ts +0 -1
- package/node_modules/@openclaw/fs-safe/dist/root-directory-list.js +6 -12
- package/node_modules/@openclaw/fs-safe/dist/root-errors.js +25 -4
- package/node_modules/@openclaw/fs-safe/dist/root-impl.js +18 -18
- 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/root-paths.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/root-walk.js +4 -4
- package/node_modules/@openclaw/fs-safe/dist/root-write-verification.d.ts +10 -1
- package/node_modules/@openclaw/fs-safe/dist/root-write-verification.js +20 -0
- package/node_modules/@openclaw/fs-safe/dist/secure-temp-dir.js +19 -7
- package/node_modules/@openclaw/fs-safe/dist/sibling-staged-file.js +18 -3
- package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-acquire.js +25 -3
- package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-admission.d.ts +2 -0
- package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-retained.d.ts +14 -0
- package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-retained.js +187 -0
- package/node_modules/@openclaw/fs-safe/dist/sidecar-lock.js +15 -1
- package/node_modules/@openclaw/fs-safe/dist/staged-file-settlement.js +4 -1
- package/node_modules/@openclaw/fs-safe/dist/temp-directory-diagnostics.d.ts +10 -0
- package/node_modules/@openclaw/fs-safe/dist/temp-directory-diagnostics.js +29 -0
- package/node_modules/@openclaw/fs-safe/dist/temp-target.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/temp-workspace-admission.js +6 -6
- package/node_modules/@openclaw/fs-safe/dist/temp-workspace-child-admission.d.ts +4 -4
- package/node_modules/@openclaw/fs-safe/dist/temp-workspace-child-admission.js +17 -16
- package/node_modules/@openclaw/fs-safe/dist/temp-workspace-descriptor.js +20 -28
- 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/dist/walk.js +2 -13
- package/node_modules/@openclaw/fs-safe/dist/watch-native.d.ts +2 -2
- package/node_modules/@openclaw/fs-safe/dist/watch-native.js +3 -1
- package/node_modules/@openclaw/fs-safe/dist/watch-rescan.js +3 -9
- package/node_modules/@openclaw/fs-safe/dist/watch.js +2 -3
- package/node_modules/@openclaw/fs-safe/docs/advanced.md +1 -0
- package/node_modules/@openclaw/fs-safe/docs/archive.md +5 -1
- package/node_modules/@openclaw/fs-safe/docs/atomic.md +11 -3
- package/node_modules/@openclaw/fs-safe/docs/contributing.md +1 -1
- package/node_modules/@openclaw/fs-safe/docs/copy.md +109 -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/guest.md +21 -4
- 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/path-scope.md +4 -0
- package/node_modules/@openclaw/fs-safe/docs/public-api.md +3 -1
- package/node_modules/@openclaw/fs-safe/docs/sidecar-lock.md +46 -8
- package/node_modules/@openclaw/fs-safe/docs/staged-file.md +10 -3
- package/node_modules/@openclaw/fs-safe/docs/temp.md +12 -0
- package/node_modules/@openclaw/fs-safe/docs/watch.md +6 -3
- package/node_modules/@openclaw/fs-safe/docs/writing.md +40 -11
- package/node_modules/@openclaw/fs-safe/package.json +16 -16
- 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
- package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root-arbitration.d.ts +0 -17
- package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root-arbitration.js +0 -66
- package/node_modules/@openclaw/fs-safe/dist/root-move-preflight.d.ts +0 -7
- package/node_modules/@openclaw/fs-safe/dist/root-move-preflight.js +0 -16
|
@@ -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.
|
|
@@ -43,9 +43,26 @@ The fragment has no argument preflight of its own.
|
|
|
43
43
|
|
|
44
44
|
On Linux it uses `renameat2(RENAME_NOREPLACE)`; on macOS it uses
|
|
45
45
|
`renameatx_np(RENAME_EXCL)`. If Linux lacks that function or reports it as
|
|
46
|
-
unsupported, it falls back to `link(..., follow_symlinks=False)`,
|
|
47
|
-
source
|
|
48
|
-
|
|
46
|
+
unsupported, it falls back to `link(..., follow_symlinks=False)`, verifying the
|
|
47
|
+
source and target against the admitted inode with exactly two links
|
|
48
|
+
before unlinking the source. Admission and final target verification require
|
|
49
|
+
one link. A successful file publication consumes the source in both paths.
|
|
50
|
+
|
|
51
|
+
For directories, a definitive Linux rejection (`EINVAL`, `ENOSYS`, `ENOTSUP`,
|
|
52
|
+
or `EOPNOTSUPP`) or missing `renameat2` rechecks the source identity and target
|
|
53
|
+
absence, then uses plain rename through the same directory descriptors. A
|
|
54
|
+
trailing slash enforces directory source type at the syscall. Directory rename
|
|
55
|
+
can replace only an **empty directory created concurrently**; non-empty
|
|
56
|
+
directories and non-directory targets are refused as `EEXIST`. An existing
|
|
57
|
+
empty directory observed by the absence check is also refused. No empty claim
|
|
58
|
+
directory is exposed before publishing the completed source.
|
|
59
|
+
|
|
60
|
+
Identity checks and mutation remain separate, not source-name compare-and-swap.
|
|
61
|
+
The fallback verifies the published identity afterward. A file fallback failure
|
|
62
|
+
can leave both names, and post-publication failures preserve the destination
|
|
63
|
+
for caller reconciliation. Exclusive-create cleanup preserves changed payloads
|
|
64
|
+
and incomplete link pairs. The guest has no strict/require mode; host native
|
|
65
|
+
policy does not apply. macOS and other errors retain their fail-closed behavior.
|
|
49
66
|
|
|
50
67
|
## Invocation protocol
|
|
51
68
|
|
|
@@ -107,7 +124,7 @@ entries, including symlinks, without following them. A force removal tolerates
|
|
|
107
124
|
a missing leaf but does not suppress failure to open its parent.
|
|
108
125
|
|
|
109
126
|
Write preserves an existing regular file's mode and otherwise creates private
|
|
110
|
-
files. Copy
|
|
127
|
+
files. Copy and cross-device file moves preserve the admitted source descriptor's mode. Exclusive create publishes a mode-0600
|
|
111
128
|
file from a private staging directory. Staging names retain the `.openclaw-*`
|
|
112
129
|
prefixes and use short random suffixes independent of the destination basename,
|
|
113
130
|
so legal names near the filesystem's component limit also work for writes and
|
|
@@ -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
|
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# pathScope()
|
|
2
2
|
|
|
3
|
+
Paths supplied to a scope are literal filesystem names. In particular, `~/file`
|
|
4
|
+
selects a file inside a directory named `~` beneath the scope; it does not expand
|
|
5
|
+
the user's home directory.
|
|
6
|
+
|
|
3
7
|
`pathScope()` prepares absolute paths and returns plain `{ ok, path }` results. `resolve()` and `resolveAll()` check lexical containment without touching the filesystem; `existing()`, `files()`, and `writable()` add the filesystem checks described below. Use it to prepare paths before handing them to another library, whose file-opening and mutation behavior still applies.
|
|
4
8
|
|
|
5
9
|
```ts
|
|
@@ -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
|
|
|
@@ -35,7 +35,7 @@ try {
|
|
|
35
35
|
|
|
36
36
|
The lock file sits next to the protected resource. If a process crashes mid-lock, the next acquirer notices the held entry, inspects its payload (PID, host, acquired-at timestamp), and decides — via `shouldReclaim` (defaulting to "is the lock older than `staleMs`?") — whether it should keep waiting or fail.
|
|
37
37
|
|
|
38
|
-
On natural event-loop shutdown, a globally deduplicated `process.on("beforeExit")` handler attempts asynchronous cleanup of held Root-backed locks through their
|
|
38
|
+
On natural event-loop shutdown, a globally deduplicated `process.on("beforeExit")` handler attempts asynchronous cleanup of held Root-backed locks through their creation receipt (or the Root capability when native retention is unavailable). The synchronous `process.on("exit")` handler also cleans retained native sidecars, raw locks, and raw-path reclaim guards. Changed sidecars and failed Root cleanup remain in place; cleanup does not keep retrying during shutdown unless another acquisition re-arms it. Locks acquired with `retainOnExit: true` are exempt from both handlers: their sidecar stays in place after exit and is governed only by the caller's own stale policy. Because exit handlers are globally deduplicated across package copies, `retainOnExit` fails closed with `helper-unavailable` if an older copy that cannot honor it registered the handlers first.
|
|
39
39
|
|
|
40
40
|
Asynchronous Root-backed stale recovery uses a regular file at the
|
|
41
41
|
`.reclaim` name, created, verified, and removed through that Root. Its ownership
|
|
@@ -57,17 +57,17 @@ even if guard ownership has also changed.
|
|
|
57
57
|
Root guard; let its original attempt settle before retrying that guarded path.
|
|
58
58
|
It stops compromise monitoring for forgotten holders, including callbacks from checks already in flight.
|
|
59
59
|
|
|
60
|
-
Always release locks in a `finally` block. Application-managed graceful shutdown can await `release()` or `manager.drain()` before terminating. Explicit `process.exit()
|
|
60
|
+
Always release locks in a `finally` block. Application-managed graceful shutdown can await `release()` or `manager.drain()` before terminating. Explicit `process.exit()` runs synchronous cleanup for retained native sidecars, but cannot run asynchronous fallback Root cleanup. Uncaught failures, crashes, default signal handling, and fatal termination (including `SIGKILL`) may leave sidecars. Recover only after an application-owned liveness policy proves the holder cannot still be writing.
|
|
61
61
|
|
|
62
62
|
Exit cleanup tolerates shared managers created by older package copies that lack reclaim-guard state, and continues through later manager domains. Handler ownership remains first-registration-wins: loading an updated copy does not replace an older copy's registered handler. First registration is committed only after Node accepts the listener; synchronous `newListener` reentry fails closed and a thrown registration rolls back so a later acquisition can retry. Restart with the updated copy registering first to obtain the fix. After building, `node scripts/legacy-lock-exit-proof.mjs` checks clean exit, removal of legacy and modern raw locks, and preservation of a retained lock using synthetic temporary files.
|
|
63
63
|
|
|
64
|
-
Each new sidecar also carries an internal random ownership token encoded as JSON trailing whitespace. `JSON.parse()` and every payload callback still see exactly the caller-provided object. Only the process that successfully created the sidecar keeps that token as release authority; merely reading token-shaped bytes from disk does not enable this mode.
|
|
64
|
+
Each new sidecar also carries an internal random ownership token encoded as JSON trailing whitespace. `JSON.parse()` and every payload callback still see exactly the caller-provided object. Only the process that successfully created the sidecar keeps that token as release authority; merely reading token-shaped bytes from disk does not enable this mode. Raw and JavaScript fallback release compare the in-memory token and exact serialized bytes, and require the pathname to remain a regular file, instead of requiring an opened descriptor and pathname lookup to report the same inode identity. Retained native Root locks additionally require the exact created inode and a single link, as described below. This preserves ownership checks on filesystems such as Docker Desktop VirtioFS where those two views can legitimately differ. Sidecars created by older releases have no token and retain the legacy identity-plus-content check. Raw acquisition, snapshot, and exit cleanup capture bigint device/inode values; unsafe numeric identities from older in-process receipts cannot authorize cleanup. If Windows reports a zero device or inode for either identity observation, that check is inconclusive and removal is skipped.
|
|
65
65
|
|
|
66
66
|
Last-chance raw-lock exit cleanup requires known Windows device and inode values from both pathname observations and the opened descriptor. A zero value leaves the sidecar in place, including for token-owned locks. Known descriptor/path identity differences remain supported; a pathname identity change during the read still prevents cleanup.
|
|
67
67
|
|
|
68
68
|
The raw sidecar bytes are not a canonical JSON representation: tools that trim or rewrite the trailing whitespace invalidate the ownership token, so release leaves the changed sidecar in place and fails closed. The token distinguishes cooperating acquisitions; it is not a secret and does not make pathname compare-and-remove atomic against a hostile process that can replace files outside the lock protocol.
|
|
69
69
|
|
|
70
|
-
`release()` propagates an I/O failure that prevents deletion of an unchanged, owned sidecar; it never reports successful cleanup while leaving that lock behind.
|
|
70
|
+
`release()` propagates an I/O failure that prevents deletion of an unchanged, owned sidecar; it never reports successful cleanup while leaving that lock behind. Raw and JavaScript fallback handles and managers retain the cleanup receipt after an I/O failure, so the same handle can retry `release()` and `manager.drain()` can retry cleanup. Retained native Root locks settle once and close their retained handles on every outcome, including failure; a failed deletion requires application-owned recovery. A changed sidecar is left untouched. Retained native Root release reports `FsSafeError` code `path-mismatch`, distinguishing a changed identity from a changed owner record; this includes a replacement inode containing an exact copy of the original bytes. If both a `withFileLock()` or `withFileLockSync()` callback and release fail, the release error is the primary `SuppressedError.error` and the callback failure remains available as `SuppressedError.suppressed`. Failed asynchronous acquisition cleanup uses the same shape, with the cleanup error primary and the acquisition failure suppressed. On Node runtimes without the global `SuppressedError` constructor, fs-safe returns the equivalent `Error` shape with the same name and properties.
|
|
71
71
|
|
|
72
72
|
## API
|
|
73
73
|
|
|
@@ -265,10 +265,48 @@ its separate canonical-path mutation queue. The synchronous APIs implement the
|
|
|
265
265
|
same owner/refcount rules; a mismatched synchronous acquisition blocks the
|
|
266
266
|
calling thread according to its retry and timeout options.
|
|
267
267
|
|
|
268
|
-
Pass `lockRoot` to
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
268
|
+
Pass `lockRoot` to admit sidecar creation, reads, and verification through an
|
|
269
|
+
existing `Root` capability. `lockPath` must resolve inside that root.
|
|
270
|
+
|
|
271
|
+
With the maintained native helper, asynchronous acquisition retains cleanup
|
|
272
|
+
authority over only the sidecar it created: an identity-checked parent descriptor
|
|
273
|
+
and file descriptor on POSIX, or a shared-delete parent directory handle on Windows
|
|
274
|
+
(local NTFS). `release()` and process-exit cleanup verify the original named
|
|
275
|
+
entry's exact identity, regular-file type, single link, and complete owner record
|
|
276
|
+
through those retained handles. They then use guarded descriptor-relative removal
|
|
277
|
+
or Windows disposition-delete, without resolving the Root again. Cleanup therefore
|
|
278
|
+
works after the Root moves, is replaced, or its parent becomes a symlink. A foreign
|
|
279
|
+
entry at either the original Root path or the retained sidecar name is preserved.
|
|
280
|
+
The retained handles close on success, mismatch, failed acquisition, and exit
|
|
281
|
+
(including `retainOnExit`, which closes handles without deleting).
|
|
282
|
+
If retention itself fails after creation, rollback checks the original identity
|
|
283
|
+
and owner bytes through the creator's still-open descriptors before removing the
|
|
284
|
+
claim. A changed entry is preserved and both the retention and cleanup errors are
|
|
285
|
+
reported; cleanup never falls back to token-only deletion of a replacement.
|
|
286
|
+
Retention duplicates the already-admitted parent authority instead of walking
|
|
287
|
+
its pathname again or requiring extra access to its ancestors.
|
|
288
|
+
Windows opens the named file through the retained parent only for settlement,
|
|
289
|
+
with delete access and sharing; keeping a file handle open would prevent ordinary
|
|
290
|
+
directory relocation. Retained cleanup compares the owner bytes directly and does
|
|
291
|
+
not invoke `parsePayload`; verification and stale-policy reads still do.
|
|
292
|
+
Windows permits renaming that retained parent itself, but refuses renaming an
|
|
293
|
+
ancestor containing the open directory handle, even with delete sharing and no
|
|
294
|
+
data access. Such a refused move leaves the Root and lock in place; release or
|
|
295
|
+
exit cleanup closes the pin so the ancestor can then move. POSIX also supports
|
|
296
|
+
cleanup after a successful ancestor move or symlink swap.
|
|
297
|
+
|
|
298
|
+
`verifyStillHeld()` retains its Root-based semantics and can still reject a moved
|
|
299
|
+
Root even though `release()` can remove its own sidecar. This creation-specific
|
|
300
|
+
cleanup receipt does not authorize other Root operations or stale reclamation;
|
|
301
|
+
those continue to enforce current confinement and mutation policy. As with the
|
|
302
|
+
sidecar protocol generally, the POSIX final identity check and unlink are not an
|
|
303
|
+
atomic compare-and-delete against an uncooperative process racing the same name.
|
|
304
|
+
|
|
305
|
+
When native retention is unavailable or explicitly disabled, release keeps the
|
|
306
|
+
existing Root-based fail-closed behavior; a moved Root can leave a sidecar requiring
|
|
307
|
+
application-owned recovery. Use the matching maintained native package to obtain
|
|
308
|
+
relocation-safe cleanup. Synchronous Root locks keep their existing Root authority.
|
|
309
|
+
Identity-conditioned removal remains the only release and reclaim deletion path.
|
|
272
310
|
Root mutation refusals during stale removal propagate unchanged, including
|
|
273
311
|
`null`, `undefined`, and errors carrying `ENOENT`; they do not become missing-sidecar retries.
|
|
274
312
|
|
|
@@ -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.
|
|
@@ -35,6 +35,18 @@ mode, correction uses a verified directory descriptor.
|
|
|
35
35
|
An initial mode-descriptor admission error is preserved if closing that rejected
|
|
36
36
|
descriptor also fails; close failures after successful admission remain reportable.
|
|
37
37
|
|
|
38
|
+
These rules apply equally to effective UID 0: root-owned `0700` or `0755` temp
|
|
39
|
+
roots beneath root-owned sticky `1777` `/tmp` are accepted. Passing `/tmp` itself
|
|
40
|
+
as `rootDir` is rejected because the workspace root must be private. Permission
|
|
41
|
+
and ownership errors name the rejected directory (including an unsafe ancestor),
|
|
42
|
+
the observed mode or owner UID, the expected property, and a recovery path.
|
|
43
|
+
Prefer `resolveSecureTempRoot()` or a dedicated directory owned by the effective
|
|
44
|
+
user with mode `0700`. For a non-sticky writable ancestor, have its administrator
|
|
45
|
+
restore appropriate permissions (`1777` for shared system temp) or move the root
|
|
46
|
+
under trusted ancestry. Do not make shared `/tmp` private or take ownership of
|
|
47
|
+
another user's directory. A caller's preferred root takes precedence over
|
|
48
|
+
`TMPDIR`; changing `TMPDIR` only changes the resolver's fallback location.
|
|
49
|
+
|
|
38
50
|
On Linux, non-identity `/proc/self/uid_map` and `/proc/self/gid_map` evidence
|
|
39
51
|
permits ancestors whose UID and GID equal unmapped kernel overflow IDs,
|
|
40
52
|
provided the same ancestor mode checks pass.
|
|
@@ -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
|
|
@@ -150,8 +152,9 @@ FSEvents drops, including during recursive deletion. A shallow tree anchored abo
|
|
|
150
152
|
a busy unselected subtree still receives recursive traffic; pending hints fold
|
|
151
153
|
under pressure, while genuine FSEvents drops can still overflow.
|
|
152
154
|
Absolute hints are reduced lexically against the admitted canonical Root;
|
|
153
|
-
outside paths never become detail. Dropped/wrapped streams, RootChanged
|
|
154
|
-
|
|
155
|
+
outside paths never become detail. Dropped/wrapped streams, RootChanged, Unmount
|
|
156
|
+
and notifications naming the Root itself trigger guarded reconciliation without detail.
|
|
157
|
+
Pathname hints can reflect activity
|
|
155
158
|
after a swap, but the Root is never replaced and names require guarded admission.
|
|
156
159
|
Removal synchronously stops, invalidates and releases the stream on its queue.
|
|
157
160
|
|
|
@@ -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.25.0",
|
|
4
4
|
"description": "Capability-style filesystem roots for Node.js apps that handle untrusted relative paths.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"filesystem",
|
|
@@ -173,32 +173,32 @@
|
|
|
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.25.0",
|
|
177
|
+
"@openclaw/fs-safe-darwin-x64": "0.25.0",
|
|
178
|
+
"@openclaw/fs-safe-linux-arm64-gnu": "0.25.0",
|
|
179
|
+
"@openclaw/fs-safe-linux-arm64-musl": "0.25.0",
|
|
180
|
+
"@openclaw/fs-safe-linux-x64-gnu": "0.25.0",
|
|
181
|
+
"@openclaw/fs-safe-linux-x64-musl": "0.25.0",
|
|
182
|
+
"@openclaw/fs-safe-win32-x64-msvc": "0.25.0",
|
|
183
183
|
"jszip": "^3.10.2"
|
|
184
184
|
},
|
|
185
185
|
"devDependencies": {
|
|
186
|
-
"@emnapi/runtime": "2.0.0-alpha.
|
|
187
|
-
"@napi-rs/cli": "3.10.
|
|
188
|
-
"@types/node": "^26.6.
|
|
189
|
-
"@vitest/coverage-v8": "5.0.
|
|
190
|
-
"fast-check": "^4.10.
|
|
186
|
+
"@emnapi/runtime": "2.0.0-alpha.6",
|
|
187
|
+
"@napi-rs/cli": "3.10.8",
|
|
188
|
+
"@types/node": "^26.6.4",
|
|
189
|
+
"@vitest/coverage-v8": "5.0.3",
|
|
190
|
+
"fast-check": "^4.10.2",
|
|
191
191
|
"istanbul-lib-coverage": "3.2.2",
|
|
192
192
|
"istanbul-lib-report": "3.0.1",
|
|
193
193
|
"istanbul-reports": "3.2.0",
|
|
194
194
|
"sigstore": "5.0.0",
|
|
195
195
|
"tar": "7.5.22",
|
|
196
196
|
"typescript": "^7.0.2",
|
|
197
|
-
"vite": "8.3.
|
|
198
|
-
"vitest": "^5.0.
|
|
197
|
+
"vite": "8.3.3",
|
|
198
|
+
"vitest": "^5.0.3"
|
|
199
199
|
},
|
|
200
200
|
"engines": {
|
|
201
201
|
"node": ">=22"
|
|
202
202
|
},
|
|
203
|
-
"packageManager": "pnpm@12.
|
|
203
|
+
"packageManager": "pnpm@12.10.1"
|
|
204
204
|
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|