@openclaw/feishu 2026.9.8 → 2026.10.1-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.setup/{channel-DV4hM2Uf.mjs → channel-CX62T71S.mjs} +146 -245
- package/dist/.setup/{channel.runtime-CXQcH5zZ.mjs → channel.runtime-COiaq-Rv.mjs} +6 -12
- package/dist/.setup/{chat-D8MlFAZi.mjs → chat-BdyvImsY.mjs} +6 -9
- package/dist/.setup/{client-BYG-_IZl.mjs → client-BsBVWJzN.mjs} +1 -1
- package/dist/.setup/{doctor-contract-3XC0vbIw.mjs → doctor-contract-B1B9ogdd.mjs} +212 -27
- package/dist/.setup/{monitor-D_rsHBZ6.mjs → monitor-7DvwUs4Y.mjs} +5 -6
- package/dist/.setup/{monitor.account-DvvY7i6k.mjs → monitor.account-7xVu7XWY.mjs} +190 -393
- package/dist/.setup/{probe-_ViqSi0j.mjs → probe-B9o7iey6.mjs} +13 -39
- package/dist/.setup/{reply-delivery-result-COAjaukc.mjs → reply-delivery-result-Bt-BWBEA.mjs} +151 -297
- package/dist/.setup/{setup-api-C4S5I3ac.mjs → setup-api-DmzX9S01.mjs} +1 -1
- package/dist/.setup/{subagent-hooks-BaFoMu8W.mjs → subagent-hooks-CUXg0pC-.mjs} +5 -11
- package/dist/.setup/{thread-bindings-EdLXPXzw.mjs → thread-bindings-jW2x5VuN.mjs} +23 -41
- package/dist/api.js +234 -297
- package/dist/channel-plugin-api.js +1 -1
- package/dist/doctor-contract-api.js +2 -2
- package/dist/session-binding-contract-api.js +1 -1
- package/dist/setup-api.js +1 -1
- package/dist/setup-entry.js +1 -1
- package/dist/subagent-hooks-api.js +1 -1
- package/node_modules/@openclaw/fs-safe/CHANGELOG.md +95 -0
- package/node_modules/@openclaw/fs-safe/README.md +70 -379
- package/node_modules/@openclaw/fs-safe/dist/absolute-path.js +35 -51
- package/node_modules/@openclaw/fs-safe/dist/advanced.d.ts +2 -0
- package/node_modules/@openclaw/fs-safe/dist/advanced.js +1 -0
- package/node_modules/@openclaw/fs-safe/dist/archive-input.js +2 -0
- package/node_modules/@openclaw/fs-safe/dist/archive-parser.wasm +0 -0
- package/node_modules/@openclaw/fs-safe/dist/archive-staging.d.ts +1 -1
- package/node_modules/@openclaw/fs-safe/dist/archive-staging.js +9 -10
- package/node_modules/@openclaw/fs-safe/dist/archive.js +2 -5
- package/node_modules/@openclaw/fs-safe/dist/atomic-io.d.ts +51 -0
- package/node_modules/@openclaw/fs-safe/dist/atomic-io.js +242 -0
- package/node_modules/@openclaw/fs-safe/dist/config.d.ts +1 -1
- package/node_modules/@openclaw/fs-safe/dist/config.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/copy-file-input.d.ts +1 -1
- package/node_modules/@openclaw/fs-safe/dist/copy-file-input.js +2 -2
- package/node_modules/@openclaw/fs-safe/dist/copy-tree-portable.js +2 -0
- package/node_modules/@openclaw/fs-safe/dist/create.js +12 -5
- package/node_modules/@openclaw/fs-safe/dist/directory-guard.d.ts +11 -0
- package/node_modules/@openclaw/fs-safe/dist/directory-guard.js +4 -7
- package/node_modules/@openclaw/fs-safe/dist/entry-publication-types.d.ts +55 -0
- package/node_modules/@openclaw/fs-safe/dist/entry-publication-types.js +1 -0
- package/node_modules/@openclaw/fs-safe/dist/entry-publication.d.ts +8 -0
- package/node_modules/@openclaw/fs-safe/dist/entry-publication.js +262 -0
- package/node_modules/@openclaw/fs-safe/dist/exclusive-create.d.ts +3 -0
- package/node_modules/@openclaw/fs-safe/dist/exclusive-create.js +22 -0
- package/node_modules/@openclaw/fs-safe/dist/file-cleanup.d.ts +1 -6
- package/node_modules/@openclaw/fs-safe/dist/file-cleanup.js +3 -6
- package/node_modules/@openclaw/fs-safe/dist/file-identity.js +3 -6
- package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root-io.js +2 -2
- package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root-mutation.js +9 -0
- package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root.js +4 -6
- package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-stale-admission.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/file-lock-sync.js +8 -0
- package/node_modules/@openclaw/fs-safe/dist/file-store-boundary.js +2 -0
- package/node_modules/@openclaw/fs-safe/dist/file-store-sync-write.js +20 -18
- package/node_modules/@openclaw/fs-safe/dist/file-store.js +4 -10
- package/node_modules/@openclaw/fs-safe/dist/fs.d.ts +2 -10
- package/node_modules/@openclaw/fs-safe/dist/fs.js +2 -10
- package/node_modules/@openclaw/fs-safe/dist/guarded-mkdir.d.ts +3 -4
- package/node_modules/@openclaw/fs-safe/dist/guarded-mkdir.js +6 -18
- package/node_modules/@openclaw/fs-safe/dist/guest-dispatch-python.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/guest-native-python.js +0 -2
- package/node_modules/@openclaw/fs-safe/dist/guest.js +3 -9
- package/node_modules/@openclaw/fs-safe/dist/index.d.ts +1 -1
- package/node_modules/@openclaw/fs-safe/dist/index.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/json-durable-queue-directory.js +1 -5
- package/node_modules/@openclaw/fs-safe/dist/json.js +19 -37
- package/node_modules/@openclaw/fs-safe/dist/move-path.js +2 -0
- package/node_modules/@openclaw/fs-safe/dist/mutation-authority.d.ts +1 -0
- package/node_modules/@openclaw/fs-safe/dist/mutation-authority.js +9 -1
- package/node_modules/@openclaw/fs-safe/dist/native-binding.d.ts +47 -2
- package/node_modules/@openclaw/fs-safe/dist/native-config.d.ts +1 -9
- package/node_modules/@openclaw/fs-safe/dist/native-config.js +6 -40
- package/node_modules/@openclaw/fs-safe/dist/native-parent-admission.d.ts +2 -0
- package/node_modules/@openclaw/fs-safe/dist/native-parent-admission.js +5 -2
- package/node_modules/@openclaw/fs-safe/dist/native-pinned-write.js +11 -316
- package/node_modules/@openclaw/fs-safe/dist/native-policy-parent-windows.d.ts +7 -7
- package/node_modules/@openclaw/fs-safe/dist/native-policy-parent-windows.js +28 -175
- package/node_modules/@openclaw/fs-safe/dist/native-policy-parent.d.ts +15 -0
- package/node_modules/@openclaw/fs-safe/dist/native-policy-parent.js +418 -0
- package/node_modules/@openclaw/fs-safe/dist/native-rename-outcome.js +9 -4
- package/node_modules/@openclaw/fs-safe/dist/native-staged-file.js +17 -13
- package/node_modules/@openclaw/fs-safe/dist/native-staged-symlink.js +6 -24
- package/node_modules/@openclaw/fs-safe/dist/native.js +5 -1
- package/node_modules/@openclaw/fs-safe/dist/path-case.js +10 -9
- package/node_modules/@openclaw/fs-safe/dist/path-scope-lexical.js +4 -1
- package/node_modules/@openclaw/fs-safe/dist/path-segment-route.d.ts +1 -0
- package/node_modules/@openclaw/fs-safe/dist/path-segment-route.js +3 -0
- package/node_modules/@openclaw/fs-safe/dist/path.js +4 -1
- package/node_modules/@openclaw/fs-safe/dist/permissions-windows.d.ts +0 -1
- package/node_modules/@openclaw/fs-safe/dist/pinned-mutation-admission.js +1 -3
- package/node_modules/@openclaw/fs-safe/dist/pinned-write-staged.js +2 -0
- package/node_modules/@openclaw/fs-safe/dist/pinned-write.js +9 -10
- package/node_modules/@openclaw/fs-safe/dist/private-producer-handoff.js +3 -6
- package/node_modules/@openclaw/fs-safe/dist/publish-copy-stage.js +2 -2
- package/node_modules/@openclaw/fs-safe/dist/publish-file.js +4 -3
- package/node_modules/@openclaw/fs-safe/dist/replace-file-buffer.d.ts +3 -4
- package/node_modules/@openclaw/fs-safe/dist/replace-file-buffer.js +18 -32
- package/node_modules/@openclaw/fs-safe/dist/replace-file-copy-fallback.d.ts +5 -17
- package/node_modules/@openclaw/fs-safe/dist/replace-file-copy-fallback.js +92 -230
- package/node_modules/@openclaw/fs-safe/dist/replace-file-copy-source.d.ts +4 -15
- package/node_modules/@openclaw/fs-safe/dist/replace-file-copy-source.js +22 -63
- package/node_modules/@openclaw/fs-safe/dist/replace-file-descriptor.d.ts +9 -34
- package/node_modules/@openclaw/fs-safe/dist/replace-file-descriptor.js +78 -83
- package/node_modules/@openclaw/fs-safe/dist/replace-file-destination.d.ts +18 -20
- package/node_modules/@openclaw/fs-safe/dist/replace-file-destination.js +64 -98
- package/node_modules/@openclaw/fs-safe/dist/replace-file-temp-owner.d.ts +16 -33
- package/node_modules/@openclaw/fs-safe/dist/replace-file-temp-owner.js +61 -200
- package/node_modules/@openclaw/fs-safe/dist/replace-file-types.d.ts +1 -6
- package/node_modules/@openclaw/fs-safe/dist/replace-file.js +76 -216
- package/node_modules/@openclaw/fs-safe/dist/root-boundary.js +14 -24
- package/node_modules/@openclaw/fs-safe/dist/root-context.d.ts +1 -4
- package/node_modules/@openclaw/fs-safe/dist/root-context.js +4 -19
- package/node_modules/@openclaw/fs-safe/dist/root-create-native.d.ts +52 -0
- package/node_modules/@openclaw/fs-safe/dist/root-create-native.js +350 -0
- package/node_modules/@openclaw/fs-safe/dist/root-directory-list-types.d.ts +35 -0
- package/node_modules/@openclaw/fs-safe/dist/root-directory-list-types.js +1 -0
- package/node_modules/@openclaw/fs-safe/dist/root-directory-list.d.ts +2 -27
- package/node_modules/@openclaw/fs-safe/dist/root-directory-list.js +31 -28
- package/node_modules/@openclaw/fs-safe/dist/root-directory.js +1 -4
- package/node_modules/@openclaw/fs-safe/dist/root-errors.js +34 -0
- package/node_modules/@openclaw/fs-safe/dist/root-file.js +9 -21
- package/node_modules/@openclaw/fs-safe/dist/root-impl.js +105 -37
- package/node_modules/@openclaw/fs-safe/dist/root-move-noreplace.d.ts +7 -2
- package/node_modules/@openclaw/fs-safe/dist/root-move-noreplace.js +67 -38
- package/node_modules/@openclaw/fs-safe/dist/root-observed-path.d.ts +1 -1
- package/node_modules/@openclaw/fs-safe/dist/root-observed-path.js +0 -2
- package/node_modules/@openclaw/fs-safe/dist/root-options.d.ts +2 -3
- package/node_modules/@openclaw/fs-safe/dist/root-path-existing.js +3 -1
- package/node_modules/@openclaw/fs-safe/dist/root-path-stat.js +68 -85
- package/node_modules/@openclaw/fs-safe/dist/root-path.js +6 -1
- package/node_modules/@openclaw/fs-safe/dist/root-paths-lexical.d.ts +4 -0
- package/node_modules/@openclaw/fs-safe/dist/root-paths-lexical.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/root-paths.js +4 -9
- package/node_modules/@openclaw/fs-safe/dist/root-remove-native.d.ts +4 -0
- package/node_modules/@openclaw/fs-safe/dist/root-remove-native.js +311 -0
- package/node_modules/@openclaw/fs-safe/dist/root-remove.d.ts +11 -0
- package/node_modules/@openclaw/fs-safe/dist/root-remove.js +3 -1
- package/node_modules/@openclaw/fs-safe/dist/root-walk.d.ts +2 -2
- package/node_modules/@openclaw/fs-safe/dist/root-walk.js +1 -3
- package/node_modules/@openclaw/fs-safe/dist/root-write-admission.d.ts +3 -3
- package/node_modules/@openclaw/fs-safe/dist/root-write-admission.js +8 -10
- package/node_modules/@openclaw/fs-safe/dist/safe-path-segment.d.ts +0 -1
- package/node_modules/@openclaw/fs-safe/dist/safe-path-segment.js +0 -4
- package/node_modules/@openclaw/fs-safe/dist/secret-file.js +12 -9
- package/node_modules/@openclaw/fs-safe/dist/secure-temp-dir.d.ts +0 -2
- package/node_modules/@openclaw/fs-safe/dist/sibling-staged-file.js +2 -0
- package/node_modules/@openclaw/fs-safe/dist/sibling-temp.js +7 -4
- package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-acquire.js +13 -6
- package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-reclaim.d.ts +0 -3
- package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-reclaim.js +5 -23
- package/node_modules/@openclaw/fs-safe/dist/staged-file-settlement.d.ts +2 -1
- package/node_modules/@openclaw/fs-safe/dist/staged-file-settlement.js +11 -6
- package/node_modules/@openclaw/fs-safe/dist/temp-cleanup.js +1 -8
- package/node_modules/@openclaw/fs-safe/dist/temp-workspace-child-admission.d.ts +1 -1
- package/node_modules/@openclaw/fs-safe/dist/temp-workspace-child-admission.js +2 -9
- package/node_modules/@openclaw/fs-safe/dist/text-atomic.d.ts +1 -6
- package/node_modules/@openclaw/fs-safe/dist/trash.js +6 -2
- package/node_modules/@openclaw/fs-safe/dist/walk.js +100 -46
- package/node_modules/@openclaw/fs-safe/dist/watch-hints.d.ts +7 -0
- package/node_modules/@openclaw/fs-safe/dist/watch-hints.js +235 -19
- package/node_modules/@openclaw/fs-safe/dist/watch-native.d.ts +21 -2
- package/node_modules/@openclaw/fs-safe/dist/watch-native.js +21 -3
- package/node_modules/@openclaw/fs-safe/dist/watch-rescan.d.ts +6 -0
- package/node_modules/@openclaw/fs-safe/dist/watch-rescan.js +111 -0
- package/node_modules/@openclaw/fs-safe/dist/watch-scan.d.ts +13 -0
- package/node_modules/@openclaw/fs-safe/dist/watch-scan.js +27 -4
- package/node_modules/@openclaw/fs-safe/dist/watch-stream.js +2 -0
- package/node_modules/@openclaw/fs-safe/dist/watch.js +131 -40
- package/node_modules/@openclaw/fs-safe/dist/windows-path-alias.d.ts +8 -0
- package/node_modules/@openclaw/fs-safe/dist/windows-path-alias.js +24 -6
- package/node_modules/@openclaw/fs-safe/dist/windows-path-syntax.d.ts +13 -0
- package/node_modules/@openclaw/fs-safe/dist/windows-path-syntax.js +51 -0
- package/node_modules/@openclaw/fs-safe/docs/advanced.md +4 -0
- package/node_modules/@openclaw/fs-safe/docs/atomic.md +4 -1
- package/node_modules/@openclaw/fs-safe/docs/config.md +6 -14
- package/node_modules/@openclaw/fs-safe/docs/contributing.md +4 -14
- package/node_modules/@openclaw/fs-safe/docs/durability.md +6 -29
- package/node_modules/@openclaw/fs-safe/docs/entry-publication.md +146 -0
- package/node_modules/@openclaw/fs-safe/docs/errors.md +16 -0
- package/node_modules/@openclaw/fs-safe/docs/file-store.md +28 -5
- package/node_modules/@openclaw/fs-safe/docs/index.md +2 -31
- package/node_modules/@openclaw/fs-safe/docs/install.md +10 -31
- package/node_modules/@openclaw/fs-safe/docs/json.md +1 -1
- package/node_modules/@openclaw/fs-safe/docs/local-roots.md +0 -1
- package/node_modules/@openclaw/fs-safe/docs/migrating-to-0.5.md +12 -12
- package/node_modules/@openclaw/fs-safe/docs/native-helper.md +35 -96
- package/node_modules/@openclaw/fs-safe/docs/native.md +22 -20
- package/node_modules/@openclaw/fs-safe/docs/path.md +1 -1
- package/node_modules/@openclaw/fs-safe/docs/permissions.md +1 -2
- package/node_modules/@openclaw/fs-safe/docs/public-api.md +1 -2
- package/node_modules/@openclaw/fs-safe/docs/reading.md +1 -2
- package/node_modules/@openclaw/fs-safe/docs/root.md +26 -184
- package/node_modules/@openclaw/fs-safe/docs/secret-file.md +1 -1
- package/node_modules/@openclaw/fs-safe/docs/security-model.md +115 -3
- package/node_modules/@openclaw/fs-safe/docs/store.md +2 -2
- package/node_modules/@openclaw/fs-safe/docs/temp.md +29 -65
- package/node_modules/@openclaw/fs-safe/docs/testing.md +90 -58
- package/node_modules/@openclaw/fs-safe/docs/types.md +3 -16
- package/node_modules/@openclaw/fs-safe/docs/walk.md +33 -5
- package/node_modules/@openclaw/fs-safe/docs/watch.md +96 -14
- package/node_modules/@openclaw/fs-safe/docs/writing.md +172 -16
- package/node_modules/@openclaw/fs-safe/package.json +10 -10
- package/node_modules/@openclaw/fs-safe-darwin-arm64/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-darwin-arm64/package.json +1 -1
- package/node_modules/@openclaw/fs-safe-darwin-x64/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-darwin-x64/package.json +1 -1
- package/node_modules/@openclaw/fs-safe-linux-arm64-gnu/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-linux-arm64-gnu/package.json +1 -1
- package/node_modules/@openclaw/fs-safe-linux-arm64-musl/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-linux-arm64-musl/package.json +1 -1
- package/node_modules/@openclaw/fs-safe-linux-x64-gnu/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-linux-x64-gnu/package.json +1 -1
- package/node_modules/@openclaw/fs-safe-linux-x64-musl/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-linux-x64-musl/package.json +1 -1
- package/node_modules/@openclaw/fs-safe-win32-x64-msvc/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-win32-x64-msvc/package.json +1 -1
- package/package.json +5 -5
- package/skills/feishu-wiki/SKILL.md +1 -1
- package/dist/.setup/accounts-wRqItHug.mjs +0 -206
- package/node_modules/@openclaw/fs-safe/dist/watch-alias.d.ts +0 -6
- package/node_modules/@openclaw/fs-safe/dist/watch-alias.js +0 -88
- package/node_modules/@openclaw/fs-safe/docs/mutation-policy-proof.md +0 -69
- package/node_modules/@openclaw/fs-safe/docs/private-file-store.md +0 -68
- package/node_modules/@openclaw/fs-safe/docs/test-hooks.md +0 -110
|
@@ -49,6 +49,24 @@ If you need full sandboxing, run the worker under reduced privileges (uid, conta
|
|
|
49
49
|
|
|
50
50
|
Every path is resolved against the canonicalized real path of the root, then checked at that boundary. On Windows, an exact-case structural Root prefix stays on the lexical fast path; a prefix accepted only by case folding must have the Root's exact directory identity and is rebased onto the trusted Root spelling before use. Alias resolution walks components before applying a later `..`, so a symlink cannot change what that parent segment means after validation. Parent traversal, an absolute spelling, or any alias whose canonical result is outside the root throws `outside-workspace`; absolute spellings that remain inside the root are accepted.
|
|
51
51
|
|
|
52
|
+
On Windows, Root, root-file readers, `pathScope`, `isPathInside`, secret-file
|
|
53
|
+
writers, sibling-temp output, trash admission, and archive output preparation
|
|
54
|
+
reject foreign share or device roots before filesystem access can contact an
|
|
55
|
+
attacker-chosen host. Shares are compared by exact host and share name with
|
|
56
|
+
ASCII-only case folding, so Unicode case folding (for example the Kelvin sign
|
|
57
|
+
for `k`) cannot make another host look trusted. Namespaced drive roots keep
|
|
58
|
+
their existing handling, and explicitly selected shares, such as a Root,
|
|
59
|
+
allowed trash root, or destination on `\\server\share`, remain supported.
|
|
60
|
+
`\\?\` and `\\.\` spellings under `GLOBALROOT`/`Global`, or whose components
|
|
61
|
+
Windows would rewrite (empty or trailing-dot/space authority components, or
|
|
62
|
+
dot segments climbing above the drive, share, or device), are foreign unless
|
|
63
|
+
spelled exactly under the trusted boundary, because their target cannot be
|
|
64
|
+
identified from the spelling.
|
|
65
|
+
|
|
66
|
+
An absolute path on a different share or device is rejected even when a
|
|
67
|
+
filesystem alias would resolve it back inside the boundary. Pass a path
|
|
68
|
+
relative to the boundary, or spell it on the boundary's own share.
|
|
69
|
+
|
|
52
70
|
Guarded pathname APIs reject Windows `:` namespace aliases before normalization
|
|
53
71
|
or filesystem access. The only colon admitted in a Windows filesystem path is
|
|
54
72
|
the rooted ASCII drive designator (including extended-drive syntax); relative
|
|
@@ -73,6 +91,19 @@ publication, and cleanup. File writers preserve the raw suffix. Root-relative
|
|
|
73
91
|
APIs and caller-constructed relative directory receipts continue to reject
|
|
74
92
|
drive designators.
|
|
75
93
|
|
|
94
|
+
### Exclusive creation on Windows
|
|
95
|
+
|
|
96
|
+
Windows Node exclusive creation can follow a dangling file symlink before an
|
|
97
|
+
opened-descriptor check can reject it. Fallback creators therefore inspect the
|
|
98
|
+
final leaf before opening it, including create-only Root writes, standalone
|
|
99
|
+
creators, exclusive copy publication, lock records, and internal staging files. This preserves an unchanged
|
|
100
|
+
link and its missing target, including a target outside the intended directory.
|
|
101
|
+
Random staging names and freshly created workspaces reduce the opportunity to
|
|
102
|
+
preplace such a link, but are not a replacement for this check. The preflight is
|
|
103
|
+
best-effort: a concurrent replacement between inspection and open can still
|
|
104
|
+
redirect a pathname operation in native `auto`/`off` mode. Use the documented
|
|
105
|
+
native `require` creation paths and OS isolation when hostile concurrency is in scope.
|
|
106
|
+
|
|
76
107
|
### Symlinks (read side)
|
|
77
108
|
|
|
78
109
|
`open()` and `read()` use `fs.open` with `O_NOFOLLOW` on POSIX where available. The library then `fstat`s the open fd and `realpath`s the original input, asserting the two refer to the same inode. A symlink swap that happens between resolve and open will fail at the identity check rather than silently following the new target.
|
|
@@ -149,7 +180,24 @@ final component again after awaited staging and parent fences, immediately befor
|
|
|
149
180
|
the rename or exclusive open. These are best-effort symlink checks, not an atomic
|
|
150
181
|
expected-entry/CAS replacement: a concurrent process can still replace the final
|
|
151
182
|
entry between its check and rename. Existing parent containment guarantees remain
|
|
152
|
-
as described below. Omitting `mutationSymlinks` preserves
|
|
183
|
+
as described below. Omitting `mutationSymlinks` preserves each implementation's
|
|
184
|
+
existing mutation behavior; it does not currently provide uniform Windows link
|
|
185
|
+
handling.
|
|
186
|
+
|
|
187
|
+
For Windows buffered replacement `write()` and `writeJson()` calls (`overwrite`
|
|
188
|
+
omitted or `true`), the native pinned path rejects
|
|
189
|
+
a final file symlink with `path-alias`. The legacy JavaScript path can follow an
|
|
190
|
+
unchanged contained final link, keep the link itself, and replace the admitted
|
|
191
|
+
target. Its configured mutation policy reauthorizes the original target before
|
|
192
|
+
staging and publication, and its file/parent identity checks remain in force.
|
|
193
|
+
Likewise, native Windows parent admission refuses junction/reparse traversal
|
|
194
|
+
and can report `invalid-path`, while the legacy path can write through an
|
|
195
|
+
admitted contained parent-junction target.
|
|
196
|
+
|
|
197
|
+
The legacy writer is selected by native `off`, missing-binding `auto`, or
|
|
198
|
+
`renameIdentity: "verify-content-with-lock"`. Callers requiring the same link
|
|
199
|
+
rejection across implementations must set `mutationSymlinks: "reject"`
|
|
200
|
+
explicitly. See [Windows link modes](writing.md#windows-link-modes).
|
|
153
201
|
|
|
154
202
|
The JavaScript fallback used by `off`, by `auto` when no binding loads, and by
|
|
155
203
|
the explicit `renameIdentity: "verify-content-with-lock"` compatibility policy
|
|
@@ -158,7 +206,9 @@ It asserts directory identity around a pathname mutation and detects many
|
|
|
158
206
|
swaps, but detection occurs after the kernel may already have followed a new
|
|
159
207
|
parent symlink. A same-privilege peer with write access to the parent can
|
|
160
208
|
therefore cause an out-of-root side effect before the operation throws. Use
|
|
161
|
-
native `require` mode
|
|
209
|
+
native `require` mode to refuse implicit pathname mutation fallbacks, then check
|
|
210
|
+
the operation capabilities below. Loading an addon alone does not establish
|
|
211
|
+
confinement for every method.
|
|
162
212
|
|
|
163
213
|
The separate [retained-directory staging lifecycle](staged-file.md) keeps abort
|
|
164
214
|
cleanup anchored to the original directory after a parent or ancestor move.
|
|
@@ -233,7 +283,7 @@ The library does not modify or constrain the global Node.js `fs` namespace, and
|
|
|
233
283
|
| Mechanism | Reported containment | Boundary |
|
|
234
284
|
|---|---|---|
|
|
235
285
|
| Linux native with `openat2` | `kernel-atomic` | `openat2(RESOLVE_BENEATH | RESOLVE_NO_MAGICLINKS)` resolves and opens under the root in one kernel operation. |
|
|
236
|
-
| Linux native without `openat2` | `best-effort` | A no-follow `openat` component walk retains each parent and
|
|
286
|
+
| Linux native without `openat2` | `best-effort` | A no-follow `openat` component walk retains each parent, follows admitted in-root relative symlinks, and rechecks exact directory/link identities and link targets before and after the final open. |
|
|
237
287
|
| macOS native | `best-effort` | macOS 15.4 and newer use `O_RESOLVE_BENEATH` first; older kernels use the guarded `openat(O_NOFOLLOW)` component walk. Both verify the opened descriptor with `F_GETPATH`. |
|
|
238
288
|
| Windows native | `best-effort` | Handle-relative `NtCreateFile` rejects reparse points, but this package does not claim a Linux-style atomic beneath guarantee. |
|
|
239
289
|
| JavaScript fallback | `best-effort` | Canonical checks, no-follow opens where Node exposes them, and post-open identity checks form a check-then-use sequence. |
|
|
@@ -244,6 +294,68 @@ The Linux fallback's identity checks are also detection-based: a directory can b
|
|
|
244
294
|
|
|
245
295
|
The public `OpenResult`, `ReadResult`, and `WritableOpenResult` expose `containment`. Those root APIs currently report `best-effort`; direct native `openBeneath()` reports the platform value above. No-replace publication uses `renameat2(RENAME_NOREPLACE)` on Linux, `renameatx_np(RENAME_EXCL)` on macOS, and `FileRenameInfoEx` with replacement disabled on Windows, but those separate mutation semantics do not upgrade an open result's containment label.
|
|
246
296
|
|
|
297
|
+
### Native Root mutation capabilities
|
|
298
|
+
|
|
299
|
+
For removal, recursive removal, mkdir, overwrite move, and writable-open creation,
|
|
300
|
+
`require` selects the hardened native capability or rejects with
|
|
301
|
+
`helper-unavailable`; it does not silently perform the corresponding Node
|
|
302
|
+
pathname mutation. Default `auto` keeps its previous best-effort paths for these
|
|
303
|
+
operations, even with a native addon loaded. **`auto` does not confine these
|
|
304
|
+
operations under hostile concurrency.** The matrix below describes `require`.
|
|
305
|
+
The existing native publication paths for `write`, `create`, `writeJson`, and
|
|
306
|
+
`copyIn` are unchanged, and no-clobber move still requires native support in every mode.
|
|
307
|
+
|
|
308
|
+
| Root operation | Linux with `openat2` | Linux guarded fallback | macOS native | Windows native |
|
|
309
|
+
|---|---|---|---|---|
|
|
310
|
+
| `write`, `create`, `writeJson`, `copyIn` publication | Retained parent descriptors | Retained parents; best-effort admission | Retained parents; best-effort admission | Handle-relative publication; best-effort admission |
|
|
311
|
+
| `remove` file, symlink, or empty directory | Identity check and `unlinkat` at retained parent | Same unlink; best-effort parent admission | Same unlink; best-effort parent admission | Handle-relative open and identity-checked `FileDispositionInfoEx`; best-effort parent admission |
|
|
312
|
+
| Recursive `remove` | Descriptor-relative enumeration and deletion; `RESOLVE_NO_XDEV` on descent | `require` rejects; `auto` uses JavaScript | Descriptor-relative traversal with mount-identity checks; best-effort admission | `require` rejects; `auto` uses JavaScript |
|
|
313
|
+
| `mkdir` | `mkdirat` and retained child identity checks | Same creation; best-effort admission | Same creation plus guarded admission | Handle-relative directory creation, including the existing protected private creator; best-effort admission |
|
|
314
|
+
| `move` with overwrite | Retained parents, source identity check, `renameat` | Same rename; best-effort admission | Retained parents and rename; best-effort admission | Source handle identity check and handle-relative rename; best-effort admission |
|
|
315
|
+
| `append` / `openWritable` creating a file | Beneath exclusive open, no-follow final component, identity-checked FileHandle handoff | Same creation through guarded native open; best-effort admission | Guarded native creation and identity-checked FileHandle handoff; best-effort admission | Handle-relative exclusive creation, identity-checked FileHandle handoff and handle-bound cleanup; best-effort admission |
|
|
316
|
+
|
|
317
|
+
These guarantees have a per-call cost in `require`: removal pays for retained
|
|
318
|
+
parent admission and entry checks, recursive removal additionally fences each
|
|
319
|
+
visited directory, mkdir admits and verifies each created parent, overwrite move
|
|
320
|
+
retains both parents, and open-create verifies the FileHandle handoff. Adjacent
|
|
321
|
+
native steps are combined where no authorization callback must intervene, but
|
|
322
|
+
the identity and policy fences remain. `auto` keeps its previous operation paths
|
|
323
|
+
and avoids this added cost.
|
|
324
|
+
|
|
325
|
+
On Linux, `openat2(RESOLVE_BENEATH | RESOLVE_NO_MAGICLINKS)` admits the parent
|
|
326
|
+
under the Root atomically. Later mutations use that retained parent, so replacing
|
|
327
|
+
its pathname with a symlink cannot redirect the syscall to the link's target.
|
|
328
|
+
Recursive removal never follows an enumerated symlink; it unlinks the link itself
|
|
329
|
+
unless an explicit mutation policy rejects it. The fallback Linux walk rejects
|
|
330
|
+
symlinks and keeps its `best-effort` label. macOS uses the existing guarded
|
|
331
|
+
`O_RESOLVE_BENEATH`/`F_GETPATH` admission and also remains `best-effort`.
|
|
332
|
+
|
|
333
|
+
Directory pinning is not an atomic check of the entire namespace at mutation
|
|
334
|
+
time. A peer can move an admitted directory, and POSIX has no expected-inode
|
|
335
|
+
conditional unlink or rename: a final name can change after its identity check.
|
|
336
|
+
Post-operation rejection does not roll back a completed mutation. Policy and
|
|
337
|
+
identity checks remain defense in depth; OS isolation is needed for stronger
|
|
338
|
+
same-privilege namespace or authorization guarantees.
|
|
339
|
+
|
|
340
|
+
Writable-open creation retains a native descriptor while reopening the same
|
|
341
|
+
inode as a Node `FileHandle`, without create or truncate flags. If the requested
|
|
342
|
+
mode or umask cannot be preserved through initial creation and that handoff,
|
|
343
|
+
`require` rejects rather than widening permissions. `auto` keeps its existing
|
|
344
|
+
JavaScript creation path. The native
|
|
345
|
+
creation syscall receives the requested mode, preserving inherited ACLs.
|
|
346
|
+
If the resulting kernel permissions prevent handoff, the call fails closed and
|
|
347
|
+
attempts identity-bound cleanup of its empty created file; it never broadens
|
|
348
|
+
those permissions with `chmod`.
|
|
349
|
+
Existing-file writable opens, reads, advisory methods, and caller operations on
|
|
350
|
+
returned handles retain their existing `best-effort` contracts. Explicit
|
|
351
|
+
`renameIdentity: "verify-content-with-lock"` also retains its documented
|
|
352
|
+
JavaScript compatibility path, including in `require` mode.
|
|
353
|
+
|
|
354
|
+
Compatibility: an older or incomplete addon, unavailable mount-bounded descent,
|
|
355
|
+
or another missing operation capability now causes `helper-unavailable` in
|
|
356
|
+
`require`, including cases that previously fell through to JavaScript. The
|
|
357
|
+
public result types and containment labels are unchanged.
|
|
358
|
+
|
|
247
359
|
## Limitations to keep in mind
|
|
248
360
|
|
|
249
361
|
| Limitation | What it means |
|
|
@@ -30,7 +30,7 @@ import {
|
|
|
30
30
|
| `fileStoreSync()` | Synchronous variant of `fileStore()` for places that genuinely cannot await. |
|
|
31
31
|
| [`jsonStore()`](json-store.md) | A single keyed JSON state file with explicit fallback, atomic writes, and optional sidecar locking around read-modify-write updates. |
|
|
32
32
|
| Durable JSON queue helpers | Append/load/ack JSON entry files using atomic writes and delivered markers. |
|
|
33
|
-
| [Private file-store mode](
|
|
33
|
+
| [Private file-store mode](file-store.md#private-mode) | `fileStore({ private: true })` for credentials, tokens, and per-agent state at `0600` files under `0700` directories. |
|
|
34
34
|
|
|
35
35
|
`fileStore().json("rel.json")` and `jsonStore({ filePath })` are intentionally separate primitives. Use `fileStore().json(...)` when JSON state lives alongside other files in the same managed directory; use `jsonStore({ filePath })` when you have one trusted path, resolved to an absolute path at construction, and want the keyed JSON shape directly.
|
|
36
36
|
|
|
@@ -124,6 +124,6 @@ rejects non-files, symlinks, hardlinks, and entries over the byte limit.
|
|
|
124
124
|
|
|
125
125
|
- [`fileStore`](file-store.md) — full API for the multi-file store.
|
|
126
126
|
- [`jsonStore`](json-store.md) — single-file JSON store with locking.
|
|
127
|
-
- [Private file-store mode](
|
|
127
|
+
- [Private file-store mode](file-store.md#private-mode) — credential-shaped variant.
|
|
128
128
|
- [JSON files](json.md) — lower-level `readJson` / `writeJson` helpers.
|
|
129
129
|
- [Atomic writes](atomic.md) — what `fileStore` and `jsonStore` use under the hood.
|
|
@@ -54,55 +54,31 @@ unavailable namespace evidence leaves admission unchanged.
|
|
|
54
54
|
The first admitted unmapped ancestor emits `FS_SAFE_UNMAPPED_TEMP_ANCESTOR`
|
|
55
55
|
through Node's warning event. The warning contains no caller paths.
|
|
56
56
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
The direct sync path opens the new child without following its final component
|
|
84
|
-
and captures one exact descriptor observation after the parent replay. The new
|
|
85
|
-
child's exact identity, type, owner, private bits, and complete `0o7777` mode are
|
|
86
|
-
checked before mode initialization. When its creation mode already
|
|
87
|
-
matches `dirMode` (including the default `0o700`), creation avoids an extra mode
|
|
88
|
-
descriptor and chmod. If the observed creation mode differs from `dirMode`, the
|
|
89
|
-
immediate synchronous correction consumes that one-shot observation, checks the
|
|
90
|
-
fresh child name, replays the parent, and applies correction through the retained
|
|
91
|
-
descriptor. Later admission always performs fresh descriptor and name checks.
|
|
92
|
-
Permission failures propagate. POSIX `dirMode`
|
|
93
|
-
must not grant group/world write access; it only controls the new workspace,
|
|
94
|
-
not existing supplied directories. After the
|
|
95
|
-
first exact child observation, final adoption retains a no-follow child
|
|
96
|
-
descriptor, rechecks complete ancestry and retained cleanup-parent authority,
|
|
97
|
-
and then validates the original child's descriptor and current name for exact
|
|
98
|
-
identity, owner, private bits, and requested mode before cleanup is registered.
|
|
99
|
-
Linux and macOS may replay exact identities through round-trip-safe nonnegative
|
|
100
|
-
numeric `dev`/`ino` projections. Initial receipts that cannot be represented
|
|
101
|
-
exactly stay on the BigInt path; a malformed or mismatched numeric replay fails
|
|
102
|
-
closed without an exact retry.
|
|
103
|
-
Parent or child replacements observed during creation reject before cleanup
|
|
104
|
-
ownership is registered. Unverified artifacts are left in place for
|
|
105
|
-
caller-directed recovery.
|
|
57
|
+
Existing canonical-root discovery retains immutable exact identity; parent retention is provisional until,
|
|
58
|
+
after any native probe, creation validates complete ancestry, rechecks the root, and binds the parent descriptor.
|
|
59
|
+
Existing aliases and missing-component roots keep guarded admission. Async creation, default `0o700`, and
|
|
60
|
+
ineligible sync modes dispatch `mkdtemp` (initial mode `0o700`) immediately after admission, without another yield or probe.
|
|
61
|
+
|
|
62
|
+
Linux/macOS sync creation can use exclusive `mkdir` with a six-character random suffix when an explicit
|
|
63
|
+
mode other than `0o700` grants owner `rwx`, no special bits, and no group/world write. Each of at most
|
|
64
|
+
64 attempts generates a candidate, replays admitted ancestry and descriptor receipts, then creates immediately.
|
|
65
|
+
Colliding entries are never inspected, adopted, corrected, registered, or deleted. Observed complete mode bits
|
|
66
|
+
are authoritative: umask, inherited ACLs, or special bits can change the requested mode. POSIX modes do not
|
|
67
|
+
establish ACL privacy, and this optimization does not promise identical Linux/macOS syscalls or ACL behavior.
|
|
68
|
+
|
|
69
|
+
The direct sync path opens the child without following its final component and captures one exact descriptor
|
|
70
|
+
observation after parent replay. Creation checks identity, type, owner, private bits, and complete `0o7777` mode.
|
|
71
|
+
Matching `dirMode` avoids an extra mode descriptor and chmod; mismatches use descriptor-bound correction.
|
|
72
|
+
Immediate sync correction consumes the initial observation, checks the fresh child name, and replays the parent;
|
|
73
|
+
later admission uses fresh descriptor/name checks. Permission failures propagate. POSIX `dirMode` cannot grant
|
|
74
|
+
group/world write and affects only the new workspace, never existing supplied directories.
|
|
75
|
+
|
|
76
|
+
Before registering cleanup, final adoption retains a no-follow child descriptor, rechecks complete ancestry
|
|
77
|
+
and retained cleanup-parent authority, then verifies the original child's descriptor and current name for exact
|
|
78
|
+
identity, owner, private bits, and requested mode. Linux/macOS replay retained identities through round-trip-safe
|
|
79
|
+
nonnegative numeric `dev`/`ino` projections when exact (otherwise BigInt); malformed or mismatched numeric
|
|
80
|
+
observations fail closed without an exact retry. Observed parent/child replacements reject before ownership
|
|
81
|
+
registration; unverifiable artifacts remain for caller-directed recovery.
|
|
106
82
|
|
|
107
83
|
On Windows, POSIX mode/UID metadata does not establish ACL privacy, and these
|
|
108
84
|
factories neither claim nor initialize a POSIX `dirMode`; every requested value
|
|
@@ -247,9 +223,7 @@ name before quarantine returns `"identity-mismatch"` when the parent is stable;
|
|
|
247
223
|
an ambiguous parent returns `"indeterminate"`. After successful removal,
|
|
248
224
|
repeated cleanup returns `"missing"` without touching a recreated public name.
|
|
249
225
|
Other statuses remain stable. Compatible recursive-removal failures propagate
|
|
250
|
-
the
|
|
251
|
-
negative numeric zero, bigint zero, an empty string, and `NaN`; they are never
|
|
252
|
-
inferred from value identity or truthiness. Uncertain quarantine and
|
|
226
|
+
the original thrown value, including falsy values. Uncertain quarantine and
|
|
253
227
|
retained-parent checks instead return
|
|
254
228
|
`"indeterminate"`. After a propagated removal failure, later cleanup returns
|
|
255
229
|
`"indeterminate"` without retrying. Disposal and scoped helpers ignore returned
|
|
@@ -293,16 +267,8 @@ The callback receives the same workspace shape as `tempWorkspace()`. Cleanup is
|
|
|
293
267
|
|
|
294
268
|
### Manual lifetime
|
|
295
269
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
```ts
|
|
299
|
-
const workspace = await tempWorkspace({ rootDir: "/tmp/my-app", prefix: "scan-" });
|
|
300
|
-
try {
|
|
301
|
-
// …work in workspace.dir…
|
|
302
|
-
} finally {
|
|
303
|
-
await workspace.cleanup();
|
|
304
|
-
}
|
|
305
|
-
```
|
|
270
|
+
Manage the lifetime with `try/finally` and inspect the cleanup result, as in the
|
|
271
|
+
[receipt-inspecting example above](#tempworkspace).
|
|
306
272
|
|
|
307
273
|
### Sync variants
|
|
308
274
|
|
|
@@ -634,7 +600,6 @@ type ResolveSecureTempRootOptions = {
|
|
|
634
600
|
getuid?: () => number | undefined;
|
|
635
601
|
tmpdir?: () => string;
|
|
636
602
|
accessSync?: typeof import("node:fs").accessSync;
|
|
637
|
-
chmodSync?: typeof import("node:fs").chmodSync; // deprecated, never read or called
|
|
638
603
|
descriptor?: SecureTempRootDescriptorAdapter; // complete bundle; see below
|
|
639
604
|
lstatSync?: (path: string) => {
|
|
640
605
|
isDirectory(): boolean;
|
|
@@ -710,8 +675,7 @@ flags make repair/finalization unavailable. Injecting `lstatSync`, `accessSync`,
|
|
|
710
675
|
or `mkdirSync` disables the default host descriptor bundle. Injected observations
|
|
711
676
|
also require an explicit `mkdirSync` for creation; supplying a descriptor bundle
|
|
712
677
|
likewise never implicitly authorizes host mkdir. A custom mkdir requires the
|
|
713
|
-
complete descriptor bundle for POSIX finalization.
|
|
714
|
-
option is inert and alone does not disable normal host behavior.
|
|
678
|
+
complete descriptor bundle for POSIX finalization.
|
|
715
679
|
|
|
716
680
|
These checks bind chmod to the admitted object and reject observed replacements;
|
|
717
681
|
the returned path is not a retained capability. Pathname access and identity
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# Testing
|
|
2
2
|
|
|
3
|
+
The [seeded differential harness](https://github.com/openclaw/fs-safe/blob/main/scripts/differential-root.md) compares
|
|
4
|
+
isolated Node/Bun, native/fallback and sync/async public API runs, retaining
|
|
5
|
+
replayable return/error/tree receipts and bounded reduced repros. Its small
|
|
6
|
+
`node scripts/differential-root.mjs --ci` corpus runs in native CI lanes.
|
|
7
|
+
|
|
3
8
|
## Coverage gates
|
|
4
9
|
|
|
5
10
|
The coverage workflow measures `src/**/*.ts` with V8 on Linux, macOS, and
|
|
@@ -23,6 +28,23 @@ Percentages complement behavioral gates: mutation-policy proof, nightly watch
|
|
|
23
28
|
stress, and platform lanes are equally important. High coverage cannot establish
|
|
24
29
|
root confinement, race safety, event delivery, or bounded resource retirement.
|
|
25
30
|
|
|
31
|
+
## Hosted mutation-policy proof
|
|
32
|
+
|
|
33
|
+
The [hosted workflow](https://github.com/openclaw/fs-safe/blob/main/.github/workflows/mutation-policy-proof.yml)
|
|
34
|
+
builds the event-head package and host addon on Node 24 Linux, macOS, and Windows.
|
|
35
|
+
The [harness](https://github.com/openclaw/fs-safe/blob/main/scripts/mutation-policy-proof.mjs)
|
|
36
|
+
runs isolated temporary fixtures and binds sources, built modules, and the addon
|
|
37
|
+
to the tested revision. Its [receipt contract](https://github.com/openclaw/fs-safe/blob/main/test/mutation-policy-proof-contract.test.ts)
|
|
38
|
+
and [case contract](https://github.com/openclaw/fs-safe/blob/main/test/mutation-policy-proof-cases-contract.test.ts)
|
|
39
|
+
define the executable inventory and bounds.
|
|
40
|
+
|
|
41
|
+
Receipts describe representative observations: final listings and sentinels
|
|
42
|
+
neither count native syscalls nor exclude transient effects. Windows compatibility
|
|
43
|
+
payload writes remain JavaScript even when native-required sidecar publication
|
|
44
|
+
uses `Root.create`. Hosted cases complement internal interleaving tests; they
|
|
45
|
+
are neither exhaustive race proof nor performance clearance. Inspect exact
|
|
46
|
+
hosted artifacts before relying on a receipt's claims.
|
|
47
|
+
|
|
26
48
|
## Watch stress campaign
|
|
27
49
|
|
|
28
50
|
Build from the exact revision being qualified with `pnpm install --frozen-lockfile`,
|
|
@@ -149,20 +171,6 @@ requires the same openat2/NO_XDEV primitive and runs in the ordinary Linux
|
|
|
149
171
|
lanes. Bun's full compatibility
|
|
150
172
|
suite remains in the normal native lanes, because it includes bounded cleanup.
|
|
151
173
|
|
|
152
|
-
`@openclaw/fs-safe/test-hooks` exposes test-only injection points. Registration
|
|
153
|
-
is allowed only when `process.env.NODE_ENV === "test"` or
|
|
154
|
-
`process.env.VITEST === "true"`; registering a non-empty hook set elsewhere
|
|
155
|
-
throws. Production code must not import this subpath.
|
|
156
|
-
|
|
157
|
-
```ts
|
|
158
|
-
import {
|
|
159
|
-
__setFsSafeTestHooksForTest,
|
|
160
|
-
type FsSafeTestHooks,
|
|
161
|
-
} from "@openclaw/fs-safe/test-hooks";
|
|
162
|
-
```
|
|
163
|
-
|
|
164
|
-
The double-underscore prefix is a deliberate "hands off" signal: production code should never import this module. ESLint or your equivalent linter should flag it.
|
|
165
|
-
|
|
166
174
|
## When to reach for hooks
|
|
167
175
|
|
|
168
176
|
- Reproduce a TOCTOU race deterministically: simulate a symlink swap between resolve and open, or between write and rename.
|
|
@@ -173,30 +181,71 @@ If you don't need to inject a race, you don't need hooks — most tests should d
|
|
|
173
181
|
|
|
174
182
|
## Hooks API
|
|
175
183
|
|
|
184
|
+
`@openclaw/fs-safe/test-hooks` exposes injection points for downstream tests,
|
|
185
|
+
not a supported runtime API. Production code must not import it; enforce that
|
|
186
|
+
with your linter. New optional fields may appear between minor versions.
|
|
187
|
+
|
|
176
188
|
```ts
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
// Additional archive, store, root-fallback, temp, and trash race hooks are
|
|
184
|
-
// documented on the focused Test hooks reference page.
|
|
185
|
-
};
|
|
189
|
+
import {
|
|
190
|
+
getFsSafeTestHooks,
|
|
191
|
+
__setFsSafeTestHooksForTest,
|
|
192
|
+
type FsSafeTestHooks,
|
|
193
|
+
} from "@openclaw/fs-safe/test-hooks";
|
|
194
|
+
```
|
|
186
195
|
|
|
196
|
+
```ts
|
|
187
197
|
function __setFsSafeTestHooksForTest(hooks?: FsSafeTestHooks): void;
|
|
188
198
|
function getFsSafeTestHooks(): FsSafeTestHooks | undefined;
|
|
189
199
|
```
|
|
190
200
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
`
|
|
201
|
+
Registering any truthy hook set (including `{}`) requires
|
|
202
|
+
`process.env.NODE_ENV === "test"` or `process.env.VITEST === "true"`; otherwise
|
|
203
|
+
the setter throws. Clearing with `undefined` is allowed in any environment.
|
|
204
|
+
The getter returns the registered set, or `undefined` when none is registered;
|
|
205
|
+
changing the environment does not erase registered hooks.
|
|
206
|
+
|
|
207
|
+
All fields are optional. Callbacks return `Promise<void> | void` and are awaited
|
|
208
|
+
unless marked **sync**, which requires `void` and must not return a promise.
|
|
209
|
+
Path arguments are strings, `flags` is a number, `withFileTypes` is a boolean,
|
|
210
|
+
and `handle` is a Node `FileHandle`. Publication `method` is `"hardlink"`,
|
|
211
|
+
`"exclusive-copy"`, or `"rename-noreplace"`; `identity` carries `dev` and `ino`
|
|
212
|
+
as numbers or bigints.
|
|
213
|
+
|
|
214
|
+
| Hook | Callback arguments | Timing |
|
|
215
|
+
|---|---|---|
|
|
216
|
+
| `beforeWatchRegistration` | `path` | Before a scanned directory's registration step, including when its existing registration is reused. |
|
|
217
|
+
| `afterWatchRegistration` | `path` | After that registration step, before recording a newly acquired registration. |
|
|
218
|
+
| `afterWatchBackendOverflow` (**sync**) | `root, phase` (`"received"` or `"reconciled"`) | On an overflow hint, and after a noninitial reconciliation produces an overflow invalidation. |
|
|
219
|
+
| `afterWatchBackendCreated` (**sync**) | `root, emit, nativeEvent?` | After creating/configuring the backend, before scanning. `emit(batch)` injects a native watch batch; `nativeEvent(path, flags)` injects a decoder event. |
|
|
220
|
+
| `afterPreOpenLstat` | `filePath` | After pre-open `lstat`, before opening the file. |
|
|
221
|
+
| `beforeOpen` | `filePath, flags` | Immediately before the guarded file open. |
|
|
222
|
+
| `afterOpen` | `filePath, handle` | After open, before the post-open identity check. |
|
|
223
|
+
| `afterOpenedPathIdentityCheck` | `filePath, handle` | A standalone local-file or absolute copy-source descriptor matches its pathname, before the generic opened-path resolver. Root reads use final admission hooks instead. |
|
|
224
|
+
| `afterRootReadPathResolution` | `filePath` | After Root read path resolution, before local-file open admission. |
|
|
225
|
+
| `beforeRootReadFinalFence` | `filePath, handle` | After descriptor identity and hardlink checks, before the final root/path/canonical-path/root admission fence. |
|
|
226
|
+
| `afterRootReadFinalPathIdentityCheck` (**sync**) | `filePath, handle` | After the final pathname-to-descriptor comparison, before the second root check. |
|
|
227
|
+
| `beforeArchiveOutputMutation` | `operation` (`"mkdir"` or `"chmod"`), `targetPath` | Before archive staging creates a directory or applies a mode. |
|
|
228
|
+
| `beforeFileStorePruneDescend` | `dirPath` | Before file-store pruning descends into a directory. |
|
|
229
|
+
| `beforeFileStoreSyncPrivateWrite` (**sync**) | `filePath` | Before a synchronous private-store write mutates its target. |
|
|
230
|
+
| `beforeRootFallbackMutation` | `operation` (`"mkdir"`, `"move"`, or `"remove"`), `targetPath` | Before a guarded JavaScript Root fallback mutation. |
|
|
231
|
+
| `beforePinnedWriteParentAdmission` | `targetPath` | After pinned-write policy preflight, before parent admission; also before refreshing retained JavaScript write authority and parent checks. |
|
|
232
|
+
| `beforeRootStatObservation` | `targetPath` | After `Root.stat()` admits the target and parent, before collecting returned metadata. |
|
|
233
|
+
| `beforeRootStatInitialObservation` | `targetPath` | After `Root.stat()` admits the parent, before its first target inspection. |
|
|
234
|
+
| `beforeRootListObservation` | `directoryPath, withFileTypes` | After `Root.list()` admits the selected directory, before collecting names and optional metadata. |
|
|
235
|
+
| `afterPinnedWriteFallbackRename` | `targetPath` | After fallback rename commits, before post-commit identity checks. |
|
|
236
|
+
| `beforeSiblingTempWrite` | `tempPath` | Before the `writeViaSiblingTempPath` producer runs, with its selected output pathname still absent. |
|
|
237
|
+
| `beforeSidecarLockSnapshotOpen` | `lockPath` | After sidecar inspection, before opening it for a bounded snapshot read. |
|
|
238
|
+
| `beforeRegularFileAppendOpen` | `filePath` | After append preflight and the initial size-budget check, before async open. |
|
|
239
|
+
| `beforeRegularFileAppendOpenSync` (**sync**) | `filePath` | The corresponding sync append point, before `openSync`. |
|
|
240
|
+
| `beforeTempWorkspaceNativeRemoval` | `quarantinePath` | After workspace quarantine admission, immediately before native owned-tree removal. |
|
|
241
|
+
| `beforeTempWorkspaceNativeRemovalSync` (**sync**) | `quarantinePath` | The corresponding sync native-removal point. |
|
|
242
|
+
| `beforeTrashMove` (**sync**) | `targetPath, destPath` | Before trash handling moves the target. |
|
|
243
|
+
| `afterPublishTargetCreated` | `method, targetPath, identity` | After exclusive publication creates the target, before final fences. |
|
|
244
|
+
| `beforePublishDirectorySync` | `method, targetPath, identity` | After publication verifies the target, immediately before strict parent sync. |
|
|
245
|
+
|
|
246
|
+
The watch `emit` callback accepts `{ hints, overflow, error? }`: `overflow` is
|
|
247
|
+
boolean, `error` is a string, and each hint has string `directory` and `name`
|
|
248
|
+
fields plus an `event` of `"rename"`, `"change"`, or `"children"`.
|
|
200
249
|
|
|
201
250
|
## Example: simulate a TOCTOU swap
|
|
202
251
|
|
|
@@ -263,18 +312,9 @@ it("runs without the native helper", async () => {
|
|
|
263
312
|
|
|
264
313
|
Hooks set by `__setFsSafeTestHooksForTest` persist across tests until explicitly cleared. Always clear in `afterEach` (or your test framework's equivalent) — leaked hooks will silently change behavior in unrelated tests and cause maddening intermittent failures.
|
|
265
314
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
afterEach(() => {
|
|
271
|
-
__setFsSafeTestHooksForTest(undefined);
|
|
272
|
-
});
|
|
273
|
-
```
|
|
274
|
-
|
|
275
|
-
A global hook clear in your test setup file is a good safety net.
|
|
276
|
-
|
|
277
|
-
See the [complete Test hooks reference](test-hooks.md) for every optional hook.
|
|
315
|
+
Use `__setFsSafeTestHooksForTest(undefined)` as in the TOCTOU example above.
|
|
316
|
+
A global hook clear in your test setup file is a good safety net; the
|
|
317
|
+
[Hooks API](#hooks-api) lists every optional hook.
|
|
278
318
|
|
|
279
319
|
## Patterns for testing fs-safe-using code
|
|
280
320
|
|
|
@@ -343,20 +383,12 @@ workspace reads that bypass pinned file descriptors.
|
|
|
343
383
|
|
|
344
384
|
The original soak rule rejected more than 64 MiB RSS growth after minute five.
|
|
345
385
|
That outcome remains in `memory.legacyRss`, with its original limit and pass/fail
|
|
346
|
-
value; it is no longer the soak pass criterion.
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
The 60-minute qualification separates this capacity warm-up from the later RSS
|
|
353
|
-
trend. The measured Linux event run had under 1 MiB collected-heap drift and a
|
|
354
|
-
0.55 MiB/minute second-half RSS slope; its final ten-minute RSS range was 1.60 MiB.
|
|
355
|
-
The 8 MiB live-memory allowance and 1 MiB/minute RSS slope leave measurement
|
|
356
|
-
margin while rejecting retained growth and continued rapid RSS growth. The
|
|
357
|
-
512 MiB peak ceiling is unchanged. Native allocation leaks need independent
|
|
358
|
-
accounting/profiling too: the investigation found a much smaller cleanup-hook
|
|
359
|
-
context leak even when registrations, pending sets, and TSFN counters retired.
|
|
386
|
+
value; it is no longer the soak pass criterion. V8 capacity expansion and
|
|
387
|
+
allocator retention can raise RSS while collected live memory stays flat.
|
|
388
|
+
The normal harness keeps Node's default nursery sizing and separates warm-up
|
|
389
|
+
from later RSS growth with the [current stress gates](#watch-stress-campaign).
|
|
390
|
+
Native allocation leaks still need independent accounting/profiling, even when
|
|
391
|
+
registrations, pending sets, and thread-safe function counters retire.
|
|
360
392
|
|
|
361
393
|
Build the native addon and package from the same revision, then run each control
|
|
362
394
|
in a fresh Node process on a disposable machine:
|
|
@@ -112,7 +112,6 @@ type RootDefaults = {
|
|
|
112
112
|
maxBytes?: number;
|
|
113
113
|
mkdir?: boolean; // default true for mutation methods
|
|
114
114
|
mode?: number;
|
|
115
|
-
nonBlockingRead?: boolean;
|
|
116
115
|
renameIdentity?: RenameIdentityPolicy;
|
|
117
116
|
symlinks?: "reject" | "follow-within-root" | "follow-parents-within-root";
|
|
118
117
|
mutationSymlinks?: MutationSymlinkPolicy;
|
|
@@ -136,7 +135,7 @@ type RootOptions = {
|
|
|
136
135
|
```ts
|
|
137
136
|
import type { CopyCloneMode, RootCopyPublicationReceipt } from "@openclaw/fs-safe";
|
|
138
137
|
|
|
139
|
-
type RootReadOptions = Pick<RootDefaults, "hardlinks" | "maxBytes" | "
|
|
138
|
+
type RootReadOptions = Pick<RootDefaults, "hardlinks" | "maxBytes" | "symlinks">;
|
|
140
139
|
type RootWriteOptions = Pick<RootDefaults, "assertBeforeMutation" | "denyMutations" | "durable" | "mkdir" | "mode" | "renameIdentity" | "mutationSymlinks"> & {
|
|
141
140
|
encoding?: BufferEncoding;
|
|
142
141
|
overwrite?: boolean;
|
|
@@ -194,21 +193,9 @@ policy; omission preserves each mutation method's existing behavior.
|
|
|
194
193
|
|
|
195
194
|
## `FsSafeErrorCode` / `FsSafeErrorCategory`
|
|
196
195
|
|
|
197
|
-
|
|
198
|
-
type FsSafeErrorCode =
|
|
199
|
-
| "already-exists" | "denied-path" | "device-path" | "hardlink"
|
|
200
|
-
| "helper-failed"
|
|
201
|
-
| "helper-unavailable" | "insecure-permissions" | "invalid-path"
|
|
202
|
-
| "not-empty" | "not-file" | "not-found" | "not-owned"
|
|
203
|
-
| "not-removable" | "outside-workspace" | "path-alias"
|
|
204
|
-
| "path-mismatch" | "permission-unverified" | "read-failed" | "secret-exists"
|
|
205
|
-
| "store-reentrant-update" | "symlink"
|
|
206
|
-
| "timeout" | "too-large" | "unsupported-platform";
|
|
207
|
-
```
|
|
208
|
-
|
|
209
|
-
Closed union you switch on. See the [Errors](errors.md) reference for what each one means.
|
|
196
|
+
`FsSafeErrorCode` is a closed union you switch on; the [code union](errors.md#code-union) lists every member and the [code reference](errors.md#code-reference) explains each one.
|
|
210
197
|
|
|
211
|
-
`FsSafeError.category` is `"policy"` for unsafe input or target state rejected by a safety policy and `"operational"` for routine filesystem outcomes or environment/runtime failures.
|
|
198
|
+
`FsSafeError.category` is `"policy"` for unsafe input or target state rejected by a safety policy and `"operational"` for routine filesystem outcomes or environment/runtime failures. [Errors](errors.md#shape) lists the exact operational set.
|
|
212
199
|
|
|
213
200
|
## See also
|
|
214
201
|
|
|
@@ -51,7 +51,7 @@ Each entry's `path` is absolute and retains the normalized spelling of the
|
|
|
51
51
|
supplied root, including followed directory aliases. Paths do not switch to
|
|
52
52
|
the canonical symlink target during descent.
|
|
53
53
|
|
|
54
|
-
`walkDirectory()` and `walkDirectorySync()` always return `failedDirs`; the property remains optional on the exported `WalkDirectoryResult` type so existing callers that manually construct the legacy result shape remain source-compatible. It lists every directory whose
|
|
54
|
+
`walkDirectory()` and `walkDirectorySync()` always return `failedDirs`; the property remains optional on the exported `WalkDirectoryResult` type so existing callers that manually construct the legacy result shape remain source-compatible. It lists every directory whose directory resolution, opening, reading, or closing threw, so some or all of its contents may be absent from `entries`. `error` is the thrown value (a `NodeJS.ErrnoException` at runtime), so callers can distinguish a benign missing-directory race (`ENOENT`) from a real read failure (`EACCES`, `EIO`, `ESTALE`, …). The walk-root failure has an empty `relativePath` and `depth: 0`. Failures resolving a symlink's target kind are not reported here.
|
|
55
55
|
|
|
56
56
|
## Options
|
|
57
57
|
|
|
@@ -102,6 +102,14 @@ This prunes a directory after finding its marker without listing that directory'
|
|
|
102
102
|
|
|
103
103
|
Unreadable directories are skipped rather than throwing, but every skipped directory is recorded in `failedDirs`. This keeps the helper suitable for best-effort inventories while letting pruning jobs tell an incomplete scan from an empty one: a destructive reconcile that deletes state for paths missing from `entries` must first confirm `failedDirs` holds no real read failures, or a transient `EIO`/`EACCES` blip would be mistaken for mass deletion. Use a stricter root-bounded operation when every entry must be accounted for.
|
|
104
104
|
|
|
105
|
+
With `maxEntries`, both standalone walkers stream in filesystem order with a
|
|
106
|
+
Node.js's default 32-entry directory buffer. They examine at most the remaining
|
|
107
|
+
budget plus one lookahead per visited directory; Node may prefetch the rest of
|
|
108
|
+
the current 32-entry batch without invoking filters for those names. Memory is
|
|
109
|
+
bounded independently of directory width. Streams close on completion,
|
|
110
|
+
truncation, and callback failure. Filtering consumes the budget. Without an entry
|
|
111
|
+
budget, they retain eager directory snapshots for complete scans. Neither standalone API sorts its output.
|
|
112
|
+
|
|
105
113
|
## Root-bounded async iteration
|
|
106
114
|
|
|
107
115
|
`Root.walk(rel, options)` is the root-bounded counterpart to these standalone
|
|
@@ -148,9 +156,13 @@ home-directory expansion, prefix it with `./`, as in
|
|
|
148
156
|
|
|
149
157
|
The default `order: "sorted"` visits each directory's names in lexicographic
|
|
150
158
|
order before descending depth first. It reads and sorts all names in each
|
|
151
|
-
visited directory
|
|
152
|
-
|
|
153
|
-
|
|
159
|
+
visited directory, even with `maxEntries`, so truncated walks select the globally
|
|
160
|
+
smallest names within each directory rather than a filesystem-order-dependent
|
|
161
|
+
subset. For an unchanged tree this preserves deterministic results. The entry
|
|
162
|
+
budget bounds metadata and filtering work, but does not bound sorted name
|
|
163
|
+
enumeration memory or time. With `maxEntries`, it prepares small metadata batches
|
|
164
|
+
capped by the remaining global entry budget. Every batch stops at the first
|
|
165
|
+
directory or symlink, so recursive descent cannot spend a budget already used by later
|
|
154
166
|
siblings. An early `break` may leave metadata from the current batch unused;
|
|
155
167
|
the total still stays within `maxEntries`. Filtering requires metadata and
|
|
156
168
|
consumes the entry budget, including entries skipped by the filter.
|
|
@@ -237,7 +249,9 @@ for await (const entry of capability.walk("", {
|
|
|
237
249
|
```
|
|
238
250
|
|
|
239
251
|
Filters run serially outside metadata batches and retain the supplied options
|
|
240
|
-
object as their `this` receiver.
|
|
252
|
+
object as their `this` receiver. Cancellation is checked after every filter
|
|
253
|
+
decision, including synchronous callbacks, before yielding or descending.
|
|
254
|
+
When an awaited filter resolves, the walk
|
|
241
255
|
checks cancellation and revalidates the current listing directory and Root
|
|
242
256
|
identities before using the decision. These checks do not refresh the entry's
|
|
243
257
|
captured metadata or pin a later operation.
|
|
@@ -276,6 +290,20 @@ root-bounded and reports failures inline because an async iterator has no final
|
|
|
276
290
|
result summary. Its default remains to throw on unreadable or invalid
|
|
277
291
|
directories.
|
|
278
292
|
|
|
293
|
+
`Root.list()` always returns a complete sorted array and has no entry budget.
|
|
294
|
+
`Root.entries()` defaults to streaming filesystem order; its sorted mode buffers
|
|
295
|
+
names, and `maxEntries` caps that buffer with one lookahead before throwing
|
|
296
|
+
`too-large`. Watch scans use the same guarded filesystem-order stream and enforce
|
|
297
|
+
their examined-entry budget before metadata lookup. Every Root listing mode
|
|
298
|
+
rejects invalid UTF-8 names before application metadata lookup, including the
|
|
299
|
+
lookahead; unexamined suffixes are not validated.
|
|
300
|
+
|
|
301
|
+
Bun 1.4.2 implements `Dir` with an internal eager `readdir`, including when
|
|
302
|
+
`bufferSize` is one. On that runtime, these budgets bound fs-safe's admitted
|
|
303
|
+
names, metadata, and results, but cannot bound Bun's internal enumeration memory.
|
|
304
|
+
Async scans retain async directory reads there; use Node.js when the directory
|
|
305
|
+
width itself must not determine enumeration allocation.
|
|
306
|
+
|
|
279
307
|
## See also
|
|
280
308
|
|
|
281
309
|
- [`fileStore`](file-store.md) — managed stores use bounded walking for pruning.
|