@openclaw/openshell-sandbox 2026.10.5-beta.4 → 2026.10.10-beta.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/README.md +1 -1
- package/node_modules/@openclaw/fs-safe/CHANGELOG.md +148 -0
- package/node_modules/@openclaw/fs-safe/README.md +20 -4
- package/node_modules/@openclaw/fs-safe/dist/advanced.d.ts +2 -1
- package/node_modules/@openclaw/fs-safe/dist/advanced.js +1 -0
- package/node_modules/@openclaw/fs-safe/dist/archive-merge.js +28 -3
- package/node_modules/@openclaw/fs-safe/dist/archive-parser.wasm +0 -0
- package/node_modules/@openclaw/fs-safe/dist/archive-read.js +11 -5
- package/node_modules/@openclaw/fs-safe/dist/archive-zip-loader.js +4 -2
- package/node_modules/@openclaw/fs-safe/dist/archive.js +2 -2
- package/node_modules/@openclaw/fs-safe/dist/atomic-io.js +2 -2
- package/node_modules/@openclaw/fs-safe/dist/bounded-read.d.ts +1 -0
- package/node_modules/@openclaw/fs-safe/dist/bounded-read.js +9 -2
- package/node_modules/@openclaw/fs-safe/dist/clone-metadata.d.ts +2 -0
- package/node_modules/@openclaw/fs-safe/dist/clone-metadata.js +2 -1
- package/node_modules/@openclaw/fs-safe/dist/config.d.ts +6 -0
- package/node_modules/@openclaw/fs-safe/dist/config.js +5 -0
- package/node_modules/@openclaw/fs-safe/dist/copy-file-input.d.ts +3 -1
- package/node_modules/@openclaw/fs-safe/dist/copy-file-input.js +8 -7
- package/node_modules/@openclaw/fs-safe/dist/copy-link.d.ts +26 -0
- package/node_modules/@openclaw/fs-safe/dist/copy-link.js +170 -0
- package/node_modules/@openclaw/fs-safe/dist/copy-metadata.d.ts +9 -0
- package/node_modules/@openclaw/fs-safe/dist/copy-metadata.js +39 -0
- package/node_modules/@openclaw/fs-safe/dist/copy-path-admission.js +11 -2
- package/node_modules/@openclaw/fs-safe/dist/copy-root-file-sync.d.ts +2 -4
- package/node_modules/@openclaw/fs-safe/dist/copy-root-file-sync.js +4 -2
- package/node_modules/@openclaw/fs-safe/dist/copy-tree-portable.js +3 -6
- package/node_modules/@openclaw/fs-safe/dist/copy.js +3 -3
- package/node_modules/@openclaw/fs-safe/dist/create.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/creation-boundary.js +7 -4
- package/node_modules/@openclaw/fs-safe/dist/darwin-acl.d.ts +12 -0
- package/node_modules/@openclaw/fs-safe/dist/darwin-acl.js +29 -0
- package/node_modules/@openclaw/fs-safe/dist/directory-durability.d.ts +7 -4
- package/node_modules/@openclaw/fs-safe/dist/directory-guard.js +20 -5
- package/node_modules/@openclaw/fs-safe/dist/directory-mode-node.d.ts +3 -0
- package/node_modules/@openclaw/fs-safe/dist/directory-mode-node.js +27 -2
- package/node_modules/@openclaw/fs-safe/dist/directory-receipt.d.ts +7 -7
- package/node_modules/@openclaw/fs-safe/dist/directory-receipt.js +9 -4
- package/node_modules/@openclaw/fs-safe/dist/durability.d.ts +2 -1
- package/node_modules/@openclaw/fs-safe/dist/entry-publication.js +2 -2
- package/node_modules/@openclaw/fs-safe/dist/exact-identity.d.ts +17 -0
- package/node_modules/@openclaw/fs-safe/dist/exact-identity.js +29 -0
- package/node_modules/@openclaw/fs-safe/dist/file-handle-transfer.js +4 -2
- package/node_modules/@openclaw/fs-safe/dist/file-hash.d.ts +1 -1
- package/node_modules/@openclaw/fs-safe/dist/file-hash.js +11 -9
- package/node_modules/@openclaw/fs-safe/dist/file-identity.d.ts +7 -1
- package/node_modules/@openclaw/fs-safe/dist/file-identity.js +27 -5
- package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-admission.js +1 -2
- package/node_modules/@openclaw/fs-safe/dist/file-lock-sync.js +8 -1
- package/node_modules/@openclaw/fs-safe/dist/file-lock.d.ts +1 -0
- package/node_modules/@openclaw/fs-safe/dist/file-lock.js +1 -0
- package/node_modules/@openclaw/fs-safe/dist/file-write-lease.d.ts +9 -0
- package/node_modules/@openclaw/fs-safe/dist/file-write-lease.js +42 -0
- package/node_modules/@openclaw/fs-safe/dist/guarded-mkdir.d.ts +1 -0
- package/node_modules/@openclaw/fs-safe/dist/guarded-mkdir.js +22 -15
- package/node_modules/@openclaw/fs-safe/dist/guest-native-python.js +3 -0
- package/node_modules/@openclaw/fs-safe/dist/hardlink-fallback.d.ts +1 -0
- package/node_modules/@openclaw/fs-safe/dist/hardlink-fallback.js +5 -0
- package/node_modules/@openclaw/fs-safe/dist/index.d.ts +1 -0
- package/node_modules/@openclaw/fs-safe/dist/index.js +1 -0
- package/node_modules/@openclaw/fs-safe/dist/json-durable-queue-ownership.js +16 -3
- package/node_modules/@openclaw/fs-safe/dist/local-file-descriptor.js +24 -13
- package/node_modules/@openclaw/fs-safe/dist/native-binding.d.ts +85 -18
- package/node_modules/@openclaw/fs-safe/dist/native-config.d.ts +5 -0
- package/node_modules/@openclaw/fs-safe/dist/native-config.js +11 -0
- package/node_modules/@openclaw/fs-safe/dist/native-directory-observation.js +6 -4
- package/node_modules/@openclaw/fs-safe/dist/native-fallback-warning.d.ts +2 -1
- package/node_modules/@openclaw/fs-safe/dist/native-fallback-warning.js +26 -2
- package/node_modules/@openclaw/fs-safe/dist/native-guidance.d.ts +3 -0
- package/node_modules/@openclaw/fs-safe/dist/native-guidance.js +21 -0
- package/node_modules/@openclaw/fs-safe/dist/native-move-fallback.d.ts +1 -1
- package/node_modules/@openclaw/fs-safe/dist/native-move-fallback.js +2 -0
- package/node_modules/@openclaw/fs-safe/dist/native-operations.js +8 -6
- package/node_modules/@openclaw/fs-safe/dist/native-parent-admission.d.ts +44 -4
- package/node_modules/@openclaw/fs-safe/dist/native-parent-admission.js +85 -15
- package/node_modules/@openclaw/fs-safe/dist/native-pinned-write-windows.d.ts +3 -3
- package/node_modules/@openclaw/fs-safe/dist/native-pinned-write-windows.js +33 -6
- package/node_modules/@openclaw/fs-safe/dist/native-pinned-write.d.ts +4 -1
- package/node_modules/@openclaw/fs-safe/dist/native-pinned-write.js +22 -3
- package/node_modules/@openclaw/fs-safe/dist/native-policy-directory-observation.js +4 -3
- package/node_modules/@openclaw/fs-safe/dist/native-policy-parent.d.ts +1 -1
- package/node_modules/@openclaw/fs-safe/dist/native-policy-parent.js +3 -0
- package/node_modules/@openclaw/fs-safe/dist/native-staged-file-metadata.d.ts +3 -0
- package/node_modules/@openclaw/fs-safe/dist/native-staged-file-metadata.js +16 -0
- 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 +15 -5
- package/node_modules/@openclaw/fs-safe/dist/native-stat-identity.d.ts +3 -0
- package/node_modules/@openclaw/fs-safe/dist/native-stat-identity.js +7 -0
- package/node_modules/@openclaw/fs-safe/dist/native-status.d.ts +17 -0
- package/node_modules/@openclaw/fs-safe/dist/native-status.js +30 -0
- package/node_modules/@openclaw/fs-safe/dist/native.d.ts +31 -4
- package/node_modules/@openclaw/fs-safe/dist/native.js +107 -9
- package/node_modules/@openclaw/fs-safe/dist/opened-realpath.js +9 -1
- package/node_modules/@openclaw/fs-safe/dist/overwrite-file-handle.js +4 -1
- package/node_modules/@openclaw/fs-safe/dist/owner-dacl-batch.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/owner-dacl.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/permission-exec.js +9 -1
- package/node_modules/@openclaw/fs-safe/dist/permissions-public.d.ts +1 -0
- package/node_modules/@openclaw/fs-safe/dist/permissions-public.js +1 -0
- package/node_modules/@openclaw/fs-safe/dist/permissions-windows.js +83 -47
- package/node_modules/@openclaw/fs-safe/dist/pinned-open.d.ts +2 -0
- package/node_modules/@openclaw/fs-safe/dist/pinned-open.js +8 -3
- package/node_modules/@openclaw/fs-safe/dist/pinned-write-input.js +13 -0
- package/node_modules/@openclaw/fs-safe/dist/pinned-write-staged.js +10 -8
- package/node_modules/@openclaw/fs-safe/dist/pinned-write-types.d.ts +2 -1
- package/node_modules/@openclaw/fs-safe/dist/pinned-write.js +7 -5
- package/node_modules/@openclaw/fs-safe/dist/pipe.d.ts +9 -0
- package/node_modules/@openclaw/fs-safe/dist/pipe.js +26 -0
- package/node_modules/@openclaw/fs-safe/dist/private-directory.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/private-producer-handoff.js +2 -5
- package/node_modules/@openclaw/fs-safe/dist/private-temp-workspace.d.ts +3 -2
- package/node_modules/@openclaw/fs-safe/dist/private-temp-workspace.js +38 -10
- package/node_modules/@openclaw/fs-safe/dist/publish-copy-stage.js +92 -10
- package/node_modules/@openclaw/fs-safe/dist/publish-file-failure.d.ts +3 -0
- package/node_modules/@openclaw/fs-safe/dist/publish-file-failure.js +3 -0
- package/node_modules/@openclaw/fs-safe/dist/publish-file.d.ts +5 -3
- package/node_modules/@openclaw/fs-safe/dist/publish-file.js +26 -20
- package/node_modules/@openclaw/fs-safe/dist/read-opened-file.d.ts +2 -0
- package/node_modules/@openclaw/fs-safe/dist/read-opened-file.js +3 -4
- package/node_modules/@openclaw/fs-safe/dist/realpath.js +32 -0
- package/node_modules/@openclaw/fs-safe/dist/regular-file.d.ts +4 -0
- package/node_modules/@openclaw/fs-safe/dist/regular-file.js +11 -15
- package/node_modules/@openclaw/fs-safe/dist/replace-file-descriptor.js +3 -2
- package/node_modules/@openclaw/fs-safe/dist/root-copy.d.ts +18 -0
- package/node_modules/@openclaw/fs-safe/dist/root-copy.js +128 -0
- package/node_modules/@openclaw/fs-safe/dist/root-create-native.d.ts +8 -13
- package/node_modules/@openclaw/fs-safe/dist/root-create-native.js +252 -27
- package/node_modules/@openclaw/fs-safe/dist/root-directory-creation.js +5 -1
- package/node_modules/@openclaw/fs-safe/dist/root-directory-list-types.d.ts +11 -8
- package/node_modules/@openclaw/fs-safe/dist/root-directory-list.js +35 -13
- package/node_modules/@openclaw/fs-safe/dist/root-file-open-batch-sync.d.ts +8 -0
- package/node_modules/@openclaw/fs-safe/dist/root-file-open-batch-sync.js +108 -0
- package/node_modules/@openclaw/fs-safe/dist/root-file.d.ts +7 -0
- package/node_modules/@openclaw/fs-safe/dist/root-file.js +15 -0
- package/node_modules/@openclaw/fs-safe/dist/root-impl.d.ts +2 -0
- package/node_modules/@openclaw/fs-safe/dist/root-impl.js +154 -175
- package/node_modules/@openclaw/fs-safe/dist/root-mkdir-native.d.ts +2 -0
- package/node_modules/@openclaw/fs-safe/dist/root-mkdir-native.js +239 -0
- package/node_modules/@openclaw/fs-safe/dist/root-move-noreplace.js +37 -15
- package/node_modules/@openclaw/fs-safe/dist/root-native-creation-path.d.ts +32 -0
- package/node_modules/@openclaw/fs-safe/dist/root-native-creation-path.js +56 -0
- package/node_modules/@openclaw/fs-safe/dist/root-options.d.ts +2 -0
- package/node_modules/@openclaw/fs-safe/dist/root-public-types.d.ts +1 -0
- package/node_modules/@openclaw/fs-safe/dist/root-remove-native.js +95 -16
- package/node_modules/@openclaw/fs-safe/dist/root-remove.js +6 -4
- package/node_modules/@openclaw/fs-safe/dist/root-walk.js +23 -19
- package/node_modules/@openclaw/fs-safe/dist/root-write-admission.js +8 -6
- package/node_modules/@openclaw/fs-safe/dist/root-write-publication.js +5 -1
- package/node_modules/@openclaw/fs-safe/dist/secure-file-windows.js +3 -1
- package/node_modules/@openclaw/fs-safe/dist/secure-file.d.ts +2 -0
- package/node_modules/@openclaw/fs-safe/dist/secure-file.js +10 -4
- package/node_modules/@openclaw/fs-safe/dist/sibling-staged-file.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-reclaim.js +2 -2
- package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-retained.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-target.js +8 -1
- package/node_modules/@openclaw/fs-safe/dist/staged-directory.d.ts +13 -0
- package/node_modules/@openclaw/fs-safe/dist/staged-directory.js +29 -0
- package/node_modules/@openclaw/fs-safe/dist/temp-workspace-child-admission.js +5 -3
- package/node_modules/@openclaw/fs-safe/dist/temp-workspace-descriptor.d.ts +8 -0
- package/node_modules/@openclaw/fs-safe/dist/temp-workspace-descriptor.js +13 -2
- package/node_modules/@openclaw/fs-safe/dist/temp-workspace-owner.js +71 -3
- package/node_modules/@openclaw/fs-safe/dist/temp-workspace-ownership.d.ts +2 -1
- package/node_modules/@openclaw/fs-safe/dist/temp-workspace-ownership.js +22 -0
- package/node_modules/@openclaw/fs-safe/dist/temp-workspace-types.d.ts +3 -0
- package/node_modules/@openclaw/fs-safe/dist/test-hooks.d.ts +1 -0
- package/node_modules/@openclaw/fs-safe/dist/test-hooks.js +1 -0
- package/node_modules/@openclaw/fs-safe/dist/trash.js +2 -2
- package/node_modules/@openclaw/fs-safe/dist/types.d.ts +4 -0
- package/node_modules/@openclaw/fs-safe/dist/watch-native.js +2 -2
- package/node_modules/@openclaw/fs-safe/dist/windows-appcontainer.d.ts +5 -0
- package/node_modules/@openclaw/fs-safe/dist/windows-appcontainer.js +39 -0
- package/node_modules/@openclaw/fs-safe/dist/windows-owner.d.ts +1 -0
- package/node_modules/@openclaw/fs-safe/dist/windows-owner.js +25 -36
- package/node_modules/@openclaw/fs-safe/dist/windows-security-command.js +24 -5
- package/node_modules/@openclaw/fs-safe/dist/windows-security-facts.d.ts +2 -1
- package/node_modules/@openclaw/fs-safe/dist/windows-security-facts.js +4 -0
- package/node_modules/@openclaw/fs-safe/dist/windows-security-helper.d.ts +2 -0
- package/node_modules/@openclaw/fs-safe/dist/windows-security-helper.exe +0 -0
- package/node_modules/@openclaw/fs-safe/dist/windows-security-helper.js +7 -0
- package/node_modules/@openclaw/fs-safe/dist/windows-test-hooks.d.ts +16 -0
- package/node_modules/@openclaw/fs-safe/dist/windows-test-hooks.js +51 -0
- package/node_modules/@openclaw/fs-safe/dist/write-file-handle.d.ts +1 -0
- package/node_modules/@openclaw/fs-safe/dist/write-file-handle.js +12 -10
- package/node_modules/@openclaw/fs-safe/docs/advanced.md +2 -1
- package/node_modules/@openclaw/fs-safe/docs/archive.md +16 -0
- package/node_modules/@openclaw/fs-safe/docs/config.md +71 -1
- package/node_modules/@openclaw/fs-safe/docs/contributing.md +45 -5
- package/node_modules/@openclaw/fs-safe/docs/copy.md +45 -5
- package/node_modules/@openclaw/fs-safe/docs/durability.md +34 -7
- package/node_modules/@openclaw/fs-safe/docs/errors.md +8 -0
- package/node_modules/@openclaw/fs-safe/docs/guest.md +2 -0
- package/node_modules/@openclaw/fs-safe/docs/install.md +47 -27
- package/node_modules/@openclaw/fs-safe/docs/migrating-to-0.5.md +6 -6
- package/node_modules/@openclaw/fs-safe/docs/migrating-to-0.6.md +1 -1
- package/node_modules/@openclaw/fs-safe/docs/native-helper.md +79 -6
- package/node_modules/@openclaw/fs-safe/docs/native.md +279 -8
- package/node_modules/@openclaw/fs-safe/docs/permissions.md +64 -15
- package/node_modules/@openclaw/fs-safe/docs/pipe.md +81 -0
- package/node_modules/@openclaw/fs-safe/docs/platform-capabilities.md +63 -0
- package/node_modules/@openclaw/fs-safe/docs/public-api.md +10 -0
- package/node_modules/@openclaw/fs-safe/docs/queue-link-free-design.md +237 -0
- package/node_modules/@openclaw/fs-safe/docs/quickstart.md +23 -3
- package/node_modules/@openclaw/fs-safe/docs/reading.md +56 -2
- package/node_modules/@openclaw/fs-safe/docs/regular-file.md +2 -0
- package/node_modules/@openclaw/fs-safe/docs/secret-file.md +5 -5
- package/node_modules/@openclaw/fs-safe/docs/secure-file.md +1 -1
- package/node_modules/@openclaw/fs-safe/docs/security-model.md +35 -0
- package/node_modules/@openclaw/fs-safe/docs/sidecar-lock.md +31 -13
- package/node_modules/@openclaw/fs-safe/docs/staged-file.md +18 -0
- package/node_modules/@openclaw/fs-safe/docs/store.md +15 -0
- package/node_modules/@openclaw/fs-safe/docs/temp.md +39 -0
- package/node_modules/@openclaw/fs-safe/docs/testing.md +13 -0
- package/node_modules/@openclaw/fs-safe/docs/types.md +51 -81
- package/node_modules/@openclaw/fs-safe/docs/walk.md +4 -1
- package/node_modules/@openclaw/fs-safe/docs/watch.md +3 -2
- package/node_modules/@openclaw/fs-safe/docs/writing.md +121 -14
- package/node_modules/@openclaw/fs-safe/package.json +21 -12
- 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-freebsd-arm64/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-freebsd-arm64/package.json +29 -0
- package/node_modules/@openclaw/fs-safe-freebsd-x64/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-freebsd-x64/package.json +29 -0
- 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-arm64-msvc/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-win32-arm64-msvc/package.json +29 -0
- 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/windows-security-bridge.cs +0 -336
- package/node_modules/@openclaw/fs-safe/dist/windows-security-bridge.ps1 +0 -90
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Platform filesystem capabilities
|
|
2
|
+
|
|
3
|
+
These synchronous capabilities load the native helper only when called. All
|
|
4
|
+
three entry points import on every supported platform.
|
|
5
|
+
|
|
6
|
+
## Linux write leases
|
|
7
|
+
|
|
8
|
+
`tryAcquireWriteLease(fd)` from `@openclaw/fs-safe/file-lock` returns a frozen
|
|
9
|
+
`FileWriteLease`, or `null` when another open descriptor prevents acquisition
|
|
10
|
+
(`EAGAIN` or `EBUSY`). Other failures throw `FsSafeError`; unsupported systems
|
|
11
|
+
throw `unsupported-platform` and unavailable helpers throw `helper-unavailable`.
|
|
12
|
+
|
|
13
|
+
The caller owns the descriptor, must keep it open until release, and must install
|
|
14
|
+
its own process-wide `SIGIO` handler **before** acquiring a lease. Prefer a
|
|
15
|
+
disposable child process when an application cannot own that signal. The API
|
|
16
|
+
does not install a signal handler. A competing open starts the kernel's lease
|
|
17
|
+
break; `isHeld()` then returns false, including during a pending downgrade.
|
|
18
|
+
Release promptly so the competing opener can proceed.
|
|
19
|
+
|
|
20
|
+
`lease.fd` is the borrowed descriptor. `lease.release()` and
|
|
21
|
+
`lease[Symbol.dispose]()` settle once without closing it; `isHeld()` returns
|
|
22
|
+
false after release. Never close or reuse the fd before settling the lease.
|
|
23
|
+
Lease acquisition does not replace the caller's path identity and age checks.
|
|
24
|
+
|
|
25
|
+
## Darwin ACL inspection
|
|
26
|
+
|
|
27
|
+
`inspectDarwinAcl(path)` from `@openclaw/fs-safe/permissions` opens the final
|
|
28
|
+
component without following symlinks, then inspects a descriptor-bound security
|
|
29
|
+
snapshot. Its `DarwinAclInspection` result is one of:
|
|
30
|
+
|
|
31
|
+
- `{ kind: "none" }` for a proven absent or empty extended ACL.
|
|
32
|
+
- `{ kind: "present", inheritsToFiles, inheritsToDirectories }` for an ACL with
|
|
33
|
+
entries, including when both inheritance flags are false.
|
|
34
|
+
- `{ kind: "unknown", reason }` when opening or inspection fails, the helper
|
|
35
|
+
is unavailable, or the native facts are incomplete. Other platforms return
|
|
36
|
+
`reason: "unsupported-platform"`.
|
|
37
|
+
|
|
38
|
+
Unknown is never evidence of absence. The two inheritance booleans report flag
|
|
39
|
+
presence, not effective permissions or volume ownership enforcement. Intermediate
|
|
40
|
+
path components follow ordinary path resolution; this is inspection, not a Root
|
|
41
|
+
confinement capability.
|
|
42
|
+
|
|
43
|
+
## Windows test fixtures
|
|
44
|
+
|
|
45
|
+
The following exports from `@openclaw/fs-safe/test-hooks` are for tests, not
|
|
46
|
+
production policy. They require the Windows native helper and reject final
|
|
47
|
+
reparse points. Other platforms throw `unsupported-platform`.
|
|
48
|
+
|
|
49
|
+
- `holdWindowsSharingLock(path)` returns a frozen disposable with idempotent
|
|
50
|
+
`close()`. Its native owner holds a non-inheritable handle with read/write
|
|
51
|
+
sharing, omitting delete sharing, until closed or collected.
|
|
52
|
+
- `setWindowsFileAttributes(path, { readOnly?, hidden?, system? })` updates the
|
|
53
|
+
supplied bits through the opened handle and preserves unspecified attributes.
|
|
54
|
+
- `readWindowsFileExtents(path)` returns `{ vcn, lcn, clusters }[]`, all bigint.
|
|
55
|
+
Empty or resident files may have no extents; `lcn: -1n` denotes sparse clusters.
|
|
56
|
+
Enumeration validates each response and fails on incomplete or invalid data.
|
|
57
|
+
It is an observation, not a snapshot against concurrent file modification.
|
|
58
|
+
|
|
59
|
+
Use Node's `fs.statfsSync(path).bsize` for Windows cluster size. Windows CI
|
|
60
|
+
compares it directly against `GetDiskFreeSpaceW`'s sectors-per-cluster times
|
|
61
|
+
bytes-per-sector on NTFS. CI does not provide ReFS; this does not establish ReFS
|
|
62
|
+
clone sharing. The extent reader reports mappings independently of the clone
|
|
63
|
+
implementation.
|
|
@@ -187,3 +187,13 @@ budgets, `persistent`, and lifecycle.
|
|
|
187
187
|
Every runtime and type name in `test/public-api.json` must appear somewhere in
|
|
188
188
|
`README.md` or `docs/`. Documentation examples are also checked so a named
|
|
189
189
|
import cannot silently move to another package subpath.
|
|
190
|
+
|
|
191
|
+
## Exact receipt types
|
|
192
|
+
|
|
193
|
+
`ExactFileIdentity` is the immutable bigint `dev`/`ino` pair returned alongside
|
|
194
|
+
legacy numeric metadata. `ExactIdentityReceipt` describes an object with that
|
|
195
|
+
`exactIdentity` companion. `FileIdentityInput` accepts either a traditional
|
|
196
|
+
`FileIdentityStat` pair or an explicit exact-companion receipt.
|
|
197
|
+
`ExactDirectoryReceipt` guarantees the exact companion on a returned directory
|
|
198
|
+
receipt while the caller-constructible `DirectoryReceipt` remains compatible.
|
|
199
|
+
See [the identity round-trip guidance](security-model.md#exact-receipts-and-legacy-numeric-metadata).
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Link-free queue design
|
|
3
|
+
description: "Unimplemented durable JSON queue v2 proposal, crash recovery, and barriers to mixed-version Android support."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Link-free durable JSON queue design
|
|
7
|
+
|
|
8
|
+
**Status: design only; not implemented.** The current queue still requires
|
|
9
|
+
hardlinks for claim, quarantine, and retirement recovery. Android/Termux systems
|
|
10
|
+
that refuse those links receive `helper-unavailable`. This document does not
|
|
11
|
+
announce Android support or change any existing API, error code, or disk format.
|
|
12
|
+
|
|
13
|
+
The candidate below moves ownership into one atomic state file in a separate,
|
|
14
|
+
versioned store. It explains how link-free transitions could work for cooperating
|
|
15
|
+
v2 clients. It does **not** solve transparent migration with concurrent legacy
|
|
16
|
+
writers, arbitrary processing paths, or existing failed-file destinations.
|
|
17
|
+
Those gaps prevent implementation as a replacement or fallback for v1.
|
|
18
|
+
|
|
19
|
+
## Existing guarantees
|
|
20
|
+
|
|
21
|
+
The [durable queue contract](store.md#durable-json-queues) uses plain payloads at
|
|
22
|
+
`<id>.json`, `<id>.processing`, and `<id>.delivered`. A transfer lock serializes
|
|
23
|
+
consumer transitions for the JSON path; producers atomically replace `.json`
|
|
24
|
+
without taking that lock. The lock is not a worker lease. Another load can return
|
|
25
|
+
the existing processing generation, so delivery is at least once.
|
|
26
|
+
|
|
27
|
+
Claim creates a no-replace hardlink from the pending name to the processing
|
|
28
|
+
name. The generation at that syscall is the claimed generation. Source
|
|
29
|
+
retirement moves the then-current pending file into a journal and compares exact
|
|
30
|
+
device/inode identity with the claim. A different generation is restored
|
|
31
|
+
create-only if no newer pending generation exists. Thus acknowledging an older
|
|
32
|
+
claim cannot remove a newer enqueue. Replacing a still-pending generation by a
|
|
33
|
+
later enqueue for the same ID is already permitted; this is not a FIFO of all
|
|
34
|
+
writes to that ID.
|
|
35
|
+
|
|
36
|
+
Quarantine publishes create-only to the failed destination. A shared inode
|
|
37
|
+
distinguishes an interrupted publication from an unrelated destination collision.
|
|
38
|
+
Acknowledgement moves only the claim through the delivered marker. Migration
|
|
39
|
+
retains the opened claim and rejects a stale generation after its callback.
|
|
40
|
+
Completed transitions synchronize affected directories; ambiguous ownership,
|
|
41
|
+
identity, and stale transfer locks fail closed. Existing same-privilege race
|
|
42
|
+
limitations in the [security model](security-model.md) still apply.
|
|
43
|
+
|
|
44
|
+
These are ownership guarantees, not consequences that can be recovered from
|
|
45
|
+
equal JSON bytes. Two independent enqueues may contain identical JSON. A digest,
|
|
46
|
+
timestamp, or rounded numeric inode cannot establish that they are one generation.
|
|
47
|
+
|
|
48
|
+
## Why an in-place fallback is insufficient
|
|
49
|
+
|
|
50
|
+
| Candidate | Lost guarantee |
|
|
51
|
+
|---|---|
|
|
52
|
+
| Ordinary rename after testing for absence | A competing processing or failed destination can be overwritten. Different queues can share a failed destination without sharing a transfer lock. |
|
|
53
|
+
| Copy into an exclusive destination | A crash can expose partial JSON; equal bytes cannot identify interrupted publication versus unrelated collision evidence. |
|
|
54
|
+
| Exclusive empty placeholder, then record its owner | A crash between creation and recording ownership leaves an empty file indistinguishable from a pre-existing collision. |
|
|
55
|
+
| Add a producer lock or format marker | Old producers ignore both and can replace `.json` during retirement recovery. |
|
|
56
|
+
| Rename a retirement record back to `.json` | An old producer can publish after the absence check; replacing rename then destroys that newer generation. |
|
|
57
|
+
|
|
58
|
+
Native atomic no-replace rename could remove some of these obstacles without
|
|
59
|
+
making old producers cooperate. It is a distinct implementation route, requiring
|
|
60
|
+
qualified Android support and full claim, retirement, and quarantine crash proof.
|
|
61
|
+
An unavailable primitive, cross-device move, or unsupported filesystem must not
|
|
62
|
+
select a replacing rename or a weaker copy fallback.
|
|
63
|
+
|
|
64
|
+
## Candidate v2 disk format
|
|
65
|
+
|
|
66
|
+
V2 would be an additive, explicitly selected API with a dedicated store root,
|
|
67
|
+
not an option that silently changes the existing helpers. Proposed layout:
|
|
68
|
+
|
|
69
|
+
```text
|
|
70
|
+
store/
|
|
71
|
+
.fs-safe-queue-v2/
|
|
72
|
+
format.json
|
|
73
|
+
state.json
|
|
74
|
+
objects/
|
|
75
|
+
<object-token>/
|
|
76
|
+
payload.json
|
|
77
|
+
.state-transfer.lock
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
`format.json` identifies protocol `fs-safe-json-queue`, integer version `2`, and
|
|
81
|
+
a random store ID. `state.json` repeats those values, has a monotonically
|
|
82
|
+
increasing decimal-string revision, and contains queue/ID slots plus a shared
|
|
83
|
+
failed index. Queue names and IDs use validated portable segments; object tokens
|
|
84
|
+
are library-generated and never interpreted as caller-supplied paths.
|
|
85
|
+
|
|
86
|
+
Each queue/ID slot has nullable `pending` and `claimed` references. A reference
|
|
87
|
+
contains a logical delivery-generation token and an object token. Migration can
|
|
88
|
+
replace the object while retaining the delivery generation. Failed slots contain
|
|
89
|
+
the originating queue, ID, delivery generation, and object reference. All queues
|
|
90
|
+
sharing a failed namespace must live in this same store and share its state
|
|
91
|
+
lock. Two separately configured stores cannot advertise one shared failed index.
|
|
92
|
+
|
|
93
|
+
Payload objects are immutable. They remain ordinary JSON, not envelopes exposed
|
|
94
|
+
to the caller. Their unique, exclusively created directories distinguish
|
|
95
|
+
independent equal-byte enqueues. Tokens select protocol records; they never
|
|
96
|
+
replace exact bigint filesystem identity, no-follow admission, or pinned-file
|
|
97
|
+
checks. Reused tokens, references to missing objects, unexpected links, malformed
|
|
98
|
+
state, unknown versions, and inconsistent store IDs fail closed.
|
|
99
|
+
|
|
100
|
+
Opening v2 requires both valid format and state files and an admitted dedicated
|
|
101
|
+
root. Merely finding a directory, an empty file, or a partial initialization is
|
|
102
|
+
not permission to adopt or delete it. Initialization exclusively creates the
|
|
103
|
+
namespace, writes and synchronizes its files and directory edges before success,
|
|
104
|
+
and leaves interrupted/ambiguous initialization for explicit recovery. An
|
|
105
|
+
existing v1 directory remains v1; absence of a marker never means an empty v2
|
|
106
|
+
store. No v2 file uses a top-level legacy queue filename.
|
|
107
|
+
|
|
108
|
+
## State transitions and persistence ordering
|
|
109
|
+
|
|
110
|
+
All v2 producers, consumers, migrations, and collectors take the same store
|
|
111
|
+
transfer lock. Start with a coarse lock and bounded state/object sizes; splitting
|
|
112
|
+
it requires a separate transaction design. Stale locks require operator recovery
|
|
113
|
+
after proving the owner is gone. No timeout grants ownership automatically.
|
|
114
|
+
Callbacks run outside the lock with their payload descriptor retained.
|
|
115
|
+
|
|
116
|
+
Every state transaction re-admits the root, format, state, and referenced objects,
|
|
117
|
+
including exact identity checks around awaited work. It writes a private sibling
|
|
118
|
+
state temp, synchronizes that file, atomically replaces the admitted state file,
|
|
119
|
+
then strictly synchronizes its parent before reporting success. Replacement has
|
|
120
|
+
the same documented platform confinement limits as existing guarded writes; a
|
|
121
|
+
format token or cooperative lock cannot upgrade those limits.
|
|
122
|
+
|
|
123
|
+
Before publishing a new object reference, synchronize the complete payload file,
|
|
124
|
+
its object directory, and the new edge in `objects/`. Persist store initialization
|
|
125
|
+
edges first. No mutable state may refer to an incomplete object. Before consuming
|
|
126
|
+
a state observed after an interrupted commit, synchronize the state parent under
|
|
127
|
+
the lock. A visible post-rename sync failure is uncertain completion: retain all
|
|
128
|
+
potentially referenced objects, report the error, and retry recovery. Never undo
|
|
129
|
+
the transaction by deleting its payload.
|
|
130
|
+
|
|
131
|
+
| Operation | One atomic state replacement |
|
|
132
|
+
|---|---|
|
|
133
|
+
| Enqueue | Set `pending` to a newly persisted object and delivery generation; retain `claimed`. A later enqueue may supersede this pending reference. |
|
|
134
|
+
| Load without a claim | Move the pending reference to `claimed` and clear `pending`; synchronize before returning bytes. |
|
|
135
|
+
| Load with a claim | Keep the same reference and retry that generation before any pending replacement. |
|
|
136
|
+
| Acknowledge | After processing succeeds, clear only the expected claimed generation. A retry with an absent claim succeeds; a different claim rejects. Never clear pending. |
|
|
137
|
+
| Quarantine | Require an absent failed slot, add its claim reference, and clear that claim in the same state update. An identical origin/generation/object reference is an idempotent retry; any other occupant is a collision. |
|
|
138
|
+
| Migrate | After the callback, require both the delivery generation and original object reference still match the claim; publish a fully persisted replacement object and change only that claim's object reference. |
|
|
139
|
+
|
|
140
|
+
The failed index is terminal inspection evidence, not pending work. It remains
|
|
141
|
+
create-only even across different queues in the store. A loader never scans
|
|
142
|
+
objects to infer pending work. The authoritative state alone determines whether
|
|
143
|
+
an object is pending, claimed, quarantined, superseded, or acknowledged.
|
|
144
|
+
|
|
145
|
+
Garbage collection holds the same lock, first completes parent synchronization,
|
|
146
|
+
and removes only objects unreachable from the committed state. It must preserve
|
|
147
|
+
objects pinned by active readers/migration callbacks; uncertain ownership or
|
|
148
|
+
unverifiable pin lifetime defers collection. Version 2 can initially omit
|
|
149
|
+
collection entirely. Leaked unreferenced objects are preferable to lost entries.
|
|
150
|
+
There is no age-based deletion of potentially referenced data.
|
|
151
|
+
|
|
152
|
+
## Crash-point table
|
|
153
|
+
|
|
154
|
+
This table is the proposed protocol invariant, **not implementation proof**.
|
|
155
|
+
It assumes cooperative v2 participants, an already initialized store, atomic
|
|
156
|
+
whole-file replacement, a filesystem honoring successful file/directory syncs,
|
|
157
|
+
and recovery after proven-stale lock removal. Filesystem corruption, concurrent
|
|
158
|
+
hostile mutation, and an unavailable durability mechanism remain explicit errors.
|
|
159
|
+
Power-loss behavior on each supported Android filesystem needs qualification.
|
|
160
|
+
|
|
161
|
+
For a durably accepted generation that has not been superseded, acknowledged,
|
|
162
|
+
or quarantined, recovery must find exactly one authoritative pending or claimed
|
|
163
|
+
reference. Physical orphan objects are never redelivered. Quarantine and
|
|
164
|
+
acknowledgement are deliberate terminal states, not exceptions silently counted
|
|
165
|
+
as pending. Processing can repeat if its external side effect precedes durable
|
|
166
|
+
acknowledgement; this is the existing at-least-once boundary.
|
|
167
|
+
|
|
168
|
+
| Crash after / during | Recovery and surviving generation |
|
|
169
|
+
|---|---|
|
|
170
|
+
| Acquiring a lock, before any transaction | Prior state stays authoritative; stale lock blocks until explicit recovery. |
|
|
171
|
+
| Exclusive object directory creation or partial payload write | No state reference exists. Prior pending/claimed state survives; enqueue has not succeeded. Never expose the incomplete object. |
|
|
172
|
+
| Payload file sync, before all object-directory edges sync | Same as above; the object is not yet eligible for publication. |
|
|
173
|
+
| All object syncs, before state replacement | Complete but unreferenced object is retained or later collected; prior state survives. |
|
|
174
|
+
| Partial state-temp write or its file sync | Prior state survives. Temp contents never authorize delivery or cleanup. |
|
|
175
|
+
| State replacement, before parent sync | After restart, admit the complete old or new state and sync its parent before acting. Retain objects needed by either outcome until recovery. No operation has reported durable success. Missing/invalid state is a hard failure, never an empty queue. |
|
|
176
|
+
| Enqueue parent sync, before response | New generation is pending (or subsequently superseded under the ordinary same-ID contract). Producer may retry after an uncertain response; no exactly-once enqueue is promised. |
|
|
177
|
+
| Claim replacement/sync, before returning payload | Old state has pending; new state has the same generation claimed. Recovery retries the claim first. Never both. |
|
|
178
|
+
| Read or callback, before ack | Claim remains; side effects may repeat. A concurrent enqueue changes only pending. |
|
|
179
|
+
| Migration object persistence, before state commit | Original claim survives; new unreferenced object is not delivered. |
|
|
180
|
+
| Migration commit/sync, before response | Old or new object belongs to the same claimed delivery generation; stale callbacks cannot overwrite the new object reference. |
|
|
181
|
+
| Ack replacement, before parent sync | Old state retains the claim and may redeliver; new state has an acknowledged generation. A newer pending reference survives both. |
|
|
182
|
+
| Ack parent sync, before response | Acknowledged generation is absent; retry never clears a different claim. Objects left behind cannot resurrect it. |
|
|
183
|
+
| Quarantine replacement, before parent sync | Old state retains the claim; new state has only terminal failed evidence for it. No intermediate state removes both references or exposes both as deliverable. |
|
|
184
|
+
| Quarantine parent sync, before response | Failed reference remains; identical retry succeeds, different-origin collision preserves both generations. |
|
|
185
|
+
| Collection unlink or directory sync | Only unreferenced, unpinned garbage is affected; pending/claimed/failed objects stay intact. |
|
|
186
|
+
|
|
187
|
+
## Mixed-version behavior and cutover
|
|
188
|
+
|
|
189
|
+
An old writer cannot be made to honor a new marker or lock by updating the
|
|
190
|
+
consumer. For example: read legacy generation A, copy it into v2, then let an
|
|
191
|
+
old writer replace `.json` with B. Removing `.json` can now delete B; leaving
|
|
192
|
+
it can deliver A twice through independent protocols. Equality of bytes does
|
|
193
|
+
not distinguish these events. Watching for a quiet interval cannot prove the
|
|
194
|
+
old writer will not publish later.
|
|
195
|
+
|
|
196
|
+
| Participants | Permitted behavior |
|
|
197
|
+
|---|---|
|
|
198
|
+
| Old and upgraded clients on v1 | Keep the current v1 protocol and error codes. Upgraded clients do not auto-convert or copy around refused links. |
|
|
199
|
+
| All cooperating clients on a new v2 store | Candidate state protocol above; requires an explicit new API and validation before implementation. |
|
|
200
|
+
| Old v1 writers alongside an isolated v2 store | Namespace coexistence only: separate backlogs. V2 must neither delete nor adopt v1 files. Old writers' work is not silently included in v2 success/drain results. |
|
|
201
|
+
| Old writers with a requested single merged backlog on link-refusing storage | Unsupported. Report the capability/migration failure and preserve legacy evidence; do not claim the queue was drained. |
|
|
202
|
+
| Quiesced legacy queue | A future explicit importer could retain migration receipts and copy under exclusive ownership. No importer is specified or authorized by this proposal. |
|
|
203
|
+
|
|
204
|
+
Old batch loaders do not recurse into the v2 namespace, but that is only format
|
|
205
|
+
separation, not a guarantee that an application shares a logical queue safely.
|
|
206
|
+
Applications must stop and fence old producers/consumers before any destructive
|
|
207
|
+
cutover. A directory scan, PID timeout, or absence of a current lock is not that
|
|
208
|
+
fence. Deployments unable to stop old writers need a qualified native primitive
|
|
209
|
+
route that continues the v1 ownership protocol.
|
|
210
|
+
|
|
211
|
+
## Implementation gates and decision
|
|
212
|
+
|
|
213
|
+
Do not retrofit this candidate into `writeJsonDurableQueueEntry()` or the
|
|
214
|
+
existing path-based lifecycle helpers. A separate proposal must settle the
|
|
215
|
+
public API, callback path semantics, expected-generation receipts, bounded state
|
|
216
|
+
growth, active-reader collection, and fail-closed initialization recovery.
|
|
217
|
+
Arbitrary `processingPath`/`deliveredPath`, shared external `failed/<id>.json`,
|
|
218
|
+
and cross-device destinations cannot be represented by this single-store
|
|
219
|
+
transaction without another protocol. They must retain v1 behavior, not acquire
|
|
220
|
+
a silently weaker fallback. V2 is not an implementation of those contracts.
|
|
221
|
+
|
|
222
|
+
Before implementing an approved scope, require deterministic crashes before and
|
|
223
|
+
after every mutation and sync above, subprocess termination/restart tests,
|
|
224
|
+
identical-payload distinct-generation tests, stale migration/ack tests, failed
|
|
225
|
+
index collisions across queues, invalid-format/identity tests, and initialization
|
|
226
|
+
and garbage-collection failures. Run a link-refusal matrix for `EACCES`, `EPERM`,
|
|
227
|
+
`EXDEV`, `ENOTSUP`, `EOPNOTSUPP`, and `ENOSYS` in `off`, `auto`, and `require`.
|
|
228
|
+
V1 must preserve its typed failures; v2 must make no ownership link attempt.
|
|
229
|
+
Use real Linux seccomp denial of `link`/`linkat` and Android/Termux filesystem
|
|
230
|
+
crash/durability qualification as separate evidence, not substitutes for one
|
|
231
|
+
another. Native-capable operations must retain their native security mechanism;
|
|
232
|
+
any performance change needs measured native/off comparison with an A/A control.
|
|
233
|
+
|
|
234
|
+
**Decision: retain the existing fail-closed implementation and land this design
|
|
235
|
+
only.** Transparent link-free v1 support with concurrent old writers has not
|
|
236
|
+
been established. A separately scoped v2 API or qualified native no-replace
|
|
237
|
+
implementation remains follow-up work; neither is required for 0.26.3.
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# Quickstart
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Build a `root()` and use it to read, write, atomically replace, and unpack an
|
|
4
|
+
archive under a trusted workspace. Confinement strength and concurrent-mutation
|
|
5
|
+
limits depend on the operation and platform; see the [security model](security-model.md).
|
|
4
6
|
|
|
5
7
|
If you have used Go's `os.Root` / `OpenInRoot` or Rust's [`cap-std`](https://github.com/bytecodealliance/cap-std), this is the same shape: a capability-style handle that carries the boundary across every operation. The first thing to internalize is that you stop reasoning about *paths* and start reasoning about *the handle*.
|
|
6
8
|
|
|
@@ -31,7 +33,9 @@ await fs.write("notes/today.txt", "hello\n");
|
|
|
31
33
|
const text = await fs.readText("notes/today.txt");
|
|
32
34
|
```
|
|
33
35
|
|
|
34
|
-
|
|
36
|
+
Replacement writes use a sibling temp file plus `rename`, so a partial write
|
|
37
|
+
never appears at the destination. Reads open with `O_NOFOLLOW` where available
|
|
38
|
+
and verify the opened fd matches the path identity before returning the buffer.
|
|
35
39
|
|
|
36
40
|
`create()` is the don't-clobber variant of `write()` and throws `already-exists` when the target is already there:
|
|
37
41
|
|
|
@@ -39,6 +43,13 @@ Writes use a sibling temp file plus `rename`, so a partial write never appears a
|
|
|
39
43
|
await fs.create("notes/README.md", "seed\n"); // throws if it already exists
|
|
40
44
|
```
|
|
41
45
|
|
|
46
|
+
Default create-only writes can expose incomplete content on the JavaScript
|
|
47
|
+
fallback. Pass `{ atomic: true }` to stage complete content before publication.
|
|
48
|
+
When hardlinks are refused, staged publication can still expose an empty
|
|
49
|
+
destination placeholder before replacing it with the completed file. See
|
|
50
|
+
[atomic creation](writing.md#atomic-buffered-creation) and
|
|
51
|
+
[publication without hardlinks](writing.md#publication-without-hardlinks).
|
|
52
|
+
|
|
42
53
|
## 3. JSON, with parsing
|
|
43
54
|
|
|
44
55
|
```ts
|
|
@@ -60,7 +71,16 @@ await fs.move("notes/today.txt", "notes/archive/today.txt", { overwrite: true })
|
|
|
60
71
|
await fs.remove("notes/archive/today.txt");
|
|
61
72
|
```
|
|
62
73
|
|
|
63
|
-
`move()` defaults to no clobber
|
|
74
|
+
`move()` defaults to no clobber and requires native support. Linux `auto` can
|
|
75
|
+
fall back when no-replace rename is unsupported; its directory fallback can
|
|
76
|
+
replace a concurrently created empty directory. Use native `require` when
|
|
77
|
+
that must fail closed. See the [move contract](writing.md#fs-move-from-to-options).
|
|
78
|
+
Pass `{ overwrite: true }` when replacement is intended.
|
|
79
|
+
|
|
80
|
+
`remove()` removes files and empty directories by default. For a non-empty
|
|
81
|
+
directory, pass `{ recursive: true }`; use `maxEntries`, `maxDepth`, and `signal`
|
|
82
|
+
to bound the work. See [`root()`](root.md) for removal ordering, limits, and
|
|
83
|
+
partial-removal semantics.
|
|
64
84
|
|
|
65
85
|
## 5. Inspect
|
|
66
86
|
|
|
@@ -81,6 +81,12 @@ For tighter control over malformed-or-missing JSON, use the standalone helpers i
|
|
|
81
81
|
|
|
82
82
|
Returns a `FileHandle` plus `containment: "best-effort"`, the verified `realPath`, and `stat`. Use this for streaming or partial reads, and **always close the handle**:
|
|
83
83
|
|
|
84
|
+
`open()` accepts `RootOpenOptions`: the read symlink and hardlink policies,
|
|
85
|
+
without `maxBytes`. It does not consume bytes or apply the Root's read-size
|
|
86
|
+
budget to later handle or stream operations. Bound that I/O in the caller, or
|
|
87
|
+
use `read()` / `readBytes()` when the library should enforce the whole-file cap.
|
|
88
|
+
The result also supports `await using` for automatic handle cleanup.
|
|
89
|
+
|
|
84
90
|
```ts
|
|
85
91
|
const opened = await fs.open("large.log");
|
|
86
92
|
try {
|
|
@@ -103,9 +109,9 @@ type RootReadOptions = {
|
|
|
103
109
|
};
|
|
104
110
|
```
|
|
105
111
|
|
|
106
|
-
`maxBytes` is enforced eagerly: the library reads up to `maxBytes + 1` and throws `too-large` if there is more, so a hostile target cannot silently exhaust memory. Values must be non-negative safe integers or positive `Infinity`; zero is an active cap, while `Infinity` disables it. Explicitly forwarding `undefined` preserves the Root default.
|
|
112
|
+
For the byte-consuming read methods, `maxBytes` is enforced eagerly: the library reads up to `maxBytes + 1` and throws `too-large` if there is more, so a hostile target cannot silently exhaust memory. Values must be non-negative safe integers or positive `Infinity`; zero is an active cap, while `Infinity` disables it. Explicitly forwarding `undefined` preserves the Root default.
|
|
107
113
|
|
|
108
|
-
Safe reads always add the platform's nonblocking open flag where available so a raced FIFO cannot pin a worker indefinitely
|
|
114
|
+
Safe reads always add the platform's nonblocking open flag where available so a raced FIFO cannot pin a worker indefinitely. Whole-file reads continue after short chunks, including on WSL 9p mounts; a short read alone is not treated as EOF. This also applies when the byte cap is disabled.
|
|
109
115
|
|
|
110
116
|
## `readAbsolute()` and `reader()`
|
|
111
117
|
|
|
@@ -175,6 +181,44 @@ try {
|
|
|
175
181
|
}
|
|
176
182
|
```
|
|
177
183
|
|
|
184
|
+
## Synchronous open batches
|
|
185
|
+
|
|
186
|
+
`createRootFileOpenBatchSync()` from `@openclaw/fs-safe/advanced` returns an
|
|
187
|
+
explicitly scoped batch for repeated `openRootFileSync` calls:
|
|
188
|
+
|
|
189
|
+
```ts
|
|
190
|
+
import fs from "node:fs";
|
|
191
|
+
import { createRootFileOpenBatchSync } from "@openclaw/fs-safe/advanced";
|
|
192
|
+
|
|
193
|
+
using batch = createRootFileOpenBatchSync();
|
|
194
|
+
for (const absolutePath of sourceFiles) {
|
|
195
|
+
const opened = batch.openFile({ rootPath: sourceRoot, absolutePath, boundaryLabel: "source" });
|
|
196
|
+
if (!opened.ok) throw opened.error;
|
|
197
|
+
try {
|
|
198
|
+
consume(fs.readFileSync(opened.fd));
|
|
199
|
+
} finally {
|
|
200
|
+
fs.closeSync(opened.fd);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
`openFile()` accepts `OpenRootFileSyncParams` and returns the same
|
|
206
|
+
`RootFileOpenResult` as `openRootFileSync()`. On Windows, canonical drive paths
|
|
207
|
+
with the default symlink rejection reuse a native root descriptor within the
|
|
208
|
+
batch. Native opens reject every reparse component beneath that root. Each open
|
|
209
|
+
still checks the root and current leaf identities, applies its hardlink and
|
|
210
|
+
byte-limit policy, and runs the final root/file/root confinement fence. Replacing
|
|
211
|
+
an admitted root rejects later native opens; file contents and file identities
|
|
212
|
+
are never cached. Other platforms, unavailable native support, other path
|
|
213
|
+
spellings, explicit canonical-root hints, custom filesystem adapters, and
|
|
214
|
+
symlink-following policies retain the ordinary per-open admission path.
|
|
215
|
+
|
|
216
|
+
Successful opens transfer descriptor ownership to the caller. `close()` and
|
|
217
|
+
`Symbol.dispose` release retained root descriptors, are idempotent, and do not close
|
|
218
|
+
previously returned descriptors. Opening after close returns a validation
|
|
219
|
+
failure with code `invalid-path`. Keep batches scoped to one operation rather
|
|
220
|
+
than retaining them as a process-wide filesystem cache.
|
|
221
|
+
|
|
178
222
|
## Common errors
|
|
179
223
|
|
|
180
224
|
- **`outside-workspace`** — relative path escaped the root, or `readAbsolute` got an absolute path outside.
|
|
@@ -193,3 +237,13 @@ See [Errors](errors.md) for the full list.
|
|
|
193
237
|
- [Writing](writing.md) — companion verbs for produce-side I/O.
|
|
194
238
|
- [JSON files](json.md) — standalone strict/lenient JSON helpers.
|
|
195
239
|
- [Secure file reads](secure-file.md) — pinned absolute file reads with permission checks.
|
|
240
|
+
|
|
241
|
+
## Exact identity metadata
|
|
242
|
+
|
|
243
|
+
`OpenResult`, `WritableOpenResult`, `ReadResult`, and advanced `openRootFile()` /
|
|
244
|
+
`openRootFileSync()` results include immutable bigint `exactIdentity` alongside
|
|
245
|
+
`stat: Stats`. The legacy stat and companion derive from the same descriptor
|
|
246
|
+
observation. Use the companion for later identity checks, especially on Windows;
|
|
247
|
+
retain it rather than re-statting a pathname after closing the handle.
|
|
248
|
+
`readRegularFile()` / `readRegularFileSync()`, `statRegularFile()` /
|
|
249
|
+
`statRegularFileSync()`, and `readSecureFile()` follow the same rule.
|
|
@@ -72,6 +72,8 @@ re-inspection without reopening; persistent unknowns or replacements throw
|
|
|
72
72
|
|
|
73
73
|
Throws `FsSafeError` with code `too-large` if the file exceeds `maxBytes`. Other I/O errors propagate as `NodeJS.ErrnoException`.
|
|
74
74
|
|
|
75
|
+
Reads continue after short filesystem chunks even when `maxBytes` is omitted. Nonblocking opens remain enabled to reject raced FIFOs without hanging.
|
|
76
|
+
|
|
75
77
|
### `readRegularFileSync(params)`
|
|
76
78
|
|
|
77
79
|
Synchronous variant. Same shape; the only required field is `filePath`. `maxBytes` is optional.
|
|
@@ -27,11 +27,11 @@ import {
|
|
|
27
27
|
|
|
28
28
|
## Constants
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
30
|
+
| Exported constant | Value |
|
|
31
|
+
|---|---|
|
|
32
|
+
| `DEFAULT_SECRET_FILE_MAX_BYTES` | `16 * 1024` (16 KiB) |
|
|
33
|
+
| `PRIVATE_SECRET_DIR_MODE` | `0o700` |
|
|
34
|
+
| `PRIVATE_SECRET_FILE_MODE` | `0o600` |
|
|
35
35
|
|
|
36
36
|
The 16 KiB cap is intentionally aggressive — credentials should be small. If you need bigger, pass `maxBytes` explicitly.
|
|
37
37
|
|
|
@@ -29,7 +29,7 @@ The helper:
|
|
|
29
29
|
|
|
30
30
|
On POSIX, unsafe permissions mean group/world writable, and group/world readable unless `permissions.allowReadableByOthers` is true. On Windows, the helper queries owner, DACL, and locality from the same open descriptor that supplies the bytes. Both native and system-command queries return the 32-bit volume serial and 64-bit file-index projection used by Node, which must equal Node's bigint descriptor receipt before its ACL facts are trusted. This avoids JavaScript number rounding but does not represent the full 128-bit file identity available on ReFS. Only the current user, LocalSystem, and built-in Administrators are trusted owner classes.
|
|
31
31
|
|
|
32
|
-
Windows secure reads prefer the matching optional native package. In native `auto` or `off` mode, a missing binding or descriptor-inspection capability uses a packaged,
|
|
32
|
+
Windows secure reads prefer the matching optional native package. In native `auto` or `off` mode, a missing binding or descriptor-inspection capability uses a packaged, prebuilt .NET Framework helper to inspect the borrowed file handle. This route requires the [Windows security fallback prerequisites](install.md#windows-security-fallback), including permission to run the executable under normal system policy. The command does not read file contents or reopen the pathname. Successful inspection waits for the child to exit and its output pipes to close. This emits a path-free `FS_SAFE_NATIVE_FALLBACK` warning once per process for secure reads and starts a fresh helper process for each inspection.
|
|
33
33
|
|
|
34
34
|
Descriptor commands have a 30-second deadline. After a timeout or transport failure, fs-safe requests termination, waits at most one further second, and then rejects even if process exit or pipe closure remains unconfirmed. It closes its own output pipes and reports the observed exit separately from the termination attempt in the error cause. If the OS refuses termination, the child may retain its independently inherited Windows handle; closing the caller's descriptor cannot retarget that handle. No file bytes are returned from a failed inspection.
|
|
35
35
|
|
|
@@ -91,6 +91,18 @@ publication, and cleanup. File writers preserve the raw suffix. Root-relative
|
|
|
91
91
|
APIs and caller-constructed relative directory receipts continue to reject
|
|
92
92
|
drive designators.
|
|
93
93
|
|
|
94
|
+
### Portable Windows security transport
|
|
95
|
+
|
|
96
|
+
The portable fallback runs a prebuilt .NET Framework helper for each security
|
|
97
|
+
check. Node passes the borrowed fd as the child's stdin; C# inspects that exact
|
|
98
|
+
handle using `GetStdHandle(-10)`. Replacement descriptors and every verification
|
|
99
|
+
across callbacks, publication, link-count changes, and chmod retain the same
|
|
100
|
+
ordering and ownership boundaries as the native fallback protocol. No facts are
|
|
101
|
+
cached between calls. A persistent helper cannot inherit later-opened fds, and
|
|
102
|
+
portable Node does not expose the OS HANDLE or `DuplicateHandle` needed to send
|
|
103
|
+
them. See the [reproducible build](native.md#windows-helper-build) and
|
|
104
|
+
[execution policy requirements](install.md#windows-security-fallback).
|
|
105
|
+
|
|
94
106
|
### Exclusive creation on Windows
|
|
95
107
|
|
|
96
108
|
Windows Node exclusive creation can follow a dangling file symlink before an
|
|
@@ -382,3 +394,26 @@ public result types and containment labels are unchanged.
|
|
|
382
394
|
## Reporting issues
|
|
383
395
|
|
|
384
396
|
Suspected security issues belong in private disclosure first. See [`SECURITY.md`](https://github.com/openclaw/fs-safe/blob/main/SECURITY.md) in the repo for the current contact path.
|
|
397
|
+
|
|
398
|
+
## Exact receipts and legacy numeric metadata
|
|
399
|
+
|
|
400
|
+
Identity comparisons require bigint or safe-integer components. Unsafe Number
|
|
401
|
+
IDs remain rejected: converting a rounded Number to bigint cannot recover the
|
|
402
|
+
lost bits. This matters on NTFS, whose file IDs can exceed Number precision.
|
|
403
|
+
|
|
404
|
+
Public open/read results, standalone publication results, directory durability
|
|
405
|
+
receipts, and temp-workspace receipts carry `exactIdentity`. Publication failures
|
|
406
|
+
carry `exactTargetIdentity` when the created target is known. Retain and pass
|
|
407
|
+
those exact companions back to identity-checking APIs. Existing numeric fields
|
|
408
|
+
keep their types and serve as legacy metadata views. Both views come from the
|
|
409
|
+
same admitted observation, never an extra pathname stat to recover identity.
|
|
410
|
+
`sameFileIdentity()` and standalone publication prefer an explicit
|
|
411
|
+
`exactIdentity` on a whole receipt. They do not guess between nested source,
|
|
412
|
+
target, directory, or staged identities.
|
|
413
|
+
|
|
414
|
+
`Root.stat()`, typed `list()`, and `entries()` expose a non-enumerable
|
|
415
|
+
`exactIdentity`, preserving their established numeric JSON representation.
|
|
416
|
+
Object spread and JSON serialization omit that field: retain the returned object
|
|
417
|
+
or copy its exact companion explicitly. Other bigint receipt fields also require
|
|
418
|
+
an application-defined encoding for JSON. Exact receipts record identity at an
|
|
419
|
+
observation; they add no atomic rename, deletion, ownership, or durability guarantee.
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# File lock
|
|
2
2
|
|
|
3
|
+
For [Linux kernel write leases](platform-capabilities.md#linux-write-leases), `FileWriteLease.isHeld() === false` means “do not act on the file,” including while the kernel is still processing a lease break. The caller’s `SIGIO` handler still owns break notification.
|
|
4
|
+
|
|
3
5
|
`acquireFileLock()` and `withFileLock()` provide a cross-process file lock with retry and process-exit cleanup. The lock is implemented as a sidecar file (e.g. `state.json` ↔ `state.json.lock`) — only one acquirer can create the sidecar with `O_CREAT | O_EXCL` at a time.
|
|
4
6
|
|
|
5
7
|
On Windows, both the target and an explicitly supplied `lockPath` reject NTFS
|
|
@@ -72,21 +74,29 @@ The raw sidecar bytes are not a canonical JSON representation: tools that trim o
|
|
|
72
74
|
## API
|
|
73
75
|
|
|
74
76
|
```ts
|
|
75
|
-
|
|
77
|
+
import type {
|
|
78
|
+
FileLockAcquireOptions,
|
|
79
|
+
FileLockHandle,
|
|
80
|
+
FileLockManager,
|
|
81
|
+
FileLockSyncAcquireOptions,
|
|
82
|
+
FileLockSyncHandle,
|
|
83
|
+
} from "@openclaw/fs-safe/file-lock";
|
|
84
|
+
|
|
85
|
+
declare function acquireFileLock<TPayload extends Record<string, unknown>>(
|
|
76
86
|
targetPath: string,
|
|
77
87
|
options: FileLockAcquireOptions<TPayload>,
|
|
78
88
|
): Promise<FileLockHandle>;
|
|
79
89
|
|
|
80
|
-
function withFileLock<T, TPayload
|
|
90
|
+
declare function withFileLock<T, TPayload extends Record<string, unknown>>(
|
|
81
91
|
targetPath: string,
|
|
82
92
|
options: FileLockAcquireOptions<TPayload>,
|
|
83
93
|
fn: () => Promise<T>,
|
|
84
94
|
): Promise<T>;
|
|
85
95
|
|
|
86
|
-
function createFileLockManager(key: string): FileLockManager;
|
|
96
|
+
declare function createFileLockManager(key: string): FileLockManager;
|
|
87
97
|
|
|
88
|
-
function acquireFileLockSync<TPayload
|
|
89
|
-
function withFileLockSync<T, TPayload
|
|
98
|
+
declare function acquireFileLockSync<TPayload extends Record<string, unknown>>(targetPath: string, options: FileLockSyncAcquireOptions<TPayload>): FileLockSyncHandle;
|
|
99
|
+
declare function withFileLockSync<T, TPayload extends Record<string, unknown>>(targetPath: string, options: FileLockSyncAcquireOptions<TPayload>, fn: () => T): T;
|
|
90
100
|
```
|
|
91
101
|
|
|
92
102
|
`managerKey` is an optional identifier used to keep state isolated across multiple lock domains in the same process. Use distinct keys for distinct domains (`"snapshot"`, `"compact"`, `"build"`). If omitted, fs-safe derives one from the target path.
|
|
@@ -141,7 +151,7 @@ type FileLockAcquireOptions<TPayload extends Record<string, unknown>> = {
|
|
|
141
151
|
shouldReclaim?: (params: {
|
|
142
152
|
lockPath: string;
|
|
143
153
|
normalizedTargetPath: string;
|
|
144
|
-
payload:
|
|
154
|
+
payload: unknown;
|
|
145
155
|
staleMs: number;
|
|
146
156
|
nowMs: number;
|
|
147
157
|
heldByThisProcess: boolean;
|
|
@@ -150,7 +160,7 @@ type FileLockAcquireOptions<TPayload extends Record<string, unknown>> = {
|
|
|
150
160
|
lockPath: string;
|
|
151
161
|
normalizedTargetPath: string;
|
|
152
162
|
raw: string;
|
|
153
|
-
payload:
|
|
163
|
+
payload: unknown;
|
|
154
164
|
}) => boolean | Promise<boolean>;
|
|
155
165
|
metadata?: Record<string, unknown>; // attached to heldEntries() output for diagnostics
|
|
156
166
|
parsePayload?: (raw: string) => unknown;
|
|
@@ -542,24 +552,32 @@ holder still alive":
|
|
|
542
552
|
|
|
543
553
|
```ts
|
|
544
554
|
import { kill } from "node:process";
|
|
555
|
+
import { acquireFileLock } from "@openclaw/fs-safe/file-lock";
|
|
545
556
|
|
|
546
557
|
const handle = await acquireFileLock(targetPath, {
|
|
547
558
|
staleMs: 60_000,
|
|
548
559
|
payload: () => ({ pid: process.pid }),
|
|
549
|
-
shouldReclaim: ({ payload
|
|
550
|
-
if (!payload) return true;
|
|
551
|
-
const pid =
|
|
552
|
-
if (!Number.
|
|
560
|
+
shouldReclaim: ({ payload }) => {
|
|
561
|
+
if (!payload || typeof payload !== "object" || !("pid" in payload)) return true;
|
|
562
|
+
const pid = payload.pid;
|
|
563
|
+
if (typeof pid !== "number" || !Number.isSafeInteger(pid) || pid <= 0) return true;
|
|
553
564
|
try {
|
|
554
565
|
kill(pid, 0);
|
|
555
566
|
return false; // process still alive — keep waiting
|
|
556
|
-
} catch {
|
|
557
|
-
return
|
|
567
|
+
} catch (error) {
|
|
568
|
+
return error !== null && typeof error === "object" &&
|
|
569
|
+
"code" in error && error.code === "ESRCH"; // only a missing process is stale
|
|
558
570
|
}
|
|
559
571
|
},
|
|
560
572
|
});
|
|
561
573
|
```
|
|
562
574
|
|
|
575
|
+
The observed payload is `unknown`, even when this caller writes a typed payload.
|
|
576
|
+
Validate it before reading fields. Permission failures such as `EPERM` do not
|
|
577
|
+
prove that the holder exited. PID checks assume a shared local PID namespace;
|
|
578
|
+
PID reuse can keep a stale lock waiting. This callback classifies staleness,
|
|
579
|
+
not ownership or permission to remove another holder's lock.
|
|
580
|
+
|
|
563
581
|
`heldByThisProcess` is true when this manager already holds the lock. A `true` result marks the observed sidecar as stale; `staleRecovery` then decides whether acquisition fails closed or attempts caller-approved removal.
|
|
564
582
|
|
|
565
583
|
## Stale recovery: guarded `remove-if-unchanged`
|