@openclaw/fs-safe 0.4.6 → 0.5.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 +69 -0
- package/README.md +110 -41
- package/dist/archive-entry.d.ts.map +1 -1
- package/dist/archive-entry.js +5 -4
- package/dist/archive-errors.d.ts +11 -0
- package/dist/archive-errors.d.ts.map +1 -0
- package/dist/archive-errors.js +16 -0
- package/dist/archive-input.d.ts +12 -0
- package/dist/archive-input.d.ts.map +1 -0
- package/dist/archive-input.js +72 -0
- package/dist/archive-kind.d.ts +1 -1
- package/dist/archive-kind.d.ts.map +1 -1
- package/dist/archive-kind.js +18 -0
- package/dist/archive-limits.d.ts +10 -0
- package/dist/archive-limits.d.ts.map +1 -1
- package/dist/archive-limits.js +18 -0
- package/dist/archive-native.d.ts +18 -0
- package/dist/archive-native.d.ts.map +1 -0
- package/dist/archive-native.js +115 -0
- package/dist/archive-options.d.ts +21 -0
- package/dist/archive-options.d.ts.map +1 -0
- package/dist/archive-options.js +1 -0
- package/dist/archive-policy.d.ts +20 -0
- package/dist/archive-policy.d.ts.map +1 -0
- package/dist/archive-policy.js +29 -0
- package/dist/archive-read.d.ts +6 -0
- package/dist/archive-read.d.ts.map +1 -0
- package/dist/archive-read.js +200 -0
- package/dist/archive-staging.d.ts +2 -5
- package/dist/archive-staging.d.ts.map +1 -1
- package/dist/archive-staging.js +19 -10
- package/dist/archive-tar-meta.d.ts +6 -0
- package/dist/archive-tar-meta.d.ts.map +1 -0
- package/dist/archive-tar-meta.js +143 -0
- package/dist/archive-tar-runtime.d.ts +43 -0
- package/dist/archive-tar-runtime.d.ts.map +1 -0
- package/dist/archive-tar-runtime.js +8 -0
- package/dist/archive-tar.d.ts +5 -1
- package/dist/archive-tar.d.ts.map +1 -1
- package/dist/archive-tar.js +24 -6
- package/dist/archive-zip-entry.d.ts +15 -0
- package/dist/archive-zip-entry.d.ts.map +1 -0
- package/dist/archive-zip-entry.js +17 -0
- package/dist/archive.d.ts +7 -17
- package/dist/archive.d.ts.map +1 -1
- package/dist/archive.js +111 -108
- package/dist/atomic.d.ts +1 -1
- package/dist/atomic.d.ts.map +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +1 -1
- package/dist/containment.d.ts +2 -0
- package/dist/containment.d.ts.map +1 -0
- package/dist/containment.js +1 -0
- package/dist/directory-durability.d.ts +44 -0
- package/dist/directory-durability.d.ts.map +1 -0
- package/dist/directory-durability.js +299 -0
- package/dist/durability.d.ts +4 -0
- package/dist/durability.d.ts.map +1 -0
- package/dist/durability.js +3 -0
- package/dist/errors.d.ts +4 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +2 -0
- package/dist/file-hash.d.ts +10 -0
- package/dist/file-hash.d.ts.map +1 -0
- package/dist/file-hash.js +73 -0
- package/dist/file-lock-sync.d.ts +35 -0
- package/dist/file-lock-sync.d.ts.map +1 -0
- package/dist/file-lock-sync.js +233 -0
- package/dist/file-lock.d.ts +2 -0
- package/dist/file-lock.d.ts.map +1 -1
- package/dist/file-lock.js +1 -0
- package/dist/filename.d.ts.map +1 -1
- package/dist/filename.js +4 -1
- package/dist/fsync.d.ts +1 -1
- package/dist/fsync.d.ts.map +1 -1
- package/dist/fsync.js +1 -21
- package/dist/guarded-mkdir.d.ts +7 -1
- package/dist/guarded-mkdir.d.ts.map +1 -1
- package/dist/guarded-mkdir.js +45 -3
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/json-document-store.d.ts.map +1 -1
- package/dist/json-document-store.js +30 -15
- package/dist/move-path.d.ts.map +1 -1
- package/dist/move-path.js +101 -17
- package/dist/native/darwin-arm64/fs-safe-native.node +0 -0
- package/dist/native/darwin-x64/fs-safe-native.node +0 -0
- package/dist/native/linux-arm64-gnu/fs-safe-native.node +0 -0
- package/dist/native/linux-arm64-musl/fs-safe-native.node +0 -0
- package/dist/native/linux-x64-gnu/fs-safe-native.node +0 -0
- package/dist/native/linux-x64-musl/fs-safe-native.node +0 -0
- package/dist/native/win32-x64-msvc/fs-safe-native.node +0 -0
- package/dist/native-binding.d.ts +79 -0
- package/dist/native-binding.d.ts.map +1 -0
- package/dist/native-binding.js +1 -0
- package/dist/native-config.d.ts +17 -0
- package/dist/native-config.d.ts.map +1 -0
- package/dist/native-config.js +71 -0
- package/dist/native-operations.d.ts +21 -0
- package/dist/native-operations.d.ts.map +1 -0
- package/dist/native-operations.js +115 -0
- package/dist/native-pinned-write.d.ts +5 -0
- package/dist/native-pinned-write.d.ts.map +1 -0
- package/dist/native-pinned-write.js +115 -0
- package/dist/native.d.ts +14 -0
- package/dist/native.d.ts.map +1 -0
- package/dist/native.js +204 -0
- package/dist/output-sibling.d.ts +8 -0
- package/dist/output-sibling.d.ts.map +1 -0
- package/dist/output-sibling.js +122 -0
- package/dist/output.d.ts +2 -0
- package/dist/output.d.ts.map +1 -1
- package/dist/output.js +32 -5
- package/dist/owner-dacl.d.ts +31 -0
- package/dist/owner-dacl.d.ts.map +1 -0
- package/dist/owner-dacl.js +27 -0
- package/dist/path-policy.js +1 -1
- package/dist/permissions-public.d.ts +2 -0
- package/dist/permissions-public.d.ts.map +1 -1
- package/dist/permissions-public.js +2 -0
- package/dist/permissions.d.ts.map +1 -1
- package/dist/permissions.js +6 -0
- package/dist/pinned-operation.d.ts +2 -0
- package/dist/pinned-operation.d.ts.map +1 -0
- package/dist/pinned-operation.js +35 -0
- package/dist/pinned-write.d.ts +3 -16
- package/dist/pinned-write.d.ts.map +1 -1
- package/dist/pinned-write.js +20 -85
- package/dist/private-directory.d.ts +5 -0
- package/dist/private-directory.d.ts.map +1 -0
- package/dist/private-directory.js +13 -0
- package/dist/private-temp-workspace.d.ts +6 -2
- package/dist/private-temp-workspace.d.ts.map +1 -1
- package/dist/private-temp-workspace.js +40 -13
- package/dist/publish-file-failure.d.ts +27 -0
- package/dist/publish-file-failure.d.ts.map +1 -0
- package/dist/publish-file-failure.js +21 -0
- package/dist/publish-file.d.ts +21 -0
- package/dist/publish-file.d.ts.map +1 -0
- package/dist/publish-file.js +351 -0
- package/dist/read-opened-file.d.ts +3 -0
- package/dist/read-opened-file.d.ts.map +1 -1
- package/dist/read-opened-file.js +1 -0
- package/dist/replace-file-copy-fallback.d.ts +34 -0
- package/dist/replace-file-copy-fallback.d.ts.map +1 -0
- package/dist/replace-file-copy-fallback.js +339 -0
- package/dist/replace-file.d.ts +6 -2
- package/dist/replace-file.d.ts.map +1 -1
- package/dist/replace-file.js +39 -90
- package/dist/root-context.d.ts +4 -1
- package/dist/root-context.d.ts.map +1 -1
- package/dist/root-context.js +20 -1
- package/dist/root-impl.d.ts +5 -0
- package/dist/root-impl.d.ts.map +1 -1
- package/dist/root-impl.js +128 -225
- package/dist/root-path-existing.d.ts +3 -0
- package/dist/root-path-existing.d.ts.map +1 -0
- package/dist/root-path-existing.js +69 -0
- package/dist/root-path.d.ts +1 -2
- package/dist/root-path.d.ts.map +1 -1
- package/dist/root-path.js +67 -81
- package/dist/root-walk.d.ts +41 -0
- package/dist/root-walk.d.ts.map +1 -0
- package/dist/root-walk.js +114 -0
- package/dist/root.d.ts +2 -0
- package/dist/root.d.ts.map +1 -1
- package/dist/secret-file.d.ts +5 -2
- package/dist/secret-file.d.ts.map +1 -1
- package/dist/secret-file.js +20 -2
- package/dist/secret-read-async.d.ts +4 -0
- package/dist/secret-read-async.d.ts.map +1 -0
- package/dist/secret-read-async.js +95 -0
- package/dist/secret.d.ts +2 -1
- package/dist/secret.d.ts.map +1 -1
- package/dist/secret.js +2 -1
- package/dist/sibling-temp.d.ts.map +1 -1
- package/dist/sibling-temp.js +2 -13
- package/dist/sidecar-lock-handle.d.ts +20 -0
- package/dist/sidecar-lock-handle.d.ts.map +1 -0
- package/dist/sidecar-lock-handle.js +28 -0
- package/dist/sidecar-lock-policy.d.ts +10 -0
- package/dist/sidecar-lock-policy.d.ts.map +1 -0
- package/dist/sidecar-lock-policy.js +29 -0
- package/dist/sidecar-lock-reclaim.d.ts +22 -6
- package/dist/sidecar-lock-reclaim.d.ts.map +1 -1
- package/dist/sidecar-lock-reclaim.js +106 -20
- package/dist/sidecar-lock-types.d.ts +56 -0
- package/dist/sidecar-lock-types.d.ts.map +1 -0
- package/dist/sidecar-lock-types.js +1 -0
- package/dist/sidecar-lock.d.ts +2 -45
- package/dist/sidecar-lock.d.ts.map +1 -1
- package/dist/sidecar-lock.js +94 -55
- package/dist/temp-cleanup.d.ts +7 -1
- package/dist/temp-cleanup.d.ts.map +1 -1
- package/dist/temp-cleanup.js +35 -5
- package/dist/temp.d.ts +2 -1
- package/dist/temp.d.ts.map +1 -1
- package/dist/test-hooks.d.ts +3 -0
- package/dist/test-hooks.d.ts.map +1 -1
- package/dist/windows-permissions-native.d.ts +9 -0
- package/dist/windows-permissions-native.d.ts.map +1 -0
- package/dist/windows-permissions-native.js +31 -0
- package/docs/advanced.md +1 -1
- package/docs/archive.md +131 -13
- package/docs/atomic.md +30 -1
- package/docs/config.md +51 -21
- package/docs/durability.md +299 -0
- package/docs/errors.md +36 -6
- package/docs/filename.md +0 -0
- package/docs/index.md +11 -7
- package/docs/install.md +27 -26
- package/docs/json-store.md +18 -3
- package/docs/migrating-to-0.5.md +196 -0
- package/docs/native-helper.md +85 -0
- package/docs/native.md +134 -0
- package/docs/output.md +37 -11
- package/docs/path.md +1 -1
- package/docs/permissions.md +84 -1
- package/docs/quickstart.md +5 -2
- package/docs/reading.md +4 -4
- package/docs/root.md +31 -15
- package/docs/secret-file.md +50 -1
- package/docs/security-model.md +42 -8
- package/docs/sidecar-lock.md +92 -7
- package/docs/temp.md +24 -1
- package/docs/test-hooks.md +22 -2
- package/docs/testing.md +18 -7
- package/docs/types.md +3 -1
- package/docs/walk.md +55 -0
- package/docs/writing.md +3 -3
- package/package.json +12 -2
- package/dist/pinned-helper.d.ts +0 -9
- package/dist/pinned-helper.d.ts.map +0 -1
- package/dist/pinned-helper.js +0 -19
- package/dist/pinned-path.d.ts +0 -7
- package/dist/pinned-path.d.ts.map +0 -1
- package/dist/pinned-path.js +0 -21
- package/dist/pinned-python-config.d.ts +0 -9
- package/dist/pinned-python-config.d.ts.map +0 -1
- package/dist/pinned-python-config.js +0 -38
- package/dist/pinned-python.d.ts +0 -12
- package/dist/pinned-python.d.ts.map +0 -1
- package/dist/pinned-python.js +0 -688
- package/docs/python-helper.md +0 -103
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Directory durability
|
|
3
|
+
description: "Pin directory identities, fsync publication metadata, and durably create nested directory paths."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Directory durability
|
|
7
|
+
|
|
8
|
+
`@openclaw/fs-safe/durability` provides the directory side of crash-safe file
|
|
9
|
+
publication. Flushing a file does not guarantee that its containing directory
|
|
10
|
+
entry reached storage; callers that promise durable create, link, rename, or
|
|
11
|
+
unlink operations must also synchronize the affected directory.
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import {
|
|
15
|
+
ensureDurableDirectory,
|
|
16
|
+
pinDirectory,
|
|
17
|
+
} from "@openclaw/fs-safe/durability";
|
|
18
|
+
|
|
19
|
+
const repository = await ensureDurableDirectory({
|
|
20
|
+
directoryPath: "/srv/backups/sqlite",
|
|
21
|
+
mode: 0o700,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const pinned = await pinDirectory(repository, { label: "backup repository" });
|
|
25
|
+
try {
|
|
26
|
+
await publishSnapshot();
|
|
27
|
+
const outcome = await pinned.sync();
|
|
28
|
+
if (outcome.status === "unsupported") {
|
|
29
|
+
// Decide at the product boundary whether this platform can weaken the promise.
|
|
30
|
+
}
|
|
31
|
+
} finally {
|
|
32
|
+
await pinned.close();
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Outcomes and failure semantics
|
|
37
|
+
|
|
38
|
+
`syncDirectory()` and `PinnedDirectory.sync()` return:
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
type DirectorySyncOutcome =
|
|
42
|
+
| { status: "synced" }
|
|
43
|
+
| { status: "unsupported"; code?: string };
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
POSIX synchronization failures propagate. Windows directory handles do not
|
|
47
|
+
portably support `FlushFileBuffers`; the known unsupported error family is
|
|
48
|
+
reported as `unsupported` after the pathname and pinned identity are checked
|
|
49
|
+
again. Directory-open access failures and other Windows I/O failures still
|
|
50
|
+
propagate.
|
|
51
|
+
|
|
52
|
+
`syncDirectoryBestEffort()` and `syncDirectoryBestEffortSync()` intentionally
|
|
53
|
+
discard both unsupported outcomes and failures. Use them only when the primary
|
|
54
|
+
write remains useful without a crash-durability promise.
|
|
55
|
+
|
|
56
|
+
## Pinned directories
|
|
57
|
+
|
|
58
|
+
`pinDirectory()` rejects final symlinks and non-directories. On POSIX it opens
|
|
59
|
+
with `O_DIRECTORY`, `O_NOFOLLOW`, and `O_NONBLOCK`, then compares the open
|
|
60
|
+
descriptor, pathname identity, and canonical path. `assertCurrent()` repeats
|
|
61
|
+
those checks. This prevents a pathname replacement from turning a later sync
|
|
62
|
+
into proof for a different directory.
|
|
63
|
+
|
|
64
|
+
Call `close()` in `finally`. Closing is idempotent; using a closed pin fails.
|
|
65
|
+
|
|
66
|
+
## Durable directory creation
|
|
67
|
+
|
|
68
|
+
`ensureDurableDirectory()` finds and pins the nearest existing ancestor,
|
|
69
|
+
creates the requested path, opens every new directory segment, and synchronizes
|
|
70
|
+
each new parent-to-child edge from the leaf upward. It returns the final
|
|
71
|
+
directory receipt plus the aggregate parent-sync outcome.
|
|
72
|
+
|
|
73
|
+
By default it uses fs-safe's guarded one-segment-at-a-time absolute-directory
|
|
74
|
+
creator. Advanced callers can pass `create` when directory creation needs
|
|
75
|
+
platform-specific ACLs or another product-owned policy. The callback owns the
|
|
76
|
+
safety of its mutations and must create exactly `directoryPath`; fs-safe
|
|
77
|
+
validates and pins every resulting segment before any synchronization is
|
|
78
|
+
accepted.
|
|
79
|
+
|
|
80
|
+
`expectedExistingIdentity` binds an existing target to an identity observed by
|
|
81
|
+
the caller before a separate permission or policy check. A missing or replaced
|
|
82
|
+
target fails with `FsSafeError("path-mismatch")`.
|
|
83
|
+
|
|
84
|
+
## Exclusive file publication
|
|
85
|
+
|
|
86
|
+
`publishFileExclusive()` materializes one file without clobbering an existing
|
|
87
|
+
target. It pins the source with `O_NOFOLLOW`, optionally verifies
|
|
88
|
+
`expectedSourceIdentity`, tries a hardlink first, then synchronizes the target
|
|
89
|
+
parent directory.
|
|
90
|
+
|
|
91
|
+
For example, a backup archive is complete before publication. If directory
|
|
92
|
+
sync fails, keeping that complete file is more useful than conditionally
|
|
93
|
+
deleting it by pathname:
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
import { FsSafeError } from "@openclaw/fs-safe/errors";
|
|
97
|
+
import { publishFileExclusive } from "@openclaw/fs-safe/durability";
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
const result = await publishFileExclusive({
|
|
101
|
+
sourcePath: stagedArchive,
|
|
102
|
+
targetPath: finalArchive,
|
|
103
|
+
strategy: "link-or-copy",
|
|
104
|
+
onSyncFailure: "preserve",
|
|
105
|
+
parentReceipt: backupDirectory,
|
|
106
|
+
});
|
|
107
|
+
recordDurableBackup(result.identity, result.directorySync);
|
|
108
|
+
} catch (error) {
|
|
109
|
+
if (
|
|
110
|
+
error instanceof FsSafeError &&
|
|
111
|
+
error.details?.phase === "directory-sync" &&
|
|
112
|
+
error.details.cleanup === "preserved"
|
|
113
|
+
) {
|
|
114
|
+
recordCompleteButPossiblyNonDurableBackup(finalArchive, error.details);
|
|
115
|
+
} else {
|
|
116
|
+
throw error;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Strategies
|
|
122
|
+
|
|
123
|
+
| Strategy | Behavior | Native requirement |
|
|
124
|
+
|---|---|---|
|
|
125
|
+
| `link-required` | Create a same-filesystem hardlink or propagate the failure. | No; guarded JS `link` fallback remains. |
|
|
126
|
+
| `link-or-copy` | Try hardlink, then clone, Linux `copy_file_range`, then the JS byte loop for classified unsupported errors. | No; acceleration is optional. |
|
|
127
|
+
| `rename-noreplace` | Atomically move the source without replacing an existing target. Success consumes `sourcePath`. | Yes. |
|
|
128
|
+
|
|
129
|
+
`"link-required"` propagates an unsupported hardlink failure.
|
|
130
|
+
`"link-or-copy"` falls back only for `EPERM`, `EXDEV`, `ENOTSUP`,
|
|
131
|
+
`EOPNOTSUPP`, or `ENOSYS`; `isHardlinkFallbackError()` exposes that exact
|
|
132
|
+
classifier. The fallback copies from the pinned source into a `wx` target,
|
|
133
|
+
fsyncs it, and fences source and target identity and content before reporting
|
|
134
|
+
success. `parentReceipt`, when supplied, must name the target's direct parent.
|
|
135
|
+
|
|
136
|
+
With a native binding, the copy fallback first attempts a copy-on-write clone
|
|
137
|
+
(`fclonefileat` on macOS, `FICLONE` on Linux), then Linux
|
|
138
|
+
`copy_file_range`, and finally the existing JavaScript byte loop. Every route
|
|
139
|
+
creates the target exclusively, normalizes its mode to `0o600`, and goes
|
|
140
|
+
through the same post-copy identity and SHA-256 fencing. Hashing uses an async
|
|
141
|
+
native task when available, so large verification reads do not occupy the
|
|
142
|
+
JavaScript event loop.
|
|
143
|
+
|
|
144
|
+
On a clone-capable filesystem, publication of a large file becomes mostly a
|
|
145
|
+
metadata operation: data blocks are shared copy-on-write until either file is
|
|
146
|
+
modified. Clone support is filesystem- and mount-dependent, so callers must
|
|
147
|
+
not infer durability or physical independence from timing; an unsupported
|
|
148
|
+
clone or `copy_file_range` transparently continues down the fallback chain.
|
|
149
|
+
|
|
150
|
+
## Recoverable atomic-replace fallback
|
|
151
|
+
|
|
152
|
+
`replaceFileAtomic()` normally publishes a synchronized sibling temp with an
|
|
153
|
+
atomic rename. Some Windows filesystems and file owners reject that rename with
|
|
154
|
+
`EPERM` or `EEXIST`; `copyFallbackOnPermissionError: true` permits a non-atomic
|
|
155
|
+
copy fallback.
|
|
156
|
+
|
|
157
|
+
Callers that cannot tolerate a torn in-place fallback can add:
|
|
158
|
+
|
|
159
|
+
```ts
|
|
160
|
+
await replaceFileAtomic({
|
|
161
|
+
filePath: statePath,
|
|
162
|
+
content: nextState,
|
|
163
|
+
syncTempFile: true,
|
|
164
|
+
syncParentDir: true,
|
|
165
|
+
copyFallbackOnPermissionError: true,
|
|
166
|
+
copyFallbackRestore: "restore-original",
|
|
167
|
+
maxRestoreBytes: 4 * 1024 * 1024,
|
|
168
|
+
destinationHardlinks: "reject",
|
|
169
|
+
});
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
The existing regular-file destination is pinned before its link count is
|
|
173
|
+
accepted. Its original bytes are read within `maxRestoreBytes`, then the new
|
|
174
|
+
bytes are written and synchronized through the same descriptor. If a write or
|
|
175
|
+
sync tears, fs-safe rewrites the snapshot and fsyncs it before throwing. Inspect
|
|
176
|
+
`details.cleanup`: `"restored"` means the original bytes were put back and
|
|
177
|
+
synchronized; `"restore-failed"` means the replacement and recovery both
|
|
178
|
+
failed, so the destination must be treated as indeterminate. This is recovery
|
|
179
|
+
from a live-process I/O failure, not a transaction or a substitute for an
|
|
180
|
+
application backup protocol.
|
|
181
|
+
|
|
182
|
+
## Streaming SHA-256
|
|
183
|
+
|
|
184
|
+
`sha256File()` hashes either a pathname string or an already-open Node
|
|
185
|
+
`FileHandle`. A backup verifier can pin the file itself, compare its size, and
|
|
186
|
+
keep ownership of the handle:
|
|
187
|
+
|
|
188
|
+
```ts
|
|
189
|
+
import { open } from "node:fs/promises";
|
|
190
|
+
import { sha256File } from "@openclaw/fs-safe/durability";
|
|
191
|
+
|
|
192
|
+
const snapshot = await open(stagedArchive, "r");
|
|
193
|
+
try {
|
|
194
|
+
const before = await snapshot.stat();
|
|
195
|
+
const hash = await sha256File(snapshot);
|
|
196
|
+
if (hash.bytes !== before.size || hash.digest !== manifest.sha256) {
|
|
197
|
+
throw new Error("staged backup does not match its manifest");
|
|
198
|
+
}
|
|
199
|
+
} finally {
|
|
200
|
+
await snapshot.close();
|
|
201
|
+
}
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
The result is `{ bytes, digest }`, where `digest` is lowercase hexadecimal.
|
|
205
|
+
The handle overload never closes the caller's descriptor and uses positioned
|
|
206
|
+
reads, so it does not alter the descriptor's current offset. The path overload
|
|
207
|
+
rejects symbolic links and non-regular files, verifies the opened descriptor
|
|
208
|
+
still names the requested path, and closes its own handle. POSIX opens are
|
|
209
|
+
nonblocking, so a raced FIFO or device is rejected after descriptor inspection
|
|
210
|
+
rather than waiting for a writer.
|
|
211
|
+
|
|
212
|
+
When the optional binding is active, hashing runs as an async native task and
|
|
213
|
+
does not occupy the JavaScript event loop with digest updates. With native mode
|
|
214
|
+
`off`, or in `auto` when no binding loads, the fallback performs asynchronous
|
|
215
|
+
positioned reads in 64 KiB chunks but updates Node's `Hash` on the JavaScript
|
|
216
|
+
thread. Both paths stream constant-size buffers rather than loading the file
|
|
217
|
+
into memory. Native mode `require` keeps its usual fail-closed loader semantics.
|
|
218
|
+
|
|
219
|
+
If publication fails after this call created the target, it throws an
|
|
220
|
+
`FsSafeError` with a `details` receipt:
|
|
221
|
+
|
|
222
|
+
```ts
|
|
223
|
+
type PublishFileExclusiveFailureDetails = {
|
|
224
|
+
phase:
|
|
225
|
+
| "hardlink-create" | "hardlink-verify"
|
|
226
|
+
| "copy-create" | "copy-verify"
|
|
227
|
+
| "rename-create" | "rename-verify"
|
|
228
|
+
| "directory-sync";
|
|
229
|
+
targetCreated: boolean;
|
|
230
|
+
targetIdentity?: { dev: number | bigint; ino: number | bigint };
|
|
231
|
+
cleanup: "removed" | "preserved" | "unknown";
|
|
232
|
+
directorySync?: { status: "failed"; code?: string };
|
|
233
|
+
};
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
`"removed"` means the path still matched the identity created by this call and
|
|
237
|
+
was unlinked (or was already absent). `"preserved"` means it was deliberately
|
|
238
|
+
retained—for example after a successful no-replace rename—or the pathname had
|
|
239
|
+
been replaced and therefore was not safe to remove. `"unknown"` means cleanup
|
|
240
|
+
could not verify or remove the created identity. Callers that run a second
|
|
241
|
+
application-level guard, such as SQLite snapshot validation, should branch on
|
|
242
|
+
this receipt instead of inferring ownership from path existence. The original
|
|
243
|
+
failure remains available as `cause`. Failures before target creation retain
|
|
244
|
+
their existing error shape and do not claim a cleanup result.
|
|
245
|
+
|
|
246
|
+
### Directory-sync failure policy
|
|
247
|
+
|
|
248
|
+
`onSyncFailure` applies only after target creation and content/identity fencing
|
|
249
|
+
have succeeded but synchronizing the containing directory throws:
|
|
250
|
+
|
|
251
|
+
```ts
|
|
252
|
+
type PublishFileExclusiveSyncFailurePolicy = "rollback" | "preserve";
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
A returned `{ status: "unsupported", code? }` is an explicit successful
|
|
256
|
+
publication outcome, not a thrown sync failure, so this option does not rewrite
|
|
257
|
+
or clean up that target.
|
|
258
|
+
|
|
259
|
+
- `rollback` is the default. fs-safe removes the target only if its current
|
|
260
|
+
identity still matches the file created by this call. A replacement is never
|
|
261
|
+
removed. The error reports `cleanup: "removed"`, `"preserved"`, or
|
|
262
|
+
`"unknown"` and `directorySync: { status: "failed", code? }`.
|
|
263
|
+
- `preserve` never attempts that unlink. The error reports
|
|
264
|
+
`targetCreated: true`, `cleanup: "preserved"`, the created identity, and the
|
|
265
|
+
failed directory-sync outcome. The file is complete and fenced, but its
|
|
266
|
+
directory entry is not proven crash-durable.
|
|
267
|
+
|
|
268
|
+
Choose `rollback` when the pathname must mean “durably committed” and a failed
|
|
269
|
+
commit should disappear from the live process view. Choose `preserve` when the
|
|
270
|
+
payload itself remains valuable—backup archives are the common case—and the
|
|
271
|
+
caller can record, retry, or independently validate durability. Neither choice
|
|
272
|
+
can make a failed directory sync succeed: rollback deletion is also not proven
|
|
273
|
+
durable, and a preserved name may disappear after a crash. Always use the
|
|
274
|
+
typed receipt rather than inferring ownership from `exists()`.
|
|
275
|
+
|
|
276
|
+
`rename-noreplace` always preserves its target after a successful rename,
|
|
277
|
+
because removing it would discard the source's only remaining name; its typed
|
|
278
|
+
failure receipt makes that explicit regardless of `onSyncFailure`.
|
|
279
|
+
|
|
280
|
+
`"rename-noreplace"` requires the native helper and atomically moves the
|
|
281
|
+
source to the target without replacement. A collision is reported as
|
|
282
|
+
`EEXIST`, both files remain unchanged, and a successful call returns
|
|
283
|
+
`method: "rename-noreplace"` after synchronizing the source and target parent
|
|
284
|
+
directories. Unlike the link/copy strategies, success consumes `sourcePath`.
|
|
285
|
+
|
|
286
|
+
## Scope
|
|
287
|
+
|
|
288
|
+
These primitives establish path identity and filesystem synchronization. One
|
|
289
|
+
`publishFileExclusive()` call is one no-clobber file materialization, not a
|
|
290
|
+
retention policy, multi-file transaction, or application commit protocol. They
|
|
291
|
+
do not decide application commit protocols, marker formats, permission policy,
|
|
292
|
+
or whether an unsupported platform is acceptable. Keep those decisions at the
|
|
293
|
+
owning product boundary.
|
|
294
|
+
|
|
295
|
+
## See also
|
|
296
|
+
|
|
297
|
+
- [Migrating to 0.5](migrating-to-0.5.md) — choosing a publication policy during upgrade.
|
|
298
|
+
- [Native architecture](native.md) — clone/copy/hash mechanisms and fallback guarantees.
|
|
299
|
+
- [Errors](errors.md) — typed operational failure handling.
|
package/docs/errors.md
CHANGED
|
@@ -13,13 +13,41 @@ class FsSafeError extends Error {
|
|
|
13
13
|
readonly name: "FsSafeError";
|
|
14
14
|
readonly code: FsSafeErrorCode;
|
|
15
15
|
readonly category: "policy" | "operational";
|
|
16
|
+
readonly details?: Readonly<Record<string, unknown>>;
|
|
16
17
|
|
|
17
|
-
constructor(
|
|
18
|
+
constructor(
|
|
19
|
+
code: FsSafeErrorCode,
|
|
20
|
+
message: string,
|
|
21
|
+
options?: { cause?: unknown; details?: Readonly<Record<string, unknown>> },
|
|
22
|
+
);
|
|
18
23
|
}
|
|
19
24
|
```
|
|
20
25
|
|
|
21
26
|
`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.
|
|
22
27
|
|
|
28
|
+
`details` is an operation-specific receipt, not an alternate error code. For
|
|
29
|
+
example, `publishFileExclusive()` uses it to report the failing phase, created
|
|
30
|
+
target identity, cleanup decision, and failed directory-sync outcome. Narrow
|
|
31
|
+
by `code` and a documented details field before consuming it; do not assume all
|
|
32
|
+
`FsSafeError` instances carry the same keys.
|
|
33
|
+
|
|
34
|
+
`replaceFileAtomic({ copyFallbackRestore: "restore-original" })` reports a
|
|
35
|
+
failed copy fallback with the exported `ReplaceFileAtomicRestoreFailureDetails`
|
|
36
|
+
shape:
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
type ReplaceFileAtomicRestoreFailureDetails = {
|
|
40
|
+
cleanup: "restored" | "restore-failed";
|
|
41
|
+
};
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Both outcomes use `code: "helper-failed"`. `"restored"` means the original
|
|
45
|
+
snapshot was written back and fsynced through the pinned destination handle.
|
|
46
|
+
`"restore-failed"` means both the replacement and recovery failed; `cause` is
|
|
47
|
+
an `AggregateError` containing both failures. A snapshot that exceeds
|
|
48
|
+
`maxRestoreBytes` fails earlier with `too-large` and does not overwrite the
|
|
49
|
+
destination.
|
|
50
|
+
|
|
23
51
|
`category` separates caller-policy failures from operational failures:
|
|
24
52
|
|
|
25
53
|
- `"policy"` — unsafe input or target state, such as `outside-workspace`, `symlink`, `hardlink`, or `too-large`.
|
|
@@ -46,6 +74,7 @@ type FsSafeErrorCode =
|
|
|
46
74
|
| "path-alias"
|
|
47
75
|
| "path-mismatch"
|
|
48
76
|
| "permission-unverified"
|
|
77
|
+
| "secret-exists"
|
|
49
78
|
| "symlink"
|
|
50
79
|
| "timeout"
|
|
51
80
|
| "too-large"
|
|
@@ -60,8 +89,8 @@ type FsSafeErrorCode =
|
|
|
60
89
|
| `denied-path` | A root mutation matched `denyMutations.paths` or `denyMutations.prefixes`. | Caller configured application-sensitive paths that must not be written, removed, moved, or created. |
|
|
61
90
|
| `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. |
|
|
62
91
|
| `hardlink` | Read or copy with `hardlinks: "reject"` saw `nlink > 1`. | File is hardlinked — possibly an alias of an out-of-tree inode. |
|
|
63
|
-
| `helper-failed` |
|
|
64
|
-
| `helper-unavailable` |
|
|
92
|
+
| `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. |
|
|
93
|
+
| `helper-unavailable` | Required native binding could not be loaded. | Unsupported platform, missing/incompatible bundled binary, or `FS_SAFE_NATIVE_MODE=require`. `auto` falls back where possible; `require` fails closed. |
|
|
65
94
|
| `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. |
|
|
66
95
|
| `invalid-path` | Input was empty, contained NUL, was an unparseable URL, or otherwise unusable. | Caller didn't validate input; input was a network path on Windows. |
|
|
67
96
|
| `not-empty` | `remove()` on a non-empty directory. | Use `replaceDirectoryAtomic` or remove children first. |
|
|
@@ -73,9 +102,10 @@ type FsSafeErrorCode =
|
|
|
73
102
|
| `path-alias` | A path alias check failed (e.g. canonical-real-path moved out of the root). | Symlink resolution lands outside the root. |
|
|
74
103
|
| `path-mismatch` | Post-open identity check failed: the opened fd does not match the resolved path. | TOCTOU — something else swapped the path between resolve and open. |
|
|
75
104
|
| `permission-unverified` | A secure file check could not verify required permissions. | Windows ACL inspection failed; POSIX ownership/mode was unavailable. |
|
|
105
|
+
| `secret-exists` | `createSecretFileAtomic()` found an existing final path. | First-writer-wins secret creation lost a race or the credential was already initialized. |
|
|
76
106
|
| `symlink` | Path component is a symlink, policy is `reject`. | Caller followed a symlink they shouldn't have, or `symlinks: "reject"` is set. |
|
|
77
107
|
| `timeout` | An operation with a wall-clock budget overran. | Secure file read or timed operation exceeded `timeoutMs`. |
|
|
78
|
-
| `too-large` |
|
|
108
|
+
| `too-large` | A read or bounded walk exceeded its configured budget. | Caller gave a too-permissive file or traversal limit. |
|
|
79
109
|
| `unsupported-platform` | The requested operation is not supported on the current platform. | E.g. POSIX-only helper invoked on Windows. |
|
|
80
110
|
|
|
81
111
|
## Branching
|
|
@@ -139,8 +169,8 @@ A common pattern is to wrap your domain code in a single try/catch that maps bot
|
|
|
139
169
|
A handful of helpers throw their own typed errors instead of `FsSafeError`:
|
|
140
170
|
|
|
141
171
|
- `JsonFileReadError` — thrown by [`readJson`](json.md). Carries `cause` so you can distinguish missing (`ENOENT`) from invalid (`SyntaxError`).
|
|
142
|
-
- `ArchiveLimitError` — thrown by [`extractArchive`](archive.md) when an archive size, entry count, or extracted-byte budget is exceeded. The `code` field uses `ARCHIVE_LIMIT_ERROR_CODE`
|
|
143
|
-
- `ArchiveSecurityError` — thrown by extraction when
|
|
172
|
+
- `ArchiveLimitError` — thrown by [`extractArchive`](archive.md) when an archive size, entry count, path depth, metadata, or extracted-byte budget is exceeded. The `code` field uses the string values exposed by `ARCHIVE_LIMIT_ERROR_CODE` (for example `archive-entry-path-components-exceeds-limit`).
|
|
173
|
+
- `ArchiveSecurityError` — thrown by extraction when entry policy or destination safety fails. Entry codes are `entry-path`, `entry-link`, and `entry-filtered`; destination codes cover non-directory, symlink, and symlink-traversal failures.
|
|
144
174
|
|
|
145
175
|
These are exported from their respective subpaths.
|
|
146
176
|
|
package/docs/filename.md
CHANGED
|
Binary file
|
package/docs/index.md
CHANGED
|
@@ -38,7 +38,8 @@ await fs.remove("notes/archive/today.txt");
|
|
|
38
38
|
## Pick your path
|
|
39
39
|
|
|
40
40
|
- **First time?** [Install](install.md), then walk through the [Quickstart](quickstart.md). Five minutes from `pnpm add` to a working root.
|
|
41
|
-
- **
|
|
41
|
+
- **Upgrading from 0.4?** Follow [Migrating to 0.5](migrating-to-0.5.md) in order, including the archive clamp-default audit.
|
|
42
|
+
- **Designing a workspace feature.** Read the [Security model](security-model.md) before you trust the boundary, the [native helper policy](native-helper.md) before you pick deployment defaults, and the [Errors](errors.md) reference so you know what to catch.
|
|
42
43
|
- **Replacing ad-hoc atomic writes.** Jump to [Atomic writes](atomic.md) or, for keyed JSON state, [JSON files](json.md).
|
|
43
44
|
- **Extracting an upload.** Start at [Archive extraction](archive.md) — handles ZIP and TAR with traversal, link, count, and byte limits.
|
|
44
45
|
- **Running an agent in a sandbox.** [Private temp workspaces](temp.md) plus [secret files](secret-file.md) cover the common scratch-and-credentials shape.
|
|
@@ -48,10 +49,12 @@ await fs.remove("notes/archive/today.txt");
|
|
|
48
49
|
|
|
49
50
|
| Surface | Use it for |
|
|
50
51
|
|---|---|
|
|
51
|
-
| [`root()`](root.md) | One boundary for read/write/move/remove inside a trusted directory. |
|
|
52
|
-
| [`@openclaw/fs-safe/config`](config.md) | Process-global
|
|
53
|
-
| [
|
|
52
|
+
| [`root()`](root.md) | One boundary for read/write/move/remove and bounded recursive walking inside a trusted directory. |
|
|
53
|
+
| [`@openclaw/fs-safe/config`](config.md) | Process-global native helper and lock-option defaults. |
|
|
54
|
+
| [Native helper policy](native-helper.md) | Choose `auto`, `off`, or `require` for bundled native primitives. |
|
|
55
|
+
| [Native architecture](native.md) | Understand the thin syscall layer, beneath model, platform mechanisms, and fallback boundary. |
|
|
54
56
|
| [`replaceFileAtomic`](atomic.md) | Sibling-temp + rename, fsync hooks, mode preservation, copy fallback. |
|
|
57
|
+
| [`@openclaw/fs-safe/durability`](durability.md) | Pinned directory identities, durable creation, exclusive publication, streaming SHA-256, provenance receipts, and sync-failure policy. |
|
|
55
58
|
| [`writeExternalFileWithinRoot`](output.md) | Stage external-library file output in private temp storage, then finalize under a root. |
|
|
56
59
|
| [`writeJson` / `readJson*`](json.md) | JSON state files with strict and lenient read variants. |
|
|
57
60
|
| [`@openclaw/fs-safe/store`](store.md) | Overview of `fileStore`, `fileStoreSync`, and `jsonStore`. |
|
|
@@ -60,15 +63,16 @@ await fs.remove("notes/archive/today.txt");
|
|
|
60
63
|
| [Private file-store mode](private-file-store.md) | `fileStore({ private: true })` for private JSON/text state at 0600 under 0700 dirs. |
|
|
61
64
|
| [`tempWorkspace`](temp.md) | 0700 scratch dir with auto-cleanup. |
|
|
62
65
|
| [`readSecureFile`](secure-file.md) | Absolute file reads with fd pinning, permissions, owner, size, and timeout checks. |
|
|
63
|
-
| [`walkDirectory`](walk.md) |
|
|
64
|
-
| [`extractArchive`](archive.md) | ZIP/TAR extraction with
|
|
66
|
+
| [`walkDirectory` / `Root.walk`](walk.md) | Standalone inventories plus root-bounded pruning, budgets, and partial-error reporting. |
|
|
67
|
+
| [`extractArchive`](archive.md) | Policy-driven ZIP/TAR extraction with clamp/filter, metadata/path-depth, link, count, and byte limits. |
|
|
65
68
|
| [Secret files](secret-file.md) | Mode-0600 credentials with size and TOCTOU defense. |
|
|
66
|
-
| [Permissions](permissions.md) | POSIX mode
|
|
69
|
+
| [Permissions](permissions.md) | POSIX mode helpers plus Windows ACL inspection, raw owner/ACE facts, remediation, and private-directory creation. |
|
|
67
70
|
| [`acquireFileLock`](sidecar-lock.md) | Cross-process file lock with retry and fail-closed stale-lock handling. |
|
|
68
71
|
| [`FsSafeError`](errors.md) | Closed code union (with `policy` / `operational` category) you can branch on. |
|
|
69
72
|
| [`pathScope()`](path-scope.md) | Lower-level absolute-path boundary helper; lives behind `@openclaw/fs-safe/advanced`. |
|
|
70
73
|
| [`@openclaw/fs-safe/advanced`](advanced.md) | Directory of lower-level composition helpers (path scopes, regular-file I/O, install paths, sibling-temp writes, …). |
|
|
71
74
|
| [`@openclaw/fs-safe/test-hooks`](test-hooks.md) | Test-only injection hooks for reproducing open/lstat races. |
|
|
75
|
+
| [Migrating to 0.5](migrating-to-0.5.md) | End-to-end checklist for Python removal and 0.4 API behavior changes. |
|
|
72
76
|
|
|
73
77
|
## Status
|
|
74
78
|
|
package/docs/install.md
CHANGED
|
@@ -62,20 +62,22 @@ Use the main entry for the common surface, or the focused subpaths when you want
|
|
|
62
62
|
|
|
63
63
|
| Subpath | Contents |
|
|
64
64
|
|---|---|
|
|
65
|
-
| `@openclaw/fs-safe` |
|
|
66
|
-
| `@openclaw/fs-safe/root` | `root()`, `Root`, `RootDefaults`,
|
|
67
|
-
| `@openclaw/fs-safe/config` | Process-global
|
|
65
|
+
| `@openclaw/fs-safe` | Common root, config, output, lock, native-mode, and error exports. |
|
|
66
|
+
| `@openclaw/fs-safe/root` | `root()`, `Root`, `RootDefaults`, and root-walk types. |
|
|
67
|
+
| `@openclaw/fs-safe/config` | Process-global native helper and lock defaults. |
|
|
68
68
|
| `@openclaw/fs-safe/path` | `isPathInside`, `safeRealpathSync`, `isWithinDir`, error helpers. |
|
|
69
|
+
| `@openclaw/fs-safe/output` | Guarded staging/finalization for libraries that require an absolute output path. |
|
|
69
70
|
| `@openclaw/fs-safe/json` | `tryReadJson`, `readJson`, `readJsonIfExists`, `writeJson`, sync variants. |
|
|
70
71
|
| `@openclaw/fs-safe/store` | `fileStore()`, `fileStoreSync()`, and `jsonStore<T>()`. |
|
|
71
72
|
| `@openclaw/fs-safe/secret` | Secret file read/write helpers. |
|
|
72
73
|
| `@openclaw/fs-safe/atomic` | `replaceFileAtomic`, `writeTextAtomic`, `replaceDirectoryAtomic`, `movePathWithCopyFallback`. |
|
|
74
|
+
| `@openclaw/fs-safe/durability` | Pinned directories, strict sync, durable directory creation, exclusive publication, and streaming SHA-256. |
|
|
73
75
|
| `@openclaw/fs-safe/temp` | `tempWorkspace`, `withTempWorkspace`, sync variants, `resolveSecureTempRoot`. |
|
|
74
76
|
| `@openclaw/fs-safe/secure-file` | `readSecureFile` for pinned absolute file reads with permissions checks. |
|
|
75
77
|
| `@openclaw/fs-safe/file-lock` | `acquireFileLock`, `withFileLock`, `createFileLockManager`, and related lock types. |
|
|
76
|
-
| `@openclaw/fs-safe/permissions` | POSIX mode
|
|
78
|
+
| `@openclaw/fs-safe/permissions` | POSIX mode helpers, Windows ACL inspection/remediation, raw owner/ACE facts, and private-directory creation. |
|
|
77
79
|
| `@openclaw/fs-safe/walk` | `walkDirectory`, `walkDirectorySync`, related types. Budget-bounded, not root-bounded. |
|
|
78
|
-
| `@openclaw/fs-safe/archive` | `extractArchive`, `
|
|
80
|
+
| `@openclaw/fs-safe/archive` | `extractArchive`, `readArchiveEntry`, kind resolution, policy types, limits, and preflight helpers. |
|
|
79
81
|
| `@openclaw/fs-safe/advanced` | Lower-level composition helpers: path scopes, root-file open, install paths, local-root readers, temp-file targets, sibling-temp writes, regular-file helpers, `pathExists`, `withTimeout`, and related advanced types. This surface is less stable than the focused public subpaths. |
|
|
80
82
|
| `@openclaw/fs-safe/errors` | `FsSafeError`, `FsSafeErrorCode`. |
|
|
81
83
|
| `@openclaw/fs-safe/types` | Shared types: `DirEntry`, `PathStat`, `BasePathOptions`, … |
|
|
@@ -85,40 +87,39 @@ Use the main entry for the common surface, or the focused subpaths when you want
|
|
|
85
87
|
|
|
86
88
|
`@openclaw/fs-safe` lists `jszip` and `tar` as optional dependencies for [archive extraction](archive.md). They are loaded lazily and only required when ZIP/TAR helpers run. Installs that omit optional dependencies can still import and use every non-archive subpath; archive calls fail with a clear missing-optional-dependency message.
|
|
87
89
|
|
|
88
|
-
There are no peer dependencies
|
|
90
|
+
There are no peer dependencies. The single npm package bundles all seven native binaries, so consumers do not run a native build, download platform code, or execute a postinstall step. Shipping every target increases the tarball size compared with per-platform packages, intentionally trading bandwidth for deterministic installation.
|
|
89
91
|
|
|
90
|
-
|
|
92
|
+
Upgrading an existing consumer? Follow [Migrating to 0.5](migrating-to-0.5.md)
|
|
93
|
+
before choosing a native mode or accepting the new archive clamp default.
|
|
91
94
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
95
|
+
## Native helper policy
|
|
96
|
+
|
|
97
|
+
The bundled native binaries provide fd-relative open/link/mkdir primitives,
|
|
98
|
+
atomic no-replace rename, and file identity checks. The default is `auto`: use
|
|
99
|
+
the matching binary when it loads, otherwise silently keep the guarded
|
|
100
|
+
JavaScript path. Platforms without one of the seven bundled targets therefore
|
|
101
|
+
continue through the documented fallback in `auto` mode.
|
|
96
102
|
|
|
97
103
|
```ts
|
|
98
|
-
import {
|
|
104
|
+
import { configureFsSafeNative } from "@openclaw/fs-safe/config";
|
|
99
105
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
106
|
+
configureFsSafeNative({ mode: "auto" }); // default
|
|
107
|
+
configureFsSafeNative({ mode: "off" }); // guarded JavaScript only
|
|
108
|
+
configureFsSafeNative({ mode: "require" }); // fail closed if unavailable
|
|
103
109
|
```
|
|
104
110
|
|
|
105
111
|
Environment variables are read at runtime:
|
|
106
112
|
|
|
107
113
|
```bash
|
|
108
|
-
|
|
109
|
-
FS_SAFE_PYTHON=/usr/bin/python3
|
|
114
|
+
FS_SAFE_NATIVE_MODE=off # auto | off | require
|
|
110
115
|
```
|
|
111
116
|
|
|
112
|
-
|
|
113
|
-
`OPENCLAW_FS_SAFE_PYTHON_MODE`, `OPENCLAW_FS_SAFE_PYTHON`,
|
|
114
|
-
`OPENCLAW_PINNED_PYTHON`, and `OPENCLAW_PINNED_WRITE_PYTHON`.
|
|
117
|
+
`OPENCLAW_FS_SAFE_NATIVE_MODE` is also accepted.
|
|
115
118
|
|
|
116
|
-
Disabling
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
failure instead of a fallback. The full tradeoff is documented in
|
|
121
|
-
[Python helper policy](python-helper.md).
|
|
119
|
+
Disabling native loading keeps the public API working through Node path
|
|
120
|
+
operations guarded by lexical and canonical checks plus identity verification.
|
|
121
|
+
Use `require` when native-backed operations must fail instead of falling back.
|
|
122
|
+
The exact boundary is documented in [native helper policy](native-helper.md).
|
|
122
123
|
|
|
123
124
|
## Verify the install
|
|
124
125
|
|
package/docs/json-store.md
CHANGED
|
@@ -113,7 +113,17 @@ Read, transform, write — under the lock if locking is enabled. Returns the new
|
|
|
113
113
|
const next = await store.update((prev) => ({ count: (prev?.count ?? 0) + 1 }));
|
|
114
114
|
```
|
|
115
115
|
|
|
116
|
-
`run` is async-friendly. The whole `read → run → write` sequence
|
|
116
|
+
`run` is async-friendly. The whole `read → run → write` sequence is serialized
|
|
117
|
+
by canonical file path inside the process. With locking enabled, the sidecar
|
|
118
|
+
lock is acquired inside that queue, so concurrent updaters from different
|
|
119
|
+
processes serialize cleanly too.
|
|
120
|
+
|
|
121
|
+
Do not call `write()`, `update()`, or `updateOr()` for the same file from inside
|
|
122
|
+
an update callback. That nested mutation cannot run until the outer update
|
|
123
|
+
finishes, so `jsonStore` rejects it immediately with
|
|
124
|
+
`FsSafeError("store-reentrant-update")`. Return the complete next value from the
|
|
125
|
+
outer callback instead. The check follows Node async context, including promise
|
|
126
|
+
and `queueMicrotask` boundaries.
|
|
117
127
|
|
|
118
128
|
Use `update(run)` when missing state is part of your model. Use `updateOr(fallback, run)` when the missing-file case should start from a concrete value and you want to merge into defaults:
|
|
119
129
|
|
|
@@ -137,13 +147,18 @@ const counter = jsonStore<{ count: number }>({
|
|
|
137
147
|
});
|
|
138
148
|
```
|
|
139
149
|
|
|
140
|
-
When `lock` is falsy,
|
|
150
|
+
When `lock` is falsy, writes and updates still serialize inside this process by
|
|
151
|
+
canonical file path, including across separate `jsonStore` handles. They offer
|
|
152
|
+
no concurrency guarantees if another process also writes to the file.
|
|
141
153
|
|
|
142
154
|
Process-wide lock defaults from `configureFsSafeLocks()` apply only after locking is explicitly enabled. They do not make JSON stores lock by default.
|
|
143
155
|
|
|
144
156
|
JSON store locks fail closed on stale sidecars by default. Opt-in `staleRecovery: "remove-if-unchanged"` requires caller approval and uses the same exclusive reclaim guard as the low-level sidecar-lock API.
|
|
145
157
|
|
|
146
|
-
The default `managerKey` namespaces the
|
|
158
|
+
The default `managerKey` namespaces the `FileLockManager` per absolute file
|
|
159
|
+
path. The JSON-store queue is independent of the manager key, so separate
|
|
160
|
+
handles and custom lock-manager namespaces still cannot overlap mutations of
|
|
161
|
+
the same canonical file path inside one process.
|
|
147
162
|
|
|
148
163
|
## Common patterns
|
|
149
164
|
|