@openclaw/fs-safe 0.4.7 → 0.5.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/CHANGELOG.md +58 -0
- package/README.md +82 -43
- 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/device-path.d.ts.map +1 -1
- package/dist/device-path.js +24 -2
- package/dist/durability.d.ts +2 -0
- package/dist/durability.d.ts.map +1 -1
- package/dist/durability.js +2 -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/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/path.d.ts.map +1 -1
- package/dist/path.js +3 -2
- 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/safe-path-segment.d.ts.map +1 -1
- package/dist/safe-path-segment.js +16 -4
- 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 +1 -0
- 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-target.d.ts.map +1 -1
- package/dist/temp-target.js +51 -3
- 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/archive.md +131 -13
- package/docs/atomic.md +30 -1
- package/docs/config.md +51 -21
- package/docs/durability.md +211 -1
- package/docs/errors.md +36 -6
- package/docs/filename.md +0 -0
- package/docs/index.md +11 -8
- 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 +29 -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 +10 -4
- 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
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,11 +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. |
|
|
55
|
-
| [
|
|
57
|
+
| [`@openclaw/fs-safe/durability`](durability.md) | Pinned directory identities, durable creation, exclusive publication, streaming SHA-256, provenance receipts, and sync-failure policy. |
|
|
56
58
|
| [`writeExternalFileWithinRoot`](output.md) | Stage external-library file output in private temp storage, then finalize under a root. |
|
|
57
59
|
| [`writeJson` / `readJson*`](json.md) | JSON state files with strict and lenient read variants. |
|
|
58
60
|
| [`@openclaw/fs-safe/store`](store.md) | Overview of `fileStore`, `fileStoreSync`, and `jsonStore`. |
|
|
@@ -61,15 +63,16 @@ await fs.remove("notes/archive/today.txt");
|
|
|
61
63
|
| [Private file-store mode](private-file-store.md) | `fileStore({ private: true })` for private JSON/text state at 0600 under 0700 dirs. |
|
|
62
64
|
| [`tempWorkspace`](temp.md) | 0700 scratch dir with auto-cleanup. |
|
|
63
65
|
| [`readSecureFile`](secure-file.md) | Absolute file reads with fd pinning, permissions, owner, size, and timeout checks. |
|
|
64
|
-
| [`walkDirectory`](walk.md) |
|
|
65
|
-
| [`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. |
|
|
66
68
|
| [Secret files](secret-file.md) | Mode-0600 credentials with size and TOCTOU defense. |
|
|
67
|
-
| [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. |
|
|
68
70
|
| [`acquireFileLock`](sidecar-lock.md) | Cross-process file lock with retry and fail-closed stale-lock handling. |
|
|
69
71
|
| [`FsSafeError`](errors.md) | Closed code union (with `policy` / `operational` category) you can branch on. |
|
|
70
72
|
| [`pathScope()`](path-scope.md) | Lower-level absolute-path boundary helper; lives behind `@openclaw/fs-safe/advanced`. |
|
|
71
73
|
| [`@openclaw/fs-safe/advanced`](advanced.md) | Directory of lower-level composition helpers (path scopes, regular-file I/O, install paths, sibling-temp writes, …). |
|
|
72
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. |
|
|
73
76
|
|
|
74
77
|
## Status
|
|
75
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
|
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Migrating to 0.5
|
|
3
|
+
description: "Ordered checklist for moving a 0.4 consumer from the Python helper to native mode and adopting the 0.5 API contracts."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Migrating from 0.4 to 0.5
|
|
7
|
+
|
|
8
|
+
Use this checklist from top to bottom. Version 0.5 replaces the Python worker,
|
|
9
|
+
changes the default archive mode policy, and adds explicit contracts for
|
|
10
|
+
publication, walking, locks, secrets, and native-only features. Nothing in this
|
|
11
|
+
guide requires a Rust toolchain: all supported native binaries are prebuilt and
|
|
12
|
+
bundled in `@openclaw/fs-safe`.
|
|
13
|
+
|
|
14
|
+
## 1. Update the package and runtime
|
|
15
|
+
|
|
16
|
+
- Run on Node.js 22 or newer.
|
|
17
|
+
- Update `@openclaw/fs-safe` and regenerate every lock or shrinkwrap file your
|
|
18
|
+
deployment consumes.
|
|
19
|
+
- Keep optional dependencies enabled if you want JavaScript ZIP/TAR support.
|
|
20
|
+
Native loading no longer depends on optional packages because all seven
|
|
21
|
+
binaries ship in `@openclaw/fs-safe`. An install that omits optional packages
|
|
22
|
+
can still import fs-safe, but missing JS archive decoders fail with actionable
|
|
23
|
+
errors.
|
|
24
|
+
|
|
25
|
+
If you call `resolveRootPath()` or `resolveRootPathSync()` directly, upgrade to
|
|
26
|
+
0.5: versions through 0.4.7 could approve an in-root symlink traversal that
|
|
27
|
+
resolved outside the root. `root()` handles were not affected. See the
|
|
28
|
+
[affected versions and exposure](security-model.md#affected-versions-exposure).
|
|
29
|
+
|
|
30
|
+
## 2. Replace Python helper configuration
|
|
31
|
+
|
|
32
|
+
Change startup configuration before the first filesystem operation:
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
import { configureFsSafeNative } from "@openclaw/fs-safe/config";
|
|
36
|
+
|
|
37
|
+
configureFsSafeNative({ mode: "auto" });
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
| Remove from 0.4 | Use in 0.5 |
|
|
41
|
+
|---|---|
|
|
42
|
+
| `configureFsSafePython({ mode })` | `configureFsSafeNative({ mode })` |
|
|
43
|
+
| `FS_SAFE_PYTHON_MODE` | `FS_SAFE_NATIVE_MODE` |
|
|
44
|
+
| `OPENCLAW_FS_SAFE_PYTHON_MODE` | `OPENCLAW_FS_SAFE_NATIVE_MODE` |
|
|
45
|
+
| `pythonPath`, `FS_SAFE_PYTHON`, pinned-Python aliases | Nothing; bundled native binaries do not use an interpreter |
|
|
46
|
+
|
|
47
|
+
The old names warn once and map `auto`, `off`, or `require` so a shipped 0.4
|
|
48
|
+
deployment does not silently change policy. Interpreter paths are ignored and
|
|
49
|
+
Python is never spawned. Treat that warning as an upgrade diagnostic, not as a
|
|
50
|
+
second supported helper path.
|
|
51
|
+
|
|
52
|
+
Choose the production mode deliberately:
|
|
53
|
+
|
|
54
|
+
- `auto` keeps guarded JavaScript fallbacks when a binding is unavailable.
|
|
55
|
+
- `off` makes fallback testing deterministic.
|
|
56
|
+
- `require` fails with `helper-unavailable` instead of weakening an operation
|
|
57
|
+
that expected native support.
|
|
58
|
+
|
|
59
|
+
See [Native helper policy](native-helper.md) and
|
|
60
|
+
[Native architecture](native.md).
|
|
61
|
+
|
|
62
|
+
## 3. Audit every archive call
|
|
63
|
+
|
|
64
|
+
The 0.5 default is `entryModes: "clamp"`. Directories become `0o755`; files
|
|
65
|
+
become `0o644` or `0o755` when owner-execute was archived. Set
|
|
66
|
+
`entryModes: "preserve"` explicitly only if your 0.4 consumer intentionally
|
|
67
|
+
relied on archived rwx bits. Setuid, setgid, sticky bits, and archived ownership
|
|
68
|
+
are never restored.
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
await extractArchive({
|
|
72
|
+
archivePath: uploadPath,
|
|
73
|
+
destDir: restoreRoot,
|
|
74
|
+
timeoutMs: 30_000,
|
|
75
|
+
entryModes: "clamp",
|
|
76
|
+
entryFilter: (entry) =>
|
|
77
|
+
entry.path.startsWith("snapshot/cache/") ? "skip" : "extract",
|
|
78
|
+
onFiltered: "skip-entry",
|
|
79
|
+
limits: {
|
|
80
|
+
maxArchiveBytes: 256 * 1024 * 1024,
|
|
81
|
+
maxEntries: 50_000,
|
|
82
|
+
maxExtractedBytes: 512 * 1024 * 1024,
|
|
83
|
+
maxEntryBytes: 256 * 1024 * 1024,
|
|
84
|
+
maxMetaEntryBytes: 1024 * 1024,
|
|
85
|
+
maxEntryPathComponents: 64,
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Returning `"skip"` rejects the archive unless `onFiltered: "skip-entry"` is
|
|
91
|
+
explicit. Zstd and bzip2 TAR are native-only; ZIP, TAR, and gzip retain guarded
|
|
92
|
+
JavaScript implementations. Catch `ArchiveLimitError` by its code, including
|
|
93
|
+
`archive-entry-path-components-exceeds-limit` for deep implicit-directory
|
|
94
|
+
attacks. See [Archive extraction](archive.md).
|
|
95
|
+
|
|
96
|
+
## 4. Pick a publication failure policy
|
|
97
|
+
|
|
98
|
+
`publishFileExclusive()` never replaces an existing target. Choose a strategy
|
|
99
|
+
and decide what a post-create directory-sync failure means to your application:
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
await publishFileExclusive({
|
|
103
|
+
sourcePath: stagedArchive,
|
|
104
|
+
targetPath: finalArchive,
|
|
105
|
+
strategy: "link-or-copy",
|
|
106
|
+
onSyncFailure: "preserve",
|
|
107
|
+
});
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
`rollback` is the default: an unchanged target created by this call is removed
|
|
111
|
+
when directory sync throws. `preserve` keeps a complete but possibly
|
|
112
|
+
non-durable target and reports `cleanup: "preserved"` plus
|
|
113
|
+
`directorySync: { status: "failed", code? }` in the typed error. Backup
|
|
114
|
+
archives commonly need `preserve`; transactional protocols that expose only
|
|
115
|
+
durably committed names usually want `rollback`. See
|
|
116
|
+
[Directory durability](durability.md).
|
|
117
|
+
|
|
118
|
+
## 5. Replace recursive scans with an explicit walk policy
|
|
119
|
+
|
|
120
|
+
Use `Root.walk()` for caller-controlled relative paths. Every examined entry
|
|
121
|
+
consumes the budget even when filtered:
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
for await (const entry of workspace.walk("memory", {
|
|
125
|
+
maxDepth: 12,
|
|
126
|
+
maxEntries: 50_000,
|
|
127
|
+
symlinkPolicy: "skip",
|
|
128
|
+
entryFilter: (entry) =>
|
|
129
|
+
entry.kind === "directory" && entry.relativePath.endsWith("/.git")
|
|
130
|
+
? "skip-subtree"
|
|
131
|
+
: "include",
|
|
132
|
+
onDirectoryError: "skip-and-report",
|
|
133
|
+
})) {
|
|
134
|
+
if (entry.kind === "directory-error") {
|
|
135
|
+
reportIncompleteSubtree(entry.relativePath, entry.error);
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
indexEntry(entry);
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
The default directory-error policy remains `throw`. See
|
|
143
|
+
[Directory walking](walk.md).
|
|
144
|
+
|
|
145
|
+
## 6. Adopt the focused concurrency and secret APIs
|
|
146
|
+
|
|
147
|
+
- Use `acquireFileLockSync()` only in synchronous boot or migration code; retry
|
|
148
|
+
waits block the thread. Request-serving paths should use `withFileLock()`.
|
|
149
|
+
- Remove the `allowReentrant` boolean from async file-lock options. If a logical
|
|
150
|
+
holder intentionally nests acquisition, pass the same operation-scoped
|
|
151
|
+
`reentrantOwner` string to each acquisition; different or missing owners
|
|
152
|
+
contend normally. Never replace the boolean with a process-wide constant.
|
|
153
|
+
Locked and unlocked `jsonStore` mutations serialize by canonical file path
|
|
154
|
+
and do not opt into lock reentrancy; nested same-file mutations from an update
|
|
155
|
+
callback fail with `store-reentrant-update`, so return the complete value from
|
|
156
|
+
the outer callback instead.
|
|
157
|
+
- Use `createSecretFileAtomic()` for first-writer-wins credentials and catch
|
|
158
|
+
`secret-exists`; use `writeSecretFileAtomic()` only when replacement is the
|
|
159
|
+
intended protocol.
|
|
160
|
+
- Async `readSecretFile()` is strict; `tryReadSecretFile()` returns `undefined`
|
|
161
|
+
only for missing or blank content and still rejects suspicious files.
|
|
162
|
+
- Check `tempWorkspace.cleanup()` results when ownership matters;
|
|
163
|
+
`identity-mismatch` deliberately preserves a replacement path.
|
|
164
|
+
|
|
165
|
+
See [File locks](sidecar-lock.md), [Secret files](secret-file.md), and
|
|
166
|
+
[Temp workspaces](temp.md).
|
|
167
|
+
|
|
168
|
+
## 7. Gate native-only features
|
|
169
|
+
|
|
170
|
+
`createPrivateDirectory()` is Windows-only and native-only because a pathname
|
|
171
|
+
fallback cannot promise the same creation-time DACL. Zstd/bzip2 extraction and
|
|
172
|
+
`strategy: "rename-noreplace"` are also native-only. Test the unavailable path
|
|
173
|
+
instead of assuming installation always succeeds.
|
|
174
|
+
|
|
175
|
+
## 8. Run both behavior families in CI
|
|
176
|
+
|
|
177
|
+
For each consumer workflow that matters:
|
|
178
|
+
|
|
179
|
+
1. Run once with `FS_SAFE_NATIVE_MODE=auto` on every supported OS.
|
|
180
|
+
2. Run once with `FS_SAFE_NATIVE_MODE=off` to prove the JavaScript fallback.
|
|
181
|
+
3. Run native-required or native-only cases with `FS_SAFE_NATIVE_MODE=require`.
|
|
182
|
+
4. Exercise archive traversal/link/depth limits, publication sync failure, and
|
|
183
|
+
partial-walk reporting with production-shaped fixtures.
|
|
184
|
+
|
|
185
|
+
For downstream staging and backup consumers:
|
|
186
|
+
|
|
187
|
+
- [ ] Replace private whole-file hashing with `sha256File(path | FileHandle)`
|
|
188
|
+
from `durability`; native mode keeps digest work off the event loop and
|
|
189
|
+
the JavaScript fallback remains streaming.
|
|
190
|
+
- [ ] If Windows trust policy depends on exact principals, consume
|
|
191
|
+
`readOwnerAndDacl()` from `permissions`, reject incomplete/null/remote
|
|
192
|
+
descriptors as your policy requires, skip inherit-only ACEs where
|
|
193
|
+
appropriate, and apply the application's own SID allowlist.
|
|
194
|
+
|
|
195
|
+
The [Testing](testing.md) page documents the test hooks and mode setup used by
|
|
196
|
+
fs-safe itself.
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Native helper policy
|
|
3
|
+
description: "How fs-safe loads its bundled native filesystem primitives and how auto, require, and off affect guarded fallbacks."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Native helper policy
|
|
7
|
+
|
|
8
|
+
`@openclaw/fs-safe` itself contains seven prebuilt binaries for Linux x64/arm64 (glibc or musl), macOS x64/arm64, and Windows x64. The loader selects `dist/native/<target>/fs-safe-native.node` without optional platform packages, downloads, postinstall scripts, or a consumer Rust build. Carrying every target makes the npm tarball larger, but every installation receives the same complete artifact.
|
|
9
|
+
|
|
10
|
+
```ts
|
|
11
|
+
import { configureFsSafeNative } from "@openclaw/fs-safe/config";
|
|
12
|
+
|
|
13
|
+
configureFsSafeNative({ mode: "auto" }); // default
|
|
14
|
+
configureFsSafeNative({ mode: "off" }); // guarded JavaScript only
|
|
15
|
+
configureFsSafeNative({ mode: "require" }); // fail closed when the binding is unavailable
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
The equivalent environment variables are `FS_SAFE_NATIVE_MODE` and `OPENCLAW_FS_SAFE_NATIVE_MODE`. Accepted values are `auto`, `off`, `require`, `true`, `false`, `on`, `never`, `required`, `1`, and `0`.
|
|
19
|
+
|
|
20
|
+
## Modes
|
|
21
|
+
|
|
22
|
+
| Mode | Behavior |
|
|
23
|
+
|---|---|
|
|
24
|
+
| `auto` | Prefer native primitives when the current bundled binary loads; otherwise silently use the guarded JavaScript path. |
|
|
25
|
+
| `off` | Do not load a bundled binary. Use the guarded JavaScript path deterministically. |
|
|
26
|
+
| `require` | Throw `FsSafeError("helper-unavailable")` instead of falling back when an operation needs the native binding and it cannot load. |
|
|
27
|
+
|
|
28
|
+
Configure the mode once during startup. Loading is lazy and cached; changing from `auto` to `require` after a failed load changes failure policy but does not repeatedly probe the binary.
|
|
29
|
+
|
|
30
|
+
## Native boundary
|
|
31
|
+
|
|
32
|
+
The bundled native layer exposes policy-free filesystem mechanisms: beneath-root
|
|
33
|
+
open/mkdir/link, no-replace rename, identity reads, archive decode/execution,
|
|
34
|
+
clone/copy/hash workers, and Windows security descriptor calls. The TypeScript
|
|
35
|
+
layer owns policy, retries, filters, budgets, modes, cleanup, error
|
|
36
|
+
normalization, and the decision to fall back.
|
|
37
|
+
|
|
38
|
+
- Linux uses `openat2` with `RESOLVE_BENEATH | RESOLVE_NO_MAGICLINKS` and `renameat2(RENAME_NOREPLACE)`.
|
|
39
|
+
- macOS 15.4 and newer prefer `O_RESOLVE_BENEATH`; older kernels resolve components with `O_NOFOLLOW` and restart in-root symlinks from the pinned root descriptor. Both routes use an `F_GETPATH` post-open escape detector and report `best-effort` because directory rename races are not atomic with that check. No-replace publication uses `renameatx_np(RENAME_EXCL)`.
|
|
40
|
+
- Windows uses handle-relative `NtCreateFile`, rejects reparse points, and uses `FileRenameInfoEx` with replacement disabled.
|
|
41
|
+
|
|
42
|
+
Native primitives back create-only pinned writes, async sidecar creation,
|
|
43
|
+
guarded publication, archive acceleration, and direct Windows ACL operations.
|
|
44
|
+
Equivalent JavaScript paths remain available for documented fallback-capable
|
|
45
|
+
features. See [Native architecture](native.md#javascript-fallback-guarantees-and-delta)
|
|
46
|
+
for the exact difference.
|
|
47
|
+
|
|
48
|
+
`openBeneath()` returns `{ fd, containment }`. `containment` is
|
|
49
|
+
`"kernel-atomic"` for Linux `openat2` and `"best-effort"` for macOS and
|
|
50
|
+
Windows. Public JavaScript root open/read/writable results also expose the
|
|
51
|
+
field and report `"best-effort"`; the label reports mechanism, not policy.
|
|
52
|
+
|
|
53
|
+
## Migration from the Python helper
|
|
54
|
+
|
|
55
|
+
Version 0.5 removes the Python worker and interpreter-path selection. The mode
|
|
56
|
+
contract is unchanged, so migrate startup configuration directly:
|
|
57
|
+
|
|
58
|
+
| Python helper configuration | Native replacement |
|
|
59
|
+
|---|---|
|
|
60
|
+
| `configureFsSafePython({ mode: "auto" })` | `configureFsSafeNative({ mode: "auto" })` |
|
|
61
|
+
| `configureFsSafePython({ mode: "off" })` | `configureFsSafeNative({ mode: "off" })` |
|
|
62
|
+
| `configureFsSafePython({ mode: "require" })` | `configureFsSafeNative({ mode: "require" })` |
|
|
63
|
+
| `FS_SAFE_PYTHON_MODE` | `FS_SAFE_NATIVE_MODE` |
|
|
64
|
+
| `OPENCLAW_FS_SAFE_PYTHON_MODE` | `OPENCLAW_FS_SAFE_NATIVE_MODE` |
|
|
65
|
+
| `pythonPath`, `FS_SAFE_PYTHON`, and the OpenClaw interpreter-path aliases | Remove; prebuilt bindings do not use an interpreter path |
|
|
66
|
+
|
|
67
|
+
In 0.5, `configureFsSafePython` and the legacy Python environment names
|
|
68
|
+
remain only as an upgrade bridge. On the first config read they emit one
|
|
69
|
+
`DeprecationWarning` with code `FS_SAFE_PYTHON_DEPRECATED`, state the mapped
|
|
70
|
+
native mode, and then apply that mode. A legacy interpreter path without an
|
|
71
|
+
explicit mode maps to `auto` and the path itself is ignored. Native config has
|
|
72
|
+
the normal precedence over legacy environment config.
|
|
73
|
+
|
|
74
|
+
There is no silent alias and no Python execution fallback. The bridge exists
|
|
75
|
+
only to make shipped 0.4 configuration visible and predictable while the
|
|
76
|
+
consumer performs its 0.5 upgrade.
|
|
77
|
+
|
|
78
|
+
## Related pages
|
|
79
|
+
|
|
80
|
+
- [Config](config.md)
|
|
81
|
+
- [Security model](security-model.md)
|
|
82
|
+
- [Writing](writing.md)
|
|
83
|
+
- [File locks](sidecar-lock.md)
|
|
84
|
+
- [Durability](durability.md)
|
|
85
|
+
- [Migrating to 0.5](migrating-to-0.5.md)
|