@openclaw/fs-safe 0.4.7 → 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.
Files changed (239) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/README.md +82 -43
  3. package/dist/archive-entry.d.ts.map +1 -1
  4. package/dist/archive-entry.js +5 -4
  5. package/dist/archive-errors.d.ts +11 -0
  6. package/dist/archive-errors.d.ts.map +1 -0
  7. package/dist/archive-errors.js +16 -0
  8. package/dist/archive-input.d.ts +12 -0
  9. package/dist/archive-input.d.ts.map +1 -0
  10. package/dist/archive-input.js +72 -0
  11. package/dist/archive-kind.d.ts +1 -1
  12. package/dist/archive-kind.d.ts.map +1 -1
  13. package/dist/archive-kind.js +18 -0
  14. package/dist/archive-limits.d.ts +10 -0
  15. package/dist/archive-limits.d.ts.map +1 -1
  16. package/dist/archive-limits.js +18 -0
  17. package/dist/archive-native.d.ts +18 -0
  18. package/dist/archive-native.d.ts.map +1 -0
  19. package/dist/archive-native.js +115 -0
  20. package/dist/archive-options.d.ts +21 -0
  21. package/dist/archive-options.d.ts.map +1 -0
  22. package/dist/archive-options.js +1 -0
  23. package/dist/archive-policy.d.ts +20 -0
  24. package/dist/archive-policy.d.ts.map +1 -0
  25. package/dist/archive-policy.js +29 -0
  26. package/dist/archive-read.d.ts +6 -0
  27. package/dist/archive-read.d.ts.map +1 -0
  28. package/dist/archive-read.js +200 -0
  29. package/dist/archive-staging.d.ts +2 -5
  30. package/dist/archive-staging.d.ts.map +1 -1
  31. package/dist/archive-staging.js +19 -10
  32. package/dist/archive-tar-meta.d.ts +6 -0
  33. package/dist/archive-tar-meta.d.ts.map +1 -0
  34. package/dist/archive-tar-meta.js +143 -0
  35. package/dist/archive-tar-runtime.d.ts +43 -0
  36. package/dist/archive-tar-runtime.d.ts.map +1 -0
  37. package/dist/archive-tar-runtime.js +8 -0
  38. package/dist/archive-tar.d.ts +5 -1
  39. package/dist/archive-tar.d.ts.map +1 -1
  40. package/dist/archive-tar.js +24 -6
  41. package/dist/archive-zip-entry.d.ts +15 -0
  42. package/dist/archive-zip-entry.d.ts.map +1 -0
  43. package/dist/archive-zip-entry.js +17 -0
  44. package/dist/archive.d.ts +7 -17
  45. package/dist/archive.d.ts.map +1 -1
  46. package/dist/archive.js +111 -108
  47. package/dist/atomic.d.ts +1 -1
  48. package/dist/atomic.d.ts.map +1 -1
  49. package/dist/config.d.ts +1 -1
  50. package/dist/config.d.ts.map +1 -1
  51. package/dist/config.js +1 -1
  52. package/dist/containment.d.ts +2 -0
  53. package/dist/containment.d.ts.map +1 -0
  54. package/dist/containment.js +1 -0
  55. package/dist/durability.d.ts +2 -0
  56. package/dist/durability.d.ts.map +1 -1
  57. package/dist/durability.js +2 -0
  58. package/dist/errors.d.ts +4 -1
  59. package/dist/errors.d.ts.map +1 -1
  60. package/dist/errors.js +2 -0
  61. package/dist/file-hash.d.ts +10 -0
  62. package/dist/file-hash.d.ts.map +1 -0
  63. package/dist/file-hash.js +73 -0
  64. package/dist/file-lock-sync.d.ts +35 -0
  65. package/dist/file-lock-sync.d.ts.map +1 -0
  66. package/dist/file-lock-sync.js +233 -0
  67. package/dist/file-lock.d.ts +2 -0
  68. package/dist/file-lock.d.ts.map +1 -1
  69. package/dist/file-lock.js +1 -0
  70. package/dist/filename.d.ts.map +1 -1
  71. package/dist/filename.js +4 -1
  72. package/dist/guarded-mkdir.d.ts +7 -1
  73. package/dist/guarded-mkdir.d.ts.map +1 -1
  74. package/dist/guarded-mkdir.js +45 -3
  75. package/dist/index.d.ts +4 -3
  76. package/dist/index.d.ts.map +1 -1
  77. package/dist/index.js +1 -1
  78. package/dist/json-document-store.d.ts.map +1 -1
  79. package/dist/json-document-store.js +30 -15
  80. package/dist/move-path.d.ts.map +1 -1
  81. package/dist/move-path.js +101 -17
  82. package/dist/native/darwin-arm64/fs-safe-native.node +0 -0
  83. package/dist/native/darwin-x64/fs-safe-native.node +0 -0
  84. package/dist/native/linux-arm64-gnu/fs-safe-native.node +0 -0
  85. package/dist/native/linux-arm64-musl/fs-safe-native.node +0 -0
  86. package/dist/native/linux-x64-gnu/fs-safe-native.node +0 -0
  87. package/dist/native/linux-x64-musl/fs-safe-native.node +0 -0
  88. package/dist/native/win32-x64-msvc/fs-safe-native.node +0 -0
  89. package/dist/native-binding.d.ts +79 -0
  90. package/dist/native-binding.d.ts.map +1 -0
  91. package/dist/native-binding.js +1 -0
  92. package/dist/native-config.d.ts +17 -0
  93. package/dist/native-config.d.ts.map +1 -0
  94. package/dist/native-config.js +71 -0
  95. package/dist/native-operations.d.ts +21 -0
  96. package/dist/native-operations.d.ts.map +1 -0
  97. package/dist/native-operations.js +115 -0
  98. package/dist/native-pinned-write.d.ts +5 -0
  99. package/dist/native-pinned-write.d.ts.map +1 -0
  100. package/dist/native-pinned-write.js +115 -0
  101. package/dist/native.d.ts +14 -0
  102. package/dist/native.d.ts.map +1 -0
  103. package/dist/native.js +204 -0
  104. package/dist/output-sibling.d.ts +8 -0
  105. package/dist/output-sibling.d.ts.map +1 -0
  106. package/dist/output-sibling.js +122 -0
  107. package/dist/output.d.ts +2 -0
  108. package/dist/output.d.ts.map +1 -1
  109. package/dist/output.js +32 -5
  110. package/dist/owner-dacl.d.ts +31 -0
  111. package/dist/owner-dacl.d.ts.map +1 -0
  112. package/dist/owner-dacl.js +27 -0
  113. package/dist/path-policy.js +1 -1
  114. package/dist/permissions-public.d.ts +2 -0
  115. package/dist/permissions-public.d.ts.map +1 -1
  116. package/dist/permissions-public.js +2 -0
  117. package/dist/permissions.d.ts.map +1 -1
  118. package/dist/permissions.js +6 -0
  119. package/dist/pinned-operation.d.ts +2 -0
  120. package/dist/pinned-operation.d.ts.map +1 -0
  121. package/dist/pinned-operation.js +35 -0
  122. package/dist/pinned-write.d.ts +3 -16
  123. package/dist/pinned-write.d.ts.map +1 -1
  124. package/dist/pinned-write.js +20 -85
  125. package/dist/private-directory.d.ts +5 -0
  126. package/dist/private-directory.d.ts.map +1 -0
  127. package/dist/private-directory.js +13 -0
  128. package/dist/private-temp-workspace.d.ts +6 -2
  129. package/dist/private-temp-workspace.d.ts.map +1 -1
  130. package/dist/private-temp-workspace.js +40 -13
  131. package/dist/publish-file-failure.d.ts +27 -0
  132. package/dist/publish-file-failure.d.ts.map +1 -0
  133. package/dist/publish-file-failure.js +21 -0
  134. package/dist/publish-file.d.ts +21 -0
  135. package/dist/publish-file.d.ts.map +1 -0
  136. package/dist/publish-file.js +351 -0
  137. package/dist/read-opened-file.d.ts +3 -0
  138. package/dist/read-opened-file.d.ts.map +1 -1
  139. package/dist/read-opened-file.js +1 -0
  140. package/dist/replace-file-copy-fallback.d.ts +34 -0
  141. package/dist/replace-file-copy-fallback.d.ts.map +1 -0
  142. package/dist/replace-file-copy-fallback.js +339 -0
  143. package/dist/replace-file.d.ts +6 -2
  144. package/dist/replace-file.d.ts.map +1 -1
  145. package/dist/replace-file.js +39 -90
  146. package/dist/root-context.d.ts +4 -1
  147. package/dist/root-context.d.ts.map +1 -1
  148. package/dist/root-context.js +20 -1
  149. package/dist/root-impl.d.ts +5 -0
  150. package/dist/root-impl.d.ts.map +1 -1
  151. package/dist/root-impl.js +128 -225
  152. package/dist/root-path-existing.d.ts +3 -0
  153. package/dist/root-path-existing.d.ts.map +1 -0
  154. package/dist/root-path-existing.js +69 -0
  155. package/dist/root-path.d.ts +1 -2
  156. package/dist/root-path.d.ts.map +1 -1
  157. package/dist/root-path.js +67 -81
  158. package/dist/root-walk.d.ts +41 -0
  159. package/dist/root-walk.d.ts.map +1 -0
  160. package/dist/root-walk.js +114 -0
  161. package/dist/root.d.ts +2 -0
  162. package/dist/root.d.ts.map +1 -1
  163. package/dist/secret-file.d.ts +5 -2
  164. package/dist/secret-file.d.ts.map +1 -1
  165. package/dist/secret-file.js +20 -2
  166. package/dist/secret-read-async.d.ts +4 -0
  167. package/dist/secret-read-async.d.ts.map +1 -0
  168. package/dist/secret-read-async.js +95 -0
  169. package/dist/secret.d.ts +2 -1
  170. package/dist/secret.d.ts.map +1 -1
  171. package/dist/secret.js +2 -1
  172. package/dist/sibling-temp.d.ts.map +1 -1
  173. package/dist/sibling-temp.js +1 -0
  174. package/dist/sidecar-lock-handle.d.ts +20 -0
  175. package/dist/sidecar-lock-handle.d.ts.map +1 -0
  176. package/dist/sidecar-lock-handle.js +28 -0
  177. package/dist/sidecar-lock-policy.d.ts +10 -0
  178. package/dist/sidecar-lock-policy.d.ts.map +1 -0
  179. package/dist/sidecar-lock-policy.js +29 -0
  180. package/dist/sidecar-lock-reclaim.d.ts +22 -6
  181. package/dist/sidecar-lock-reclaim.d.ts.map +1 -1
  182. package/dist/sidecar-lock-reclaim.js +106 -20
  183. package/dist/sidecar-lock-types.d.ts +56 -0
  184. package/dist/sidecar-lock-types.d.ts.map +1 -0
  185. package/dist/sidecar-lock-types.js +1 -0
  186. package/dist/sidecar-lock.d.ts +2 -45
  187. package/dist/sidecar-lock.d.ts.map +1 -1
  188. package/dist/sidecar-lock.js +94 -55
  189. package/dist/temp-cleanup.d.ts +7 -1
  190. package/dist/temp-cleanup.d.ts.map +1 -1
  191. package/dist/temp-cleanup.js +35 -5
  192. package/dist/temp.d.ts +2 -1
  193. package/dist/temp.d.ts.map +1 -1
  194. package/dist/test-hooks.d.ts +3 -0
  195. package/dist/test-hooks.d.ts.map +1 -1
  196. package/dist/windows-permissions-native.d.ts +9 -0
  197. package/dist/windows-permissions-native.d.ts.map +1 -0
  198. package/dist/windows-permissions-native.js +31 -0
  199. package/docs/archive.md +131 -13
  200. package/docs/atomic.md +30 -1
  201. package/docs/config.md +51 -21
  202. package/docs/durability.md +211 -1
  203. package/docs/errors.md +36 -6
  204. package/docs/filename.md +0 -0
  205. package/docs/index.md +11 -8
  206. package/docs/install.md +27 -26
  207. package/docs/json-store.md +18 -3
  208. package/docs/migrating-to-0.5.md +196 -0
  209. package/docs/native-helper.md +85 -0
  210. package/docs/native.md +134 -0
  211. package/docs/output.md +37 -11
  212. package/docs/path.md +1 -1
  213. package/docs/permissions.md +84 -1
  214. package/docs/quickstart.md +5 -2
  215. package/docs/reading.md +4 -4
  216. package/docs/root.md +31 -15
  217. package/docs/secret-file.md +50 -1
  218. package/docs/security-model.md +29 -8
  219. package/docs/sidecar-lock.md +92 -7
  220. package/docs/temp.md +24 -1
  221. package/docs/test-hooks.md +22 -2
  222. package/docs/testing.md +18 -7
  223. package/docs/types.md +3 -1
  224. package/docs/walk.md +55 -0
  225. package/docs/writing.md +3 -3
  226. package/package.json +8 -2
  227. package/dist/pinned-helper.d.ts +0 -9
  228. package/dist/pinned-helper.d.ts.map +0 -1
  229. package/dist/pinned-helper.js +0 -19
  230. package/dist/pinned-path.d.ts +0 -7
  231. package/dist/pinned-path.d.ts.map +0 -1
  232. package/dist/pinned-path.js +0 -21
  233. package/dist/pinned-python-config.d.ts +0 -9
  234. package/dist/pinned-python-config.d.ts.map +0 -1
  235. package/dist/pinned-python-config.js +0 -38
  236. package/dist/pinned-python.d.ts +0 -12
  237. package/dist/pinned-python.d.ts.map +0 -1
  238. package/dist/pinned-python.js +0 -688
  239. package/docs/python-helper.md +0 -103
