@openclaw/fs-safe 0.4.6 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +69 -0
- package/README.md +110 -41
- package/dist/archive-entry.d.ts.map +1 -1
- package/dist/archive-entry.js +5 -4
- package/dist/archive-errors.d.ts +11 -0
- package/dist/archive-errors.d.ts.map +1 -0
- package/dist/archive-errors.js +16 -0
- package/dist/archive-input.d.ts +12 -0
- package/dist/archive-input.d.ts.map +1 -0
- package/dist/archive-input.js +72 -0
- package/dist/archive-kind.d.ts +1 -1
- package/dist/archive-kind.d.ts.map +1 -1
- package/dist/archive-kind.js +18 -0
- package/dist/archive-limits.d.ts +10 -0
- package/dist/archive-limits.d.ts.map +1 -1
- package/dist/archive-limits.js +18 -0
- package/dist/archive-native.d.ts +18 -0
- package/dist/archive-native.d.ts.map +1 -0
- package/dist/archive-native.js +115 -0
- package/dist/archive-options.d.ts +21 -0
- package/dist/archive-options.d.ts.map +1 -0
- package/dist/archive-options.js +1 -0
- package/dist/archive-policy.d.ts +20 -0
- package/dist/archive-policy.d.ts.map +1 -0
- package/dist/archive-policy.js +29 -0
- package/dist/archive-read.d.ts +6 -0
- package/dist/archive-read.d.ts.map +1 -0
- package/dist/archive-read.js +200 -0
- package/dist/archive-staging.d.ts +2 -5
- package/dist/archive-staging.d.ts.map +1 -1
- package/dist/archive-staging.js +19 -10
- package/dist/archive-tar-meta.d.ts +6 -0
- package/dist/archive-tar-meta.d.ts.map +1 -0
- package/dist/archive-tar-meta.js +143 -0
- package/dist/archive-tar-runtime.d.ts +43 -0
- package/dist/archive-tar-runtime.d.ts.map +1 -0
- package/dist/archive-tar-runtime.js +8 -0
- package/dist/archive-tar.d.ts +5 -1
- package/dist/archive-tar.d.ts.map +1 -1
- package/dist/archive-tar.js +24 -6
- package/dist/archive-zip-entry.d.ts +15 -0
- package/dist/archive-zip-entry.d.ts.map +1 -0
- package/dist/archive-zip-entry.js +17 -0
- package/dist/archive.d.ts +7 -17
- package/dist/archive.d.ts.map +1 -1
- package/dist/archive.js +111 -108
- package/dist/atomic.d.ts +1 -1
- package/dist/atomic.d.ts.map +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +1 -1
- package/dist/containment.d.ts +2 -0
- package/dist/containment.d.ts.map +1 -0
- package/dist/containment.js +1 -0
- package/dist/directory-durability.d.ts +44 -0
- package/dist/directory-durability.d.ts.map +1 -0
- package/dist/directory-durability.js +299 -0
- package/dist/durability.d.ts +4 -0
- package/dist/durability.d.ts.map +1 -0
- package/dist/durability.js +3 -0
- package/dist/errors.d.ts +4 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +2 -0
- package/dist/file-hash.d.ts +10 -0
- package/dist/file-hash.d.ts.map +1 -0
- package/dist/file-hash.js +73 -0
- package/dist/file-lock-sync.d.ts +35 -0
- package/dist/file-lock-sync.d.ts.map +1 -0
- package/dist/file-lock-sync.js +233 -0
- package/dist/file-lock.d.ts +2 -0
- package/dist/file-lock.d.ts.map +1 -1
- package/dist/file-lock.js +1 -0
- package/dist/filename.d.ts.map +1 -1
- package/dist/filename.js +4 -1
- package/dist/fsync.d.ts +1 -1
- package/dist/fsync.d.ts.map +1 -1
- package/dist/fsync.js +1 -21
- package/dist/guarded-mkdir.d.ts +7 -1
- package/dist/guarded-mkdir.d.ts.map +1 -1
- package/dist/guarded-mkdir.js +45 -3
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/json-document-store.d.ts.map +1 -1
- package/dist/json-document-store.js +30 -15
- package/dist/move-path.d.ts.map +1 -1
- package/dist/move-path.js +101 -17
- package/dist/native/darwin-arm64/fs-safe-native.node +0 -0
- package/dist/native/darwin-x64/fs-safe-native.node +0 -0
- package/dist/native/linux-arm64-gnu/fs-safe-native.node +0 -0
- package/dist/native/linux-arm64-musl/fs-safe-native.node +0 -0
- package/dist/native/linux-x64-gnu/fs-safe-native.node +0 -0
- package/dist/native/linux-x64-musl/fs-safe-native.node +0 -0
- package/dist/native/win32-x64-msvc/fs-safe-native.node +0 -0
- package/dist/native-binding.d.ts +79 -0
- package/dist/native-binding.d.ts.map +1 -0
- package/dist/native-binding.js +1 -0
- package/dist/native-config.d.ts +17 -0
- package/dist/native-config.d.ts.map +1 -0
- package/dist/native-config.js +71 -0
- package/dist/native-operations.d.ts +21 -0
- package/dist/native-operations.d.ts.map +1 -0
- package/dist/native-operations.js +115 -0
- package/dist/native-pinned-write.d.ts +5 -0
- package/dist/native-pinned-write.d.ts.map +1 -0
- package/dist/native-pinned-write.js +115 -0
- package/dist/native.d.ts +14 -0
- package/dist/native.d.ts.map +1 -0
- package/dist/native.js +204 -0
- package/dist/output-sibling.d.ts +8 -0
- package/dist/output-sibling.d.ts.map +1 -0
- package/dist/output-sibling.js +122 -0
- package/dist/output.d.ts +2 -0
- package/dist/output.d.ts.map +1 -1
- package/dist/output.js +32 -5
- package/dist/owner-dacl.d.ts +31 -0
- package/dist/owner-dacl.d.ts.map +1 -0
- package/dist/owner-dacl.js +27 -0
- package/dist/path-policy.js +1 -1
- package/dist/permissions-public.d.ts +2 -0
- package/dist/permissions-public.d.ts.map +1 -1
- package/dist/permissions-public.js +2 -0
- package/dist/permissions.d.ts.map +1 -1
- package/dist/permissions.js +6 -0
- package/dist/pinned-operation.d.ts +2 -0
- package/dist/pinned-operation.d.ts.map +1 -0
- package/dist/pinned-operation.js +35 -0
- package/dist/pinned-write.d.ts +3 -16
- package/dist/pinned-write.d.ts.map +1 -1
- package/dist/pinned-write.js +20 -85
- package/dist/private-directory.d.ts +5 -0
- package/dist/private-directory.d.ts.map +1 -0
- package/dist/private-directory.js +13 -0
- package/dist/private-temp-workspace.d.ts +6 -2
- package/dist/private-temp-workspace.d.ts.map +1 -1
- package/dist/private-temp-workspace.js +40 -13
- package/dist/publish-file-failure.d.ts +27 -0
- package/dist/publish-file-failure.d.ts.map +1 -0
- package/dist/publish-file-failure.js +21 -0
- package/dist/publish-file.d.ts +21 -0
- package/dist/publish-file.d.ts.map +1 -0
- package/dist/publish-file.js +351 -0
- package/dist/read-opened-file.d.ts +3 -0
- package/dist/read-opened-file.d.ts.map +1 -1
- package/dist/read-opened-file.js +1 -0
- package/dist/replace-file-copy-fallback.d.ts +34 -0
- package/dist/replace-file-copy-fallback.d.ts.map +1 -0
- package/dist/replace-file-copy-fallback.js +339 -0
- package/dist/replace-file.d.ts +6 -2
- package/dist/replace-file.d.ts.map +1 -1
- package/dist/replace-file.js +39 -90
- package/dist/root-context.d.ts +4 -1
- package/dist/root-context.d.ts.map +1 -1
- package/dist/root-context.js +20 -1
- package/dist/root-impl.d.ts +5 -0
- package/dist/root-impl.d.ts.map +1 -1
- package/dist/root-impl.js +128 -225
- package/dist/root-path-existing.d.ts +3 -0
- package/dist/root-path-existing.d.ts.map +1 -0
- package/dist/root-path-existing.js +69 -0
- package/dist/root-path.d.ts +1 -2
- package/dist/root-path.d.ts.map +1 -1
- package/dist/root-path.js +67 -81
- package/dist/root-walk.d.ts +41 -0
- package/dist/root-walk.d.ts.map +1 -0
- package/dist/root-walk.js +114 -0
- package/dist/root.d.ts +2 -0
- package/dist/root.d.ts.map +1 -1
- package/dist/secret-file.d.ts +5 -2
- package/dist/secret-file.d.ts.map +1 -1
- package/dist/secret-file.js +20 -2
- package/dist/secret-read-async.d.ts +4 -0
- package/dist/secret-read-async.d.ts.map +1 -0
- package/dist/secret-read-async.js +95 -0
- package/dist/secret.d.ts +2 -1
- package/dist/secret.d.ts.map +1 -1
- package/dist/secret.js +2 -1
- package/dist/sibling-temp.d.ts.map +1 -1
- package/dist/sibling-temp.js +2 -13
- package/dist/sidecar-lock-handle.d.ts +20 -0
- package/dist/sidecar-lock-handle.d.ts.map +1 -0
- package/dist/sidecar-lock-handle.js +28 -0
- package/dist/sidecar-lock-policy.d.ts +10 -0
- package/dist/sidecar-lock-policy.d.ts.map +1 -0
- package/dist/sidecar-lock-policy.js +29 -0
- package/dist/sidecar-lock-reclaim.d.ts +22 -6
- package/dist/sidecar-lock-reclaim.d.ts.map +1 -1
- package/dist/sidecar-lock-reclaim.js +106 -20
- package/dist/sidecar-lock-types.d.ts +56 -0
- package/dist/sidecar-lock-types.d.ts.map +1 -0
- package/dist/sidecar-lock-types.js +1 -0
- package/dist/sidecar-lock.d.ts +2 -45
- package/dist/sidecar-lock.d.ts.map +1 -1
- package/dist/sidecar-lock.js +94 -55
- package/dist/temp-cleanup.d.ts +7 -1
- package/dist/temp-cleanup.d.ts.map +1 -1
- package/dist/temp-cleanup.js +35 -5
- package/dist/temp.d.ts +2 -1
- package/dist/temp.d.ts.map +1 -1
- package/dist/test-hooks.d.ts +3 -0
- package/dist/test-hooks.d.ts.map +1 -1
- package/dist/windows-permissions-native.d.ts +9 -0
- package/dist/windows-permissions-native.d.ts.map +1 -0
- package/dist/windows-permissions-native.js +31 -0
- package/docs/advanced.md +1 -1
- package/docs/archive.md +131 -13
- package/docs/atomic.md +30 -1
- package/docs/config.md +51 -21
- package/docs/durability.md +299 -0
- package/docs/errors.md +36 -6
- package/docs/filename.md +0 -0
- package/docs/index.md +11 -7
- package/docs/install.md +27 -26
- package/docs/json-store.md +18 -3
- package/docs/migrating-to-0.5.md +196 -0
- package/docs/native-helper.md +85 -0
- package/docs/native.md +134 -0
- package/docs/output.md +37 -11
- package/docs/path.md +1 -1
- package/docs/permissions.md +84 -1
- package/docs/quickstart.md +5 -2
- package/docs/reading.md +4 -4
- package/docs/root.md +31 -15
- package/docs/secret-file.md +50 -1
- package/docs/security-model.md +42 -8
- package/docs/sidecar-lock.md +92 -7
- package/docs/temp.md +24 -1
- package/docs/test-hooks.md +22 -2
- package/docs/testing.md +18 -7
- package/docs/types.md +3 -1
- package/docs/walk.md +55 -0
- package/docs/writing.md +3 -3
- package/package.json +12 -2
- package/dist/pinned-helper.d.ts +0 -9
- package/dist/pinned-helper.d.ts.map +0 -1
- package/dist/pinned-helper.js +0 -19
- package/dist/pinned-path.d.ts +0 -7
- package/dist/pinned-path.d.ts.map +0 -1
- package/dist/pinned-path.js +0 -21
- package/dist/pinned-python-config.d.ts +0 -9
- package/dist/pinned-python-config.d.ts.map +0 -1
- package/dist/pinned-python-config.js +0 -38
- package/dist/pinned-python.d.ts +0 -12
- package/dist/pinned-python.d.ts.map +0 -1
- package/dist/pinned-python.js +0 -688
- package/docs/python-helper.md +0 -103
|
@@ -0,0 +1,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)
|
package/docs/native.md
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Native architecture
|
|
3
|
+
description: "The bundled native bindings, fd-relative beneath model, platform mechanisms, loader security, and JavaScript fallback contract."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Native architecture
|
|
7
|
+
|
|
8
|
+
`@openclaw/fs-safe` bundles native bindings that supply mechanisms Node does
|
|
9
|
+
not expose directly. The Rust layer is deliberately not a second policy engine.
|
|
10
|
+
TypeScript owns trusted-root selection, path validation, archive filtering,
|
|
11
|
+
budgets, modes, identity fencing, cleanup decisions, and error normalization.
|
|
12
|
+
Rust receives already-decided relative operations and performs the smallest
|
|
13
|
+
platform syscall sequence that can preserve the boundary.
|
|
14
|
+
|
|
15
|
+
Every operation that has an equivalent safe Node implementation keeps that
|
|
16
|
+
guarded JavaScript path. Native loading is lazy; installs do not compile Rust,
|
|
17
|
+
run postinstall code, or fetch binaries. The npm tarball carries all seven
|
|
18
|
+
supported targets, so it is larger than a per-platform package by design.
|
|
19
|
+
Native-only formats and creation-time Windows DACL guarantees fail explicitly
|
|
20
|
+
instead of substituting a weaker implementation.
|
|
21
|
+
|
|
22
|
+
## The beneath model
|
|
23
|
+
|
|
24
|
+
A trusted directory descriptor is the capability. Native operations accept
|
|
25
|
+
that descriptor plus a validated relative path and never reconstruct authority
|
|
26
|
+
from a process working directory. Newly created files use exclusive creation,
|
|
27
|
+
and TypeScript compares descriptor, pathname, and expected identities before
|
|
28
|
+
accepting results.
|
|
29
|
+
|
|
30
|
+
Conceptually, a caller grants authority to an already-open root—not to a path
|
|
31
|
+
string that can be reinterpreted later:
|
|
32
|
+
|
|
33
|
+
```text
|
|
34
|
+
validated Root handle
|
|
35
|
+
└─ relative components (untrusted)
|
|
36
|
+
└─ open/link/mkdir beneath the handle
|
|
37
|
+
└─ compare descriptor + pathname + expected identity
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
The TypeScript layer validates and decides. The native layer never decides
|
|
41
|
+
whether a path, archive entry, mode, owner, or cleanup policy is acceptable.
|
|
42
|
+
|
|
43
|
+
- Linux uses `openat2(RESOLVE_BENEATH | RESOLVE_NO_MAGICLINKS)`, fd-relative
|
|
44
|
+
`mkdirat`/`linkat`/`renameat2`, `FICLONE`, and `copy_file_range`.
|
|
45
|
+
- macOS 15.4 and newer first use `openat(O_RESOLVE_BENEATH)`; older kernels walk
|
|
46
|
+
components with `openat(O_NOFOLLOW)` and restart in-root symlinks from the
|
|
47
|
+
pinned root. Both routes apply an `F_GETPATH` post-open containment detector,
|
|
48
|
+
but directory rename races mean the result remains `best-effort`, not
|
|
49
|
+
race-atomic. macOS uses `renameatx_np(RENAME_EXCL)` and permits
|
|
50
|
+
`fclonefileat` in an owned, non-shared parent. The clone is normalized inside
|
|
51
|
+
a private staging directory: flags, ACLs, extended attributes, and broad mode
|
|
52
|
+
bits are cleared before no-replace publication.
|
|
53
|
+
- Windows uses handle-relative `NtCreateFile` with `OBJ_DONT_REPARSE` and
|
|
54
|
+
`FILE_OPEN_REPARSE_POINT`, then explicitly rejects reparse points. Rename and
|
|
55
|
+
hardlink operations stay rooted in already-open handles. Owner/DACL reads
|
|
56
|
+
use `GetSecurityInfo`; private directories receive their protected DACL in
|
|
57
|
+
the `CreateDirectoryW` call itself.
|
|
58
|
+
|
|
59
|
+
## Archives
|
|
60
|
+
|
|
61
|
+
Rust streams ZIP and TAR payloads, including gzip, zstd, and bzip2. It first
|
|
62
|
+
returns a bounded manifest. TypeScript applies the shared path, filter, strip,
|
|
63
|
+
mode, and byte policies and returns an index-bound extraction plan. Rust then
|
|
64
|
+
creates only those planned entries beneath a private staging descriptor.
|
|
65
|
+
|
|
66
|
+
A fixed-512-byte pass-through meter sits between decompression and the TAR
|
|
67
|
+
crate. It reads only header type and octal/base-256 size fields. It never parses
|
|
68
|
+
metadata content. Oversized GNU long-name/link metadata is rejected before
|
|
69
|
+
buffering; PAX size overrides and GNU sparse entries are rejected as
|
|
70
|
+
unmeterable rather than guessed. The JavaScript node-tar path receives the same
|
|
71
|
+
`maxMetaEntryBytes` value and a matching fixed-header preflight.
|
|
72
|
+
|
|
73
|
+
## Publication and hashing
|
|
74
|
+
|
|
75
|
+
Exclusive publication tries a hardlink, then a copy-on-write clone, Linux
|
|
76
|
+
`copy_file_range`, and finally the existing asynchronous JavaScript byte loop.
|
|
77
|
+
All routes preserve `wx` semantics and the same source/target identity and
|
|
78
|
+
SHA-256 fencing. Native hashing and Linux whole-file copying run on N-API async
|
|
79
|
+
workers rather than the JavaScript event loop.
|
|
80
|
+
|
|
81
|
+
## Mode semantics
|
|
82
|
+
|
|
83
|
+
| Mode | Native loading | Fallback |
|
|
84
|
+
|---|---|---|
|
|
85
|
+
| `auto` | Try once, cache the result | Use guarded JavaScript when unavailable |
|
|
86
|
+
| `require` | Try once, cache the result | Throw `FsSafeError("helper-unavailable")` |
|
|
87
|
+
| `off` | Never attempt a binding load | Always use guarded JavaScript |
|
|
88
|
+
|
|
89
|
+
The one exception is functionality with no safe JavaScript implementation:
|
|
90
|
+
zstd/bzip2 TAR and Windows private-directory creation fail with
|
|
91
|
+
`helper-unavailable` when native support is absent or off.
|
|
92
|
+
|
|
93
|
+
## JavaScript fallback guarantees and delta
|
|
94
|
+
|
|
95
|
+
Public policy does not change with the selected mechanism: traversal and link
|
|
96
|
+
rejection, archive filters/limits/modes, exclusive target creation, source and
|
|
97
|
+
target identity fencing, publication cleanup receipts, and secret/lock policy
|
|
98
|
+
remain TypeScript-owned. What changes is the syscall strength or availability:
|
|
99
|
+
|
|
100
|
+
| Capability | Native path | Guarded JavaScript path |
|
|
101
|
+
|---|---|---|
|
|
102
|
+
| Root-relative opens/mutations | Descriptor-relative beneath operations. Linux reports `kernel-atomic`; macOS and Windows report `best-effort`. macOS uses `O_RESOLVE_BENEATH` when available plus an `F_GETPATH` detector, while Windows rejects reparse traversal in the object-manager call. | Reports `best-effort`: component-wise alias checks, no-follow opens where Node exposes them, private temp/rename, and post-operation identity verification. A hostile same-UID peer has a wider pathname race window. |
|
|
103
|
+
| ZIP/TAR/gzip | Rust streaming decode and fd-relative output creation. | JSZip/node-tar into a private stage, then the same guarded merge policy. |
|
|
104
|
+
| Zstd/bzip2 TAR | Supported. | Unsupported; typed `helper-unavailable`. |
|
|
105
|
+
| Publication copy | Clone, Linux `copy_file_range`, async native SHA-256. | Exclusive `wx` byte loop and Node SHA-256 with the same content/identity fences. |
|
|
106
|
+
| `rename-noreplace` | Atomic platform no-replace rename. | Unsupported; no emulation by check-then-rename. |
|
|
107
|
+
| Windows DACL read | Direct `GetSecurityInfo`; the public facts API exposes ordered basic allow/deny ACE SIDs, masks, and decoded flags without trust policy. | Established .NET/`icacls` inspection fallback for coarse permission checks; raw ACE facts are native-only. |
|
|
108
|
+
| Windows private directory | Creation-time protected DACL. | Unsupported; no weaker pathname-only substitute. |
|
|
109
|
+
|
|
110
|
+
Use `off` in CI to keep the fallback contract exercised. Use `require` when a
|
|
111
|
+
deployment depends on the stronger mechanism or a native-only feature; do not
|
|
112
|
+
infer native loading from timing.
|
|
113
|
+
|
|
114
|
+
## Loader security
|
|
115
|
+
|
|
116
|
+
Importing fs-safe never executes a child process. Linux libc selection uses
|
|
117
|
+
the Node process report, conventional musl library filenames, and the ELF
|
|
118
|
+
`PT_INTERP` field of `process.execPath`. If all probes are inconclusive, the
|
|
119
|
+
loader conservatively attempts the bundled glibc binary and lets normal module
|
|
120
|
+
loading fail into `auto` fallback. The loader requires only
|
|
121
|
+
`dist/native/<target>/fs-safe-native.node`; it never probes optional packages,
|
|
122
|
+
downloads code, or runs a postinstall step. A missing or incompatible binary
|
|
123
|
+
silently selects the JavaScript fallback in `auto`, throws typed
|
|
124
|
+
`helper-unavailable` in `require`, and is never inspected in `off`. Tests reject
|
|
125
|
+
`child_process`, `exec`, or `spawn` usage in the loader.
|
|
126
|
+
|
|
127
|
+
## Related pages
|
|
128
|
+
|
|
129
|
+
- [Native helper policy](native-helper.md)
|
|
130
|
+
- [Security model](security-model.md)
|
|
131
|
+
- [Archive extraction](archive.md)
|
|
132
|
+
- [Durability](durability.md)
|
|
133
|
+
- [Permissions](permissions.md)
|
|
134
|
+
- [Migrating to 0.5](migrating-to-0.5.md)
|
package/docs/output.md
CHANGED
|
@@ -16,10 +16,10 @@ await writeExternalFileWithinRoot({
|
|
|
16
16
|
});
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
The external writer
|
|
20
|
-
private
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
The external writer receives a staged path instead of the final destination.
|
|
20
|
+
The default private-workspace mode finalizes through `Root.copyIn()`. An
|
|
21
|
+
opt-in sibling mode stages in the destination directory and atomically renames
|
|
22
|
+
the completed file over the target.
|
|
23
23
|
|
|
24
24
|
## Signature
|
|
25
25
|
|
|
@@ -34,6 +34,8 @@ type ExternalFileWriteOptions<T = void> = {
|
|
|
34
34
|
write: (filePath: string) => Promise<T>;
|
|
35
35
|
maxBytes?: number;
|
|
36
36
|
mode?: number;
|
|
37
|
+
staging?: "workspace" | "sibling"; // default: "workspace"
|
|
38
|
+
fallbackFileName?: string; // safe staged-name fallback
|
|
37
39
|
};
|
|
38
40
|
|
|
39
41
|
type ExternalFileWriteResult<T = void> = {
|
|
@@ -46,18 +48,42 @@ The requested `path` must name a file. Missing destination parents are created
|
|
|
46
48
|
by the helper because the operation is "produce this output file under the
|
|
47
49
|
root"; callers should choose the filename before calling this API.
|
|
48
50
|
|
|
49
|
-
Use `maxBytes` when the external producer can create arbitrarily large files
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
Use `maxBytes` when the external producer can create arbitrarily large files,
|
|
52
|
+
and `mode` when the finalized file needs a specific POSIX mode. Both staging
|
|
53
|
+
modes enforce them after the producer returns and before committing the target.
|
|
54
|
+
Requested basenames containing C0/C1 controls or Windows-invalid characters are
|
|
55
|
+
sanitized portably; `fallbackFileName` supplies the name when nothing remains.
|
|
56
|
+
The same sanitized basename is used for producer staging, guarded internal
|
|
57
|
+
temps, the final rename target, and the returned `path`; raw and staged names
|
|
58
|
+
never diverge.
|
|
59
|
+
|
|
60
|
+
## Choosing a staging mode
|
|
61
|
+
|
|
62
|
+
`staging: "workspace"` is the default. The producer writes in private temp
|
|
63
|
+
storage, then fs-safe copies through the guarded root boundary. Choose it when
|
|
64
|
+
the temp and destination filesystems may differ, or when an externally produced
|
|
65
|
+
partial file must never appear in the destination directory. The final target
|
|
66
|
+
still appears only after guarded finalization.
|
|
67
|
+
|
|
68
|
+
`staging: "sibling"` gives the producer a randomized temp path in the target
|
|
69
|
+
directory. Choose it only when that directory itself is the approved writable
|
|
70
|
+
boundary and same-filesystem atomic replacement is required. After the callback
|
|
71
|
+
returns, fs-safe pins and validates the staged regular file, rejects hardlinks
|
|
72
|
+
and size-limit violations, applies `mode`, fsyncs it, and atomically renames it
|
|
73
|
+
over the target. Existing files and symlink entries are replaced without
|
|
74
|
+
following their contents or referents. The parent identity is guarded across
|
|
75
|
+
the operation and the parent directory is synchronized best-effort after
|
|
76
|
+
rename.
|
|
53
77
|
|
|
54
78
|
## Why not pass the final path to the library?
|
|
55
79
|
|
|
56
80
|
If a target parent can be swapped after validation, handing an external library
|
|
57
81
|
the final path can make the library write outside the intended root before
|
|
58
|
-
fs-safe has a chance to finalize or reject the operation.
|
|
59
|
-
|
|
60
|
-
|
|
82
|
+
fs-safe has a chance to finalize or reject the operation. Workspace staging
|
|
83
|
+
keeps the trust-boundary write inside fs-safe's root-aware copy/atomic-write
|
|
84
|
+
path. Sibling staging intentionally shifts the writable boundary to the
|
|
85
|
+
destination directory, while keeping pathname validation, staged-file identity
|
|
86
|
+
checks, and the final rename under fs-safe's control.
|
|
61
87
|
|
|
62
88
|
## Browser download example
|
|
63
89
|
|
package/docs/path.md
CHANGED
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
} from "@openclaw/fs-safe/path";
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
-
Only `root()`, `FsSafeError`, and the
|
|
22
|
+
Only `root()`, `FsSafeError`, and the native helper config live on the main entry. Path helpers are deliberately a subpath import so the main entry stays small.
|
|
23
23
|
|
|
24
24
|
## Boundary checks
|
|
25
25
|
|
package/docs/permissions.md
CHANGED
|
@@ -64,7 +64,88 @@ createIcaclsResetCommand(targetPath, { isDir, env });
|
|
|
64
64
|
resolveWindowsUserPrincipal(env);
|
|
65
65
|
```
|
|
66
66
|
|
|
67
|
-
The
|
|
67
|
+
The fallback Windows inspector calls `icacls.exe <path>` using its supported
|
|
68
|
+
path-only inspection syntax and classifies principals as trusted, world, or
|
|
69
|
+
group. Trusted defaults include the current user, SYSTEM, and Administrators.
|
|
70
|
+
The parser is on the advanced surface so tests and CLIs can process captured
|
|
71
|
+
`icacls` output without spawning a process.
|
|
72
|
+
|
|
73
|
+
When the bundled native binding is available, `inspectPathPermissions()`
|
|
74
|
+
reads the owner and DACL directly with Windows security APIs. It classifies the
|
|
75
|
+
current user, LocalSystem, and built-in Administrators as trusted and reports
|
|
76
|
+
the world/group read/write facts consumed by secure reads. Descriptor forms it
|
|
77
|
+
cannot classify equivalently fall back to the established owner/.NET and
|
|
78
|
+
`icacls` path; `mode: "off"` exercises that fallback deterministically.
|
|
79
|
+
|
|
80
|
+
## Policy-free owner and DACL facts
|
|
81
|
+
|
|
82
|
+
`readOwnerAndDacl()` exposes the direct Windows descriptor facts needed by a
|
|
83
|
+
consumer that owns a principal allowlist. It deliberately does not decide
|
|
84
|
+
which SID is trusted or calculate effective access. For example, snapshot
|
|
85
|
+
staging can reject an incomplete descriptor and ignore inherit-only ACEs before
|
|
86
|
+
applying its own exact SID policy:
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
import { readOwnerAndDacl } from "@openclaw/fs-safe/permissions";
|
|
90
|
+
|
|
91
|
+
const facts = readOwnerAndDacl(stagingDirectory);
|
|
92
|
+
if (facts.status === "unsupported-platform") {
|
|
93
|
+
throw new Error(`Windows ACL facts unavailable on ${facts.platform}`);
|
|
94
|
+
}
|
|
95
|
+
if (!facts.isLocal || !facts.daclPresent || !facts.complete) {
|
|
96
|
+
throw new Error("staging DACL cannot be evaluated completely");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
for (const ace of facts.aces) {
|
|
100
|
+
if (ace.flags.inheritOnly) continue;
|
|
101
|
+
if (!trustedSids.has(ace.sid)) {
|
|
102
|
+
throw new Error(`unexpected staging principal: ${ace.sid}`);
|
|
103
|
+
}
|
|
104
|
+
evaluateMaskAndDenyOrder(ace.aceType, ace.mask);
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
On Windows the supported result contains `ownerSid`, `currentUserSid`,
|
|
109
|
+
`daclPresent`, `isLocal`, `complete`, `unsupportedAceTypes`, and ordered basic
|
|
110
|
+
allow/deny `aces`. `currentUserSid` is the process token's `TokenUser` SID, so
|
|
111
|
+
callers can compare it with the owner or their own allowlist without fs-safe
|
|
112
|
+
applying trust policy. Each ACE has `{ sid, mask, aceType, flags }`; `flags`
|
|
113
|
+
retains the raw byte and decoded
|
|
114
|
+
`objectInherit`, `containerInherit`, `noPropagateInherit`, `inheritOnly`,
|
|
115
|
+
`inherited`, `successfulAccess`, and `failedAccess` facts. SID strings are
|
|
116
|
+
lowercase Windows SID notation. `daclPresent: false` represents a null DACL,
|
|
117
|
+
which grants unrestricted access; it must not be mistaken for an empty DACL.
|
|
118
|
+
|
|
119
|
+
Object-specific and other ACE layouts are not guessed: they are omitted,
|
|
120
|
+
`complete` becomes false, and their numeric types appear in
|
|
121
|
+
`unsupportedAceTypes`, allowing a security-sensitive caller to fail closed.
|
|
122
|
+
Non-Windows systems return `{ status: "unsupported-platform", platform }`.
|
|
123
|
+
Windows requires the bundled native binding; if it is unavailable or forced
|
|
124
|
+
off, the call throws `FsSafeError("helper-unavailable")`. The existing coarse
|
|
125
|
+
`inspectPathPermissions()` API still owns its compatibility fallback and trust
|
|
126
|
+
classification.
|
|
127
|
+
|
|
128
|
+
## Private directories
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
import path from "node:path";
|
|
132
|
+
import { createPrivateDirectory } from "@openclaw/fs-safe/permissions";
|
|
133
|
+
|
|
134
|
+
const sqliteDirectory =
|
|
135
|
+
"C:\\Users\\me\\AppData\\Local\\OpenClaw\\private-databases";
|
|
136
|
+
await createPrivateDirectory(sqliteDirectory);
|
|
137
|
+
await openSqlite(path.join(sqliteDirectory, "sessions.sqlite"));
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
On Windows with native support, this creates the directory and applies a
|
|
141
|
+
protected owner + LocalSystem + Administrators full-control DACL directly with
|
|
142
|
+
an atomic security descriptor; no PowerShell or `icacls` process is launched.
|
|
143
|
+
This API is Windows-only and native-only; it fails closed with
|
|
144
|
+
`FsSafeError("helper-unavailable")` on other platforms, when native mode is off,
|
|
145
|
+
or when the binding is unavailable. POSIX callers should create private
|
|
146
|
+
directories through their existing trusted-root creation policy rather than a
|
|
147
|
+
pathname-only compatibility shim. Existing Windows permission inspection still
|
|
148
|
+
retains its .NET/`icacls` compatibility fallback.
|
|
68
149
|
|
|
69
150
|
Use `createIcaclsResetCommand()` when you need a structured command and argv pair. Use `formatIcaclsResetCommand()` when you only need a remediation string for a user-facing message.
|
|
70
151
|
|
|
@@ -96,3 +177,5 @@ type PermissionCheck = {
|
|
|
96
177
|
|
|
97
178
|
- [Secure file reads](secure-file.md) — fd-pinned reads that enforce these checks.
|
|
98
179
|
- [Errors](errors.md) — permission-related `FsSafeError` codes.
|
|
180
|
+
- [Native architecture](native.md) — direct Windows security descriptor mechanisms.
|
|
181
|
+
- [Migrating to 0.5](migrating-to-0.5.md) — native-only feature checklist.
|
package/docs/quickstart.md
CHANGED
|
@@ -67,7 +67,7 @@ const names = await fs.list("state"); // string[]
|
|
|
67
67
|
const entries = await fs.list("state", { withFileTypes: true }); // DirEntry[]
|
|
68
68
|
```
|
|
69
69
|
|
|
70
|
-
`exists`, `stat`, and `list` are boundary-checked but **do not pin a later operation** to the same filesystem object. For
|
|
70
|
+
`exists`, `stat`, and `list` are boundary-checked but **do not pin a later operation** to the same filesystem object. For operation-local identity checks, use `read()`, `open()`, `write()`, `create()`, `copyIn()`, `move()`, or `remove()`. Linux native beneath opens are kernel-atomic; other mechanisms remain best-effort as documented in the [security model](security-model.md#containment-guarantees-by-platform).
|
|
71
71
|
|
|
72
72
|
## 6. Catch escapes
|
|
73
73
|
|
|
@@ -121,7 +121,10 @@ await extractArchive({
|
|
|
121
121
|
maxEntries: 50_000,
|
|
122
122
|
maxExtractedBytes: 512 * 1024 * 1024,
|
|
123
123
|
maxEntryBytes: 256 * 1024 * 1024,
|
|
124
|
+
maxMetaEntryBytes: 1024 * 1024,
|
|
125
|
+
maxEntryPathComponents: 64,
|
|
124
126
|
},
|
|
127
|
+
entryModes: "clamp",
|
|
125
128
|
});
|
|
126
129
|
```
|
|
127
130
|
|
|
@@ -133,7 +136,7 @@ Extraction stages into a private dir and merges through the same boundary used b
|
|
|
133
136
|
import { withTempWorkspace } from "@openclaw/fs-safe/temp";
|
|
134
137
|
|
|
135
138
|
await withTempWorkspace({ rootDir: "/srv/jobs/tmp", prefix: "build-" }, async (workspace) => {
|
|
136
|
-
await
|
|
139
|
+
await workspace.copyIn("input.bin", "/tmp/source.bin");
|
|
137
140
|
// ...do work in workspace.dir; auto-cleaned on exit
|
|
138
141
|
});
|
|
139
142
|
```
|
package/docs/reading.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
The `Root` handle exposes five read shapes. Pick the narrowest one that gives you what you need — narrower shapes do less work and surface fewer footguns.
|
|
4
4
|
|
|
5
5
|
```ts
|
|
6
|
-
const result = await fs.read("notes/today.txt"); // { buffer, realPath, stat }
|
|
6
|
+
const result = await fs.read("notes/today.txt"); // { buffer, containment, realPath, stat }
|
|
7
7
|
const text = await fs.readText("notes/today.txt"); // string
|
|
8
8
|
const bytes = await fs.readBytes("image.png"); // Buffer
|
|
9
9
|
const json = await fs.readJson<Config>("config.json"); // T
|
|
@@ -30,7 +30,7 @@ Regardless of shape, every read goes through the same boundary checks:
|
|
|
30
30
|
The full result. Use it when you need both the bytes and the verified `realPath` or `stat`:
|
|
31
31
|
|
|
32
32
|
```ts
|
|
33
|
-
const { buffer, realPath, stat } = await fs.read("notes/today.txt");
|
|
33
|
+
const { buffer, containment, realPath, stat } = await fs.read("notes/today.txt");
|
|
34
34
|
console.log(`${stat.size} bytes at ${realPath}`);
|
|
35
35
|
```
|
|
36
36
|
|
|
@@ -63,7 +63,7 @@ For tighter control over malformed-or-missing JSON, use the standalone helpers i
|
|
|
63
63
|
|
|
64
64
|
### `fs.open(rel, options?)`
|
|
65
65
|
|
|
66
|
-
Returns a `FileHandle` plus the verified `realPath
|
|
66
|
+
Returns a `FileHandle` plus `containment: "best-effort"`, the verified `realPath`, and `stat`. Use this for streaming or partial reads, and **always close the handle**:
|
|
67
67
|
|
|
68
68
|
```ts
|
|
69
69
|
const opened = await fs.open("large.log");
|
|
@@ -121,7 +121,7 @@ if (await fs.exists("notes/today.txt")) {
|
|
|
121
121
|
}
|
|
122
122
|
```
|
|
123
123
|
|
|
124
|
-
A symlink swap between `exists` and `readText` is
|
|
124
|
+
A symlink swap between `exists` and `readText` is checked again by the read; the boundary and its documented race window are per-call.
|
|
125
125
|
|
|
126
126
|
## Streaming patterns
|
|
127
127
|
|