@openclaw/fs-safe 0.9.0 → 0.11.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 +134 -0
- package/LICENSE +1 -0
- package/README.md +52 -4
- package/dist/absolute-path.d.ts.map +1 -1
- package/dist/absolute-path.js +3 -2
- package/dist/advanced.d.ts +5 -0
- package/dist/advanced.d.ts.map +1 -1
- package/dist/advanced.js +5 -0
- package/dist/archive-crc32.d.ts.map +1 -1
- package/dist/archive-crc32.js +6 -1
- package/dist/archive-deadline.d.ts.map +1 -1
- package/dist/archive-deadline.js +3 -4
- package/dist/archive-durability.d.ts +6 -6
- package/dist/archive-durability.d.ts.map +1 -1
- package/dist/archive-durability.js +1 -1
- package/dist/archive-entry.d.ts.map +1 -1
- package/dist/archive-entry.js +4 -5
- package/dist/archive-gzip-tail.d.ts +3 -0
- package/dist/archive-gzip-tail.d.ts.map +1 -1
- package/dist/archive-gzip-tail.js +23 -3
- package/dist/archive-input.d.ts.map +1 -1
- package/dist/archive-input.js +4 -2
- package/dist/archive-merge.d.ts +5 -1
- package/dist/archive-merge.d.ts.map +1 -1
- package/dist/archive-merge.js +16 -13
- package/dist/archive-native.d.ts.map +1 -1
- package/dist/archive-native.js +7 -6
- package/dist/archive-parser.wasm +0 -0
- package/dist/archive-read.d.ts.map +1 -1
- package/dist/archive-read.js +83 -74
- package/dist/archive-staging.d.ts +6 -3
- package/dist/archive-staging.d.ts.map +1 -1
- package/dist/archive-staging.js +42 -22
- package/dist/archive-tar-stream.d.ts +11 -4
- package/dist/archive-tar-stream.d.ts.map +1 -1
- package/dist/archive-tar-stream.js +23 -10
- package/dist/archive-tar-wasm.d.ts.map +1 -1
- package/dist/archive-tar-wasm.js +18 -14
- package/dist/archive-zip-admission.d.ts +1 -1
- package/dist/archive-zip-admission.d.ts.map +1 -1
- package/dist/archive-zip-admission.js +48 -12
- package/dist/archive-zip-loader.d.ts +6 -0
- package/dist/archive-zip-loader.d.ts.map +1 -0
- package/dist/archive-zip-loader.js +38 -0
- package/dist/archive-zip-names.d.ts.map +1 -1
- package/dist/archive-zip-names.js +13 -8
- package/dist/archive-zip-preflight.d.ts +2 -3
- package/dist/archive-zip-preflight.d.ts.map +1 -1
- package/dist/archive-zip-preflight.js +2 -34
- package/dist/archive.d.ts.map +1 -1
- package/dist/archive.js +12 -10
- package/dist/bounded-read.d.ts +12 -0
- package/dist/bounded-read.d.ts.map +1 -1
- package/dist/bounded-read.js +82 -45
- package/dist/clone-metadata.d.ts +19 -0
- package/dist/clone-metadata.d.ts.map +1 -0
- package/dist/clone-metadata.js +32 -0
- package/dist/copy-file-input.d.ts +22 -0
- package/dist/copy-file-input.d.ts.map +1 -0
- package/dist/copy-file-input.js +69 -0
- package/dist/copy-policy.d.ts +3 -0
- package/dist/copy-policy.d.ts.map +1 -0
- package/dist/copy-policy.js +8 -0
- package/dist/copy-publication.d.ts +10 -1
- package/dist/copy-publication.d.ts.map +1 -1
- package/dist/copy-publication.js +27 -0
- package/dist/copy-tree-portable.d.ts +9 -0
- package/dist/copy-tree-portable.d.ts.map +1 -0
- package/dist/copy-tree-portable.js +222 -0
- package/dist/copy.d.ts +16 -0
- package/dist/copy.d.ts.map +1 -0
- package/dist/copy.js +125 -0
- package/dist/directory-durability.d.ts.map +1 -1
- package/dist/directory-durability.js +5 -4
- package/dist/directory-guard.d.ts +11 -1
- package/dist/directory-guard.d.ts.map +1 -1
- package/dist/directory-guard.js +53 -11
- package/dist/durability.d.ts +1 -1
- package/dist/durability.d.ts.map +1 -1
- package/dist/durability.js +1 -1
- package/dist/error-detail.d.ts.map +1 -1
- package/dist/error-detail.js +4 -1
- package/dist/file-handle-transfer.d.ts +14 -0
- package/dist/file-handle-transfer.d.ts.map +1 -0
- package/dist/file-handle-transfer.js +64 -0
- package/dist/file-hash.d.ts +9 -2
- package/dist/file-hash.d.ts.map +1 -1
- package/dist/file-hash.js +135 -39
- package/dist/file-lock-sync.d.ts.map +1 -1
- package/dist/file-lock-sync.js +8 -4
- package/dist/file-store-boundary.d.ts.map +1 -1
- package/dist/file-store-boundary.js +7 -5
- package/dist/file-store-path.d.ts +3 -0
- package/dist/file-store-path.d.ts.map +1 -0
- package/dist/file-store-path.js +27 -0
- package/dist/file-store-prune.d.ts.map +1 -1
- package/dist/file-store-prune.js +6 -4
- package/dist/file-store-sync-write.d.ts.map +1 -1
- package/dist/file-store-sync-write.js +56 -44
- package/dist/file-store.d.ts.map +1 -1
- package/dist/file-store.js +2 -18
- package/dist/filename.d.ts.map +1 -1
- package/dist/filename.js +4 -13
- package/dist/guarded-mkdir.d.ts +1 -0
- package/dist/guarded-mkdir.d.ts.map +1 -1
- package/dist/guarded-mkdir.js +4 -2
- package/dist/guarded-mutation.d.ts +2 -0
- package/dist/guarded-mutation.d.ts.map +1 -1
- package/dist/guarded-mutation.js +8 -4
- package/dist/guest-dispatch-python.d.ts +2 -0
- package/dist/guest-dispatch-python.d.ts.map +1 -0
- package/dist/guest-dispatch-python.js +117 -0
- package/dist/guest-native-python.d.ts +4 -0
- package/dist/guest-native-python.d.ts.map +1 -0
- package/dist/guest-native-python.js +135 -0
- package/dist/guest.d.ts +9 -0
- package/dist/guest.d.ts.map +1 -0
- package/dist/guest.js +413 -0
- package/dist/home-dir.d.ts.map +1 -1
- package/dist/home-dir.js +9 -7
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/install-path.d.ts.map +1 -1
- package/dist/install-path.js +5 -8
- package/dist/json-durable-queue-directory.js +3 -3
- package/dist/json-durable-queue.d.ts.map +1 -1
- package/dist/json-durable-queue.js +19 -18
- package/dist/json.d.ts.map +1 -1
- package/dist/json.js +2 -1
- package/dist/local-roots.d.ts.map +1 -1
- package/dist/local-roots.js +24 -25
- package/dist/move-path-stage.d.ts +8 -0
- package/dist/move-path-stage.d.ts.map +1 -0
- package/dist/move-path-stage.js +56 -0
- package/dist/move-path.d.ts.map +1 -1
- package/dist/move-path.js +30 -34
- package/dist/mutation-authority.d.ts +9 -0
- package/dist/mutation-authority.d.ts.map +1 -0
- package/dist/mutation-authority.js +36 -0
- package/dist/native-binding.d.ts +29 -1
- package/dist/native-binding.d.ts.map +1 -1
- package/dist/native-operations.d.ts +1 -1
- package/dist/native-operations.d.ts.map +1 -1
- package/dist/native-operations.js +11 -5
- package/dist/native-pinned-write-windows.d.ts.map +1 -1
- package/dist/native-pinned-write-windows.js +19 -7
- package/dist/native-pinned-write.d.ts.map +1 -1
- package/dist/native-pinned-write.js +3 -1
- package/dist/native-staged-file.d.ts +3 -3
- package/dist/native-staged-file.d.ts.map +1 -1
- package/dist/native-staged-file.js +35 -8
- package/dist/opened-realpath.d.ts.map +1 -1
- package/dist/opened-realpath.js +5 -4
- package/dist/output.d.ts +2 -0
- package/dist/output.d.ts.map +1 -1
- package/dist/output.js +2 -0
- package/dist/overwrite-file-handle.d.ts +8 -0
- package/dist/overwrite-file-handle.d.ts.map +1 -0
- package/dist/overwrite-file-handle.js +42 -0
- package/dist/path-case.d.ts +7 -0
- package/dist/path-case.d.ts.map +1 -0
- package/dist/path-case.js +136 -0
- package/dist/path.d.ts.map +1 -1
- package/dist/path.js +5 -2
- package/dist/permissions-windows.d.ts +1 -1
- package/dist/permissions-windows.d.ts.map +1 -1
- package/dist/permissions-windows.js +85 -53
- package/dist/pinned-open.d.ts.map +1 -1
- package/dist/pinned-open.js +3 -1
- package/dist/pinned-operation.js +1 -1
- package/dist/pinned-write.d.ts +7 -3
- package/dist/pinned-write.d.ts.map +1 -1
- package/dist/pinned-write.js +51 -30
- package/dist/positional-read.d.ts +9 -0
- package/dist/positional-read.d.ts.map +1 -0
- package/dist/positional-read.js +36 -0
- package/dist/private-temp-workspace.d.ts.map +1 -1
- package/dist/private-temp-workspace.js +6 -4
- package/dist/publish-copy-stage.d.ts +13 -0
- package/dist/publish-copy-stage.d.ts.map +1 -0
- package/dist/publish-copy-stage.js +47 -0
- package/dist/publish-file.d.ts.map +1 -1
- package/dist/publish-file.js +6 -5
- package/dist/realpath.d.ts +4 -0
- package/dist/realpath.d.ts.map +1 -0
- package/dist/realpath.js +43 -0
- package/dist/recursive-mkdir-path.d.ts +3 -0
- package/dist/recursive-mkdir-path.d.ts.map +1 -0
- package/dist/recursive-mkdir-path.js +8 -0
- package/dist/replace-directory.d.ts.map +1 -1
- package/dist/replace-directory.js +2 -1
- package/dist/replace-file-copy-fallback.d.ts.map +1 -1
- package/dist/replace-file-copy-fallback.js +23 -31
- package/dist/replace-file-copy-source.d.ts.map +1 -1
- package/dist/replace-file-copy-source.js +7 -12
- package/dist/replace-file-mode.d.ts +3 -0
- package/dist/replace-file-mode.d.ts.map +1 -0
- package/dist/replace-file-mode.js +10 -0
- package/dist/replace-file-temp-owner.d.ts.map +1 -1
- package/dist/replace-file-temp-owner.js +6 -2
- package/dist/replace-file.d.ts +1 -0
- package/dist/replace-file.d.ts.map +1 -1
- package/dist/replace-file.js +14 -8
- package/dist/root-context.d.ts +3 -0
- package/dist/root-context.d.ts.map +1 -1
- package/dist/root-context.js +53 -28
- package/dist/root-create-input.d.ts +10 -0
- package/dist/root-create-input.d.ts.map +1 -0
- package/dist/root-create-input.js +80 -0
- package/dist/root-directory-list.d.ts +26 -0
- package/dist/root-directory-list.d.ts.map +1 -0
- package/dist/root-directory-list.js +219 -0
- package/dist/root-entries.d.ts +11 -0
- package/dist/root-entries.d.ts.map +1 -0
- package/dist/root-entries.js +61 -0
- package/dist/root-errors.d.ts +6 -5
- package/dist/root-errors.d.ts.map +1 -1
- package/dist/root-errors.js +16 -12
- package/dist/root-file.d.ts +2 -0
- package/dist/root-file.d.ts.map +1 -1
- package/dist/root-file.js +12 -4
- package/dist/root-impl.d.ts +17 -51
- package/dist/root-impl.d.ts.map +1 -1
- package/dist/root-impl.js +358 -234
- package/dist/root-options.d.ts +77 -0
- package/dist/root-options.d.ts.map +1 -0
- package/dist/root-options.js +18 -0
- package/dist/root-path-existing.d.ts +5 -0
- package/dist/root-path-existing.d.ts.map +1 -1
- package/dist/root-path-existing.js +59 -3
- package/dist/root-path-symlink.d.ts.map +1 -1
- package/dist/root-path-symlink.js +3 -2
- package/dist/root-path.d.ts +1 -0
- package/dist/root-path.d.ts.map +1 -1
- package/dist/root-path.js +74 -162
- package/dist/root-paths.d.ts.map +1 -1
- package/dist/root-paths.js +13 -9
- package/dist/root-remove.d.ts +5 -0
- package/dist/root-remove.d.ts.map +1 -0
- package/dist/root-remove.js +286 -0
- package/dist/root-symlink-policy.d.ts +14 -0
- package/dist/root-symlink-policy.d.ts.map +1 -0
- package/dist/root-symlink-policy.js +34 -0
- package/dist/root-walk.d.ts +5 -4
- package/dist/root-walk.d.ts.map +1 -1
- package/dist/root-walk.js +99 -50
- package/dist/root-write-mode.d.ts.map +1 -1
- package/dist/root-write-mode.js +3 -5
- package/dist/root.d.ts +4 -1
- package/dist/root.d.ts.map +1 -1
- package/dist/secret-file.d.ts.map +1 -1
- package/dist/secret-file.js +2 -1
- package/dist/secret-read-async.d.ts.map +1 -1
- package/dist/secret-read-async.js +2 -1
- package/dist/secure-file.d.ts.map +1 -1
- package/dist/secure-file.js +25 -8
- package/dist/secure-temp-dir.d.ts.map +1 -1
- package/dist/secure-temp-dir.js +2 -1
- package/dist/sibling-staged-file.d.ts +1 -0
- package/dist/sibling-staged-file.d.ts.map +1 -1
- package/dist/sibling-staged-file.js +42 -8
- package/dist/sibling-temp.d.ts +2 -0
- package/dist/sibling-temp.d.ts.map +1 -1
- package/dist/sibling-temp.js +6 -4
- package/dist/sidecar-lock-acquire.d.ts.map +1 -1
- package/dist/sidecar-lock-acquire.js +17 -5
- package/dist/sidecar-lock-policy.d.ts +2 -0
- package/dist/sidecar-lock-policy.d.ts.map +1 -1
- package/dist/sidecar-lock-policy.js +20 -1
- package/dist/staged-directory.d.ts.map +1 -1
- package/dist/staged-directory.js +4 -3
- package/dist/temp-cleanup.d.ts.map +1 -1
- package/dist/temp-cleanup.js +3 -2
- package/dist/temp-target.d.ts +14 -12
- package/dist/temp-target.d.ts.map +1 -1
- package/dist/temp-target.js +12 -6
- package/dist/timing.d.ts +1 -0
- package/dist/timing.d.ts.map +1 -1
- package/dist/timing.js +25 -6
- package/dist/trash.d.ts.map +1 -1
- package/dist/trash.js +9 -7
- package/dist/unicode-path.d.ts +3 -0
- package/dist/unicode-path.d.ts.map +1 -0
- package/dist/unicode-path.js +13 -0
- package/dist/walk.d.ts.map +1 -1
- package/dist/walk.js +9 -28
- package/dist/windows-owner.d.ts +9 -12
- package/dist/windows-owner.d.ts.map +1 -1
- package/dist/windows-owner.js +24 -58
- package/dist/write-file-handle.d.ts +7 -0
- package/dist/write-file-handle.d.ts.map +1 -0
- package/dist/write-file-handle.js +26 -0
- package/docs/advanced.md +22 -4
- package/docs/archive.md +44 -9
- package/docs/atomic.md +24 -1
- package/docs/config.md +1 -0
- package/docs/contributing.md +36 -1
- package/docs/copy.md +155 -0
- package/docs/directory-identity.md +85 -0
- package/docs/durability.md +53 -3
- package/docs/entries.md +109 -0
- package/docs/errors.md +4 -4
- package/docs/file-store.md +15 -0
- package/docs/guest.md +141 -0
- package/docs/in-place-write.md +81 -0
- package/docs/index.md +2 -0
- package/docs/install.md +31 -0
- package/docs/local-roots.md +8 -1
- package/docs/native-helper.md +10 -3
- package/docs/native.md +18 -1
- package/docs/output.md +32 -6
- package/docs/path-case.md +64 -0
- package/docs/path-scope.md +1 -1
- package/docs/permissions.md +29 -10
- package/docs/positional-read.md +63 -0
- package/docs/public-api.md +31 -2
- package/docs/root.md +196 -6
- package/docs/secure-file.md +2 -0
- package/docs/security-model.md +14 -0
- package/docs/sidecar-lock.md +12 -3
- package/docs/temp.md +35 -6
- package/docs/timing.md +2 -0
- package/docs/types.md +19 -12
- package/docs/walk.md +54 -5
- package/docs/writing.md +173 -5
- package/package.json +19 -8
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Directory identity
|
|
3
|
+
description: "Exact directory observations and synchronous identity assertions for application-owned workflows."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Directory identity
|
|
7
|
+
|
|
8
|
+
Use `readDirectoryIdentity()` and `assertDirectoryIdentitySync()` from
|
|
9
|
+
`@openclaw/fs-safe/advanced` when an application owns a staging or recovery flow
|
|
10
|
+
and needs to verify that a pathname still identifies an observed directory.
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
import {
|
|
14
|
+
readDirectoryIdentity,
|
|
15
|
+
assertDirectoryIdentitySync,
|
|
16
|
+
} from "@openclaw/fs-safe/advanced";
|
|
17
|
+
|
|
18
|
+
const expected = await readDirectoryIdentity(directoryPath);
|
|
19
|
+
await prepareOutput();
|
|
20
|
+
assertDirectoryIdentitySync(directoryPath, expected);
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
`readDirectoryIdentity(path)` returns a frozen `DirectoryIdentity`:
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
type DirectoryIdentity = Readonly<{
|
|
27
|
+
dev: bigint;
|
|
28
|
+
ino: bigint;
|
|
29
|
+
realPath: string;
|
|
30
|
+
}>;
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Both operations reject a final symlink or a non-directory, including paths with
|
|
34
|
+
one or more trailing separators. Filesystem, drive, and UNC roots remain valid.
|
|
35
|
+
Parent aliases and `..` retain their filesystem traversal meaning; POSIX
|
|
36
|
+
backslashes and whitespace remain literal filename characters. These helpers do
|
|
37
|
+
not confine a path to a root or reject every symlink ancestor. Keep the
|
|
38
|
+
application's path policy, or use the [Root API](root.md) for paths that must
|
|
39
|
+
remain beneath a root.
|
|
40
|
+
|
|
41
|
+
## Checking the selected path
|
|
42
|
+
|
|
43
|
+
`assertDirectoryIdentitySync(observedPath, expected)` reads the supplied path
|
|
44
|
+
and compares its exact `dev` and `ino` against the expected bigint values. It
|
|
45
|
+
returns `undefined` on success and throws synchronously on failure.
|
|
46
|
+
|
|
47
|
+
If `expected.realPath` is present, the current canonical path must also match
|
|
48
|
+
that string exactly. Pass the complete observation to keep both checks:
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
assertDirectoryIdentitySync(newlyOpenedRootPath, expected);
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
For a directory intentionally moved to another name, omit `realPath` while
|
|
55
|
+
retaining the expected identity:
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
assertDirectoryIdentitySync(movedPath, { dev: expected.dev, ino: expected.ino });
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Only `dev`, `ino`, and optional `realPath` participate in the assertion. A
|
|
62
|
+
`MovePathPublicationReceipt` can supply the identity after a move; its `path`
|
|
63
|
+
does not implicitly require the previous pathname to remain current.
|
|
64
|
+
|
|
65
|
+
## Errors and ownership
|
|
66
|
+
|
|
67
|
+
| Condition | Result |
|
|
68
|
+
|---|---|
|
|
69
|
+
| Final symlink or non-directory | `FsSafeError("not-file")` |
|
|
70
|
+
| Different expected identity or supplied canonical path | `FsSafeError("path-mismatch")` |
|
|
71
|
+
| Numeric expected identity or persistently unknown Windows identity | `FsSafeError("path-mismatch")` |
|
|
72
|
+
| Filesystem failure such as `ENOENT` or `EACCES` | The original error, unchanged |
|
|
73
|
+
|
|
74
|
+
Windows can temporarily report a zero device or inode. The shared identity
|
|
75
|
+
owner permits one re-inspection, retaining every known component so a later
|
|
76
|
+
observation cannot erase a definite mismatch. It rejects identities that remain
|
|
77
|
+
unknown and does not retry operational filesystem errors.
|
|
78
|
+
|
|
79
|
+
These helpers observe directory identity. They do not open a retained handle,
|
|
80
|
+
hold a lock, create or remove directories, change permissions, or authorize
|
|
81
|
+
application work. A pathname can change after an assertion; continue to use
|
|
82
|
+
guarded mutation APIs and recheck application authority at the operation's
|
|
83
|
+
existing submission point. Keep publication, rollback, and cleanup decisions
|
|
84
|
+
with the caller. Use [pinned directories](durability.md#pinned-directories) when
|
|
85
|
+
the operation specifically needs the directory synchronization lifecycle.
|
package/docs/durability.md
CHANGED
|
@@ -199,7 +199,7 @@ import { sha256File } from "@openclaw/fs-safe/durability";
|
|
|
199
199
|
const snapshot = await open(stagedArchive, "r");
|
|
200
200
|
try {
|
|
201
201
|
const before = await snapshot.stat();
|
|
202
|
-
const hash = await sha256File(snapshot);
|
|
202
|
+
const hash = await sha256File(snapshot, { maxBytes: before.size });
|
|
203
203
|
if (hash.bytes !== before.size || hash.digest !== manifest.sha256) {
|
|
204
204
|
throw new Error("staged backup does not match its manifest");
|
|
205
205
|
}
|
|
@@ -209,6 +209,21 @@ try {
|
|
|
209
209
|
```
|
|
210
210
|
|
|
211
211
|
The result is `{ bytes, digest }`, where `digest` is lowercase hexadecimal.
|
|
212
|
+
The optional `Sha256FileOptions` argument supports `maxBytes` and `signal`.
|
|
213
|
+
`maxBytes` accepts a non-negative safe integer (including zero), or
|
|
214
|
+
`Infinity` for no limit, which is also the default. Oversized files reject with
|
|
215
|
+
`FsSafeError("too-large")`; reads stay bounded to at most `maxBytes + 1` bytes,
|
|
216
|
+
even if the file grows after its initial size check. Hashing never silently
|
|
217
|
+
truncates to the limit.
|
|
218
|
+
|
|
219
|
+
Pass `signal` to cancel. A pre-aborted signal rejects before file I/O or native
|
|
220
|
+
loading. In-flight cancellation is cooperative between reads and rejects with
|
|
221
|
+
the signal's original reason only after the pending read or native task stops.
|
|
222
|
+
Callers may close their handle after awaiting rejection; do not close it while
|
|
223
|
+
the operation is pending. A signal can be shared by successive or concurrent
|
|
224
|
+
hashes. Neither mode provides a snapshot of concurrently modified contents;
|
|
225
|
+
callers requiring stable content must also fence identity and metadata.
|
|
226
|
+
|
|
212
227
|
The handle overload never closes the caller's descriptor and uses positioned
|
|
213
228
|
reads, so it does not alter the descriptor's current offset. The path overload
|
|
214
229
|
rejects symbolic links and non-regular files, compares lossless bigint identities
|
|
@@ -226,11 +241,46 @@ descriptor inspection rather than waiting for a writer.
|
|
|
226
241
|
When the optional binding is active, hashing runs as an async native task and
|
|
227
242
|
does not occupy the JavaScript event loop with digest updates. With native mode
|
|
228
243
|
`off`, or in `auto` when no binding loads, the fallback performs asynchronous
|
|
229
|
-
positioned reads in
|
|
244
|
+
positioned reads in chunks of up to 256 KiB but updates Node's `Hash` on the JavaScript
|
|
230
245
|
thread. Both paths stream constant-size buffers rather than loading the file
|
|
231
246
|
into memory. Native mode `require` keeps its usual fail-closed loader semantics.
|
|
232
247
|
|
|
233
|
-
|
|
248
|
+
### Synchronous hashing
|
|
249
|
+
|
|
250
|
+
`sha256FileSync()` accepts a pathname or a borrowed numeric file descriptor and
|
|
251
|
+
returns the same `{ bytes, digest }` result. It shares `Sha256FileOptions`,
|
|
252
|
+
including the default unlimited byte budget and `too-large` errors for growth
|
|
253
|
+
beyond `maxBytes`. It always reads from offset zero with bounded positional
|
|
254
|
+
`readSync` calls and leaves a borrowed descriptor open at its original position.
|
|
255
|
+
Path inputs use the same regular-file, final-symlink, nonblocking-open, and exact
|
|
256
|
+
bigint admission checks described above, then close their owned descriptor.
|
|
257
|
+
These checks do not provide ancestor confinement or a snapshot of concurrent edits.
|
|
258
|
+
|
|
259
|
+
```ts
|
|
260
|
+
import { closeSync, openSync } from "node:fs";
|
|
261
|
+
import { sha256FileSync } from "@openclaw/fs-safe/durability";
|
|
262
|
+
|
|
263
|
+
const fd = openSync(stagedArchive, "r");
|
|
264
|
+
try {
|
|
265
|
+
const hash = sha256FileSync(fd, { maxBytes: manifest.sizeBytes });
|
|
266
|
+
if (hash.bytes !== manifest.sizeBytes || hash.digest !== manifest.sha256) {
|
|
267
|
+
throw new Error("staged backup does not match its manifest");
|
|
268
|
+
}
|
|
269
|
+
} finally {
|
|
270
|
+
closeSync(fd);
|
|
271
|
+
}
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
The synchronous API uses Node's crypto implementation in every native mode,
|
|
275
|
+
including `require`; it never loads a native binding. It blocks the calling
|
|
276
|
+
thread until hashing finishes or throws. A pre-aborted signal fails before I/O,
|
|
277
|
+
and synchronous signal changes are checked between operations with the original
|
|
278
|
+
reason preserved. Timers and other JavaScript callbacks cannot run while the
|
|
279
|
+
hash is executing; use `sha256File()` when responsive cancellation is needed.
|
|
280
|
+
|
|
281
|
+
## Publication failure receipts
|
|
282
|
+
|
|
283
|
+
If `publishFileExclusive()` fails after creating the target, it throws an
|
|
234
284
|
`FsSafeError` with a `details` receipt:
|
|
235
285
|
|
|
236
286
|
```ts
|
package/docs/entries.md
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# Directory entries
|
|
2
|
+
|
|
3
|
+
`Root.entries()` observes one directory at a time. Use it when the application
|
|
4
|
+
owns traversal order or must inspect symlinks itself, such as an installer that
|
|
5
|
+
validates selected dependency links or a manifest builder that rejects all links.
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { root } from "@openclaw/fs-safe";
|
|
9
|
+
|
|
10
|
+
const workspace = await root("/srv/workspace");
|
|
11
|
+
for await (const entry of workspace.entries("plugins", {
|
|
12
|
+
maxEntries: 1_000,
|
|
13
|
+
signal: AbortSignal.timeout(5_000),
|
|
14
|
+
})) {
|
|
15
|
+
if (entry.isSymbolicLink) {
|
|
16
|
+
throw new Error(`unexpected link: ${entry.name}`);
|
|
17
|
+
}
|
|
18
|
+
console.log(entry.name, entry.size);
|
|
19
|
+
}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## API
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
interface Root {
|
|
26
|
+
entries(relativePath: string, options?: RootEntriesOptions): AsyncIterableIterator<DirEntry>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
type RootEntriesOptions = {
|
|
30
|
+
maxEntries?: number;
|
|
31
|
+
order?: "filesystem" | "sorted";
|
|
32
|
+
signal?: AbortSignal;
|
|
33
|
+
symlinks?: "reject" | "follow-within-root" | "follow-parents-within-root";
|
|
34
|
+
};
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Each result is the existing [`DirEntry`](types.md) shape: a basename and advisory
|
|
38
|
+
`lstat` metadata, including `isFile`, `isDirectory`, `isSymbolicLink`, `size`,
|
|
39
|
+
`mode`, `nlink`, `dev`, and `ino`. Entries are immediate children; the iterator
|
|
40
|
+
never descends. An empty path or `"."` selects the root directory.
|
|
41
|
+
|
|
42
|
+
Child symlinks are always reported without following them, including dangling
|
|
43
|
+
links and links whose targets lie outside the root. Their metadata describes
|
|
44
|
+
the link, not its target. Hardlinked files are also reported regardless of the
|
|
45
|
+
Root's read hardlink policy. Reporting a name grants no read or mutation access.
|
|
46
|
+
Use the Root operation methods when consuming or changing an entry.
|
|
47
|
+
|
|
48
|
+
The `symlinks` option applies only to the path of the selected directory. It
|
|
49
|
+
inherits the Root's read policy and defaults to `"reject"`. The two follow
|
|
50
|
+
policies allow only contained aliases; `"follow-parents-within-root"` also
|
|
51
|
+
rejects a link as the final directory component. Child-link reporting is
|
|
52
|
+
independent of this path policy.
|
|
53
|
+
|
|
54
|
+
## Work limits and ordering
|
|
55
|
+
|
|
56
|
+
`maxEntries` is an optional non-negative safe integer. Every child counts,
|
|
57
|
+
including directories, symlinks, special files, and entries the caller later
|
|
58
|
+
ignores. Omit it to leave the count unbounded. An empty directory satisfies a
|
|
59
|
+
zero limit. Exceeding the limit throws `FsSafeError` with code `"too-large"`;
|
|
60
|
+
there is no silent truncation or success marker for a partial scan.
|
|
61
|
+
|
|
62
|
+
The default `order: "filesystem"` reads names incrementally in the filesystem's
|
|
63
|
+
nondeterministic order. It observes one child's metadata per iterator step,
|
|
64
|
+
with one name of lookahead to distinguish an exact limit from an overflow.
|
|
65
|
+
Entries already yielded before overflow remain partial observations. A caller
|
|
66
|
+
that stops early has not established that the whole directory fits the limit.
|
|
67
|
+
|
|
68
|
+
`order: "sorted"` collects names first and orders them with JavaScript's default
|
|
69
|
+
string sort, not locale collation. With `maxEntries`, names are collected from
|
|
70
|
+
a bounded directory stream; overflow rejects before yielding any entries or
|
|
71
|
+
requesting their full metadata. Without a limit, sorted mode enumerates the
|
|
72
|
+
complete name list. Metadata is observed only as each sorted entry is consumed.
|
|
73
|
+
Applications that need locale-specific ordering can collect with an explicit
|
|
74
|
+
limit and apply their own comparator.
|
|
75
|
+
|
|
76
|
+
The count bounds logical directory reads and fs-safe metadata requests. Node
|
|
77
|
+
may classify a directory entry with `lstat` when the filesystem omits type
|
|
78
|
+
information, including the single lookahead entry. It does not bound elapsed
|
|
79
|
+
time for an individual filesystem operation or the size of one filename.
|
|
80
|
+
|
|
81
|
+
Cancellation is checked before setup and around awaited work. Directory
|
|
82
|
+
handles close on completion, overflow, cancellation, failure, and early
|
|
83
|
+
`break` or iterator return. In-flight filesystem work settles before rejection;
|
|
84
|
+
an individual syscall cannot be interrupted. If iteration and disposal both
|
|
85
|
+
fail, a `SuppressedError` retains the close failure in `error` and the original
|
|
86
|
+
failure in `suppressed`.
|
|
87
|
+
|
|
88
|
+
## Identity and caller responsibilities
|
|
89
|
+
|
|
90
|
+
The iterator reuses Root's guarded directory-listing owner. It validates the
|
|
91
|
+
selected path, pins exact Root and directory identities, and checks them around
|
|
92
|
+
directory observations, including after control returns from the caller. A
|
|
93
|
+
replaced directory rejects instead of continuing under the replacement.
|
|
94
|
+
|
|
95
|
+
These are pure-Node, best-effort checks. The iterator does not hold descriptors
|
|
96
|
+
for every path component and cannot sandbox a hostile process that repeatedly
|
|
97
|
+
swaps and restores directories. Results are not an atomic snapshot, an exact
|
|
98
|
+
identity receipt, or permission to use the name later. Contents and metadata
|
|
99
|
+
can change between entries; a removed entry may cause iteration to reject.
|
|
100
|
+
Use an admitted descriptor for metadata and hashing that must refer to the same
|
|
101
|
+
opened file, and keep snapshot consistency or cooperative locking with its
|
|
102
|
+
application owner.
|
|
103
|
+
|
|
104
|
+
Traversal strategy, global budgets, ignored names, and approved external peers
|
|
105
|
+
remain application policy. A per-directory physical-entry limit cannot replace
|
|
106
|
+
a global file-only or unique-directory budget. Full metadata also requires a
|
|
107
|
+
child `lstat`; a caller that previously needed only `Dirent` types should assess
|
|
108
|
+
that cost. `Root.walk()` remains the recursive option for its supported link,
|
|
109
|
+
pruning, and error policies; `Root.list()` returns an eager advisory listing.
|
package/docs/errors.md
CHANGED
|
@@ -117,11 +117,11 @@ type FsSafeErrorCode =
|
|
|
117
117
|
| `helper-unavailable` | Required native binding could not be loaded. | Unsupported platform, omitted/missing/incompatible platform package, or `FS_SAFE_NATIVE_MODE=require`. `auto` falls back where possible; `require` fails closed. |
|
|
118
118
|
| `insecure-permissions` | A secure file or path permission check found a mode/ACL that allows broader access than requested. | File or directory is group/world writable/readable; Windows ACL grants broad read. |
|
|
119
119
|
| `invalid-path` | Input was empty, contained NUL, was an unparseable URL, or otherwise unusable; a FileStore key used a noncanonical spelling. | Noncanonical FileStore aliases, backslashes, or complete parent segments; a network path on Windows; a drive-relative segment in a portable relative path or store key; or a leading drive-relative spelling such as `C:name` used as a Root destination. Existing-object Root lookups retain broader confined path compatibility, including legal POSIX drive-like names. |
|
|
120
|
-
| `not-empty` | `remove()` on a non-empty directory. | Use `
|
|
120
|
+
| `not-empty` | Nonrecursive `remove()` on a non-empty directory, or new children appeared during recursive removal. | Use bounded `recursive: true` removal or coordinate concurrent writers. |
|
|
121
121
|
| `not-file` | Read or copy targeted a non-regular file, or a path walk found a non-directory ancestor. | Target was a directory, FIFO, socket, device, or an existing file was followed by another segment. |
|
|
122
122
|
| `not-found` | The target does not exist (or its parent does not, with `mkdir: false`). | Typical missing-file case. |
|
|
123
123
|
| `not-owned` | A secure file owner check failed. | File is owned by another UID. |
|
|
124
|
-
| `not-removable` | `remove()` couldn't `unlink`/`rmdir` for a reason other than non-empty. | Permissions, device busy, immutable bit
|
|
124
|
+
| `not-removable` | `remove()` couldn't inspect a directory stream or `unlink`/`rmdir` for a reason other than non-empty. | Permissions, device busy, immutable bit; the original filesystem error remains in `cause`. |
|
|
125
125
|
| `outside-workspace` | Path resolves outside the configured root. | `..` traversal; absolute path outside the root; symlink resolved out. |
|
|
126
126
|
| `path-alias` | A path alias check failed (e.g. canonical-real-path moved out of the root). | Symlink resolution lands outside the root. |
|
|
127
127
|
| `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. |
|
|
@@ -131,8 +131,8 @@ type FsSafeErrorCode =
|
|
|
131
131
|
| `store-reentrant-update` | A `JsonStore.update()` callback called `update()` or `updateOr()` for the same canonical store before returning. | Reentrant mutation would deadlock or lose an update; return the complete next value from the outer callback. |
|
|
132
132
|
| `symlink` | Path component is a symlink, policy is `reject`. | Caller followed a symlink they shouldn't have, or `symlinks: "reject"` is set. |
|
|
133
133
|
| `timeout` | An operation with a wall-clock budget overran. | Secure file read or timed operation exceeded `timeoutMs`. |
|
|
134
|
-
| `too-large` | A read
|
|
135
|
-
| `unsupported-platform` |
|
|
134
|
+
| `too-large` | A read, bounded walk, or recursive removal exceeded its configured budget. | Review the expected file or tree size before increasing the limit; recursive removal may have completed earlier entries. |
|
|
135
|
+
| `unsupported-platform` | The platform or filesystem cannot perform the requested operation. | `createCloneSource` and `copyTree({ clone: "always" })` require native cloning support. The default `copyTree({ clone: "auto" })` selects portable byte copying when cloning is unavailable; unsupported source contents or metadata still fail. See [directory copying](copy.md) for backend limits and fallback behavior. |
|
|
136
136
|
|
|
137
137
|
Secret writes reject invalid `mode` / `dirMode` values with `invalid-path` before directory creation. Existing secret directories with a mode different from the requested `dirMode` report `insecure-permissions` without chmod; a created directory whose descriptor ownership no longer matches its initializing effective user reports `not-owned`.
|
|
138
138
|
|
package/docs/file-store.md
CHANGED
|
@@ -121,6 +121,21 @@ metadata where lower latency matters more than crash-durability. Per-call
|
|
|
121
121
|
`undefined` override preserves the store default. Modes, path confinement,
|
|
122
122
|
and publication identity checks are unchanged.
|
|
123
123
|
|
|
124
|
+
Synchronous writes retain their original write-only descriptor through rename
|
|
125
|
+
and publication checks, using exact bigint file identities. When Windows cannot
|
|
126
|
+
report a pathname's identity, verification reopens the name only to compare its
|
|
127
|
+
descriptor with the retained writer; it never reads file contents. A substituted
|
|
128
|
+
file is rejected even if its bytes match, and a post-publication failure leaves
|
|
129
|
+
the published entry intact for caller-owned recovery. Ordinary write-only and
|
|
130
|
+
mode-000 outputs do not require a readable descriptor when pathname metadata is
|
|
131
|
+
available.
|
|
132
|
+
|
|
133
|
+
If an opaque pathname cannot be reopened because of an ACL denial or sharing
|
|
134
|
+
restriction, the synchronous writer intentionally rejects with `path-mismatch`:
|
|
135
|
+
its exact publication identity cannot be verified. There is no equal-content
|
|
136
|
+
fallback. The published entry remains present, so callers must inspect or
|
|
137
|
+
recover that outcome instead of assuming the write did not occur.
|
|
138
|
+
|
|
124
139
|
| Method | Durability support |
|
|
125
140
|
|---|---|
|
|
126
141
|
| `write`, `writeText`, `writeJson` (async and sync) | Per-call option overrides store default. |
|
package/docs/guest.md
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# Guest filesystem source
|
|
2
|
+
|
|
3
|
+
`@openclaw/fs-safe/guest` exports Python 3 source for filesystem operations in
|
|
4
|
+
Linux and macOS guests that do not have Node installed. The host imports a
|
|
5
|
+
string and passes it to its existing container, SSH, or process transport.
|
|
6
|
+
Importing this subpath performs no I/O and launches no process.
|
|
7
|
+
|
|
8
|
+
This is the filesystem engine extracted from OpenClaw's sandbox bridge. The
|
|
9
|
+
caller still owns root admission, mount selection, read-only policy, canonical
|
|
10
|
+
path authorization, live authority, argument framing, and process lifetime.
|
|
11
|
+
Use [Root](root.md) for ordinary filesystem operations in the Node process.
|
|
12
|
+
This source artifact is separate from the retired host-side Python worker
|
|
13
|
+
described in [Migrating to 0.5](migrating-to-0.5.md).
|
|
14
|
+
|
|
15
|
+
## Exports and requirements
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import {
|
|
19
|
+
GUEST_FILESYSTEM_PYTHON,
|
|
20
|
+
GUEST_FILESYSTEM_CREATE_EXISTS_EXIT_CODE,
|
|
21
|
+
GUEST_FILESYSTEM_READ_NOT_FOUND_EXIT_CODE,
|
|
22
|
+
GUEST_FILESYSTEM_RENAME_NO_REPLACE_PYTHON,
|
|
23
|
+
} from "@openclaw/fs-safe/guest";
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
`GUEST_FILESYSTEM_PYTHON` is the complete single-invocation program. The two
|
|
27
|
+
exit constants are `17` for an exclusive-create collision and `2` for a
|
|
28
|
+
missing read parent or leaf after the root opens. Other nonzero statuses are
|
|
29
|
+
errors; stderr is diagnostic text, not a structured error protocol.
|
|
30
|
+
|
|
31
|
+
The guest needs Python 3 with `ctypes`, descriptor-relative POSIX operations,
|
|
32
|
+
`O_DIRECTORY`, and `O_NOFOLLOW`. Linux and macOS are the supported guest
|
|
33
|
+
platforms. The guest does not need fs-safe's Node package, native addon, Rust,
|
|
34
|
+
or WASM. Native mode configuration in the host does not configure this program.
|
|
35
|
+
|
|
36
|
+
`GUEST_FILESYSTEM_RENAME_NO_REPLACE_PYTHON` contains just the
|
|
37
|
+
`rename_no_replace(src_parent_fd, src_basename, dst_parent_fd, dst_basename)`
|
|
38
|
+
definition. OpenClaw's workspace bootstrap consumes this same fragment when
|
|
39
|
+
publishing a prepared workspace; exporting it avoids a second source owner.
|
|
40
|
+
Its embedding program must import `ctypes`, `errno`, `os`, and `sys`, admit
|
|
41
|
+
the descriptors and single-component names, and own cleanup and syncing.
|
|
42
|
+
The fragment has no argument preflight of its own.
|
|
43
|
+
|
|
44
|
+
On Linux it uses `renameat2(RENAME_NOREPLACE)`; on macOS it uses
|
|
45
|
+
`renameatx_np(RENAME_EXCL)`. If Linux lacks that function or reports it as
|
|
46
|
+
unsupported, it falls back to `link(..., follow_symlinks=False)`, leaving the
|
|
47
|
+
source entry for caller cleanup. That fallback supports files, not directory
|
|
48
|
+
publication. Existing destinations are never replaced by this fragment.
|
|
49
|
+
|
|
50
|
+
## Invocation protocol
|
|
51
|
+
|
|
52
|
+
Pass the source through `python3 -c` and arguments as literal argv elements.
|
|
53
|
+
Do not concatenate untrusted values into a shell command. Transport adapters
|
|
54
|
+
that require a shell must apply their existing quoting rules to every element.
|
|
55
|
+
The following example uses an already admitted guest root and parent path:
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
import { spawnSync } from "node:child_process";
|
|
59
|
+
import { GUEST_FILESYSTEM_PYTHON } from "@openclaw/fs-safe/guest";
|
|
60
|
+
|
|
61
|
+
const result = spawnSync("python3", [
|
|
62
|
+
"-c", GUEST_FILESYSTEM_PYTHON,
|
|
63
|
+
"write", "/srv/admitted-workspace", "notes", "today.txt", "1",
|
|
64
|
+
], { input: Buffer.from("hello\n"), timeout: 10_000 });
|
|
65
|
+
if (result.error) throw result.error;
|
|
66
|
+
if (result.status !== 0) throw new Error(result.stderr.toString());
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
All frames below start at `sys.argv[1]`. `root` is an admitted guest directory;
|
|
70
|
+
`parent` and `directory` are paths relative to that root. An empty relative
|
|
71
|
+
path selects the root itself. Flags are strings: `"1"` enables and `"0"`
|
|
72
|
+
disables. Do not omit required fields.
|
|
73
|
+
|
|
74
|
+
| Operation | Positional frame | Input / output |
|
|
75
|
+
|---|---|---|
|
|
76
|
+
| Read | `read root parent basename [maxBytes]` | Raw bytes on stdout; optional nonnegative integer byte limit. |
|
|
77
|
+
| Write | `write root parent basename mkdir` | Raw stdin; atomically replaces a file entry. |
|
|
78
|
+
| Create | `create root parent basename mkdir` | Raw stdin; exclusively publishes a completed file. |
|
|
79
|
+
| Copy | `copy srcRoot srcParent srcBasename dstRoot dstParent dstBasename mkdir` | Regular-file copy, atomically replaces destination. |
|
|
80
|
+
| Rename | `rename srcRoot srcParent srcBasename dstRoot dstParent dstBasename mkdir` | Rename with cross-device copy/delete fallback. |
|
|
81
|
+
| Remove | `remove root parent basename recursive force` | Removes the leaf, or recursively removes its tree. |
|
|
82
|
+
| Make directories | `mkdirp root directory` | Creates missing relative directory components. |
|
|
83
|
+
| List directory | `readdir root directory` | JSON array of `{ name, isDirectory }`; no sorting guarantee. |
|
|
84
|
+
|
|
85
|
+
The complete program rejects empty, `.`, `..`, slash-containing, and NUL
|
|
86
|
+
basenames before opening roots or creating parents. Both leaf operands of copy
|
|
87
|
+
and rename are checked. Backslashes, colons, quotes, and newlines remain legal
|
|
88
|
+
POSIX basename characters. Relative directory traversal rejects `..`; empty
|
|
89
|
+
and `.` components are skipped. Supply admitted relative paths, not arbitrary
|
|
90
|
+
absolute paths whose spelling happens to pass that component walk.
|
|
91
|
+
|
|
92
|
+
## Boundary and operation behavior
|
|
93
|
+
|
|
94
|
+
The supplied root spelling is trusted. Opening it does not prove the root is
|
|
95
|
+
the mount or inode previously authorized by the caller. Basename syntax checks
|
|
96
|
+
are not authorization. Callers must admit their roots and paths, preserve
|
|
97
|
+
read-only shadows and live authority, and run the program inside the intended
|
|
98
|
+
OS isolation boundary.
|
|
99
|
+
|
|
100
|
+
Parent traversal and operation bodies use directory descriptors. Reads and
|
|
101
|
+
copies refuse final symlinks, hardlinked files, and nonregular files. Bounded
|
|
102
|
+
reads check both admitted size and consumed bytes; a growing file can produce
|
|
103
|
+
partial stdout before rejection. Consumers must discard read output when the
|
|
104
|
+
exit status indicates failure. Removal unlinks a final symlink without
|
|
105
|
+
following it; rename moves symlink entries. Write and copy replace destination
|
|
106
|
+
entries, including symlinks, without following them. A force removal tolerates
|
|
107
|
+
a missing leaf but does not suppress failure to open its parent.
|
|
108
|
+
|
|
109
|
+
Write preserves an existing regular file's mode and otherwise creates private
|
|
110
|
+
files. Copy preserves the source mode. Exclusive create publishes a mode-0600
|
|
111
|
+
file from a private staging directory. Staging names retain the `.openclaw-*`
|
|
112
|
+
prefixes and use short random suffixes independent of the destination basename,
|
|
113
|
+
so legal names near the filesystem's component limit also work for writes and
|
|
114
|
+
cross-device moves.
|
|
115
|
+
|
|
116
|
+
Cross-device directory moves build a copy manifest and check it during source
|
|
117
|
+
cleanup. Source changes can leave the published destination and some or all
|
|
118
|
+
of the source. Regular-file and symlink move fallbacks unlink the source
|
|
119
|
+
pathname after publication; they do not perform the directory manifest's
|
|
120
|
+
identity checks. Directory cleanup also has check-to-unlink race windows.
|
|
121
|
+
These mechanics do not promise content integrity against same-UID peers.
|
|
122
|
+
|
|
123
|
+
## Failure, cancellation, and budgets
|
|
124
|
+
|
|
125
|
+
Nonzero exit is not proof that nothing was published. A post-publication
|
|
126
|
+
identity or sync failure, or cross-device source-cleanup failure, can leave a
|
|
127
|
+
destination. The caller owns reconciliation and retry policy; do not assume
|
|
128
|
+
it is safe to remove that destination or replay a mutation blindly.
|
|
129
|
+
|
|
130
|
+
Ordinary Python failures run `finally` cleanup. There is no cancellation frame
|
|
131
|
+
or signal handler. Process death closes descriptors, but forced termination
|
|
132
|
+
can leave staging names. The transport owns termination, waiting for children
|
|
133
|
+
to settle, and any application-specific recovery.
|
|
134
|
+
|
|
135
|
+
Read byte limits are optional. Copy, write, recursive removal, directory
|
|
136
|
+
listing, and cross-device tree moves have no byte, entry, or depth budget in
|
|
137
|
+
this protocol. Recursive traversal and listing collect entries eagerly.
|
|
138
|
+
Use caller-owned isolation and resource limits appropriate to the operation.
|
|
139
|
+
The existing fsync sequence is preserved; this is not a recursive transaction,
|
|
140
|
+
rollback facility, or a stronger durability guarantee than the underlying
|
|
141
|
+
filesystem provides.
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: In-place writes
|
|
3
|
+
description: "Replace bytes through a borrowed file handle while preserving its inode and attempting rollback on failure."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# In-place writes
|
|
7
|
+
|
|
8
|
+
`overwriteFileHandle()` replaces a regular file's contents through a handle the
|
|
9
|
+
caller already owns. Use it when replacing the inode would break hardlinked
|
|
10
|
+
aliases, or when an existing writable file lives in a directory where the caller
|
|
11
|
+
cannot create a sibling temporary file.
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { open } from "node:fs/promises";
|
|
15
|
+
import { overwriteFileHandle } from "@openclaw/fs-safe/advanced";
|
|
16
|
+
|
|
17
|
+
const handle = await open(filePath, "r+");
|
|
18
|
+
try {
|
|
19
|
+
await overwriteFileHandle(handle, Buffer.from(nextContents, "utf8"), {
|
|
20
|
+
beforeWrite: () => assertCurrentOwner(),
|
|
21
|
+
});
|
|
22
|
+
} finally {
|
|
23
|
+
await handle.close();
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Contract
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
overwriteFileHandle(
|
|
31
|
+
handle: FileHandle,
|
|
32
|
+
data: Uint8Array,
|
|
33
|
+
options?: { beforeWrite?: () => void },
|
|
34
|
+
): Promise<void>;
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
The handle must be readable and writable, refer to a regular file, and have been
|
|
38
|
+
opened **without append mode**. Node cannot portably inspect a handle's append
|
|
39
|
+
flag, and some operating systems ignore positioned-write offsets for append
|
|
40
|
+
handles. Opening, path admission, symlink and hardlink policy, and closing remain
|
|
41
|
+
the caller's responsibility. The helper never reopens or replaces the inode,
|
|
42
|
+
changes the handle's current position, or closes it. Every hardlinked alias sees
|
|
43
|
+
the in-place changes.
|
|
44
|
+
|
|
45
|
+
The payload is borrowed, including its byte offset and length. Keep it unchanged,
|
|
46
|
+
attached, and accessible until the returned promise settles. Do not close the
|
|
47
|
+
handle or run concurrent I/O against the file, including through other aliases,
|
|
48
|
+
during preparation, writing, or rollback. This helper does not acquire a lock.
|
|
49
|
+
|
|
50
|
+
## Preparation, ordering, and failure
|
|
51
|
+
|
|
52
|
+
The helper checks the file type and original size, then saves only the prefix
|
|
53
|
+
that will be overwritten: `min(data.byteLength, originalSize)` bytes. A short
|
|
54
|
+
prefix read fails with `FsSafeError("read-failed")` before writing. Memory for the
|
|
55
|
+
backup is bounded by that prefix length; a short replacement of a large file does
|
|
56
|
+
not read or buffer the untouched tail.
|
|
57
|
+
|
|
58
|
+
After preparation, `beforeWrite` runs synchronously once. A thrown value is
|
|
59
|
+
propagated unchanged without any file mutation. A Promise or thenable return is
|
|
60
|
+
rejected with `TypeError`; asynchronous callbacks cannot admit a write. Once
|
|
61
|
+
admitted, the operation finishes the write or its failure recovery without
|
|
62
|
+
calling `beforeWrite` again. This callback is a whole-operation admission point,
|
|
63
|
+
not the per-mutation `assertBeforeMutation` callback used by Root operations.
|
|
64
|
+
It must not change the file, handle, or payload. There is no cancellation option.
|
|
65
|
+
|
|
66
|
+
For growth, additional tail bytes are written before the existing prefix is
|
|
67
|
+
touched. The prefix is then overwritten, completing partial writes. For shrinkage,
|
|
68
|
+
truncation is last. If a write or truncation fails, the helper attempts to restore
|
|
69
|
+
the saved prefix, if touched, and the original length. It waits for both recovery
|
|
70
|
+
attempts and then rethrows the original error, even when recovery also fails.
|
|
71
|
+
Zero-progress writes fail with `FsSafeError("helper-failed")`.
|
|
72
|
+
|
|
73
|
+
Recovery is best effort, not atomic publication or a crash-recovery guarantee.
|
|
74
|
+
Other observers can see intermediate contents; failed recovery can leave partial
|
|
75
|
+
bytes. The helper does not chmod or synchronize the file or its directory.
|
|
76
|
+
Normal filesystem effects such as timestamp updates or clearing special mode
|
|
77
|
+
bits can still occur. Callers own any durability or broader transaction policy.
|
|
78
|
+
|
|
79
|
+
The implementation uses Node positional reads and writes in every native mode;
|
|
80
|
+
it does not load a native binding. Prefer [`Root.write`](writing.md) when atomic
|
|
81
|
+
replacement and root-based path admission are the intended contract.
|
package/docs/index.md
CHANGED
|
@@ -52,6 +52,7 @@ await fs.remove("notes/archive/today.txt");
|
|
|
52
52
|
|---|---|
|
|
53
53
|
| [`root()`](root.md) | One boundary for read/write/move/remove and bounded recursive walking inside a trusted directory. |
|
|
54
54
|
| [`@openclaw/fs-safe/config`](config.md) | Process-global native helper and lock-option defaults. |
|
|
55
|
+
| [`@openclaw/fs-safe/guest`](guest.md) | Python filesystem source for caller-owned guest transports, with admitted roots and descriptor-relative operations. |
|
|
55
56
|
| [Native helper policy](native-helper.md) | Choose `auto`, `off`, or `require` for platform-native primitives. |
|
|
56
57
|
| [Native architecture](native.md) | Understand the thin syscall layer, beneath model, platform mechanisms, and fallback boundary. |
|
|
57
58
|
| [`replaceFileAtomic`](atomic.md) | Sibling-temp + rename, fsync hooks, mode preservation, copy fallback. |
|
|
@@ -65,6 +66,7 @@ await fs.remove("notes/archive/today.txt");
|
|
|
65
66
|
| [`tempWorkspace`](temp.md) | 0700 scratch dir with auto-cleanup. |
|
|
66
67
|
| [`readSecureFile`](secure-file.md) | Absolute file reads with fd pinning, permissions, owner, size, and timeout checks. |
|
|
67
68
|
| [`walkDirectory` / `Root.walk`](walk.md) | Standalone inventories plus root-bounded pruning, budgets, and partial-error reporting. |
|
|
69
|
+
| [`Root.entries`](entries.md) | Guarded nonrecursive entries, bounded name collection, and caller-owned symlink validation. |
|
|
68
70
|
| [`extractArchive`](archive.md) | Policy-driven ZIP/TAR extraction with clamp/filter, metadata/path-depth, link, count, and byte limits. |
|
|
69
71
|
| [Secret files](secret-file.md) | Mode-0600 credentials with size and TOCTOU defense. |
|
|
70
72
|
| [Permissions](permissions.md) | POSIX mode helpers plus Windows ACL inspection, raw owner/ACE facts, remediation, and private-directory creation. |
|
package/docs/install.md
CHANGED
|
@@ -31,6 +31,37 @@ node --version
|
|
|
31
31
|
# v22.0.0 or newer
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
+
## Bun runtime
|
|
35
|
+
|
|
36
|
+
Bun 1.4.2 can run the same public APIs and load the matching native package.
|
|
37
|
+
On macOS and Linux, fs-safe uses its Rust N-API addon to call the system
|
|
38
|
+
`realpath` implementation for native resolution. Ordinary resolution follows
|
|
39
|
+
Node's component walk, including lexical normalization of expanded symlink
|
|
40
|
+
targets, in Rust, with a 1,024-link expansion limit that returns `ELOOP` for
|
|
41
|
+
excessive or cyclic expansion. This works around Bun path-resolution defects that
|
|
42
|
+
otherwise reject restrictive permissions and confuse literal POSIX backslashes
|
|
43
|
+
with directory separators. The OS still resolves symlinks and canonical file
|
|
44
|
+
names; fs-safe retains its confinement and file-identity checks.
|
|
45
|
+
|
|
46
|
+
This works with `bun --jitless` and needs no runtime FFI or JIT. Use native mode
|
|
47
|
+
`auto` or `require` with the matching addon installed. `FS_SAFE_NATIVE_MODE=off`
|
|
48
|
+
still disables all addon loading; `auto` without the addon falls back to Bun's
|
|
49
|
+
resolver. Those configurations retain Bun 1.4.2's limitations with restrictive
|
|
50
|
+
permissions, sockets, literal backslashes, and symlink/parent traversal. Use
|
|
51
|
+
Node if you need full compatibility without the addon. On Bun POSIX, `require`
|
|
52
|
+
also rejects canonicalization when the addon or its canonicalizer is unavailable.
|
|
53
|
+
|
|
54
|
+
Node and Windows use their existing runtime canonicalizers. On Windows, Bun's
|
|
55
|
+
recursive directory creation receives an absolute spelling that preserves raw
|
|
56
|
+
path components, working around its rejection of existing relative `.` and `..`
|
|
57
|
+
directories. Public paths and caller-supplied filesystem adapters remain unchanged.
|
|
58
|
+
|
|
59
|
+
The upstream fix is tracked in [Bun #42374](https://github.com/oven-sh/bun/pull/42374).
|
|
60
|
+
The adapter can be removed when the supported Bun baseline includes that fix.
|
|
61
|
+
Run native compatibility checks with `pnpm test:bun:native` after building the
|
|
62
|
+
package and addon. See [contributing](contributing.md) for the Node/pnpm toolchain
|
|
63
|
+
and the broader diagnostic suite.
|
|
64
|
+
|
|
34
65
|
## TypeScript
|
|
35
66
|
|
|
36
67
|
Types ship with the package — no `@types/openclaw__fs-safe` needed. The `exports` map in `package.json` provides typed entries for every subpath:
|
package/docs/local-roots.md
CHANGED
|
@@ -71,6 +71,13 @@ component that does not exist: dangling symlinks, descendants of dangling
|
|
|
71
71
|
symlinks, and candidates whose existing ancestors cannot be canonicalized are
|
|
72
72
|
rejected rather than treated as safe missing paths.
|
|
73
73
|
|
|
74
|
+
Filesystem path inputs retain symlinks and parent components until boundary
|
|
75
|
+
resolution, including after home expansion. A followed `link/../file` resolves
|
|
76
|
+
the parent of the link's target; default reads reject the link instead of
|
|
77
|
+
normalizing it away. File URLs retain the URL parser's normal path semantics.
|
|
78
|
+
An existing non-directory component cannot be traversed further, including by
|
|
79
|
+
`..`; the helpers reject that input instead of selecting a different file.
|
|
80
|
+
|
|
74
81
|
## `readLocalFileFromRoots(options)`
|
|
75
82
|
|
|
76
83
|
The asynchronous helper opens the candidate through the matched [`Root`](root.md),
|
|
@@ -82,7 +89,7 @@ type ReadLocalFileFromRootsOptions = LocalRootsInputOptions & {
|
|
|
82
89
|
hardlinks?: "reject" | "allow";
|
|
83
90
|
maxBytes?: number;
|
|
84
91
|
nonBlockingRead?: boolean;
|
|
85
|
-
symlinks?: "reject" | "follow-within-root";
|
|
92
|
+
symlinks?: "reject" | "follow-within-root" | "follow-parents-within-root";
|
|
86
93
|
};
|
|
87
94
|
|
|
88
95
|
type LocalRootsReadResult = ReadResult & {
|