@openclaw/fs-safe 0.4.7 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +58 -0
- package/README.md +82 -43
- package/dist/archive-entry.d.ts.map +1 -1
- package/dist/archive-entry.js +5 -4
- package/dist/archive-errors.d.ts +11 -0
- package/dist/archive-errors.d.ts.map +1 -0
- package/dist/archive-errors.js +16 -0
- package/dist/archive-input.d.ts +12 -0
- package/dist/archive-input.d.ts.map +1 -0
- package/dist/archive-input.js +72 -0
- package/dist/archive-kind.d.ts +1 -1
- package/dist/archive-kind.d.ts.map +1 -1
- package/dist/archive-kind.js +18 -0
- package/dist/archive-limits.d.ts +10 -0
- package/dist/archive-limits.d.ts.map +1 -1
- package/dist/archive-limits.js +18 -0
- package/dist/archive-native.d.ts +18 -0
- package/dist/archive-native.d.ts.map +1 -0
- package/dist/archive-native.js +115 -0
- package/dist/archive-options.d.ts +21 -0
- package/dist/archive-options.d.ts.map +1 -0
- package/dist/archive-options.js +1 -0
- package/dist/archive-policy.d.ts +20 -0
- package/dist/archive-policy.d.ts.map +1 -0
- package/dist/archive-policy.js +29 -0
- package/dist/archive-read.d.ts +6 -0
- package/dist/archive-read.d.ts.map +1 -0
- package/dist/archive-read.js +200 -0
- package/dist/archive-staging.d.ts +2 -5
- package/dist/archive-staging.d.ts.map +1 -1
- package/dist/archive-staging.js +19 -10
- package/dist/archive-tar-meta.d.ts +6 -0
- package/dist/archive-tar-meta.d.ts.map +1 -0
- package/dist/archive-tar-meta.js +143 -0
- package/dist/archive-tar-runtime.d.ts +43 -0
- package/dist/archive-tar-runtime.d.ts.map +1 -0
- package/dist/archive-tar-runtime.js +8 -0
- package/dist/archive-tar.d.ts +5 -1
- package/dist/archive-tar.d.ts.map +1 -1
- package/dist/archive-tar.js +24 -6
- package/dist/archive-zip-entry.d.ts +15 -0
- package/dist/archive-zip-entry.d.ts.map +1 -0
- package/dist/archive-zip-entry.js +17 -0
- package/dist/archive.d.ts +7 -17
- package/dist/archive.d.ts.map +1 -1
- package/dist/archive.js +111 -108
- package/dist/atomic.d.ts +1 -1
- package/dist/atomic.d.ts.map +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +1 -1
- package/dist/containment.d.ts +2 -0
- package/dist/containment.d.ts.map +1 -0
- package/dist/containment.js +1 -0
- package/dist/device-path.d.ts.map +1 -1
- package/dist/device-path.js +24 -2
- package/dist/durability.d.ts +2 -0
- package/dist/durability.d.ts.map +1 -1
- package/dist/durability.js +2 -0
- package/dist/errors.d.ts +4 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +2 -0
- package/dist/file-hash.d.ts +10 -0
- package/dist/file-hash.d.ts.map +1 -0
- package/dist/file-hash.js +73 -0
- package/dist/file-lock-sync.d.ts +35 -0
- package/dist/file-lock-sync.d.ts.map +1 -0
- package/dist/file-lock-sync.js +233 -0
- package/dist/file-lock.d.ts +2 -0
- package/dist/file-lock.d.ts.map +1 -1
- package/dist/file-lock.js +1 -0
- package/dist/filename.d.ts.map +1 -1
- package/dist/filename.js +4 -1
- package/dist/guarded-mkdir.d.ts +7 -1
- package/dist/guarded-mkdir.d.ts.map +1 -1
- package/dist/guarded-mkdir.js +45 -3
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/json-document-store.d.ts.map +1 -1
- package/dist/json-document-store.js +30 -15
- package/dist/move-path.d.ts.map +1 -1
- package/dist/move-path.js +101 -17
- package/dist/native/darwin-arm64/fs-safe-native.node +0 -0
- package/dist/native/darwin-x64/fs-safe-native.node +0 -0
- package/dist/native/linux-arm64-gnu/fs-safe-native.node +0 -0
- package/dist/native/linux-arm64-musl/fs-safe-native.node +0 -0
- package/dist/native/linux-x64-gnu/fs-safe-native.node +0 -0
- package/dist/native/linux-x64-musl/fs-safe-native.node +0 -0
- package/dist/native/win32-x64-msvc/fs-safe-native.node +0 -0
- package/dist/native-binding.d.ts +79 -0
- package/dist/native-binding.d.ts.map +1 -0
- package/dist/native-binding.js +1 -0
- package/dist/native-config.d.ts +17 -0
- package/dist/native-config.d.ts.map +1 -0
- package/dist/native-config.js +71 -0
- package/dist/native-operations.d.ts +21 -0
- package/dist/native-operations.d.ts.map +1 -0
- package/dist/native-operations.js +115 -0
- package/dist/native-pinned-write.d.ts +5 -0
- package/dist/native-pinned-write.d.ts.map +1 -0
- package/dist/native-pinned-write.js +115 -0
- package/dist/native.d.ts +14 -0
- package/dist/native.d.ts.map +1 -0
- package/dist/native.js +204 -0
- package/dist/output-sibling.d.ts +8 -0
- package/dist/output-sibling.d.ts.map +1 -0
- package/dist/output-sibling.js +122 -0
- package/dist/output.d.ts +2 -0
- package/dist/output.d.ts.map +1 -1
- package/dist/output.js +32 -5
- package/dist/owner-dacl.d.ts +31 -0
- package/dist/owner-dacl.d.ts.map +1 -0
- package/dist/owner-dacl.js +27 -0
- package/dist/path-policy.js +1 -1
- package/dist/path.d.ts.map +1 -1
- package/dist/path.js +3 -2
- package/dist/permissions-public.d.ts +2 -0
- package/dist/permissions-public.d.ts.map +1 -1
- package/dist/permissions-public.js +2 -0
- package/dist/permissions.d.ts.map +1 -1
- package/dist/permissions.js +6 -0
- package/dist/pinned-operation.d.ts +2 -0
- package/dist/pinned-operation.d.ts.map +1 -0
- package/dist/pinned-operation.js +35 -0
- package/dist/pinned-write.d.ts +3 -16
- package/dist/pinned-write.d.ts.map +1 -1
- package/dist/pinned-write.js +20 -85
- package/dist/private-directory.d.ts +5 -0
- package/dist/private-directory.d.ts.map +1 -0
- package/dist/private-directory.js +13 -0
- package/dist/private-temp-workspace.d.ts +6 -2
- package/dist/private-temp-workspace.d.ts.map +1 -1
- package/dist/private-temp-workspace.js +40 -13
- package/dist/publish-file-failure.d.ts +27 -0
- package/dist/publish-file-failure.d.ts.map +1 -0
- package/dist/publish-file-failure.js +21 -0
- package/dist/publish-file.d.ts +21 -0
- package/dist/publish-file.d.ts.map +1 -0
- package/dist/publish-file.js +351 -0
- package/dist/read-opened-file.d.ts +3 -0
- package/dist/read-opened-file.d.ts.map +1 -1
- package/dist/read-opened-file.js +1 -0
- package/dist/replace-file-copy-fallback.d.ts +34 -0
- package/dist/replace-file-copy-fallback.d.ts.map +1 -0
- package/dist/replace-file-copy-fallback.js +339 -0
- package/dist/replace-file.d.ts +6 -2
- package/dist/replace-file.d.ts.map +1 -1
- package/dist/replace-file.js +39 -90
- package/dist/root-context.d.ts +4 -1
- package/dist/root-context.d.ts.map +1 -1
- package/dist/root-context.js +20 -1
- package/dist/root-impl.d.ts +5 -0
- package/dist/root-impl.d.ts.map +1 -1
- package/dist/root-impl.js +128 -225
- package/dist/root-path-existing.d.ts +3 -0
- package/dist/root-path-existing.d.ts.map +1 -0
- package/dist/root-path-existing.js +69 -0
- package/dist/root-path.d.ts +1 -2
- package/dist/root-path.d.ts.map +1 -1
- package/dist/root-path.js +67 -81
- package/dist/root-walk.d.ts +41 -0
- package/dist/root-walk.d.ts.map +1 -0
- package/dist/root-walk.js +114 -0
- package/dist/root.d.ts +2 -0
- package/dist/root.d.ts.map +1 -1
- package/dist/safe-path-segment.d.ts.map +1 -1
- package/dist/safe-path-segment.js +16 -4
- package/dist/secret-file.d.ts +5 -2
- package/dist/secret-file.d.ts.map +1 -1
- package/dist/secret-file.js +20 -2
- package/dist/secret-read-async.d.ts +4 -0
- package/dist/secret-read-async.d.ts.map +1 -0
- package/dist/secret-read-async.js +95 -0
- package/dist/secret.d.ts +2 -1
- package/dist/secret.d.ts.map +1 -1
- package/dist/secret.js +2 -1
- package/dist/sibling-temp.d.ts.map +1 -1
- package/dist/sibling-temp.js +1 -0
- package/dist/sidecar-lock-handle.d.ts +20 -0
- package/dist/sidecar-lock-handle.d.ts.map +1 -0
- package/dist/sidecar-lock-handle.js +28 -0
- package/dist/sidecar-lock-policy.d.ts +10 -0
- package/dist/sidecar-lock-policy.d.ts.map +1 -0
- package/dist/sidecar-lock-policy.js +29 -0
- package/dist/sidecar-lock-reclaim.d.ts +22 -6
- package/dist/sidecar-lock-reclaim.d.ts.map +1 -1
- package/dist/sidecar-lock-reclaim.js +106 -20
- package/dist/sidecar-lock-types.d.ts +56 -0
- package/dist/sidecar-lock-types.d.ts.map +1 -0
- package/dist/sidecar-lock-types.js +1 -0
- package/dist/sidecar-lock.d.ts +2 -45
- package/dist/sidecar-lock.d.ts.map +1 -1
- package/dist/sidecar-lock.js +94 -55
- package/dist/temp-cleanup.d.ts +7 -1
- package/dist/temp-cleanup.d.ts.map +1 -1
- package/dist/temp-cleanup.js +35 -5
- package/dist/temp-target.d.ts.map +1 -1
- package/dist/temp-target.js +51 -3
- package/dist/temp.d.ts +2 -1
- package/dist/temp.d.ts.map +1 -1
- package/dist/test-hooks.d.ts +3 -0
- package/dist/test-hooks.d.ts.map +1 -1
- package/dist/windows-permissions-native.d.ts +9 -0
- package/dist/windows-permissions-native.d.ts.map +1 -0
- package/dist/windows-permissions-native.js +31 -0
- package/docs/archive.md +131 -13
- package/docs/atomic.md +30 -1
- package/docs/config.md +51 -21
- package/docs/durability.md +211 -1
- package/docs/errors.md +36 -6
- package/docs/filename.md +0 -0
- package/docs/index.md +11 -8
- package/docs/install.md +27 -26
- package/docs/json-store.md +18 -3
- package/docs/migrating-to-0.5.md +196 -0
- package/docs/native-helper.md +85 -0
- package/docs/native.md +134 -0
- package/docs/output.md +37 -11
- package/docs/path.md +1 -1
- package/docs/permissions.md +84 -1
- package/docs/quickstart.md +5 -2
- package/docs/reading.md +4 -4
- package/docs/root.md +31 -15
- package/docs/secret-file.md +50 -1
- package/docs/security-model.md +29 -8
- package/docs/sidecar-lock.md +92 -7
- package/docs/temp.md +24 -1
- package/docs/test-hooks.md +22 -2
- package/docs/testing.md +18 -7
- package/docs/types.md +3 -1
- package/docs/walk.md +55 -0
- package/docs/writing.md +3 -3
- package/package.json +10 -4
- package/dist/pinned-helper.d.ts +0 -9
- package/dist/pinned-helper.d.ts.map +0 -1
- package/dist/pinned-helper.js +0 -19
- package/dist/pinned-path.d.ts +0 -7
- package/dist/pinned-path.d.ts.map +0 -1
- package/dist/pinned-path.js +0 -21
- package/dist/pinned-python-config.d.ts +0 -9
- package/dist/pinned-python-config.d.ts.map +0 -1
- package/dist/pinned-python-config.js +0 -38
- package/dist/pinned-python.d.ts +0 -12
- package/dist/pinned-python.d.ts.map +0 -1
- package/dist/pinned-python.js +0 -688
- package/docs/python-helper.md +0 -103
package/docs/sidecar-lock.md
CHANGED
|
@@ -42,6 +42,9 @@ function withFileLock<T, TPayload>(
|
|
|
42
42
|
): Promise<T>;
|
|
43
43
|
|
|
44
44
|
function createFileLockManager(key: string): FileLockManager;
|
|
45
|
+
|
|
46
|
+
function acquireFileLockSync<TPayload>(targetPath: string, options: FileLockSyncAcquireOptions<TPayload>): FileLockSyncHandle;
|
|
47
|
+
function withFileLockSync<T, TPayload>(targetPath: string, options: FileLockSyncAcquireOptions<TPayload>, fn: () => T): T;
|
|
45
48
|
```
|
|
46
49
|
|
|
47
50
|
`managerKey` is an optional identifier used to keep state isolated across multiple lock domains in the same process. Use distinct keys for distinct domains (`"snapshot"`, `"compact"`, `"build"`). If omitted, fs-safe derives one from the target path.
|
|
@@ -56,7 +59,7 @@ type FileLockAcquireOptions<TPayload extends Record<string, unknown>> = {
|
|
|
56
59
|
timeoutMs?: number; // overall acquire deadline; default unbounded
|
|
57
60
|
retry?: FileLockRetryOptions;
|
|
58
61
|
staleRecovery?: "fail-closed" | "remove-if-unchanged"; // default "fail-closed"
|
|
59
|
-
|
|
62
|
+
reentrantOwner?: string; // logical holder identity for owner-scoped nesting
|
|
60
63
|
payload: () => TPayload | Promise<TPayload>;
|
|
61
64
|
shouldReclaim?: (params: {
|
|
62
65
|
lockPath: string;
|
|
@@ -73,11 +76,15 @@ type FileLockAcquireOptions<TPayload extends Record<string, unknown>> = {
|
|
|
73
76
|
payload: Record<string, unknown> | null;
|
|
74
77
|
}) => boolean | Promise<boolean>;
|
|
75
78
|
metadata?: Record<string, unknown>; // attached to heldEntries() output for diagnostics
|
|
79
|
+
parsePayload?: (raw: string) => unknown;
|
|
80
|
+
lockRoot?: Root;
|
|
81
|
+
onCompromised?: (info: { lockPath: string; normalizedTargetPath: string }) => void;
|
|
82
|
+
compromiseCheckIntervalMs?: number;
|
|
76
83
|
};
|
|
77
84
|
|
|
78
85
|
type FileLockRetryOptions = {
|
|
79
86
|
retries?: number; // number of retry attempts after the first failure
|
|
80
|
-
factor?: number; // exponential backoff factor (default
|
|
87
|
+
factor?: number; // exponential backoff factor (default 1: constant delay)
|
|
81
88
|
minTimeout?: number; // initial delay (ms)
|
|
82
89
|
maxTimeout?: number; // delay cap (ms)
|
|
83
90
|
randomize?: boolean; // jitter
|
|
@@ -85,6 +92,59 @@ type FileLockRetryOptions = {
|
|
|
85
92
|
```
|
|
86
93
|
|
|
87
94
|
`payload` is a function so you can re-evaluate it on each retry (e.g. timestamp, PID).
|
|
95
|
+
`parsePayload` replaces JSON parsing for legacy or custom sidecars. Its `unknown`
|
|
96
|
+
result is passed to `shouldReclaim` and `shouldRemoveStaleLock`, allowing PID,
|
|
97
|
+
process-start, argv, or role schemas to remain application-owned.
|
|
98
|
+
|
|
99
|
+
## Owner-scoped reentrancy
|
|
100
|
+
|
|
101
|
+
Version 0.5 removes the unsound process-scoped `allowReentrant` boolean and
|
|
102
|
+
replaces it with `reentrantOwner`. When a manager already holds the canonical
|
|
103
|
+
target path, another acquisition reuses that sidecar only when both acquisitions
|
|
104
|
+
provide the same owner string. Each acquisition gets an idempotent release
|
|
105
|
+
handle; the sidecar remains until the last reference is released. A different or
|
|
106
|
+
missing owner waits under the normal contention, retry, and timeout policy. A
|
|
107
|
+
known live in-process holder is never stale-reclaimed by its own manager.
|
|
108
|
+
|
|
109
|
+
This supports logical session writers that may reach one file through real and
|
|
110
|
+
symlinked parent paths:
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
const managerKey = "session-write-locks";
|
|
114
|
+
const reentrantOwner = `session:${sessionId}:operation:${operationId}`;
|
|
115
|
+
|
|
116
|
+
const outer = await acquireFileLock(realSessionPath, {
|
|
117
|
+
managerKey,
|
|
118
|
+
reentrantOwner,
|
|
119
|
+
staleMs: 60_000,
|
|
120
|
+
payload: () => ({ pid: process.pid, operationId }),
|
|
121
|
+
});
|
|
122
|
+
const nested = await acquireFileLock(symlinkedSessionPath, {
|
|
123
|
+
managerKey,
|
|
124
|
+
reentrantOwner,
|
|
125
|
+
staleMs: 60_000,
|
|
126
|
+
payload: () => ({ pid: process.pid, operationId }),
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
await nested.release(); // sidecar remains for outer
|
|
130
|
+
await outer.release(); // final reference removes it
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
The manager domain and canonical target path are part of the identity, so
|
|
134
|
+
aliased paths must use the same `managerKey`. The owner key must identify one
|
|
135
|
+
logical holder or call chain. **Never use a process-wide or other shared constant
|
|
136
|
+
for unrelated tasks**: doing so would admit concurrent work to the same critical
|
|
137
|
+
section and recreate the lost-update bug that removed `allowReentrant`.
|
|
138
|
+
|
|
139
|
+
Omit `reentrantOwner` for ordinary acquisitions. `jsonStore` does so and keeps
|
|
140
|
+
its separate canonical-path mutation queue. The synchronous APIs implement the
|
|
141
|
+
same owner/refcount rules; a mismatched synchronous acquisition blocks the
|
|
142
|
+
calling thread according to its retry and timeout options.
|
|
143
|
+
|
|
144
|
+
Pass `lockRoot` to place sidecar create, read, verification, and removal behind
|
|
145
|
+
an existing `Root` capability. `lockPath` must resolve inside that root.
|
|
146
|
+
Identity-conditioned removal remains the only release and reclaim deletion
|
|
147
|
+
path.
|
|
88
148
|
|
|
89
149
|
## Release handle
|
|
90
150
|
|
|
@@ -92,25 +152,49 @@ type FileLockRetryOptions = {
|
|
|
92
152
|
type FileLockHandle = {
|
|
93
153
|
lockPath: string;
|
|
94
154
|
normalizedTargetPath: string;
|
|
155
|
+
verifyStillHeld: () => Promise<boolean>;
|
|
95
156
|
release: () => Promise<void>;
|
|
96
157
|
[Symbol.asyncDispose](): Promise<void>;
|
|
97
158
|
};
|
|
98
159
|
```
|
|
99
160
|
|
|
161
|
+
`verifyStillHeld()` compares the current sidecar with the ownership snapshot
|
|
162
|
+
captured at acquisition. Set `compromiseCheckIntervalMs` together with
|
|
163
|
+
`onCompromised` for a cheap periodic check; the callback fires once after the
|
|
164
|
+
sidecar no longer matches. This is detection, not revocation of work already in
|
|
165
|
+
progress.
|
|
166
|
+
|
|
167
|
+
## Synchronous locks
|
|
168
|
+
|
|
169
|
+
`acquireFileLockSync()` and `withFileLockSync()` mirror filesystem arbitration,
|
|
170
|
+
retry, payload parsing, stale policy, guarded identity-conditioned reclaim,
|
|
171
|
+
verification, and compromise monitoring. They do not use the async manager
|
|
172
|
+
queue, support async callbacks, or provide same-process reentrancy. Retry waits
|
|
173
|
+
block the calling thread; use the async API in request-serving code.
|
|
174
|
+
|
|
100
175
|
Always release in a `finally`:
|
|
101
176
|
|
|
102
177
|
```ts
|
|
103
|
-
|
|
178
|
+
import { acquireFileLockSync } from "@openclaw/fs-safe/file-lock";
|
|
179
|
+
|
|
180
|
+
const handle = acquireFileLockSync("/var/lib/app/schema.json", {
|
|
104
181
|
staleMs: 60_000,
|
|
105
|
-
|
|
182
|
+
timeoutMs: 5_000,
|
|
183
|
+
retry: { retries: 20, minTimeout: 25, maxTimeout: 250 },
|
|
184
|
+
payload: () => ({ pid: process.pid, operation: "schema-migration" }),
|
|
106
185
|
});
|
|
107
186
|
try {
|
|
108
|
-
|
|
187
|
+
if (!handle.verifyStillHeld()) throw new Error("migration lock was replaced");
|
|
188
|
+
migrateSchemaSynchronously();
|
|
109
189
|
} finally {
|
|
110
|
-
|
|
190
|
+
handle.release();
|
|
111
191
|
}
|
|
112
192
|
```
|
|
113
193
|
|
|
194
|
+
The sync payload, reclaim, and parsing callbacks must also be synchronous. This
|
|
195
|
+
shape is appropriate for a short boot migration; it is a poor fit for a server
|
|
196
|
+
request because retry backoff uses a blocking wait.
|
|
197
|
+
|
|
114
198
|
If your process dies before `release()` runs and skips the exit handler, the sidecar remains. Once `staleMs` elapses (or your `shouldReclaim` returns true), acquisition fails closed by default instead of deleting by path.
|
|
115
199
|
|
|
116
200
|
## `withFileLock` — common shape made one-liner
|
|
@@ -174,7 +258,7 @@ const handle = await acquireFileLock(targetPath, {
|
|
|
174
258
|
});
|
|
175
259
|
```
|
|
176
260
|
|
|
177
|
-
`heldByThisProcess` is true when this manager already holds the lock
|
|
261
|
+
`heldByThisProcess` is true when this manager already holds the lock. A `true` result marks the observed sidecar as stale; `staleRecovery` then decides whether acquisition fails closed or attempts caller-approved removal.
|
|
178
262
|
|
|
179
263
|
## Stale recovery: guarded `remove-if-unchanged`
|
|
180
264
|
|
|
@@ -248,3 +332,4 @@ await withFileLock(
|
|
|
248
332
|
|
|
249
333
|
- [Atomic writes](atomic.md) — single-writer atomicity that often replaces the need for a lock entirely.
|
|
250
334
|
- `createAsyncLock` from `@openclaw/fs-safe/advanced` — in-process serialization for a single Node process.
|
|
335
|
+
- [Migrating to 0.5](migrating-to-0.5.md) — choosing sync versus async lock APIs.
|
package/docs/temp.md
CHANGED
|
@@ -23,6 +23,7 @@ The compact factory. Returns:
|
|
|
23
23
|
```ts
|
|
24
24
|
type TempWorkspace = {
|
|
25
25
|
dir: string;
|
|
26
|
+
identity: { dev: number | bigint; ino: number | bigint };
|
|
26
27
|
store: FileStore;
|
|
27
28
|
path(fileName: string): string;
|
|
28
29
|
write(fileName: string, data: string | Uint8Array): Promise<string>;
|
|
@@ -30,7 +31,7 @@ type TempWorkspace = {
|
|
|
30
31
|
writeJson(fileName: string, data: unknown, options?: { trailingNewline?: boolean }): Promise<string>;
|
|
31
32
|
copyIn(fileName: string, sourcePath: string): Promise<string>;
|
|
32
33
|
read(fileName: string): Promise<Buffer>;
|
|
33
|
-
cleanup(): Promise<
|
|
34
|
+
cleanup(): Promise<"removed" | "missing" | "identity-mismatch">;
|
|
34
35
|
[Symbol.asyncDispose](): Promise<void>;
|
|
35
36
|
};
|
|
36
37
|
```
|
|
@@ -58,6 +59,28 @@ await state.write({ ready: true });
|
|
|
58
59
|
The workspace owns cleanup; the store is only a view over the workspace
|
|
59
60
|
directory.
|
|
60
61
|
|
|
62
|
+
The identity receipt is captured when the workspace is created. Manual,
|
|
63
|
+
disposal, and process-exit cleanup remove the path only while `lstat` still
|
|
64
|
+
matches that receipt. If another actor renames the workspace away and places a
|
|
65
|
+
new directory at the old name, cleanup returns `"identity-mismatch"` and leaves
|
|
66
|
+
the replacement untouched. Disposal hooks perform the same check and ignore
|
|
67
|
+
the returned status.
|
|
68
|
+
|
|
69
|
+
When cleanup is part of a retention or audit decision, inspect the receipt
|
|
70
|
+
instead of treating cleanup as fire-and-forget:
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
const workspace = await tempWorkspace({ rootDir: "/var/lib/app/tmp", prefix: "restore-" });
|
|
74
|
+
try {
|
|
75
|
+
await restoreInto(workspace.dir);
|
|
76
|
+
} finally {
|
|
77
|
+
const cleanup = await workspace.cleanup();
|
|
78
|
+
if (cleanup === "identity-mismatch") {
|
|
79
|
+
alertOperator("restore workspace path was replaced; replacement preserved");
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
61
84
|
The sync variant `tempWorkspaceSync` exposes the same surface with sync return
|
|
62
85
|
types and a `FileStoreSync` at `workspace.store`.
|
|
63
86
|
|
package/docs/test-hooks.md
CHANGED
|
@@ -31,6 +31,15 @@ type FsSafeTestHooks = {
|
|
|
31
31
|
afterPreOpenLstat?: (filePath: string) => Promise<void> | void;
|
|
32
32
|
beforeOpen?: (filePath: string, flags: number) => Promise<void> | void;
|
|
33
33
|
afterOpen?: (filePath: string, handle: FileHandle) => Promise<void> | void;
|
|
34
|
+
beforeArchiveOutputMutation?: (operation: "mkdir" | "chmod", targetPath: string) => Promise<void> | void;
|
|
35
|
+
beforeFileStorePruneDescend?: (dirPath: string) => Promise<void> | void;
|
|
36
|
+
beforeFileStoreSyncPrivateWrite?: (filePath: string) => void;
|
|
37
|
+
beforeRootFallbackMutation?: (operation: "mkdir" | "move" | "remove", targetPath: string) => Promise<void> | void;
|
|
38
|
+
afterPinnedWriteFallbackRename?: (targetPath: string) => Promise<void> | void;
|
|
39
|
+
beforeSiblingTempWrite?: (tempPath: string) => Promise<void> | void;
|
|
40
|
+
beforeTrashMove?: (targetPath: string, destPath: string) => void;
|
|
41
|
+
afterPublishTargetCreated?: (method, targetPath, identity) => Promise<void> | void;
|
|
42
|
+
beforePublishDirectorySync?: (method, targetPath, identity) => Promise<void> | void;
|
|
34
43
|
};
|
|
35
44
|
```
|
|
36
45
|
|
|
@@ -39,8 +48,19 @@ type FsSafeTestHooks = {
|
|
|
39
48
|
| `afterPreOpenLstat` | A pre-open `lstat` has just resolved. Use this to swap a path between validation and open. |
|
|
40
49
|
| `beforeOpen` | The library is about to call `open(path, flags)`. Use this to inject a TOCTOU window. |
|
|
41
50
|
| `afterOpen` | An open just succeeded. Use this to mutate state before the post-open identity check runs. |
|
|
42
|
-
|
|
43
|
-
|
|
51
|
+
| `beforeArchiveOutputMutation` | Archive staging is about to create a directory or apply a mode. |
|
|
52
|
+
| `beforeFileStorePruneDescend` | File-store pruning is about to descend into a directory. |
|
|
53
|
+
| `beforeFileStoreSyncPrivateWrite` | A synchronous private-store write is about to mutate its target. |
|
|
54
|
+
| `beforeRootFallbackMutation` | A guarded JS root fallback is about to mkdir, move, or remove. |
|
|
55
|
+
| `afterPinnedWriteFallbackRename` | A fallback rename committed and post-commit identity checks have not run yet. |
|
|
56
|
+
| `beforeSiblingTempWrite` | A sibling temp file exists and its writer is about to run. |
|
|
57
|
+
| `beforeTrashMove` | Trash handling is about to move the target. |
|
|
58
|
+
| `afterPublishTargetCreated` | Exclusive publication created its target and final fences have not run yet. |
|
|
59
|
+
| `beforePublishDirectorySync` | Publication verified the target and is about to sync its parent directory. |
|
|
60
|
+
|
|
61
|
+
Hooks typed `Promise<void> | void` may be sync or async and are awaited.
|
|
62
|
+
Hooks used by synchronous code paths are typed `void` and must not return a
|
|
63
|
+
promise.
|
|
44
64
|
|
|
45
65
|
## Usage
|
|
46
66
|
|
package/docs/testing.md
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
# Testing
|
|
2
2
|
|
|
3
|
-
`@openclaw/fs-safe/test-hooks` exposes
|
|
3
|
+
`@openclaw/fs-safe/test-hooks` exposes test-only injection points. Registration
|
|
4
|
+
is allowed only when `process.env.NODE_ENV === "test"` or
|
|
5
|
+
`process.env.VITEST === "true"`; registering a non-empty hook set elsewhere
|
|
6
|
+
throws. Production code must not import this subpath.
|
|
4
7
|
|
|
5
8
|
```ts
|
|
6
9
|
import {
|
|
@@ -14,7 +17,7 @@ The double-underscore prefix is a deliberate "hands off" signal: production code
|
|
|
14
17
|
## When to reach for hooks
|
|
15
18
|
|
|
16
19
|
- Reproduce a TOCTOU race deterministically: simulate a symlink swap between resolve and open, or between write and rename.
|
|
17
|
-
- Force
|
|
20
|
+
- Force guarded JavaScript behavior without removing bundled binaries from your runners.
|
|
18
21
|
- Inject latency to test cancellation/timeout paths.
|
|
19
22
|
|
|
20
23
|
If you don't need to inject a race, you don't need hooks — most tests should drive the library through normal calls and assert on observable behavior.
|
|
@@ -26,6 +29,10 @@ type FsSafeTestHooks = {
|
|
|
26
29
|
afterPreOpenLstat?: (filePath: string) => Promise<void> | void;
|
|
27
30
|
beforeOpen?: (filePath: string, flags: number) => Promise<void> | void;
|
|
28
31
|
afterOpen?: (filePath: string, handle: import("node:fs/promises").FileHandle) => Promise<void> | void;
|
|
32
|
+
afterPublishTargetCreated?: (method, targetPath, identity) => Promise<void> | void;
|
|
33
|
+
beforePublishDirectorySync?: (method, targetPath, identity) => Promise<void> | void;
|
|
34
|
+
// Additional archive, store, root-fallback, temp, and trash race hooks are
|
|
35
|
+
// documented on the focused Test hooks reference page.
|
|
29
36
|
};
|
|
30
37
|
|
|
31
38
|
function __setFsSafeTestHooksForTest(hooks?: FsSafeTestHooks): void;
|
|
@@ -37,6 +44,8 @@ Hooks are called at well-defined points in the library's hot paths:
|
|
|
37
44
|
- **`afterPreOpenLstat`** — runs after the pre-open `lstat`. A common use is to swap the path's target via `fs.symlink`/`fs.unlink` to drive a TOCTOU race.
|
|
38
45
|
- **`beforeOpen`** — runs before `fs.open` with the exact flags the root read path will use.
|
|
39
46
|
- **`afterOpen`** — runs after the file handle is opened. Useful to wrap handle methods or inject a size race before a stream is consumed.
|
|
47
|
+
- **`afterPublishTargetCreated`** — runs after exclusive publication created a target but before its final fences.
|
|
48
|
+
- **`beforePublishDirectorySync`** — runs after target verification and immediately before strict parent sync; useful for exercising `onSyncFailure`.
|
|
40
49
|
|
|
41
50
|
`__setFsSafeTestHooksForTest(undefined)` clears all hooks. Always clean up between tests.
|
|
42
51
|
|
|
@@ -81,20 +90,20 @@ it("rejects a swap between resolve and open", async () => {
|
|
|
81
90
|
|
|
82
91
|
The `code` may be `symlink` (caught at open by `O_NOFOLLOW`) or `path-mismatch` (caught by the post-open identity check) depending on platform — both are correct refusals.
|
|
83
92
|
|
|
84
|
-
## Example: force
|
|
93
|
+
## Example: force guarded JavaScript fallback behavior
|
|
85
94
|
|
|
86
95
|
```ts
|
|
87
|
-
import {
|
|
96
|
+
import { configureFsSafeNative } from "@openclaw/fs-safe/config";
|
|
88
97
|
|
|
89
98
|
beforeEach(() => {
|
|
90
|
-
|
|
99
|
+
configureFsSafeNative({ mode: "off" });
|
|
91
100
|
});
|
|
92
101
|
|
|
93
102
|
afterEach(() => {
|
|
94
|
-
|
|
103
|
+
configureFsSafeNative({ mode: "auto" });
|
|
95
104
|
});
|
|
96
105
|
|
|
97
|
-
it("runs without the
|
|
106
|
+
it("runs without the native helper", async () => {
|
|
98
107
|
const fs = await root(dir);
|
|
99
108
|
await fs.write("file.txt", "ok");
|
|
100
109
|
await expect(fs.readText("file.txt")).resolves.toBe("ok");
|
|
@@ -116,6 +125,8 @@ afterEach(() => {
|
|
|
116
125
|
|
|
117
126
|
A global hook clear in your test setup file is a good safety net.
|
|
118
127
|
|
|
128
|
+
See the [complete Test hooks reference](test-hooks.md) for every optional hook.
|
|
129
|
+
|
|
119
130
|
## Patterns for testing fs-safe-using code
|
|
120
131
|
|
|
121
132
|
You usually don't need hooks. Most tests follow this shape:
|
package/docs/types.md
CHANGED
|
@@ -65,18 +65,20 @@ Returned by `Root.open()` and `Root.read()`:
|
|
|
65
65
|
```ts
|
|
66
66
|
type OpenResult = {
|
|
67
67
|
handle: import("node:fs/promises").FileHandle;
|
|
68
|
+
containment: "kernel-atomic" | "best-effort";
|
|
68
69
|
realPath: string;
|
|
69
70
|
stat: import("node:fs").Stats;
|
|
70
71
|
};
|
|
71
72
|
|
|
72
73
|
type ReadResult = {
|
|
73
74
|
buffer: Buffer;
|
|
75
|
+
containment: "kernel-atomic" | "best-effort";
|
|
74
76
|
realPath: string;
|
|
75
77
|
stat: import("node:fs").Stats;
|
|
76
78
|
};
|
|
77
79
|
```
|
|
78
80
|
|
|
79
|
-
`realPath` is the canonical real path the read or open landed on, after symlink resolution; `stat` is the verified `fstat` result.
|
|
81
|
+
`realPath` is the canonical real path the read or open landed on, after symlink resolution; `stat` is the verified `fstat` result. Public root results currently report `containment: "best-effort"`; the union also describes direct native `openBeneath()` results, which report `"kernel-atomic"` on Linux. See the [security model](security-model.md#containment-guarantees-by-platform).
|
|
80
82
|
|
|
81
83
|
## `RootDefaults` / `RootOptions`
|
|
82
84
|
|
package/docs/walk.md
CHANGED
|
@@ -67,7 +67,62 @@ type WalkDirectoryOptions = {
|
|
|
67
67
|
|
|
68
68
|
Unreadable directories are skipped rather than throwing, but every skipped directory is recorded in `failedDirs`. This keeps the helper suitable for best-effort inventories while letting pruning jobs tell an incomplete scan from an empty one: a destructive reconcile that deletes state for paths missing from `entries` must first confirm `failedDirs` holds no real read failures, or a transient `EIO`/`EACCES` blip would be mistaken for mass deletion. Use a stricter root-bounded operation when every entry must be accounted for.
|
|
69
69
|
|
|
70
|
+
## Root-bounded async iteration
|
|
71
|
+
|
|
72
|
+
`Root.walk(rel, options)` is the root-bounded counterpart to these standalone
|
|
73
|
+
inventory helpers. It yields `{ relativePath, kind, size }` incrementally and
|
|
74
|
+
accepts `maxDepth`, `maxEntries`, `symlinkPolicy: "skip" |
|
|
75
|
+
"follow-within-root"`, and an `AbortSignal`. The default budget behavior yields
|
|
76
|
+
one `kind: "truncated"` marker and ends; pass `limitBehavior: "throw"` for a
|
|
77
|
+
typed `FsSafeError("too-large")` instead.
|
|
78
|
+
|
|
79
|
+
`entryFilter` is evaluated for each resolved file, directory, or other entry:
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
for await (const entry of capability.walk("", {
|
|
83
|
+
symlinkPolicy: "skip",
|
|
84
|
+
entryFilter: (entry) =>
|
|
85
|
+
entry.kind === "directory" && entry.relativePath === ".git"
|
|
86
|
+
? "skip-subtree"
|
|
87
|
+
: "include",
|
|
88
|
+
onDirectoryError: "skip-and-report",
|
|
89
|
+
})) {
|
|
90
|
+
if (entry.kind === "directory-error") {
|
|
91
|
+
console.warn("incomplete subtree", entry.relativePath, entry.error);
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
consume(entry);
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
The result values are `"include"`, `"skip"`, and `"skip-subtree"`. Plain
|
|
99
|
+
`"skip"` omits an entry but still descends when it is a directory;
|
|
100
|
+
`"skip-subtree"` omits that directory and prunes its descendants. Returning
|
|
101
|
+
`"skip-subtree"` for a non-directory is equivalent to `"skip"`.
|
|
102
|
+
|
|
103
|
+
`onDirectoryError` defaults to `"throw"`, preserving the original fail-fast
|
|
104
|
+
contract. `"skip-and-report"` yields a discriminated
|
|
105
|
+
`{ kind: "directory-error", relativePath, size: 0, error }` marker for a
|
|
106
|
+
directory that cannot be resolved or listed, then continues with its siblings.
|
|
107
|
+
Every examined directory entry consumes `maxEntries` before filtering, so
|
|
108
|
+
`"skip"` cannot turn the iterator into an unbounded traversal. Reporting and
|
|
109
|
+
`"truncated"` markers describe already-reached state and do not authorize
|
|
110
|
+
further descent.
|
|
111
|
+
|
|
112
|
+
The pure-Node path validates every directory canonically inside the root,
|
|
113
|
+
revalidates each listing through the normal `Root.list()` boundary, and tracks
|
|
114
|
+
canonical directories to stop symlink cycles. It does not hold a descriptor
|
|
115
|
+
for the entire tree, so it is not a process sandbox against a hostile peer that
|
|
116
|
+
can continuously swap and restore directories. Each individual lookup retains
|
|
117
|
+
the documented Node `Root` boundary checks.
|
|
118
|
+
|
|
119
|
+
Unlike `walkDirectory()` and `walkDirectorySync()`, `Root.walk()` is
|
|
120
|
+
root-bounded and reports failures inline because an async iterator has no final
|
|
121
|
+
result summary. Its default remains to throw on unreadable or invalid
|
|
122
|
+
directories.
|
|
123
|
+
|
|
70
124
|
## See also
|
|
71
125
|
|
|
72
126
|
- [`fileStore`](file-store.md) — managed stores use bounded walking for pruning.
|
|
73
127
|
- [Path scopes](path-scope.md) — boundary checks for known absolute paths.
|
|
128
|
+
- [Migrating to 0.5](migrating-to-0.5.md) — adopting bounded pruning and partial-result handling.
|
package/docs/writing.md
CHANGED
|
@@ -17,7 +17,7 @@ await fs.mkdir("snapshots/2026/05");
|
|
|
17
17
|
|
|
18
18
|
1. Resolve the relative target against the canonical root and reject anything that escapes (`outside-workspace`).
|
|
19
19
|
2. If `mkdir: true`, create missing parent directories with the parent fd pinned.
|
|
20
|
-
3.
|
|
20
|
+
3. Pin or guard the parent directory for the selected mechanism. Native operations use a parent fd; guarded JavaScript verifies directory identity before and after mutation. Linux beneath opens are kernel-atomic, while macOS, Windows, and JavaScript routes retain the best-effort race boundaries in the [security model](security-model.md#containment-guarantees-by-platform).
|
|
21
21
|
4. Write data to a sibling temp file in the same directory.
|
|
22
22
|
5. Atomically rename the temp file over the destination.
|
|
23
23
|
6. Stat the resulting fd and verify identity.
|
|
@@ -246,9 +246,9 @@ const fs = await root("/mnt/rclone-workspace", {
|
|
|
246
246
|
await fs.write("state.json", body); // succeeds on rclone FUSE
|
|
247
247
|
```
|
|
248
248
|
|
|
249
|
-
**How it works.** The full write runs under an exclusive per-target lock named `.fs-safe-write-<sha256>.lock` in the root. Keeping the lock in the already-canonical root avoids creating an unguarded lock path through a missing or raced target parent. The guarded Node fallback accepts the source-temp-to-destination inode mismatch only when the SHA-256 of the re-read bytes matches the SHA-256 of the bytes written. Subsequent path identity checks remain strict, so this mode requires an unchanged destination path to report stable identity. It deliberately
|
|
249
|
+
**How it works.** The full write runs under an exclusive per-target lock named `.fs-safe-write-<sha256>.lock` in the root. Keeping the lock in the already-canonical root avoids creating an unguarded lock path through a missing or raced target parent. The guarded Node fallback accepts the source-temp-to-destination inode mismatch only when the SHA-256 of the re-read bytes matches the SHA-256 of the bytes written. Subsequent path identity checks remain strict, so this mode requires an unchanged destination path to report stable identity. It deliberately stays on the guarded JavaScript path because content verification replaces the normal inode-preserving rename contract. The lock is released before the call returns.
|
|
250
250
|
|
|
251
|
-
**Security note.** `verify-content-with-lock` proves that the bytes observed after rename match the requested write and prevents *cooperating* writers from interleaving. It does **not** prove that the destination still names the temp-file object, retain
|
|
251
|
+
**Security note.** `verify-content-with-lock` proves that the bytes observed after rename match the requested write and prevents *cooperating* writers from interleaving. It does **not** prove that the destination still names the temp-file object, retain fd-relative parent pinning, or stop a same-UID process that ignores the advisory lock. Do not use this option on directories writable by untrusted same-UID processes. Strict identity verification remains the default.
|
|
252
252
|
|
|
253
253
|
Lock recovery is fail-closed. If a process crashes and leaves the root-level `.fs-safe-write-<sha256>.lock`, a later write reports the stale lock instead of deleting it based on a host-local PID. Recover only under external authority that excludes every competing writer; see [File lock](sidecar-lock.md#stale-recovery-guarded-remove-if-unchanged).
|
|
254
254
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openclaw/fs-safe",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "Capability-style filesystem roots for Node.js apps that handle untrusted relative paths.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"filesystem",
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
"dist/**/*.js",
|
|
28
28
|
"dist/**/*.d.ts",
|
|
29
29
|
"dist/**/*.d.ts.map",
|
|
30
|
+
"dist/native/**/*.node",
|
|
30
31
|
"docs/**/*.md",
|
|
31
32
|
"README.md",
|
|
32
33
|
"CHANGELOG.md",
|
|
@@ -120,6 +121,7 @@
|
|
|
120
121
|
},
|
|
121
122
|
"scripts": {
|
|
122
123
|
"benchmark": "node scripts/benchmark.mjs",
|
|
124
|
+
"benchmark:publish": "pnpm build && node scripts/bench-publish.mjs",
|
|
123
125
|
"build": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.json",
|
|
124
126
|
"lint:file-size": "node scripts/check-file-size.mjs",
|
|
125
127
|
"lint:fs-boundary": "node scripts/check-fs-boundary-primitives.mjs",
|
|
@@ -129,7 +131,10 @@
|
|
|
129
131
|
"test:security": "vitest run test/fs-safe.test.ts test/read-boundary-bypass.test.ts test/write-boundary-bypass.test.ts test/additional-boundary-bypass.test.ts test/adversarial-boundary-payloads.test.ts",
|
|
130
132
|
"check": "pnpm lint:file-size && pnpm lint:fs-boundary && pnpm build && pnpm test && node scripts/check-pack.mjs",
|
|
131
133
|
"docs:site": "node scripts/build-docs-site.mjs",
|
|
134
|
+
"native:build": "pnpm --filter @openclaw/fs-safe-native-build build",
|
|
135
|
+
"native:test": "cargo test --manifest-path native/Cargo.toml",
|
|
132
136
|
"pack:check": "pnpm build && node scripts/check-pack.mjs",
|
|
137
|
+
"package:smoke": "node scripts/check-release-packages.mjs --allow-host-only --output release-artifacts",
|
|
133
138
|
"check:changed": "pnpm run check",
|
|
134
139
|
"release:notes": "node scripts/release-notes.mjs",
|
|
135
140
|
"test:changed": "pnpm run test",
|
|
@@ -140,13 +145,14 @@
|
|
|
140
145
|
},
|
|
141
146
|
"optionalDependencies": {
|
|
142
147
|
"jszip": "^3.10.1",
|
|
143
|
-
"tar": "7.5.
|
|
148
|
+
"tar": "7.5.22"
|
|
144
149
|
},
|
|
145
150
|
"devDependencies": {
|
|
146
|
-
"@
|
|
151
|
+
"@napi-rs/cli": "3.8.1",
|
|
152
|
+
"@types/node": "^26.1.2",
|
|
147
153
|
"@vitest/coverage-v8": "4.1.10",
|
|
148
154
|
"typescript": "^7.0.2",
|
|
149
|
-
"vite": "8.
|
|
155
|
+
"vite": "8.2.0",
|
|
150
156
|
"vitest": "^4.1.10"
|
|
151
157
|
},
|
|
152
158
|
"engines": {
|
package/dist/pinned-helper.d.ts
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import type { DirEntry, PathStat } from "./types.js";
|
|
2
|
-
import { isPinnedHelperUnavailable } from "./pinned-python.js";
|
|
3
|
-
type HelperOperation = "stat" | "readdir" | "mkdirp" | "remove" | "rename";
|
|
4
|
-
export { isPinnedHelperUnavailable };
|
|
5
|
-
export declare function runPinnedHelper<T>(operation: HelperOperation, rootDir: string, payload: Record<string, unknown>): Promise<T>;
|
|
6
|
-
export declare function helperStat(rootDir: string, relativePath: string): Promise<PathStat>;
|
|
7
|
-
export declare function helperReaddir(rootDir: string, relativePath: string, withFileTypes: false): Promise<string[]>;
|
|
8
|
-
export declare function helperReaddir(rootDir: string, relativePath: string, withFileTypes: true): Promise<DirEntry[]>;
|
|
9
|
-
//# sourceMappingURL=pinned-helper.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"pinned-helper.d.ts","sourceRoot":"","sources":["../src/pinned-helper.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACrD,OAAO,EACL,yBAAyB,EAG1B,MAAM,oBAAoB,CAAC;AAE5B,KAAK,eAAe,GAAG,MAAM,GAAG,SAAS,GAAG,QAAQ,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAE3E,OAAO,EAAE,yBAAyB,EAAE,CAAC;AAErC,wBAAsB,eAAe,CAAC,CAAC,EACrC,SAAS,EAAE,eAAe,EAC1B,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC/B,OAAO,CAAC,CAAC,CAAC,CAOZ;AAED,wBAAsB,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC,CAEzF;AAED,wBAAsB,aAAa,CACjC,OAAO,EAAE,MAAM,EACf,YAAY,EAAE,MAAM,EACpB,aAAa,EAAE,KAAK,GACnB,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;AACrB,wBAAsB,aAAa,CACjC,OAAO,EAAE,MAAM,EACf,YAAY,EAAE,MAAM,EACpB,aAAa,EAAE,IAAI,GAClB,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC"}
|
package/dist/pinned-helper.js
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { isPinnedHelperUnavailable, runPinnedPythonOperation, validatePinnedOperationPayload, } from "./pinned-python.js";
|
|
2
|
-
export { isPinnedHelperUnavailable };
|
|
3
|
-
export async function runPinnedHelper(operation, rootDir, payload) {
|
|
4
|
-
validatePinnedOperationPayload(payload);
|
|
5
|
-
return await runPinnedPythonOperation({
|
|
6
|
-
operation,
|
|
7
|
-
rootPath: rootDir,
|
|
8
|
-
payload,
|
|
9
|
-
});
|
|
10
|
-
}
|
|
11
|
-
export async function helperStat(rootDir, relativePath) {
|
|
12
|
-
return await runPinnedHelper("stat", rootDir, { relativePath });
|
|
13
|
-
}
|
|
14
|
-
export async function helperReaddir(rootDir, relativePath, withFileTypes) {
|
|
15
|
-
return await runPinnedHelper("readdir", rootDir, {
|
|
16
|
-
relativePath,
|
|
17
|
-
withFileTypes,
|
|
18
|
-
});
|
|
19
|
-
}
|
package/dist/pinned-path.d.ts
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
export declare function isPinnedPathHelperSpawnError(error: unknown): boolean;
|
|
2
|
-
export declare function runPinnedPathHelper(params: {
|
|
3
|
-
operation: "mkdirp" | "remove";
|
|
4
|
-
rootPath: string;
|
|
5
|
-
relativePath: string;
|
|
6
|
-
}): Promise<void>;
|
|
7
|
-
//# sourceMappingURL=pinned-path.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"pinned-path.d.ts","sourceRoot":"","sources":["../src/pinned-path.ts"],"names":[],"mappings":"AAIA,wBAAgB,4BAA4B,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAEpE;AAED,wBAAsB,mBAAmB,CAAC,MAAM,EAAE;IAChD,SAAS,EAAE,QAAQ,GAAG,QAAQ,CAAC;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;CACtB,GAAG,OAAO,CAAC,IAAI,CAAC,CAahB"}
|
package/dist/pinned-path.js
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import { FsSafeError } from "./errors.js";
|
|
2
|
-
import { canFallbackFromPythonError } from "./pinned-python-config.js";
|
|
3
|
-
import { runPinnedHelper } from "./pinned-helper.js";
|
|
4
|
-
export function isPinnedPathHelperSpawnError(error) {
|
|
5
|
-
return canFallbackFromPythonError(error);
|
|
6
|
-
}
|
|
7
|
-
export async function runPinnedPathHelper(params) {
|
|
8
|
-
try {
|
|
9
|
-
await runPinnedHelper(params.operation, params.rootPath, {
|
|
10
|
-
relativePath: params.relativePath,
|
|
11
|
-
});
|
|
12
|
-
}
|
|
13
|
-
catch (error) {
|
|
14
|
-
if (error instanceof FsSafeError) {
|
|
15
|
-
throw error;
|
|
16
|
-
}
|
|
17
|
-
throw new FsSafeError("helper-failed", "pinned path helper failed", {
|
|
18
|
-
cause: error instanceof Error ? error : undefined,
|
|
19
|
-
});
|
|
20
|
-
}
|
|
21
|
-
}
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
export type FsSafePythonMode = "auto" | "off" | "require";
|
|
2
|
-
export type FsSafePythonConfig = {
|
|
3
|
-
mode: FsSafePythonMode;
|
|
4
|
-
pythonPath?: string;
|
|
5
|
-
};
|
|
6
|
-
export declare function configureFsSafePython(config: Partial<FsSafePythonConfig>): void;
|
|
7
|
-
export declare function getFsSafePythonConfig(): FsSafePythonConfig;
|
|
8
|
-
export declare function canFallbackFromPythonError(error: unknown): boolean;
|
|
9
|
-
//# sourceMappingURL=pinned-python-config.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"pinned-python-config.d.ts","sourceRoot":"","sources":["../src/pinned-python-config.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,gBAAgB,GAAG,MAAM,GAAG,KAAK,GAAG,SAAS,CAAC;AAE1D,MAAM,MAAM,kBAAkB,GAAG;IAC/B,IAAI,EAAE,gBAAgB,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AAqBF,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,OAAO,CAAC,kBAAkB,CAAC,GAAG,IAAI,CAE/E;AAED,wBAAgB,qBAAqB,IAAI,kBAAkB,CAc1D;AAED,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAMlE"}
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
let overrideConfig = {};
|
|
2
|
-
function parseMode(value) {
|
|
3
|
-
if (!value) {
|
|
4
|
-
return undefined;
|
|
5
|
-
}
|
|
6
|
-
const normalized = value.trim().toLowerCase();
|
|
7
|
-
if (normalized === "0" || normalized === "false" || normalized === "off" || normalized === "never") {
|
|
8
|
-
return "off";
|
|
9
|
-
}
|
|
10
|
-
if (normalized === "1" || normalized === "true" || normalized === "on" || normalized === "auto") {
|
|
11
|
-
return "auto";
|
|
12
|
-
}
|
|
13
|
-
if (normalized === "required" || normalized === "require") {
|
|
14
|
-
return "require";
|
|
15
|
-
}
|
|
16
|
-
return undefined;
|
|
17
|
-
}
|
|
18
|
-
export function configureFsSafePython(config) {
|
|
19
|
-
overrideConfig = { ...overrideConfig, ...config };
|
|
20
|
-
}
|
|
21
|
-
export function getFsSafePythonConfig() {
|
|
22
|
-
return {
|
|
23
|
-
mode: overrideConfig.mode ??
|
|
24
|
-
parseMode(process.env.FS_SAFE_PYTHON_MODE) ??
|
|
25
|
-
parseMode(process.env.OPENCLAW_FS_SAFE_PYTHON_MODE) ??
|
|
26
|
-
"auto",
|
|
27
|
-
pythonPath: overrideConfig.pythonPath ??
|
|
28
|
-
process.env.FS_SAFE_PYTHON ??
|
|
29
|
-
process.env.OPENCLAW_FS_SAFE_PYTHON ??
|
|
30
|
-
process.env.OPENCLAW_PINNED_PYTHON ??
|
|
31
|
-
process.env.OPENCLAW_PINNED_WRITE_PYTHON,
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
export function canFallbackFromPythonError(error) {
|
|
35
|
-
const code = error instanceof Error && "code" in error ? error.code : undefined;
|
|
36
|
-
return (getFsSafePythonConfig().mode !== "require" &&
|
|
37
|
-
(code === "helper-unavailable" || code === "unsupported-platform"));
|
|
38
|
-
}
|
package/dist/pinned-python.d.ts
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
type PinnedPythonOperation = "copy" | "stat" | "readdir" | "mkdirp" | "remove" | "rename" | "write";
|
|
2
|
-
export declare function __resetPinnedPythonWorkerForTest(): void;
|
|
3
|
-
export declare function runPinnedPythonOperation<T>(params: {
|
|
4
|
-
operation: PinnedPythonOperation;
|
|
5
|
-
rootPath: string;
|
|
6
|
-
payload: Record<string, unknown>;
|
|
7
|
-
}): Promise<T>;
|
|
8
|
-
export declare function assertPinnedPythonOperationAvailable(): void;
|
|
9
|
-
export declare function validatePinnedOperationPayload(payload: Record<string, unknown>): void;
|
|
10
|
-
export declare function isPinnedHelperUnavailable(error: unknown): boolean;
|
|
11
|
-
export {};
|
|
12
|
-
//# sourceMappingURL=pinned-python.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"pinned-python.d.ts","sourceRoot":"","sources":["../src/pinned-python.ts"],"names":[],"mappings":"AA2ZA,KAAK,qBAAqB,GACtB,MAAM,GACN,MAAM,GACN,SAAS,GACT,QAAQ,GACR,QAAQ,GACR,QAAQ,GACR,OAAO,CAAC;AAiBZ,wBAAgB,gCAAgC,IAAI,IAAI,CAQvD;AAiOD,wBAAsB,wBAAwB,CAAC,CAAC,EAAE,MAAM,EAAE;IACxD,SAAS,EAAE,qBAAqB,CAAC;IACjC,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAClC,GAAG,OAAO,CAAC,CAAC,CAAC,CA4Bb;AAED,wBAAgB,oCAAoC,IAAI,IAAI,CAK3D;AAED,wBAAgB,8BAA8B,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAarF;AAED,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAEjE"}
|