@@ -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
- allowReentrant?: boolean; // if this process already holds it, increment a count instead of failing
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 2)
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
- const handle = await acquireFileLock(targetPath, {
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
- payload: () => ({ pid: process.pid }),
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
- await doExclusiveWork();
187
+ if (!handle.verifyStillHeld()) throw new Error("migration lock was replaced");
188
+ migrateSchemaSynchronously();
109
189
  } finally {
110
- await handle.release();
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 (relevant for the reentrant case). A `true` result marks the observed sidecar as stale; `staleRecovery` then decides whether acquisition fails closed or attempts caller-approved removal.
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<void>;
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
 
@@ -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
- Each hook may be sync or async; async hooks are awaited.
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 a small set of test-only injection points. They are inert in production: the hooks only activate when `process.env.NODE_ENV === "test"`. Outside test mode, calls to set hooks are no-ops, so leaking a test setup line into production is safe but ineffective.
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 Node-only behavior without uninstalling Python from your runners.
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 Node-only fallback behavior
93
+ ## Example: force guarded JavaScript fallback behavior
85
94
 
86
95
  ```ts
87
- import { configureFsSafePython } from "@openclaw/fs-safe/config";
96
+ import { configureFsSafeNative } from "@openclaw/fs-safe/config";
88
97
 
89
98
  beforeEach(() => {
90
- configureFsSafePython({ mode: "off" });
99
+ configureFsSafeNative({ mode: "off" });
91
100
  });
92
101
 
93
102
  afterEach(() => {
94
- configureFsSafePython({ mode: "auto", pythonPath: undefined });
103
+ configureFsSafeNative({ mode: "auto" });
95
104
  });
96
105
 
97
- it("runs without the Python helper", async () => {
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. Open the parent directory by fd. Subsequent rename/unlink uses the parent fd, not the path string, so a parent-directory symlink swap mid-call cannot divert the write.
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 bypasses the stricter fd-relative Python helper because that helper requires rename to preserve inode identity. The lock is released before the call returns.
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 the Python helper's 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.
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.4.7",
3
+ "version": "0.5.0",
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,9 +145,10 @@
140
145
  },
141
146
  "optionalDependencies": {
142
147
  "jszip": "^3.10.1",
143
- "tar": "7.5.20"
148
+ "tar": "7.5.21"
144
149
  },
145
150
  "devDependencies": {
151
+ "@napi-rs/cli": "3.7.4",
146
152
  "@types/node": "^26.1.1",
147
153
  "@vitest/coverage-v8": "4.1.10",
148
154
  "typescript": "^7.0.2",
@@ -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"}
@@ -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
- }
@@ -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"}
@@ -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
- }
@@ -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"}