@openclaw/fs-safe 0.11.0 → 0.13.0
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/CHANGELOG.md +119 -0
- package/README.md +15 -8
- package/dist/absolute-path.d.ts.map +1 -1
- package/dist/absolute-path.js +32 -11
- package/dist/advanced.d.ts +3 -1
- package/dist/advanced.d.ts.map +1 -1
- package/dist/advanced.js +3 -1
- package/dist/archive-entry.d.ts.map +1 -1
- package/dist/archive-entry.js +4 -3
- package/dist/archive-gzip-tail.d.ts.map +1 -1
- package/dist/archive-gzip-tail.js +13 -9
- package/dist/archive-input.d.ts.map +1 -1
- package/dist/archive-input.js +6 -2
- package/dist/archive-merge.d.ts.map +1 -1
- package/dist/archive-merge.js +15 -2
- package/dist/archive-native.d.ts.map +1 -1
- package/dist/archive-native.js +7 -19
- package/dist/archive-plan.js +1 -1
- package/dist/archive-read.d.ts.map +1 -1
- package/dist/archive-read.js +55 -46
- package/dist/archive-staging.d.ts.map +1 -1
- package/dist/archive-staging.js +43 -10
- package/dist/archive-tar-inspect.d.ts.map +1 -1
- package/dist/archive-tar-inspect.js +4 -1
- package/dist/archive-zip-admission.d.ts +1 -1
- package/dist/archive-zip-admission.d.ts.map +1 -1
- package/dist/archive-zip-admission.js +2 -2
- package/dist/archive-zip-directory.d.ts +3 -0
- package/dist/archive-zip-directory.d.ts.map +1 -1
- package/dist/archive-zip-directory.js +13 -2
- package/dist/archive-zip-loader.d.ts +3 -2
- package/dist/archive-zip-loader.d.ts.map +1 -1
- package/dist/archive-zip-loader.js +30 -4
- package/dist/archive-zip-manifest.d.ts +5 -0
- package/dist/archive-zip-manifest.d.ts.map +1 -0
- package/dist/archive-zip-manifest.js +22 -0
- package/dist/archive-zip-names.d.ts +6 -1
- package/dist/archive-zip-names.d.ts.map +1 -1
- package/dist/archive-zip-names.js +50 -17
- package/dist/archive-zip-preflight.d.ts.map +1 -1
- package/dist/archive-zip-preflight.js +3 -2
- package/dist/archive.d.ts.map +1 -1
- package/dist/archive.js +9 -4
- package/dist/bounded-read.js +2 -2
- package/dist/darwin-acl.d.ts +4 -0
- package/dist/darwin-acl.d.ts.map +1 -0
- package/dist/darwin-acl.js +24 -0
- package/dist/deny-mutations.d.ts.map +1 -1
- package/dist/deny-mutations.js +8 -2
- package/dist/device-path.d.ts.map +1 -1
- package/dist/device-path.js +5 -3
- package/dist/directory-durability.d.ts.map +1 -1
- package/dist/directory-durability.js +67 -23
- package/dist/directory-entry-path.d.ts +3 -0
- package/dist/directory-entry-path.d.ts.map +1 -0
- package/dist/directory-entry-path.js +21 -0
- package/dist/directory-guard.d.ts +17 -1
- package/dist/directory-guard.d.ts.map +1 -1
- package/dist/directory-guard.js +134 -48
- package/dist/directory-mode-node.d.ts +12 -0
- package/dist/directory-mode-node.d.ts.map +1 -1
- package/dist/directory-mode-node.js +102 -4
- package/dist/effective-uid.d.ts +2 -0
- package/dist/effective-uid.d.ts.map +1 -0
- package/dist/effective-uid.js +25 -0
- package/dist/file-handle-transfer.d.ts.map +1 -1
- package/dist/file-handle-transfer.js +98 -27
- package/dist/file-hash.d.ts.map +1 -1
- package/dist/file-hash.js +12 -2
- package/dist/file-lock-sync.d.ts.map +1 -1
- package/dist/file-lock-sync.js +36 -6
- package/dist/file-lock.d.ts.map +1 -1
- package/dist/file-lock.js +29 -9
- package/dist/file-observation.d.ts +1 -1
- package/dist/file-observation.d.ts.map +1 -1
- package/dist/file-store-boundary.d.ts +6 -2
- package/dist/file-store-boundary.d.ts.map +1 -1
- package/dist/file-store-boundary.js +20 -65
- package/dist/file-store-copy-source.d.ts +5 -0
- package/dist/file-store-copy-source.d.ts.map +1 -0
- package/dist/file-store-copy-source.js +31 -0
- package/dist/file-store-path.d.ts.map +1 -1
- package/dist/file-store-path.js +4 -1
- package/dist/file-store-prune.d.ts.map +1 -1
- package/dist/file-store-prune.js +9 -1
- package/dist/file-store-sync-directory.d.ts +16 -0
- package/dist/file-store-sync-directory.d.ts.map +1 -0
- package/dist/file-store-sync-directory.js +349 -0
- package/dist/file-store.d.ts.map +1 -1
- package/dist/file-store.js +11 -28
- package/dist/filename.d.ts +1 -0
- package/dist/filename.d.ts.map +1 -1
- package/dist/filename.js +85 -23
- package/dist/fs.d.ts.map +1 -1
- package/dist/fs.js +3 -2
- package/dist/guarded-mkdir.d.ts +10 -0
- package/dist/guarded-mkdir.d.ts.map +1 -1
- package/dist/guarded-mkdir.js +217 -33
- package/dist/guest.d.ts.map +1 -1
- package/dist/guest.js +16 -5
- package/dist/home-dir.d.ts.map +1 -1
- package/dist/home-dir.js +73 -10
- package/dist/install-path.d.ts +6 -0
- package/dist/install-path.d.ts.map +1 -1
- package/dist/install-path.js +67 -19
- package/dist/json-durable-queue-ownership.d.ts +8 -0
- package/dist/json-durable-queue-ownership.d.ts.map +1 -1
- package/dist/json-durable-queue-ownership.js +134 -43
- package/dist/json-durable-queue-paths.d.ts +11 -0
- package/dist/json-durable-queue-paths.d.ts.map +1 -0
- package/dist/json-durable-queue-paths.js +42 -0
- package/dist/json-durable-queue-read.d.ts +6 -0
- package/dist/json-durable-queue-read.d.ts.map +1 -0
- package/dist/json-durable-queue-read.js +61 -0
- package/dist/json-durable-queue.d.ts +1 -1
- package/dist/json-durable-queue.d.ts.map +1 -1
- package/dist/json-durable-queue.js +83 -134
- package/dist/json-store.d.ts.map +1 -1
- package/dist/json-store.js +5 -1
- package/dist/json.d.ts.map +1 -1
- package/dist/json.js +54 -22
- package/dist/local-file-access.d.ts.map +1 -1
- package/dist/local-file-access.js +4 -0
- package/dist/local-file-descriptor.d.ts +17 -0
- package/dist/local-file-descriptor.d.ts.map +1 -0
- package/dist/local-file-descriptor.js +84 -0
- package/dist/local-roots.d.ts.map +1 -1
- package/dist/local-roots.js +35 -7
- package/dist/move-path-cleanup.d.ts +2 -0
- package/dist/move-path-cleanup.d.ts.map +1 -1
- package/dist/move-path-cleanup.js +44 -18
- package/dist/move-path.d.ts.map +1 -1
- package/dist/move-path.js +31 -6
- package/dist/native-binding.d.ts +21 -0
- package/dist/native-binding.d.ts.map +1 -1
- package/dist/native-directory-observation.d.ts +17 -0
- package/dist/native-directory-observation.d.ts.map +1 -0
- package/dist/native-directory-observation.js +37 -0
- package/dist/native-parent-admission.d.ts +31 -0
- package/dist/native-parent-admission.d.ts.map +1 -0
- package/dist/native-parent-admission.js +124 -0
- package/dist/native-pinned-write-windows.d.ts +0 -1
- package/dist/native-pinned-write-windows.d.ts.map +1 -1
- package/dist/native-pinned-write-windows.js +0 -3
- package/dist/native-pinned-write.d.ts.map +1 -1
- package/dist/native-pinned-write.js +309 -40
- package/dist/output.d.ts.map +1 -1
- package/dist/output.js +15 -5
- package/dist/overwrite-file-handle.d.ts.map +1 -1
- package/dist/overwrite-file-handle.js +5 -1
- package/dist/owner-dacl.d.ts.map +1 -1
- package/dist/owner-dacl.js +2 -0
- package/dist/path-policy.d.ts.map +1 -1
- package/dist/path-policy.js +7 -2
- package/dist/path-prefix.d.ts +7 -0
- package/dist/path-prefix.d.ts.map +1 -0
- package/dist/path-prefix.js +82 -0
- package/dist/path-scope-lexical.d.ts +14 -0
- package/dist/path-scope-lexical.d.ts.map +1 -0
- package/dist/path-scope-lexical.js +37 -0
- package/dist/path-segment-route.d.ts +7 -0
- package/dist/path-segment-route.d.ts.map +1 -0
- package/dist/path-segment-route.js +24 -0
- package/dist/path-suffix-aliases.d.ts +10 -0
- package/dist/path-suffix-aliases.d.ts.map +1 -0
- package/dist/path-suffix-aliases.js +386 -0
- package/dist/path.d.ts.map +1 -1
- package/dist/path.js +35 -6
- package/dist/permissions-windows.d.ts.map +1 -1
- package/dist/permissions-windows.js +14 -3
- package/dist/permissions.d.ts.map +1 -1
- package/dist/permissions.js +37 -8
- package/dist/pinned-mutation-admission.d.ts +25 -0
- package/dist/pinned-mutation-admission.d.ts.map +1 -0
- package/dist/pinned-mutation-admission.js +425 -0
- package/dist/pinned-mutation-observation.d.ts +34 -0
- package/dist/pinned-mutation-observation.d.ts.map +1 -0
- package/dist/pinned-mutation-observation.js +142 -0
- package/dist/pinned-mutation-shared-route.d.ts +24 -0
- package/dist/pinned-mutation-shared-route.d.ts.map +1 -0
- package/dist/pinned-mutation-shared-route.js +70 -0
- package/dist/pinned-open.d.ts +6 -0
- package/dist/pinned-open.d.ts.map +1 -1
- package/dist/pinned-open.js +28 -10
- package/dist/pinned-write-types.d.ts +75 -0
- package/dist/pinned-write-types.d.ts.map +1 -0
- package/dist/pinned-write-types.js +1 -0
- package/dist/pinned-write.d.ts +7 -33
- package/dist/pinned-write.d.ts.map +1 -1
- package/dist/pinned-write.js +152 -17
- package/dist/private-directory.d.ts.map +1 -1
- package/dist/private-directory.js +2 -0
- package/dist/private-producer-handoff.d.ts +16 -0
- package/dist/private-producer-handoff.d.ts.map +1 -0
- package/dist/private-producer-handoff.js +272 -0
- package/dist/private-temp-workspace.d.ts +2 -39
- package/dist/private-temp-workspace.d.ts.map +1 -1
- package/dist/private-temp-workspace.js +183 -77
- package/dist/publish-file.d.ts.map +1 -1
- package/dist/publish-file.js +33 -29
- package/dist/regular-file.d.ts.map +1 -1
- package/dist/regular-file.js +56 -45
- package/dist/replace-directory.d.ts.map +1 -1
- package/dist/replace-directory.js +12 -5
- package/dist/replace-file-temp-owner.d.ts +1 -0
- package/dist/replace-file-temp-owner.d.ts.map +1 -1
- package/dist/replace-file-temp-owner.js +12 -1
- package/dist/replace-file.d.ts.map +1 -1
- package/dist/replace-file.js +24 -24
- package/dist/root-boundary.d.ts +29 -0
- package/dist/root-boundary.d.ts.map +1 -0
- package/dist/root-boundary.js +182 -0
- package/dist/root-context.d.ts +12 -1
- package/dist/root-context.d.ts.map +1 -1
- package/dist/root-context.js +89 -16
- package/dist/root-directory-creation.d.ts +13 -0
- package/dist/root-directory-creation.d.ts.map +1 -0
- package/dist/root-directory-creation.js +212 -0
- package/dist/root-directory-list.d.ts +16 -4
- package/dist/root-directory-list.d.ts.map +1 -1
- package/dist/root-directory-list.js +182 -33
- package/dist/root-directory.d.ts +23 -0
- package/dist/root-directory.d.ts.map +1 -0
- package/dist/root-directory.js +141 -0
- package/dist/root-errors.d.ts.map +1 -1
- package/dist/root-errors.js +12 -1
- package/dist/root-file-final-admission.d.ts +21 -0
- package/dist/root-file-final-admission.d.ts.map +1 -0
- package/dist/root-file-final-admission.js +83 -0
- package/dist/root-file.d.ts.map +1 -1
- package/dist/root-file.js +103 -24
- package/dist/root-impl.d.ts +1 -0
- package/dist/root-impl.d.ts.map +1 -1
- package/dist/root-impl.js +316 -325
- package/dist/root-move-noreplace.d.ts +14 -0
- package/dist/root-move-noreplace.d.ts.map +1 -0
- package/dist/root-move-noreplace.js +202 -0
- package/dist/root-move-preflight.d.ts +8 -0
- package/dist/root-move-preflight.d.ts.map +1 -0
- package/dist/root-move-preflight.js +16 -0
- package/dist/root-observed-path.d.ts +9 -0
- package/dist/root-observed-path.d.ts.map +1 -0
- package/dist/root-observed-path.js +95 -0
- package/dist/root-path-errors.d.ts +12 -0
- package/dist/root-path-errors.d.ts.map +1 -0
- package/dist/root-path-errors.js +13 -0
- package/dist/root-path-existing.d.ts +2 -0
- package/dist/root-path-existing.d.ts.map +1 -1
- package/dist/root-path-existing.js +63 -22
- package/dist/root-path-observation.d.ts +63 -0
- package/dist/root-path-observation.d.ts.map +1 -0
- package/dist/root-path-observation.js +180 -0
- package/dist/root-path-stat.d.ts +5 -0
- package/dist/root-path-stat.d.ts.map +1 -0
- package/dist/root-path-stat.js +101 -0
- package/dist/root-path-symlink.d.ts.map +1 -1
- package/dist/root-path-symlink.js +23 -4
- package/dist/root-path.d.ts +13 -0
- package/dist/root-path.d.ts.map +1 -1
- package/dist/root-path.js +263 -73
- package/dist/root-paths-lexical.d.ts +9 -0
- package/dist/root-paths-lexical.d.ts.map +1 -0
- package/dist/root-paths-lexical.js +22 -0
- package/dist/root-paths.d.ts +3 -29
- package/dist/root-paths.d.ts.map +1 -1
- package/dist/root-paths.js +89 -178
- package/dist/root-read-admission.d.ts +26 -0
- package/dist/root-read-admission.d.ts.map +1 -0
- package/dist/root-read-admission.js +94 -0
- package/dist/root-remove-identity.d.ts +15 -0
- package/dist/root-remove-identity.d.ts.map +1 -0
- package/dist/root-remove-identity.js +89 -0
- package/dist/root-remove-receipt.d.ts +17 -0
- package/dist/root-remove-receipt.d.ts.map +1 -0
- package/dist/root-remove-receipt.js +37 -0
- package/dist/root-remove.d.ts +2 -1
- package/dist/root-remove.d.ts.map +1 -1
- package/dist/root-remove.js +172 -10
- package/dist/root-walk.d.ts.map +1 -1
- package/dist/root-walk.js +2 -1
- package/dist/root-write-admission.d.ts +68 -0
- package/dist/root-write-admission.d.ts.map +1 -0
- package/dist/root-write-admission.js +322 -0
- package/dist/root-write-compatibility.d.ts +13 -0
- package/dist/root-write-compatibility.d.ts.map +1 -0
- package/dist/root-write-compatibility.js +74 -0
- package/dist/root-write-complete-parent.d.ts +42 -0
- package/dist/root-write-complete-parent.d.ts.map +1 -0
- package/dist/root-write-complete-parent.js +195 -0
- package/dist/root-write-mode.d.ts +2 -0
- package/dist/root-write-mode.d.ts.map +1 -1
- package/dist/root-write-mode.js +20 -7
- package/dist/root-write-publication.d.ts +24 -0
- package/dist/root-write-publication.d.ts.map +1 -0
- package/dist/root-write-publication.js +85 -0
- package/dist/root-write-verification.d.ts.map +1 -1
- package/dist/root-write-verification.js +18 -7
- package/dist/safe-path-segment.d.ts.map +1 -1
- package/dist/safe-path-segment.js +3 -1
- package/dist/secret-file.d.ts.map +1 -1
- package/dist/secret-file.js +62 -24
- package/dist/secret-read-async.d.ts.map +1 -1
- package/dist/secret-read-async.js +18 -13
- package/dist/secret-read-policy.d.ts.map +1 -1
- package/dist/secret-read-policy.js +5 -1
- package/dist/secure-file-windows.d.ts +10 -0
- package/dist/secure-file-windows.d.ts.map +1 -0
- package/dist/secure-file-windows.js +186 -0
- package/dist/secure-file.d.ts.map +1 -1
- package/dist/secure-file.js +98 -10
- package/dist/secure-temp-dir.d.ts +6 -3
- package/dist/secure-temp-dir.d.ts.map +1 -1
- package/dist/secure-temp-dir.js +121 -101
- package/dist/secure-temp-repair.d.ts +35 -0
- package/dist/secure-temp-repair.d.ts.map +1 -0
- package/dist/secure-temp-repair.js +106 -0
- package/dist/sibling-staged-file.d.ts +3 -1
- package/dist/sibling-staged-file.d.ts.map +1 -1
- package/dist/sibling-staged-file.js +181 -50
- package/dist/sibling-temp.d.ts.map +1 -1
- package/dist/sibling-temp.js +27 -12
- package/dist/sidecar-lock-acquire.d.ts.map +1 -1
- package/dist/sidecar-lock-acquire.js +45 -15
- package/dist/sidecar-lock-root.d.ts.map +1 -1
- package/dist/sidecar-lock-root.js +4 -2
- package/dist/sidecar-lock.d.ts.map +1 -1
- package/dist/sidecar-lock.js +2 -3
- package/dist/staged-directory.d.ts +14 -5
- package/dist/staged-directory.d.ts.map +1 -1
- package/dist/staged-directory.js +54 -3
- package/dist/standalone-publication-path.d.ts +2 -0
- package/dist/standalone-publication-path.d.ts.map +1 -0
- package/dist/standalone-publication-path.js +6 -0
- package/dist/stat-observation.d.ts +11 -0
- package/dist/stat-observation.d.ts.map +1 -0
- package/dist/stat-observation.js +64 -0
- package/dist/strict-file-identity.d.ts.map +1 -1
- package/dist/strict-file-identity.js +39 -2
- package/dist/symlink-parents.d.ts.map +1 -1
- package/dist/symlink-parents.js +7 -2
- package/dist/temp-target.d.ts +2 -0
- package/dist/temp-target.d.ts.map +1 -1
- package/dist/temp-target.js +133 -4
- package/dist/temp-workspace-admission.d.ts +22 -0
- package/dist/temp-workspace-admission.d.ts.map +1 -0
- package/dist/temp-workspace-admission.js +374 -0
- package/dist/temp-workspace-child-admission.d.ts +13 -0
- package/dist/temp-workspace-child-admission.d.ts.map +1 -0
- package/dist/temp-workspace-child-admission.js +95 -0
- package/dist/temp-workspace-descriptor.d.ts +48 -0
- package/dist/temp-workspace-descriptor.d.ts.map +1 -0
- package/dist/temp-workspace-descriptor.js +363 -0
- package/dist/temp-workspace-identity.d.ts +15 -0
- package/dist/temp-workspace-identity.d.ts.map +1 -0
- package/dist/temp-workspace-identity.js +41 -0
- package/dist/temp-workspace-owner.d.ts +7 -6
- package/dist/temp-workspace-owner.d.ts.map +1 -1
- package/dist/temp-workspace-owner.js +121 -61
- package/dist/temp-workspace-permissions.d.ts +4 -0
- package/dist/temp-workspace-permissions.d.ts.map +1 -0
- package/dist/temp-workspace-permissions.js +32 -0
- package/dist/temp-workspace-types.d.ts +40 -0
- package/dist/temp-workspace-types.d.ts.map +1 -0
- package/dist/temp-workspace-types.js +1 -0
- package/dist/temp.d.ts +1 -1
- package/dist/temp.d.ts.map +1 -1
- package/dist/temp.js +1 -1
- package/dist/test-hooks.d.ts +7 -0
- package/dist/test-hooks.d.ts.map +1 -1
- package/dist/text-atomic.d.ts.map +1 -1
- package/dist/text-atomic.js +3 -1
- package/dist/trash.d.ts.map +1 -1
- package/dist/trash.js +54 -8
- package/dist/walk.d.ts.map +1 -1
- package/dist/walk.js +20 -16
- package/dist/windows-owner.d.ts.map +1 -1
- package/dist/windows-owner.js +5 -0
- package/dist/windows-path-alias.d.ts +39 -0
- package/dist/windows-path-alias.d.ts.map +1 -0
- package/dist/windows-path-alias.js +153 -0
- package/docs/advanced.md +28 -1
- package/docs/archive.md +29 -2
- package/docs/atomic.md +27 -3
- package/docs/contributing.md +4 -1
- package/docs/copy.md +4 -1
- package/docs/durability.md +18 -2
- package/docs/errors.md +14 -6
- package/docs/file-store.md +30 -3
- package/docs/filename.md +21 -7
- package/docs/guest.md +5 -0
- package/docs/install-path.md +61 -15
- package/docs/install.md +12 -8
- package/docs/json-store.md +5 -1
- package/docs/json.md +11 -0
- package/docs/mutation-policy-proof.md +65 -0
- package/docs/native-helper.md +28 -9
- package/docs/native.md +46 -10
- package/docs/output.md +33 -11
- package/docs/path-prefix.md +64 -0
- package/docs/path-suffix-aliases.md +159 -0
- package/docs/path.md +12 -1
- package/docs/permissions.md +26 -1
- package/docs/private-file-store.md +14 -0
- package/docs/public-api.md +14 -0
- package/docs/quickstart.md +1 -1
- package/docs/reading.md +8 -1
- package/docs/root.md +21 -4
- package/docs/secret-file.md +3 -0
- package/docs/secure-file.md +21 -16
- package/docs/security-model.md +76 -9
- package/docs/sidecar-lock.md +28 -1
- package/docs/store.md +27 -2
- package/docs/temp.md +262 -34
- package/docs/test-hooks.md +14 -0
- package/docs/writing.md +38 -8
- package/package.json +10 -10
package/docs/errors.md
CHANGED
|
@@ -38,6 +38,14 @@ class FsSafeError extends Error {
|
|
|
38
38
|
|
|
39
39
|
`cause` is available through the standard `Error` `cause` property when the failure was triggered by a `NodeJS.ErrnoException` (e.g. a wrapped `EACCES`). Inspect it for the original `code` / `errno` / `syscall` if you need finer-grained reporting.
|
|
40
40
|
|
|
41
|
+
Guarded write preparation describes permission, read-only filesystem, and disk-space
|
|
42
|
+
failures with messages such as `permission denied (EACCES)` or
|
|
43
|
+
`no space left on device (ENOSPC)`. Other errno failures include their code in
|
|
44
|
+
`filesystem write failed (EIO)`. These wrappers retain the existing `invalid-path`
|
|
45
|
+
code and `policy` category for compatibility, along with the original `cause`;
|
|
46
|
+
they do not expose native message text or paths. Already-classified `FsSafeError`
|
|
47
|
+
instances and missing-path errors keep their existing classification.
|
|
48
|
+
|
|
41
49
|
`details` is an operation-specific receipt, not an alternate error code. For
|
|
42
50
|
example, `publishFileExclusive()` uses it to report the failing phase, created
|
|
43
51
|
target identity, cleanup decision, and failed directory-sync outcome. Narrow
|
|
@@ -114,7 +122,7 @@ type FsSafeErrorCode =
|
|
|
114
122
|
| `device-path` | A read/open target is a known unsafe device or process-fd path. | `/dev/zero`, `/dev/random`, `/dev/stdin`, `/dev/fd/*`, `/proc/*/fd/*`, or a Windows reserved device name. |
|
|
115
123
|
| `hardlink` | Read or copy with `hardlinks: "reject"` saw `nlink > 1`. | File is hardlinked — possibly an alias of an out-of-tree inode. |
|
|
116
124
|
| `helper-failed` | A native mechanism or multi-step operational helper failed. | Inspect `cause` and any operation-specific `details`; retrying may be unsafe if the operation partially completed. |
|
|
117
|
-
| `helper-unavailable` |
|
|
125
|
+
| `helper-unavailable` | A required native binding or bounded primitive could not be loaded. | Unsupported platform, omitted/missing/incompatible platform package, `FS_SAFE_NATIVE_MODE=off`, or a no-clobber `Root.move()` without safe native parent admission. `auto` falls back only where a safe fallback exists. |
|
|
118
126
|
| `insecure-permissions` | A secure file or path permission check found a mode/ACL that allows broader access than requested. | File or directory is group/world writable/readable; Windows ACL grants broad read. |
|
|
119
127
|
| `invalid-path` | Input was empty, contained NUL, was an unparseable URL, or otherwise unusable; a FileStore key used a noncanonical spelling. | Noncanonical FileStore aliases, backslashes, or complete parent segments; a network path on Windows; a drive-relative segment in a portable relative path or store key; or a leading drive-relative spelling such as `C:name` used as a Root destination. Existing-object Root lookups retain broader confined path compatibility, including legal POSIX drive-like names. |
|
|
120
128
|
| `not-empty` | Nonrecursive `remove()` on a non-empty directory, or new children appeared during recursive removal. | Use bounded `recursive: true` removal or coordinate concurrent writers. |
|
|
@@ -136,11 +144,11 @@ type FsSafeErrorCode =
|
|
|
136
144
|
|
|
137
145
|
Secret writes reject invalid `mode` / `dirMode` values with `invalid-path` before directory creation. Existing secret directories with a mode different from the requested `dirMode` report `insecure-permissions` without chmod; a created directory whose descriptor ownership no longer matches its initializing effective user reports `not-owned`.
|
|
138
146
|
|
|
139
|
-
Pathname `sha256File()` also
|
|
140
|
-
or current-path identity remains unknown after one bounded
|
|
141
|
-
if the file is benign.
|
|
142
|
-
report `symlink`, preview or descriptor non-files report
|
|
143
|
-
current-path symlink or non-file reports `path-mismatch`.
|
|
147
|
+
Pathname `sha256File()` and `sha256FileSync()` also report `path-mismatch` when
|
|
148
|
+
pre-open, descriptor, or current-path identity remains unknown after one bounded
|
|
149
|
+
Windows retry, even if the file is benign. Neither reopens to recover identity.
|
|
150
|
+
Preview symlinks report `symlink`, preview or descriptor non-files report
|
|
151
|
+
`not-file`, and a current-path symlink or non-file reports `path-mismatch`.
|
|
144
152
|
|
|
145
153
|
## Branching
|
|
146
154
|
|
package/docs/file-store.md
CHANGED
|
@@ -90,13 +90,16 @@ or converts one caller-supplied key onto another:
|
|
|
90
90
|
are rejected.
|
|
91
91
|
- Windows drive-relative segments such as `C:name` or `C:` are rejected
|
|
92
92
|
anywhere in a key, including `a/C:name`.
|
|
93
|
+
- On Windows, every other colon is rejected too, preventing a key from naming
|
|
94
|
+
an NTFS alternate stream or directory-index alias. POSIX keeps accepting
|
|
95
|
+
ordinary colon-bearing segments that are not drive-relative spellings.
|
|
93
96
|
- No segment may end in an ASCII dot or space.
|
|
94
97
|
|
|
95
98
|
Violations report `invalid-path` when key validation is reached. Ordinary nested
|
|
96
99
|
keys, NFC Unicode such as `café/日本語.txt`, `.hidden`, `a..b`, and internal spaces
|
|
97
|
-
such as `internal space/a b.txt` are accepted.
|
|
98
|
-
timestamp in `logs/2026-08-02T10:30:00Z.log`, remain lexically valid;
|
|
99
|
-
|
|
100
|
+
such as `internal space/a b.txt` are accepted. On POSIX, colons elsewhere, such
|
|
101
|
+
as the timestamp in `logs/2026-08-02T10:30:00Z.log`, remain lexically valid;
|
|
102
|
+
Windows rejects that spelling as stream syntax.
|
|
100
103
|
|
|
101
104
|
Validation retains each method's operation order. Async reads, `exists`, and
|
|
102
105
|
`remove` open the root first: if the root is missing, strict methods report
|
|
@@ -136,6 +139,24 @@ its exact publication identity cannot be verified. There is no equal-content
|
|
|
136
139
|
fallback. The published entry remains present, so callers must inspect or
|
|
137
140
|
recover that outcome instead of assuming the write did not occur.
|
|
138
141
|
|
|
142
|
+
Synchronous directory creation retains exact bigint receipts for the store root
|
|
143
|
+
and every parent component. On POSIX, an existing or newly created directory
|
|
144
|
+
whose complete requested mode differs is reopened without following the final
|
|
145
|
+
name, checked against its receipt and parent chain, and finalized through that
|
|
146
|
+
descriptor. A concurrent root or parent replacement is rejected without
|
|
147
|
+
applying the mode to the replacement. Directories already at the requested mode
|
|
148
|
+
skip the descriptor and mode operation. Windows retains its bounded `mkdir`
|
|
149
|
+
mode request and identity checks without relying on directory descriptors or a
|
|
150
|
+
pathname `chmod`, because Node does not enforce POSIX directory modes there.
|
|
151
|
+
|
|
152
|
+
Node does not expose a portable, `fchmod`-capable search-only directory
|
|
153
|
+
descriptor on Linux. If a mismatched existing directory, or one created under
|
|
154
|
+
an owner-read-removing umask, cannot be opened for reading, the synchronous
|
|
155
|
+
store therefore fails closed with `permission-unverified`; it never falls back
|
|
156
|
+
to pathname `chmod`. On supported macOS x64/arm64 hosts it also tries an
|
|
157
|
+
`O_SEARCH` descriptor, so owner-searchable directories can still be repaired.
|
|
158
|
+
Directories with neither usable read nor search access remain fail-closed.
|
|
159
|
+
|
|
139
160
|
| Method | Durability support |
|
|
140
161
|
|---|---|
|
|
141
162
|
| `write`, `writeText`, `writeJson` (async and sync) | Per-call option overrides store default. |
|
|
@@ -256,6 +277,12 @@ type FileStorePruneOptions = {
|
|
|
256
277
|
|
|
257
278
|
Symlinks are skipped. The walk is best-effort — failures on individual entries don't abort the whole prune. Compares against `mtimeMs`.
|
|
258
279
|
|
|
280
|
+
Pruning rechecks that a selected entry is still a regular file and still expired
|
|
281
|
+
immediately before guarded removal. Fresh replacements and in-place timestamp
|
|
282
|
+
refreshes are preserved; replacements that are themselves expired remain
|
|
283
|
+
eligible. This does not require read permission. The existing best-effort
|
|
284
|
+
external-process race window after dispatch still applies.
|
|
285
|
+
|
|
259
286
|
## Difference from `Root`
|
|
260
287
|
|
|
261
288
|
| `FileStore` | `Root` |
|
package/docs/filename.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Filenames
|
|
2
2
|
|
|
3
|
-
`sanitizeUntrustedFileName(name, fallback)` reduces a filename string from an untrusted source to one traversal-free path segment. Use it as a thin first pass before storing user-supplied names;
|
|
3
|
+
`sanitizeUntrustedFileName(name, fallback)` reduces a filename string from an untrusted source to one traversal-free path segment. Use it as a thin first pass before storing user-supplied names; use [`safePathSegmentHashedV2`](install-path.md#safepathsegmenthashedv2) when mapping untrusted install IDs to separate directory names.
|
|
4
4
|
|
|
5
5
|
```ts
|
|
6
6
|
import { sanitizeUntrustedFileName } from "@openclaw/fs-safe/advanced";
|
|
@@ -17,15 +17,27 @@ function sanitizeUntrustedFileName(fileName: string, fallbackName: string): stri
|
|
|
17
17
|
|
|
18
18
|
## What it does
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
The primary name goes through this pipeline first:
|
|
21
21
|
|
|
22
|
-
1. **Trim** whitespace.
|
|
22
|
+
1. **Trim** whitespace. An empty result is unusable.
|
|
23
23
|
2. **Strip path components.** Apply `path.posix.basename` then `path.win32.basename` so neither `foo/bar.txt` nor `foo\bar.txt` survives — only the final segment remains.
|
|
24
24
|
3. **Strip non-portable characters.** C0/C1 controls (`0x00`–`0x1f`, `0x7f`–`0x9f`) and the Windows-invalid set `< > : " / \\ | ? *` are removed on every platform.
|
|
25
25
|
4. **Trim again.**
|
|
26
|
-
5.
|
|
27
|
-
6. **
|
|
28
|
-
7. **
|
|
26
|
+
5. An empty result, `"."`, or `".."` is unusable.
|
|
27
|
+
6. **Truncate.** If the cleaned segment is longer than 200 UTF-16 code units, take up to the first 200 without splitting a valid Unicode surrogate pair.
|
|
28
|
+
7. **Make the final name device-safe.** Compare the part before the first `.` case-insensitively with the Windows device-name set, including `CON`, `PRN`, `AUX`, `NUL`, `CLOCK$`, `CONIN$`, `CONOUT$`, `COM1..9`, `LPT1..9`, and their superscript `¹`, `²`, and `³` variants. Windows-ignored spaces and dots at the end of that basename do not disguise a device name. A match gains `_` before its extension on every platform. If the suffix would exceed 200 code units, the unsuffixed tail is shortened first, so truncation cannot recreate a device name.
|
|
29
|
+
|
|
30
|
+
Only when the primary name is unusable does `fallbackName` go through the same
|
|
31
|
+
nonrecursive pipeline. A safe fallback is preserved exactly; path components,
|
|
32
|
+
controls, reserved device names, and overlong fallback names receive the same
|
|
33
|
+
treatment as the primary name. If both candidates are unusable, the function
|
|
34
|
+
returns the fixed safe literal `"file"`.
|
|
35
|
+
|
|
36
|
+
If truncation itself exposes a reserved-device basename after Windows ignores
|
|
37
|
+
trailing spaces or dots, the result is shortened once more and receives the
|
|
38
|
+
same underscore suffix. A name that reaches the sanitization branch therefore
|
|
39
|
+
remains at most 200 UTF-16 code units and is never a Windows reserved-device
|
|
40
|
+
alias. Fallback names pass through the same checks before they can be returned.
|
|
29
41
|
|
|
30
42
|
That's it. The function stays intentionally small: it removes traversal and
|
|
31
43
|
the most obvious cross-platform device and character hazards, but it is not a
|
|
@@ -41,6 +53,8 @@ sanitizeUntrustedFileName("a\u0000b\tc", "upload"); // "abc"
|
|
|
41
53
|
sanitizeUntrustedFileName(" ", "fallback"); // "fallback"
|
|
42
54
|
sanitizeUntrustedFileName(".", "fallback"); // "fallback"
|
|
43
55
|
sanitizeUntrustedFileName("..", "fallback"); // "fallback"
|
|
56
|
+
sanitizeUntrustedFileName("<>", "../../etc/passwd"); // "passwd"
|
|
57
|
+
sanitizeUntrustedFileName("<>", "../.."); // "file"
|
|
44
58
|
sanitizeUntrustedFileName("CON", "fallback"); // "CON_"
|
|
45
59
|
sanitizeUntrustedFileName("nul.txt", "fallback"); // "nul_.txt"
|
|
46
60
|
sanitizeUntrustedFileName("aux.c", "fallback"); // "aux_.c"
|
|
@@ -92,5 +106,5 @@ await fs.write(`uploads/${safe}`, body); // fs is a Root() handle; rejects trave
|
|
|
92
106
|
|
|
93
107
|
## See also
|
|
94
108
|
|
|
95
|
-
- [Install path helpers](install-path.md) —
|
|
109
|
+
- [Install path helpers](install-path.md) — legacy directory-segment sanitizers and `safePathSegmentHashedV2` for untrusted install IDs.
|
|
96
110
|
- [`root()`](root.md) — the boundary you'll write into after sanitizing.
|
package/docs/guest.md
CHANGED
|
@@ -113,6 +113,11 @@ prefixes and use short random suffixes independent of the destination basename,
|
|
|
113
113
|
so legal names near the filesystem's component limit also work for writes and
|
|
114
114
|
cross-device moves.
|
|
115
115
|
|
|
116
|
+
Cross-device symlink moves create the new link in a private destination-side
|
|
117
|
+
staging directory before atomically replacing the destination. Link creation or
|
|
118
|
+
publication failure preserves the existing destination and source link; ordinary
|
|
119
|
+
failure cleanup removes the staging directory.
|
|
120
|
+
|
|
116
121
|
Cross-device directory moves build a copy manifest and check it during source
|
|
117
122
|
cleanup. Source changes can leave the published destination and some or all
|
|
118
123
|
of the source. Regular-file and symlink move fallbacks unlink the source
|
package/docs/install-path.md
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
resolveSafeInstallDir,
|
|
9
9
|
safeDirName,
|
|
10
10
|
safePathSegmentHashed,
|
|
11
|
+
safePathSegmentHashedV2,
|
|
11
12
|
} from "@openclaw/fs-safe/advanced";
|
|
12
13
|
```
|
|
13
14
|
|
|
@@ -22,7 +23,7 @@ function resolveSafeInstallDir(params: {
|
|
|
22
23
|
}): { ok: true; path: string } | { ok: false; error: string };
|
|
23
24
|
```
|
|
24
25
|
|
|
25
|
-
Computes the absolute install directory for `id` under `baseDir`, after running `id` through `nameEncoder` (`safeDirName` by default). Verifies the result stays inside `baseDir` — anything that would escape returns `{ ok: false, error: invalidNameMessage }`.
|
|
26
|
+
Computes the absolute install directory for `id` under `baseDir`, after running `id` through `nameEncoder` (`safeDirName` by default). Verifies the result stays inside `baseDir` — anything that would escape returns `{ ok: false, error: invalidNameMessage }`. On Windows, contained alternate-stream and filesystem-namespace aliases are rejected with the same result.
|
|
26
27
|
|
|
27
28
|
```ts
|
|
28
29
|
const r = resolveSafeInstallDir({
|
|
@@ -35,14 +36,17 @@ if (!r.ok) return reply(400, r.error);
|
|
|
35
36
|
await fs.mkdir(r.path, { recursive: true });
|
|
36
37
|
```
|
|
37
38
|
|
|
38
|
-
For
|
|
39
|
+
For untrusted IDs that must occupy separate install directories, pass
|
|
40
|
+
`nameEncoder: safePathSegmentHashedV2`. The default `safeDirName` and legacy
|
|
41
|
+
`safePathSegmentHashed` can map distinct IDs to the same directory; the boundary
|
|
42
|
+
check does not establish which ID owns an existing directory.
|
|
39
43
|
|
|
40
44
|
```ts
|
|
41
45
|
const r = resolveSafeInstallDir({
|
|
42
|
-
baseDir: "/srv/plugins",
|
|
46
|
+
baseDir: "/srv/plugins-v2",
|
|
43
47
|
id: untrustedId,
|
|
44
48
|
invalidNameMessage: "invalid plugin name",
|
|
45
|
-
nameEncoder:
|
|
49
|
+
nameEncoder: safePathSegmentHashedV2,
|
|
46
50
|
});
|
|
47
51
|
```
|
|
48
52
|
|
|
@@ -60,7 +64,7 @@ function assertCanonicalPathWithinBase(params: {
|
|
|
60
64
|
}): Promise<void>;
|
|
61
65
|
```
|
|
62
66
|
|
|
63
|
-
Throws if the candidate resolves outside `baseDir` after `realpath`. The `boundaryLabel` is included in the error message ("Invalid path: must stay within {boundaryLabel}").
|
|
67
|
+
Throws if the candidate resolves outside `baseDir` after `realpath`. On Windows it also throws for alternate-stream or filesystem-namespace aliases in the base, candidate, or canonical path. The `boundaryLabel` is included in the containment-shaped error message ("Invalid path: must stay within {boundaryLabel}").
|
|
64
68
|
|
|
65
69
|
```ts
|
|
66
70
|
await assertCanonicalPathWithinBase({
|
|
@@ -87,11 +91,48 @@ safeDirName(""); // ""
|
|
|
87
91
|
|
|
88
92
|
`safeDirName` does *not* try to be exhaustive about Windows-reserved names or special characters. It is purely a separator-stripping pass — `resolveSafeInstallDir` adds the boundary check on top so an `"../../etc"` input cannot escape `baseDir`.
|
|
89
93
|
|
|
90
|
-
|
|
94
|
+
Use `safePathSegmentHashedV2` when distinct untrusted IDs need separate names.
|
|
95
|
+
|
|
96
|
+
### `safePathSegmentHashedV2`
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
function safePathSegmentHashedV2(input: string): string;
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Hashes every trimmed ID, including ordinary short names, into `id-v2-` followed
|
|
103
|
+
by 64 lowercase hexadecimal SHA-256 digits. The result is always 70 ASCII bytes,
|
|
104
|
+
contains no separators, and avoids Windows device names and ignored suffixes.
|
|
105
|
+
There is no readable prefix to truncate and no unchanged-name branch. Distinct
|
|
106
|
+
trimmed IDs, including an ID that looks like an encoded output, remain distinct
|
|
107
|
+
unless their full SHA-256 digests collide. The lowercase ASCII output also
|
|
108
|
+
preserves that distinction on case-insensitive and Unicode-normalizing volumes.
|
|
109
|
+
|
|
110
|
+
The stable V2 digest recipe is SHA-256 of the UTF-8 bytes of
|
|
111
|
+
`"@openclaw/fs-safe:install-path:v2\0"`, followed by the UTF-16LE bytes of
|
|
112
|
+
`input.trim()`, without a byte-order mark. The NUL-terminated prefix separates
|
|
113
|
+
this use of SHA-256 from other hash domains. UTF-16LE preserves exact JavaScript
|
|
114
|
+
code units, including lone surrogates. Inputs are not case-folded or Unicode
|
|
115
|
+
normalized. Surrounding whitespace, as removed by JavaScript `String.trim()`,
|
|
116
|
+
is the only intentional equivalence; internal whitespace remains significant.
|
|
117
|
+
|
|
118
|
+
```ts
|
|
119
|
+
const segment = safePathSegmentHashedV2("plugin/v1"); // id-v2-<64 hex digits>
|
|
120
|
+
safePathSegmentHashedV2(" plugin/v1 ") === segment; // true
|
|
121
|
+
safePathSegmentHashedV2("Plugin/v1") === segment; // false
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
This encoder computes a name; it does not authorize access or prove ownership
|
|
125
|
+
of a directory. Store the original trimmed ID in application-owned metadata and
|
|
126
|
+
verify it before reusing an existing install directory. Keep each install tree
|
|
127
|
+
on one encoding version. Switching to V2 changes existing paths: use a new base
|
|
128
|
+
directory or explicitly migrate directories after verifying their recorded IDs.
|
|
129
|
+
Do not silently fall back to a legacy path when the V2 path is missing.
|
|
91
130
|
|
|
92
131
|
### `safePathSegmentHashed`
|
|
93
132
|
|
|
94
|
-
|
|
133
|
+
Legacy readable encoding retained for path compatibility. It appends a short
|
|
134
|
+
content hash when sanitization changed the input or when the safe form is too
|
|
135
|
+
long; ordinary short names remain unchanged. Use V2 for new untrusted-ID mappings.
|
|
95
136
|
|
|
96
137
|
```ts
|
|
97
138
|
safePathSegmentHashed("plugin-v1"); // "plugin-v1" (unchanged short input)
|
|
@@ -104,31 +145,35 @@ safePathSegmentHashed("."); // "skill-cdb4ee2aea"
|
|
|
104
145
|
|
|
105
146
|
The sanitization is more aggressive than `safeDirName`: any character not in `[A-Za-z0-9._-]` becomes `-`, runs of `-` collapse, leading and trailing `-` are stripped, the empty/`.`/`..` fallback is `"skill"`. Long results are truncated to 50 chars before the hash is appended.
|
|
106
147
|
|
|
107
|
-
The suffix is the first 10 hex characters of `sha256(trimmedInput)`.
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
148
|
+
The suffix is the first 10 hex characters of `sha256(trimmedInput)`. This is a
|
|
149
|
+
40-bit identifier, and the hash is not applied to every ID: short generated
|
|
150
|
+
outputs overlap with accepted literal inputs. Case variants can also share a
|
|
151
|
+
directory on case-insensitive filesystems. Distinct IDs can therefore alias
|
|
152
|
+
without a hash collision. Do not use this legacy encoder as an identity or
|
|
153
|
+
authorization boundary for untrusted IDs. Inputs that differ only by surrounding
|
|
154
|
+
whitespace intentionally map to the same output. Its output and the default
|
|
155
|
+
encoder selected by `resolveSafeInstallDir` remain unchanged for compatibility.
|
|
111
156
|
|
|
112
157
|
## Common patterns
|
|
113
158
|
|
|
114
159
|
### Install a plugin
|
|
115
160
|
|
|
116
161
|
```ts
|
|
117
|
-
import { resolveSafeInstallDir, assertCanonicalPathWithinBase,
|
|
162
|
+
import { resolveSafeInstallDir, assertCanonicalPathWithinBase, safePathSegmentHashedV2 } from "@openclaw/fs-safe/advanced";
|
|
118
163
|
import { extractArchive } from "@openclaw/fs-safe/archive";
|
|
119
164
|
import fs from "node:fs/promises";
|
|
120
165
|
|
|
121
166
|
const r = resolveSafeInstallDir({
|
|
122
|
-
baseDir: "/srv/plugins",
|
|
167
|
+
baseDir: "/srv/plugins-v2",
|
|
123
168
|
id: untrustedName,
|
|
124
169
|
invalidNameMessage: "invalid plugin name",
|
|
125
|
-
nameEncoder:
|
|
170
|
+
nameEncoder: safePathSegmentHashedV2,
|
|
126
171
|
});
|
|
127
172
|
if (!r.ok) return reply(400, r.error);
|
|
128
173
|
|
|
129
174
|
await fs.mkdir(r.path, { recursive: true, mode: 0o755 });
|
|
130
175
|
await assertCanonicalPathWithinBase({
|
|
131
|
-
baseDir: "/srv/plugins",
|
|
176
|
+
baseDir: "/srv/plugins-v2",
|
|
132
177
|
candidatePath: r.path,
|
|
133
178
|
boundaryLabel: "plugin install dir",
|
|
134
179
|
});
|
|
@@ -148,6 +193,7 @@ const snap = resolveSafeInstallDir({
|
|
|
148
193
|
baseDir: "/srv/snapshots",
|
|
149
194
|
id: `${runId}-${version}`,
|
|
150
195
|
invalidNameMessage: "invalid snapshot id",
|
|
196
|
+
nameEncoder: safePathSegmentHashedV2,
|
|
151
197
|
});
|
|
152
198
|
if (!snap.ok) throw new Error(snap.error);
|
|
153
199
|
await fs.mkdir(snap.path, { recursive: true });
|
package/docs/install.md
CHANGED
|
@@ -54,7 +54,10 @@ also rejects canonicalization when the addon or its canonicalizer is unavailable
|
|
|
54
54
|
Node and Windows use their existing runtime canonicalizers. On Windows, Bun's
|
|
55
55
|
recursive directory creation receives an absolute spelling that preserves raw
|
|
56
56
|
path components, working around its rejection of existing relative `.` and `..`
|
|
57
|
-
directories.
|
|
57
|
+
directories. Windows native descriptor-relative operations require Bun to expose
|
|
58
|
+
the paired libuv descriptor bridge from its host executable; a missing or partial
|
|
59
|
+
bridge fails explicitly with `ENOTSUP`. Public paths and caller-supplied filesystem
|
|
60
|
+
adapters remain unchanged.
|
|
58
61
|
|
|
59
62
|
The upstream fix is tracked in [Bun #42374](https://github.com/oven-sh/bun/pull/42374).
|
|
60
63
|
The adapter can be removed when the supported Bun baseline includes that fix.
|
|
@@ -124,9 +127,10 @@ the matching binary. Consumers do not run a native build, download code at
|
|
|
124
127
|
runtime, or execute a postinstall step. Omitting optional dependencies keeps
|
|
125
128
|
non-archive fallback-capable operations working in `auto` or `off`. Native-only
|
|
126
129
|
features, including strict owned-tree temp cleanup, retained-directory staging,
|
|
127
|
-
atomic `rename-noreplace
|
|
128
|
-
|
|
129
|
-
|
|
130
|
+
atomic `rename-noreplace` (including the default no-clobber `Root.move()`),
|
|
131
|
+
zstd/bzip2 TAR handling, and Windows private-directory creation, remain
|
|
132
|
+
unavailable. Operations without a safe fallback fail with `helper-unavailable`
|
|
133
|
+
when the matching package is absent, incompatible, or disabled.
|
|
130
134
|
|
|
131
135
|
Upgrading an existing 0.5 consumer? Follow [Migrating to 0.6](migrating-to-0.6.md)
|
|
132
136
|
before deploying with native mode `require` or native-only features.
|
|
@@ -135,15 +139,15 @@ before deploying with native mode `require` or native-only features.
|
|
|
135
139
|
|
|
136
140
|
The platform native binaries provide fd-relative open/link/mkdir primitives,
|
|
137
141
|
atomic no-replace rename, and file identity checks. The default is `auto`: use
|
|
138
|
-
the matching binary when it loads, otherwise
|
|
139
|
-
|
|
140
|
-
|
|
142
|
+
the matching binary when it loads, otherwise use the guarded JavaScript path
|
|
143
|
+
where a safe fallback exists. Native-only operations fail with
|
|
144
|
+
`helper-unavailable`.
|
|
141
145
|
|
|
142
146
|
```ts
|
|
143
147
|
import { configureFsSafeNative } from "@openclaw/fs-safe/config";
|
|
144
148
|
|
|
145
149
|
configureFsSafeNative({ mode: "auto" }); // default
|
|
146
|
-
configureFsSafeNative({ mode: "off" }); // guarded JavaScript only
|
|
150
|
+
configureFsSafeNative({ mode: "off" }); // guarded JavaScript; reject native-only operations
|
|
147
151
|
configureFsSafeNative({ mode: "require" }); // fail closed if unavailable
|
|
148
152
|
```
|
|
149
153
|
|
package/docs/json-store.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# JSON store
|
|
2
2
|
|
|
3
|
-
`jsonStore` is exported from `@openclaw/fs-safe/store`. It is the
|
|
3
|
+
`jsonStore` is exported from `@openclaw/fs-safe/store`. It is the single-path
|
|
4
4
|
convenience wrapper for `fileStore(...).json(...)`: a small read-modify-write
|
|
5
5
|
handle around a single JSON file. It bakes in atomic writes, explicit fallback
|
|
6
6
|
reads, and optional cross-process locking via
|
|
@@ -71,6 +71,10 @@ type JsonStore<T> = {
|
|
|
71
71
|
|
|
72
72
|
`jsonStore({ filePath })` resolves `rootDir = dirname(filePath)` and calls
|
|
73
73
|
`fileStore({ rootDir, private: true }).json(basename(filePath), options)`.
|
|
74
|
+
On Windows, the factory rejects NTFS alternate-stream and directory-index
|
|
75
|
+
namespace spellings before preparing its private parent. An ordinary
|
|
76
|
+
drive-relative path is anchored at entry and `store.filePath` exposes the
|
|
77
|
+
resulting absolute path; ordinary colon-bearing POSIX paths remain valid.
|
|
74
78
|
|
|
75
79
|
`durable: false` keeps sibling-temp replace/rename behavior but skips the
|
|
76
80
|
temp-file and parent-directory `fsync` calls. Use it only for reconstructible
|
package/docs/json.md
CHANGED
|
@@ -38,6 +38,17 @@ Use `readJson` when missing-or-malformed is a programmer error you want to surfa
|
|
|
38
38
|
|
|
39
39
|
`JsonFileReadError` carries `cause` so you can inspect whether the underlying failure was an `ENOENT`, a `SyntaxError`, or something else.
|
|
40
40
|
|
|
41
|
+
On Windows, filesystem path inputs reject NTFS alternate-stream and
|
|
42
|
+
directory-index namespace spellings such as `file:stream` and
|
|
43
|
+
`dir::$INDEX_ALLOCATION`; ordinary colon-bearing POSIX names remain valid.
|
|
44
|
+
Standalone JSON writers preserve their released support for an ordinary
|
|
45
|
+
drive-relative destination by anchoring its leading drive designator at entry
|
|
46
|
+
without normalizing the remaining suffix. Any additional colon is still
|
|
47
|
+
rejected before filesystem access.
|
|
48
|
+
Strict standalone readers retain `JsonFileReadError` and expose the
|
|
49
|
+
`invalid-path` rejection as its cause, lenient `tryReadJson*` calls return
|
|
50
|
+
`null`, and root-bounded readers report an `open`/`validation` failure.
|
|
51
|
+
|
|
41
52
|
## Reading
|
|
42
53
|
|
|
43
54
|
### `readJson<T>(filePath, options?)`
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Hosted mutation-policy proof
|
|
2
|
+
|
|
3
|
+
The `mutation policy public behavior proof` workflow builds the exact event-head
|
|
4
|
+
package and host addon on Node 24 Linux, macOS, and Windows. Each case executes in
|
|
5
|
+
a fresh process, uses a private temporary fixture, and emits only a bounded,
|
|
6
|
+
canonical receipt. POSIX workers require both real and effective non-root UIDs.
|
|
7
|
+
|
|
8
|
+
Receipt schema `fs-safe-mutation-policy-proof-v2` deliberately does not describe
|
|
9
|
+
final directory emptiness as a mutation-dispatch count. Root replacement records
|
|
10
|
+
the unchanged contents of the original and replacement parent; denied redirect
|
|
11
|
+
records rejection and unchanged denied/displaced parents. The existing JS mkdir
|
|
12
|
+
counters name their exact child or next component. None counts native syscalls
|
|
13
|
+
or excludes transient mutations that leave no final trace.
|
|
14
|
+
|
|
15
|
+
## Additional hosted cases
|
|
16
|
+
|
|
17
|
+
Each POSIX write/create/copy worker, under both native-off and native-require,
|
|
18
|
+
performs an eligible success control, an exact deeper-parent denial after an
|
|
19
|
+
earlier parent is created, a denied redirect after preflight, and a stale-parent
|
|
20
|
+
replacement at the public pre-mkdir authority boundary. Exact directory listings
|
|
21
|
+
and sentinels verify that denied/current/displaced parents contain no prohibited
|
|
22
|
+
child, target, or stage. Copy sources must retain their original bytes.
|
|
23
|
+
|
|
24
|
+
Redirect injection uses the existing public `@openclaw/fs-safe/test-hooks` subpath
|
|
25
|
+
from `dist`, not a mocked native binding. Only these isolated workers enable
|
|
26
|
+
`NODE_ENV=test`. The exact target and single hook visit are required. The hook is
|
|
27
|
+
after policy preflight but before parent admission; it must not be described as
|
|
28
|
+
a post-parent-admission hook. Stale replacement instead uses the public authority
|
|
29
|
+
callback after child-create policy admission, with an existing sentinel parent
|
|
30
|
+
and a still-missing child. Both implementation files that enforce the following
|
|
31
|
+
freshness check are hash-bound. No unchecked callback ordinal chooses a fault.
|
|
32
|
+
|
|
33
|
+
Representative POSIX `Root.write` workers refuse authority before the first mkdir,
|
|
34
|
+
after one parent has been created and before the next mkdir, before staging, and
|
|
35
|
+
immediately before publication. Epoch selection uses actual fixture state. The
|
|
36
|
+
publication refusal requires a real single-link, caller-owned mode-0600 stage
|
|
37
|
+
containing the complete payload while the destination still contains its original
|
|
38
|
+
sentinel. The same rejection object must escape, no callback may follow refusal,
|
|
39
|
+
and the owned stage must be gone before fixture teardown.
|
|
40
|
+
|
|
41
|
+
Windows workers exercise buffer `Root.write` through a stable final-file symlink,
|
|
42
|
+
then refuse before staging a newly created placeholder, before publishing over a
|
|
43
|
+
new placeholder, and before publishing to an existing symlink-selected destination.
|
|
44
|
+
They verify alias binding, destination preservation, observed placeholder/stage
|
|
45
|
+
states, and cleanup before fixture teardown. The default native-off and explicit
|
|
46
|
+
`verify-content-with-lock` native-require configurations both select the existing
|
|
47
|
+
Windows JS buffer writer. The compatibility route is expected not to load the
|
|
48
|
+
addon; the receipt does not mislabel this as native publication or evidence that
|
|
49
|
+
content-verification fallback or lock contention was exercised.
|
|
50
|
+
|
|
51
|
+
## Bounds and interpretation
|
|
52
|
+
|
|
53
|
+
The receipt remains below 32 KiB, each worker has a 15-second process timeout and
|
|
54
|
+
4-KiB stdout limit, and the proof step has a six-minute outer timeout. Worker I/O
|
|
55
|
+
and cleanup retain their existing deadlines. The canonical pending/failed receipt
|
|
56
|
+
is preserved if setup, a worker, provenance validation, or emission fails.
|
|
57
|
+
Source, built modules, the public test seam, harness helper, contract tests, and
|
|
58
|
+
host addon are hashed and checked again after workers.
|
|
59
|
+
|
|
60
|
+
These are deterministic representative observations, not an exhaustive race proof
|
|
61
|
+
or syscall audit. Existing hash-bound internal tests remain complementary for
|
|
62
|
+
other awaited-admission, receipt-refresh, and observation-failure interleavings.
|
|
63
|
+
Windows root replacement and POSIX-specific pinned routes are not claimed on
|
|
64
|
+
Windows. Hosted CI and exact artifact inspection are required before relying on
|
|
65
|
+
new receipts; the proof supplies no performance release clearance.
|
package/docs/native-helper.md
CHANGED
|
@@ -15,7 +15,7 @@ consumer Rust build.
|
|
|
15
15
|
import { configureFsSafeNative } from "@openclaw/fs-safe/config";
|
|
16
16
|
|
|
17
17
|
configureFsSafeNative({ mode: "auto" }); // default
|
|
18
|
-
configureFsSafeNative({ mode: "off" }); // guarded JavaScript only
|
|
18
|
+
configureFsSafeNative({ mode: "off" }); // guarded JavaScript; reject native-only operations
|
|
19
19
|
configureFsSafeNative({ mode: "require" }); // fail closed when the binding is unavailable
|
|
20
20
|
```
|
|
21
21
|
|
|
@@ -25,8 +25,8 @@ The equivalent environment variables are `FS_SAFE_NATIVE_MODE` and `OPENCLAW_FS_
|
|
|
25
25
|
|
|
26
26
|
| Mode | Behavior |
|
|
27
27
|
|---|---|
|
|
28
|
-
| `auto` | Prefer native primitives when the current platform package loads; otherwise
|
|
29
|
-
| `off` | Do not load a native package. Use
|
|
28
|
+
| `auto` | Prefer native primitives when the current platform package loads; otherwise use guarded JavaScript where a safe fallback exists and reject native-only operations. |
|
|
29
|
+
| `off` | Do not load a native package. Use guarded JavaScript where safe and reject native-only operations deterministically. |
|
|
30
30
|
| `require` | Throw `FsSafeError("helper-unavailable")` instead of falling back when an operation needs the native binding and it cannot load. |
|
|
31
31
|
|
|
32
32
|
TAR/gzip in the guarded JavaScript path uses a bundled, import-free WASM build
|
|
@@ -62,6 +62,20 @@ change the mode policy of existing fallback-capable APIs.
|
|
|
62
62
|
|
|
63
63
|
## Native boundary
|
|
64
64
|
|
|
65
|
+
The internal Darwin descriptor ACL inspector requires its matching native
|
|
66
|
+
capability in both `auto` and `require`; `off`, a missing package, or an older
|
|
67
|
+
binding without `inspectDarwinAcl` rejects with `helper-unavailable`. Inspection
|
|
68
|
+
failure or malformed facts reject with `permission-unverified`; there is no
|
|
69
|
+
mode-bit or pathname fallback for this capability. Clone admission uses a fused
|
|
70
|
+
descriptor-bound metadata and ACL observation, then compares immutable receipts
|
|
71
|
+
with fresh no-follow pathname identity fences; pathnames never authorize ACL
|
|
72
|
+
state. The payload ACL-clear readback is part of that fused observation. Once
|
|
73
|
+
a clone payload exists, normalization and verification failures become terminal
|
|
74
|
+
`EIO` errors (with the underlying status and detail retained), not capability
|
|
75
|
+
signals that permit an ordinary-copy retry. Checked cleanup cannot undo that
|
|
76
|
+
terminal classification.
|
|
77
|
+
This addition does not change other APIs' native-mode or permission contracts.
|
|
78
|
+
|
|
65
79
|
The native layer exposes policy-free filesystem mechanisms: beneath-root
|
|
66
80
|
open/mkdir/link, replace and no-replace rename, identity reads, archive decode/execution,
|
|
67
81
|
clone/copy/hash workers, POSIX canonicalization, and Windows security descriptor calls. The TypeScript
|
|
@@ -70,12 +84,17 @@ normalization, and the decision to fall back.
|
|
|
70
84
|
|
|
71
85
|
- Linux uses `openat2` with `RESOLVE_BENEATH | RESOLVE_NO_MAGICLINKS`, `renameat`, and `renameat2(RENAME_NOREPLACE)`. Owned-tree cleanup enumerates and unlinks through retained directory descriptors and rejects device crossings.
|
|
72
86
|
- macOS 15.4 and newer prefer `O_RESOLVE_BENEATH`; older kernels resolve components with `O_NOFOLLOW` and restart in-root symlinks from the pinned root descriptor. Both routes use an `F_GETPATH` post-open escape detector and report `best-effort` because directory rename races are not atomic with that check. Publication uses `renameat` for replacement and `renameatx_np(RENAME_EXCL)` for no-replace; owned-tree cleanup uses descriptor-relative `openat`/`unlinkat`.
|
|
73
|
-
- Windows uses handle-relative `NtCreateFile`, rejects reparse points during root-bounded traversal, uses `FileRenameInfoEx` with replacement selected explicitly by the TypeScript policy layer, and deletes owned trees through exact opened handles with `FileDispositionInfoEx`; symlink/reparse entries in owned trees are removed as leaves and never traversed.
|
|
74
|
-
|
|
75
|
-
Native primitives back create-only and replacing pinned writes,
|
|
76
|
-
guarded publication, archive acceleration,
|
|
77
|
-
|
|
78
|
-
|
|
87
|
+
- Windows uses handle-relative `NtCreateFile`, rejects reparse points during root-bounded traversal, uses `FileRenameInfoEx` with replacement selected explicitly by the TypeScript policy layer, and deletes owned trees through exact opened handles with `FileDispositionInfoEx`; symlink/reparse entries in owned trees are removed as leaves and never traversed. Descriptors crossing N-API are converted only by the host executable's paired `uv_get_osfhandle` and `uv_open_osfhandle` exports. A runtime without both exports is unsupported for these native operations; the binding never guesses a raw HANDLE or uses a foreign CRT descriptor table.
|
|
88
|
+
|
|
89
|
+
Native primitives back create-only and replacing pinned writes, no-clobber
|
|
90
|
+
`Root.move()`, async sidecar creation, guarded publication, archive acceleration,
|
|
91
|
+
and direct Windows ACL operations. Windows secure-file reads require
|
|
92
|
+
descriptor-bound owner/DACL facts from the current helper; they do not use the
|
|
93
|
+
standalone pathname inspector's command fallback. No-clobber moves fail with
|
|
94
|
+
`helper-unavailable` when descriptor-relative parent admission or the atomic
|
|
95
|
+
no-replace rename is unavailable; they never use a check followed by a replacing
|
|
96
|
+
rename. Equivalent JavaScript paths remain available for documented
|
|
97
|
+
fallback-capable features. See [Native architecture](native.md#javascript-fallback-guarantees-and-delta)
|
|
79
98
|
for the exact difference.
|
|
80
99
|
|
|
81
100
|
The guarded JavaScript mutation path is detection-based, not containment-atomic.
|