@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.
Files changed (247) hide show
  1. package/CHANGELOG.md +58 -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/device-path.d.ts.map +1 -1
  56. package/dist/device-path.js +24 -2
  57. package/dist/durability.d.ts +2 -0
  58. package/dist/durability.d.ts.map +1 -1
  59. package/dist/durability.js +2 -0
  60. package/dist/errors.d.ts +4 -1
  61. package/dist/errors.d.ts.map +1 -1
  62. package/dist/errors.js +2 -0
  63. package/dist/file-hash.d.ts +10 -0
  64. package/dist/file-hash.d.ts.map +1 -0
  65. package/dist/file-hash.js +73 -0
  66. package/dist/file-lock-sync.d.ts +35 -0
  67. package/dist/file-lock-sync.d.ts.map +1 -0
  68. package/dist/file-lock-sync.js +233 -0
  69. package/dist/file-lock.d.ts +2 -0
  70. package/dist/file-lock.d.ts.map +1 -1
  71. package/dist/file-lock.js +1 -0
  72. package/dist/filename.d.ts.map +1 -1
  73. package/dist/filename.js +4 -1
  74. package/dist/guarded-mkdir.d.ts +7 -1
  75. package/dist/guarded-mkdir.d.ts.map +1 -1
  76. package/dist/guarded-mkdir.js +45 -3
  77. package/dist/index.d.ts +4 -3
  78. package/dist/index.d.ts.map +1 -1
  79. package/dist/index.js +1 -1
  80. package/dist/json-document-store.d.ts.map +1 -1
  81. package/dist/json-document-store.js +30 -15
  82. package/dist/move-path.d.ts.map +1 -1
  83. package/dist/move-path.js +101 -17
  84. package/dist/native/darwin-arm64/fs-safe-native.node +0 -0
  85. package/dist/native/darwin-x64/fs-safe-native.node +0 -0
  86. package/dist/native/linux-arm64-gnu/fs-safe-native.node +0 -0
  87. package/dist/native/linux-arm64-musl/fs-safe-native.node +0 -0
  88. package/dist/native/linux-x64-gnu/fs-safe-native.node +0 -0
  89. package/dist/native/linux-x64-musl/fs-safe-native.node +0 -0
  90. package/dist/native/win32-x64-msvc/fs-safe-native.node +0 -0
  91. package/dist/native-binding.d.ts +79 -0
  92. package/dist/native-binding.d.ts.map +1 -0
  93. package/dist/native-binding.js +1 -0
  94. package/dist/native-config.d.ts +17 -0
  95. package/dist/native-config.d.ts.map +1 -0
  96. package/dist/native-config.js +71 -0
  97. package/dist/native-operations.d.ts +21 -0
  98. package/dist/native-operations.d.ts.map +1 -0
  99. package/dist/native-operations.js +115 -0
  100. package/dist/native-pinned-write.d.ts +5 -0
  101. package/dist/native-pinned-write.d.ts.map +1 -0
  102. package/dist/native-pinned-write.js +115 -0
  103. package/dist/native.d.ts +14 -0
  104. package/dist/native.d.ts.map +1 -0
  105. package/dist/native.js +204 -0
  106. package/dist/output-sibling.d.ts +8 -0
  107. package/dist/output-sibling.d.ts.map +1 -0
  108. package/dist/output-sibling.js +122 -0
  109. package/dist/output.d.ts +2 -0
  110. package/dist/output.d.ts.map +1 -1
  111. package/dist/output.js +32 -5
  112. package/dist/owner-dacl.d.ts +31 -0
  113. package/dist/owner-dacl.d.ts.map +1 -0
  114. package/dist/owner-dacl.js +27 -0
  115. package/dist/path-policy.js +1 -1
  116. package/dist/path.d.ts.map +1 -1
  117. package/dist/path.js +3 -2
  118. package/dist/permissions-public.d.ts +2 -0
  119. package/dist/permissions-public.d.ts.map +1 -1
  120. package/dist/permissions-public.js +2 -0
  121. package/dist/permissions.d.ts.map +1 -1
  122. package/dist/permissions.js +6 -0
  123. package/dist/pinned-operation.d.ts +2 -0
  124. package/dist/pinned-operation.d.ts.map +1 -0
  125. package/dist/pinned-operation.js +35 -0
  126. package/dist/pinned-write.d.ts +3 -16
  127. package/dist/pinned-write.d.ts.map +1 -1
  128. package/dist/pinned-write.js +20 -85
  129. package/dist/private-directory.d.ts +5 -0
  130. package/dist/private-directory.d.ts.map +1 -0
  131. package/dist/private-directory.js +13 -0
  132. package/dist/private-temp-workspace.d.ts +6 -2
  133. package/dist/private-temp-workspace.d.ts.map +1 -1
  134. package/dist/private-temp-workspace.js +40 -13
  135. package/dist/publish-file-failure.d.ts +27 -0
  136. package/dist/publish-file-failure.d.ts.map +1 -0
  137. package/dist/publish-file-failure.js +21 -0
  138. package/dist/publish-file.d.ts +21 -0
  139. package/dist/publish-file.d.ts.map +1 -0
  140. package/dist/publish-file.js +351 -0
  141. package/dist/read-opened-file.d.ts +3 -0
  142. package/dist/read-opened-file.d.ts.map +1 -1
  143. package/dist/read-opened-file.js +1 -0
  144. package/dist/replace-file-copy-fallback.d.ts +34 -0
  145. package/dist/replace-file-copy-fallback.d.ts.map +1 -0
  146. package/dist/replace-file-copy-fallback.js +339 -0
  147. package/dist/replace-file.d.ts +6 -2
  148. package/dist/replace-file.d.ts.map +1 -1
  149. package/dist/replace-file.js +39 -90
  150. package/dist/root-context.d.ts +4 -1
  151. package/dist/root-context.d.ts.map +1 -1
  152. package/dist/root-context.js +20 -1
  153. package/dist/root-impl.d.ts +5 -0
  154. package/dist/root-impl.d.ts.map +1 -1
  155. package/dist/root-impl.js +128 -225
  156. package/dist/root-path-existing.d.ts +3 -0
  157. package/dist/root-path-existing.d.ts.map +1 -0
  158. package/dist/root-path-existing.js +69 -0
  159. package/dist/root-path.d.ts +1 -2
  160. package/dist/root-path.d.ts.map +1 -1
  161. package/dist/root-path.js +67 -81
  162. package/dist/root-walk.d.ts +41 -0
  163. package/dist/root-walk.d.ts.map +1 -0
  164. package/dist/root-walk.js +114 -0
  165. package/dist/root.d.ts +2 -0
  166. package/dist/root.d.ts.map +1 -1
  167. package/dist/safe-path-segment.d.ts.map +1 -1
  168. package/dist/safe-path-segment.js +16 -4
  169. package/dist/secret-file.d.ts +5 -2
  170. package/dist/secret-file.d.ts.map +1 -1
  171. package/dist/secret-file.js +20 -2
  172. package/dist/secret-read-async.d.ts +4 -0
  173. package/dist/secret-read-async.d.ts.map +1 -0
  174. package/dist/secret-read-async.js +95 -0
  175. package/dist/secret.d.ts +2 -1
  176. package/dist/secret.d.ts.map +1 -1
  177. package/dist/secret.js +2 -1
  178. package/dist/sibling-temp.d.ts.map +1 -1
  179. package/dist/sibling-temp.js +1 -0
  180. package/dist/sidecar-lock-handle.d.ts +20 -0
  181. package/dist/sidecar-lock-handle.d.ts.map +1 -0
  182. package/dist/sidecar-lock-handle.js +28 -0
  183. package/dist/sidecar-lock-policy.d.ts +10 -0
  184. package/dist/sidecar-lock-policy.d.ts.map +1 -0
  185. package/dist/sidecar-lock-policy.js +29 -0
  186. package/dist/sidecar-lock-reclaim.d.ts +22 -6
  187. package/dist/sidecar-lock-reclaim.d.ts.map +1 -1
  188. package/dist/sidecar-lock-reclaim.js +106 -20
  189. package/dist/sidecar-lock-types.d.ts +56 -0
  190. package/dist/sidecar-lock-types.d.ts.map +1 -0
  191. package/dist/sidecar-lock-types.js +1 -0
  192. package/dist/sidecar-lock.d.ts +2 -45
  193. package/dist/sidecar-lock.d.ts.map +1 -1
  194. package/dist/sidecar-lock.js +94 -55
  195. package/dist/temp-cleanup.d.ts +7 -1
  196. package/dist/temp-cleanup.d.ts.map +1 -1
  197. package/dist/temp-cleanup.js +35 -5
  198. package/dist/temp-target.d.ts.map +1 -1
  199. package/dist/temp-target.js +51 -3
  200. package/dist/temp.d.ts +2 -1
  201. package/dist/temp.d.ts.map +1 -1
  202. package/dist/test-hooks.d.ts +3 -0
  203. package/dist/test-hooks.d.ts.map +1 -1
  204. package/dist/windows-permissions-native.d.ts +9 -0
  205. package/dist/windows-permissions-native.d.ts.map +1 -0
  206. package/dist/windows-permissions-native.js +31 -0
  207. package/docs/archive.md +131 -13
  208. package/docs/atomic.md +30 -1
  209. package/docs/config.md +51 -21
  210. package/docs/durability.md +211 -1
  211. package/docs/errors.md +36 -6
  212. package/docs/filename.md +0 -0
  213. package/docs/index.md +11 -8
  214. package/docs/install.md +27 -26
  215. package/docs/json-store.md +18 -3
  216. package/docs/migrating-to-0.5.md +196 -0
  217. package/docs/native-helper.md +85 -0
  218. package/docs/native.md +134 -0
  219. package/docs/output.md +37 -11
  220. package/docs/path.md +1 -1
  221. package/docs/permissions.md +84 -1
  222. package/docs/quickstart.md +5 -2
  223. package/docs/reading.md +4 -4
  224. package/docs/root.md +31 -15
  225. package/docs/secret-file.md +50 -1
  226. package/docs/security-model.md +29 -8
  227. package/docs/sidecar-lock.md +92 -7
  228. package/docs/temp.md +24 -1
  229. package/docs/test-hooks.md +22 -2
  230. package/docs/testing.md +18 -7
  231. package/docs/types.md +3 -1
  232. package/docs/walk.md +55 -0
  233. package/docs/writing.md +3 -3
  234. package/package.json +10 -4
  235. package/dist/pinned-helper.d.ts +0 -9
  236. package/dist/pinned-helper.d.ts.map +0 -1
  237. package/dist/pinned-helper.js +0 -19
  238. package/dist/pinned-path.d.ts +0 -7
  239. package/dist/pinned-path.d.ts.map +0 -1
  240. package/dist/pinned-path.js +0 -21
  241. package/dist/pinned-python-config.d.ts +0 -9
  242. package/dist/pinned-python-config.d.ts.map +0 -1
  243. package/dist/pinned-python-config.js +0 -38
  244. package/dist/pinned-python.d.ts +0 -12
  245. package/dist/pinned-python.d.ts.map +0 -1
  246. package/dist/pinned-python.js +0 -688
  247. package/docs/python-helper.md +0 -103
package/docs/errors.md CHANGED
@@ -13,13 +13,41 @@ class FsSafeError extends Error {
13
13
  readonly name: "FsSafeError";
14
14
  readonly code: FsSafeErrorCode;
15
15
  readonly category: "policy" | "operational";
16
+ readonly details?: Readonly<Record<string, unknown>>;
16
17
 
17
- constructor(code: FsSafeErrorCode, message: string, options?: { cause?: unknown });
18
+ constructor(
19
+ code: FsSafeErrorCode,
20
+ message: string,
21
+ options?: { cause?: unknown; details?: Readonly<Record<string, unknown>> },
22
+ );
18
23
  }
19
24
  ```
20
25
 
21
26
  `cause` is available through the standard `Error` `cause` property when the failure was triggered by a `NodeJS.ErrnoException` (e.g. a wrapped `EACCES`). Inspect it for the original `code` / `errno` / `syscall` if you need finer-grained reporting.
22
27
 
28
+ `details` is an operation-specific receipt, not an alternate error code. For
29
+ example, `publishFileExclusive()` uses it to report the failing phase, created
30
+ target identity, cleanup decision, and failed directory-sync outcome. Narrow
31
+ by `code` and a documented details field before consuming it; do not assume all
32
+ `FsSafeError` instances carry the same keys.
33
+
34
+ `replaceFileAtomic({ copyFallbackRestore: "restore-original" })` reports a
35
+ failed copy fallback with the exported `ReplaceFileAtomicRestoreFailureDetails`
36
+ shape:
37
+
38
+ ```ts
39
+ type ReplaceFileAtomicRestoreFailureDetails = {
40
+ cleanup: "restored" | "restore-failed";
41
+ };
42
+ ```
43
+
44
+ Both outcomes use `code: "helper-failed"`. `"restored"` means the original
45
+ snapshot was written back and fsynced through the pinned destination handle.
46
+ `"restore-failed"` means both the replacement and recovery failed; `cause` is
47
+ an `AggregateError` containing both failures. A snapshot that exceeds
48
+ `maxRestoreBytes` fails earlier with `too-large` and does not overwrite the
49
+ destination.
50
+
23
51
  `category` separates caller-policy failures from operational failures:
24
52
 
25
53
  - `"policy"` — unsafe input or target state, such as `outside-workspace`, `symlink`, `hardlink`, or `too-large`.
@@ -46,6 +74,7 @@ type FsSafeErrorCode =
46
74
  | "path-alias"
47
75
  | "path-mismatch"
48
76
  | "permission-unverified"
77
+ | "secret-exists"
49
78
  | "symlink"
50
79
  | "timeout"
51
80
  | "too-large"
@@ -60,8 +89,8 @@ type FsSafeErrorCode =
60
89
  | `denied-path` | A root mutation matched `denyMutations.paths` or `denyMutations.prefixes`. | Caller configured application-sensitive paths that must not be written, removed, moved, or created. |
61
90
  | `device-path` | A read/open target is a known unsafe device or process-fd path. | `/dev/zero`, `/dev/random`, `/dev/stdin`, `/dev/fd/*`, `/proc/*/fd/*`, or a Windows reserved device name. |
62
91
  | `hardlink` | Read or copy with `hardlinks: "reject"` saw `nlink > 1`. | File is hardlinked — possibly an alias of an out-of-tree inode. |
63
- | `helper-failed` | Internal POSIX helper failed after startup. | Inspect `cause`; retrying may be unsafe if the operation may have partially completed. |
64
- | `helper-unavailable` | Persistent Python helper was disabled or could not be spawned. | `FS_SAFE_PYTHON_MODE=off`, Python missing in PATH, restricted sandbox. `auto` falls back where possible; `require` fails closed. |
92
+ | `helper-failed` | A native mechanism or multi-step operational helper failed. | Inspect `cause` and any operation-specific `details`; retrying may be unsafe if the operation partially completed. |
93
+ | `helper-unavailable` | Required native binding could not be loaded. | Unsupported platform, missing/incompatible bundled binary, or `FS_SAFE_NATIVE_MODE=require`. `auto` falls back where possible; `require` fails closed. |
65
94
  | `insecure-permissions` | A secure file or path permission check found a mode/ACL that allows broader access than requested. | File or directory is group/world writable/readable; Windows ACL grants broad read. |
66
95
  | `invalid-path` | Input was empty, contained NUL, was an unparseable URL, or otherwise unusable. | Caller didn't validate input; input was a network path on Windows. |
67
96
  | `not-empty` | `remove()` on a non-empty directory. | Use `replaceDirectoryAtomic` or remove children first. |
@@ -73,9 +102,10 @@ type FsSafeErrorCode =
73
102
  | `path-alias` | A path alias check failed (e.g. canonical-real-path moved out of the root). | Symlink resolution lands outside the root. |
74
103
  | `path-mismatch` | Post-open identity check failed: the opened fd does not match the resolved path. | TOCTOU — something else swapped the path between resolve and open. |
75
104
  | `permission-unverified` | A secure file check could not verify required permissions. | Windows ACL inspection failed; POSIX ownership/mode was unavailable. |
105
+ | `secret-exists` | `createSecretFileAtomic()` found an existing final path. | First-writer-wins secret creation lost a race or the credential was already initialized. |
76
106
  | `symlink` | Path component is a symlink, policy is `reject`. | Caller followed a symlink they shouldn't have, or `symlinks: "reject"` is set. |
77
107
  | `timeout` | An operation with a wall-clock budget overran. | Secure file read or timed operation exceeded `timeoutMs`. |
78
- | `too-large` | Read exceeded `maxBytes`. | Caller gave a too-permissive file or didn't size-cap correctly. |
108
+ | `too-large` | A read or bounded walk exceeded its configured budget. | Caller gave a too-permissive file or traversal limit. |
79
109
  | `unsupported-platform` | The requested operation is not supported on the current platform. | E.g. POSIX-only helper invoked on Windows. |
80
110
 
81
111
  ## Branching
@@ -139,8 +169,8 @@ A common pattern is to wrap your domain code in a single try/catch that maps bot
139
169
  A handful of helpers throw their own typed errors instead of `FsSafeError`:
140
170
 
141
171
  - `JsonFileReadError` — thrown by [`readJson`](json.md). Carries `cause` so you can distinguish missing (`ENOENT`) from invalid (`SyntaxError`).
142
- - `ArchiveLimitError` — thrown by [`extractArchive`](archive.md) when an archive size, entry count, or extracted-byte budget is exceeded. The `code` field uses `ARCHIVE_LIMIT_ERROR_CODE` constants (e.g. `"ARCHIVE_SIZE_EXCEEDS_LIMIT"`).
143
- - `ArchiveSecurityError` — thrown by extraction when an entry path violates safety rules (traversal, drive prefix, blocked link type). The `code` field uses `ArchiveSecurityErrorCode` values.
172
+ - `ArchiveLimitError` — thrown by [`extractArchive`](archive.md) when an archive size, entry count, path depth, metadata, or extracted-byte budget is exceeded. The `code` field uses the string values exposed by `ARCHIVE_LIMIT_ERROR_CODE` (for example `archive-entry-path-components-exceeds-limit`).
173
+ - `ArchiveSecurityError` — thrown by extraction when entry policy or destination safety fails. Entry codes are `entry-path`, `entry-link`, and `entry-filtered`; destination codes cover non-directory, symlink, and symlink-traversal failures.
144
174
 
145
175
  These are exported from their respective subpaths.
146
176
 
package/docs/filename.md CHANGED
Binary file
package/docs/index.md CHANGED
@@ -38,7 +38,8 @@ await fs.remove("notes/archive/today.txt");
38
38
  ## Pick your path
39
39
 
40
40
  - **First time?** [Install](install.md), then walk through the [Quickstart](quickstart.md). Five minutes from `pnpm add` to a working root.
41
- - **Designing a workspace feature.** Read the [Security model](security-model.md) before you trust the boundary, the [Python helper policy](python-helper.md) before you pick deployment defaults, and the [Errors](errors.md) reference so you know what to catch.
41
+ - **Upgrading from 0.4?** Follow [Migrating to 0.5](migrating-to-0.5.md) in order, including the archive clamp-default audit.
42
+ - **Designing a workspace feature.** Read the [Security model](security-model.md) before you trust the boundary, the [native helper policy](native-helper.md) before you pick deployment defaults, and the [Errors](errors.md) reference so you know what to catch.
42
43
  - **Replacing ad-hoc atomic writes.** Jump to [Atomic writes](atomic.md) or, for keyed JSON state, [JSON files](json.md).
43
44
  - **Extracting an upload.** Start at [Archive extraction](archive.md) — handles ZIP and TAR with traversal, link, count, and byte limits.
44
45
  - **Running an agent in a sandbox.** [Private temp workspaces](temp.md) plus [secret files](secret-file.md) cover the common scratch-and-credentials shape.
@@ -48,11 +49,12 @@ await fs.remove("notes/archive/today.txt");
48
49
 
49
50
  | Surface | Use it for |
50
51
  |---|---|
51
- | [`root()`](root.md) | One boundary for read/write/move/remove inside a trusted directory. |
52
- | [`@openclaw/fs-safe/config`](config.md) | Process-global Python helper and lock-option defaults. |
53
- | [Python helper policy](python-helper.md) | Choose `auto`, `off`, or `require` for POSIX fd-relative hardening. |
52
+ | [`root()`](root.md) | One boundary for read/write/move/remove and bounded recursive walking inside a trusted directory. |
53
+ | [`@openclaw/fs-safe/config`](config.md) | Process-global native helper and lock-option defaults. |
54
+ | [Native helper policy](native-helper.md) | Choose `auto`, `off`, or `require` for bundled native primitives. |
55
+ | [Native architecture](native.md) | Understand the thin syscall layer, beneath model, platform mechanisms, and fallback boundary. |
54
56
  | [`replaceFileAtomic`](atomic.md) | Sibling-temp + rename, fsync hooks, mode preservation, copy fallback. |
55
- | [Directory durability](durability.md) | Pinned directory identities, explicit sync outcomes, and durable nested-directory creation. |
57
+ | [`@openclaw/fs-safe/durability`](durability.md) | Pinned directory identities, durable creation, exclusive publication, streaming SHA-256, provenance receipts, and sync-failure policy. |
56
58
  | [`writeExternalFileWithinRoot`](output.md) | Stage external-library file output in private temp storage, then finalize under a root. |
57
59
  | [`writeJson` / `readJson*`](json.md) | JSON state files with strict and lenient read variants. |
58
60
  | [`@openclaw/fs-safe/store`](store.md) | Overview of `fileStore`, `fileStoreSync`, and `jsonStore`. |
@@ -61,15 +63,16 @@ await fs.remove("notes/archive/today.txt");
61
63
  | [Private file-store mode](private-file-store.md) | `fileStore({ private: true })` for private JSON/text state at 0600 under 0700 dirs. |
62
64
  | [`tempWorkspace`](temp.md) | 0700 scratch dir with auto-cleanup. |
63
65
  | [`readSecureFile`](secure-file.md) | Absolute file reads with fd pinning, permissions, owner, size, and timeout checks. |
64
- | [`walkDirectory`](walk.md) | Budget-bounded recursive directory scan with symlink policy and filters. |
65
- | [`extractArchive`](archive.md) | ZIP/TAR extraction with size, count, link, and traversal limits. |
66
+ | [`walkDirectory` / `Root.walk`](walk.md) | Standalone inventories plus root-bounded pruning, budgets, and partial-error reporting. |
67
+ | [`extractArchive`](archive.md) | Policy-driven ZIP/TAR extraction with clamp/filter, metadata/path-depth, link, count, and byte limits. |
66
68
  | [Secret files](secret-file.md) | Mode-0600 credentials with size and TOCTOU defense. |
67
- | [Permissions](permissions.md) | POSIX mode and Windows ACL inspection/remediation helpers. |
69
+ | [Permissions](permissions.md) | POSIX mode helpers plus Windows ACL inspection, raw owner/ACE facts, remediation, and private-directory creation. |
68
70
  | [`acquireFileLock`](sidecar-lock.md) | Cross-process file lock with retry and fail-closed stale-lock handling. |
69
71
  | [`FsSafeError`](errors.md) | Closed code union (with `policy` / `operational` category) you can branch on. |
70
72
  | [`pathScope()`](path-scope.md) | Lower-level absolute-path boundary helper; lives behind `@openclaw/fs-safe/advanced`. |
71
73
  | [`@openclaw/fs-safe/advanced`](advanced.md) | Directory of lower-level composition helpers (path scopes, regular-file I/O, install paths, sibling-temp writes, …). |
72
74
  | [`@openclaw/fs-safe/test-hooks`](test-hooks.md) | Test-only injection hooks for reproducing open/lstat races. |
75
+ | [Migrating to 0.5](migrating-to-0.5.md) | End-to-end checklist for Python removal and 0.4 API behavior changes. |
73
76
 
74
77
  ## Status
75
78
 
package/docs/install.md CHANGED
@@ -62,20 +62,22 @@ Use the main entry for the common surface, or the focused subpaths when you want
62
62
 
63
63
  | Subpath | Contents |
64
64
  |---|---|
65
- | `@openclaw/fs-safe` | Small common surface: `root`, root types, and errors. |
66
- | `@openclaw/fs-safe/root` | `root()`, `Root`, `RootDefaults`, related types. |
67
- | `@openclaw/fs-safe/config` | Process-global Python helper configuration. |
65
+ | `@openclaw/fs-safe` | Common root, config, output, lock, native-mode, and error exports. |
66
+ | `@openclaw/fs-safe/root` | `root()`, `Root`, `RootDefaults`, and root-walk types. |
67
+ | `@openclaw/fs-safe/config` | Process-global native helper and lock defaults. |
68
68
  | `@openclaw/fs-safe/path` | `isPathInside`, `safeRealpathSync`, `isWithinDir`, error helpers. |
69
+ | `@openclaw/fs-safe/output` | Guarded staging/finalization for libraries that require an absolute output path. |
69
70
  | `@openclaw/fs-safe/json` | `tryReadJson`, `readJson`, `readJsonIfExists`, `writeJson`, sync variants. |
70
71
  | `@openclaw/fs-safe/store` | `fileStore()`, `fileStoreSync()`, and `jsonStore<T>()`. |
71
72
  | `@openclaw/fs-safe/secret` | Secret file read/write helpers. |
72
73
  | `@openclaw/fs-safe/atomic` | `replaceFileAtomic`, `writeTextAtomic`, `replaceDirectoryAtomic`, `movePathWithCopyFallback`. |
74
+ | `@openclaw/fs-safe/durability` | Pinned directories, strict sync, durable directory creation, exclusive publication, and streaming SHA-256. |
73
75
  | `@openclaw/fs-safe/temp` | `tempWorkspace`, `withTempWorkspace`, sync variants, `resolveSecureTempRoot`. |
74
76
  | `@openclaw/fs-safe/secure-file` | `readSecureFile` for pinned absolute file reads with permissions checks. |
75
77
  | `@openclaw/fs-safe/file-lock` | `acquireFileLock`, `withFileLock`, `createFileLockManager`, and related lock types. |
76
- | `@openclaw/fs-safe/permissions` | POSIX mode and Windows ACL inspection/remediation helpers. |
78
+ | `@openclaw/fs-safe/permissions` | POSIX mode helpers, Windows ACL inspection/remediation, raw owner/ACE facts, and private-directory creation. |
77
79
  | `@openclaw/fs-safe/walk` | `walkDirectory`, `walkDirectorySync`, related types. Budget-bounded, not root-bounded. |
78
- | `@openclaw/fs-safe/archive` | `extractArchive`, `resolveArchiveKind`, limits, preflight helpers. |
80
+ | `@openclaw/fs-safe/archive` | `extractArchive`, `readArchiveEntry`, kind resolution, policy types, limits, and preflight helpers. |
79
81
  | `@openclaw/fs-safe/advanced` | Lower-level composition helpers: path scopes, root-file open, install paths, local-root readers, temp-file targets, sibling-temp writes, regular-file helpers, `pathExists`, `withTimeout`, and related advanced types. This surface is less stable than the focused public subpaths. |
80
82
  | `@openclaw/fs-safe/errors` | `FsSafeError`, `FsSafeErrorCode`. |
81
83
  | `@openclaw/fs-safe/types` | Shared types: `DirEntry`, `PathStat`, `BasePathOptions`, … |
@@ -85,40 +87,39 @@ Use the main entry for the common surface, or the focused subpaths when you want
85
87
 
86
88
  `@openclaw/fs-safe` lists `jszip` and `tar` as optional dependencies for [archive extraction](archive.md). They are loaded lazily and only required when ZIP/TAR helpers run. Installs that omit optional dependencies can still import and use every non-archive subpath; archive calls fail with a clear missing-optional-dependency message.
87
89
 
88
- There are no peer dependencies and no native build step.
90
+ There are no peer dependencies. The single npm package bundles all seven native binaries, so consumers do not run a native build, download platform code, or execute a postinstall step. Shipping every target increases the tarball size compared with per-platform packages, intentionally trading bandwidth for deterministic installation.
89
91
 
90
- ## Python helper policy
92
+ Upgrading an existing consumer? Follow [Migrating to 0.5](migrating-to-0.5.md)
93
+ before choosing a native mode or accepting the new archive clamp default.
91
94
 
92
- On POSIX, `root()` uses one persistent Python helper process for the
93
- fd-relative operations Node does not expose cleanly. The default is `auto`: use
94
- the helper when it starts, fall back to Node-only behavior when it is disabled
95
- or unavailable.
95
+ ## Native helper policy
96
+
97
+ The bundled native binaries provide fd-relative open/link/mkdir primitives,
98
+ atomic no-replace rename, and file identity checks. The default is `auto`: use
99
+ the matching binary when it loads, otherwise silently keep the guarded
100
+ JavaScript path. Platforms without one of the seven bundled targets therefore
101
+ continue through the documented fallback in `auto` mode.
96
102
 
97
103
  ```ts
98
- import { configureFsSafePython } from "@openclaw/fs-safe/config";
104
+ import { configureFsSafeNative } from "@openclaw/fs-safe/config";
99
105
 
100
- configureFsSafePython({ mode: "auto" }); // default
101
- configureFsSafePython({ mode: "off" }); // never spawn Python
102
- configureFsSafePython({ mode: "require" }); // fail closed if unavailable
106
+ configureFsSafeNative({ mode: "auto" }); // default
107
+ configureFsSafeNative({ mode: "off" }); // guarded JavaScript only
108
+ configureFsSafeNative({ mode: "require" }); // fail closed if unavailable
103
109
  ```
104
110
 
105
111
  Environment variables are read at runtime:
106
112
 
107
113
  ```bash
108
- FS_SAFE_PYTHON_MODE=off # auto | off | require
109
- FS_SAFE_PYTHON=/usr/bin/python3
114
+ FS_SAFE_NATIVE_MODE=off # auto | off | require
110
115
  ```
111
116
 
112
- OpenClaw compatibility aliases are also accepted:
113
- `OPENCLAW_FS_SAFE_PYTHON_MODE`, `OPENCLAW_FS_SAFE_PYTHON`,
114
- `OPENCLAW_PINNED_PYTHON`, and `OPENCLAW_PINNED_WRITE_PYTHON`.
117
+ `OPENCLAW_FS_SAFE_NATIVE_MODE` is also accepted.
115
118
 
116
- Disabling Python keeps the public API working, but downgrades POSIX mutation
117
- hardening from fd-relative syscalls to Node path operations guarded by lexical
118
- and canonical checks plus identity verification. Use `require` for
119
- security-sensitive deployments where that downgrade should be a startup/runtime
120
- failure instead of a fallback. The full tradeoff is documented in
121
- [Python helper policy](python-helper.md).
119
+ Disabling native loading keeps the public API working through Node path
120
+ operations guarded by lexical and canonical checks plus identity verification.
121
+ Use `require` when native-backed operations must fail instead of falling back.
122
+ The exact boundary is documented in [native helper policy](native-helper.md).
122
123
 
123
124
  ## Verify the install
124
125
 
@@ -113,7 +113,17 @@ Read, transform, write — under the lock if locking is enabled. Returns the new
113
113
  const next = await store.update((prev) => ({ count: (prev?.count ?? 0) + 1 }));
114
114
  ```
115
115
 
116
- `run` is async-friendly. The whole `read → run → write` sequence runs inside one `withLock` call, so concurrent updaters from different processes serialize cleanly.
116
+ `run` is async-friendly. The whole `read → run → write` sequence is serialized
117
+ by canonical file path inside the process. With locking enabled, the sidecar
118
+ lock is acquired inside that queue, so concurrent updaters from different
119
+ processes serialize cleanly too.
120
+
121
+ Do not call `write()`, `update()`, or `updateOr()` for the same file from inside
122
+ an update callback. That nested mutation cannot run until the outer update
123
+ finishes, so `jsonStore` rejects it immediately with
124
+ `FsSafeError("store-reentrant-update")`. Return the complete next value from the
125
+ outer callback instead. The check follows Node async context, including promise
126
+ and `queueMicrotask` boundaries.
117
127
 
118
128
  Use `update(run)` when missing state is part of your model. Use `updateOr(fallback, run)` when the missing-file case should start from a concrete value and you want to merge into defaults:
119
129
 
@@ -137,13 +147,18 @@ const counter = jsonStore<{ count: number }>({
137
147
  });
138
148
  ```
139
149
 
140
- When `lock` is falsy, `read` / `write` / `update` are unlocked. The `update` shape is still useful it gives you a single function for the read-modify-write pattern — but it offers no concurrency guarantees if other processes also write to the file.
150
+ When `lock` is falsy, writes and updates still serialize inside this process by
151
+ canonical file path, including across separate `jsonStore` handles. They offer
152
+ no concurrency guarantees if another process also writes to the file.
141
153
 
142
154
  Process-wide lock defaults from `configureFsSafeLocks()` apply only after locking is explicitly enabled. They do not make JSON stores lock by default.
143
155
 
144
156
  JSON store locks fail closed on stale sidecars by default. Opt-in `staleRecovery: "remove-if-unchanged"` requires caller approval and uses the same exclusive reclaim guard as the low-level sidecar-lock API.
145
157
 
146
- The default `managerKey` namespaces the in-process `FileLockManager` per absolute file path, so two `jsonStore` calls on the same file share lock state automatically.
158
+ The default `managerKey` namespaces the `FileLockManager` per absolute file
159
+ path. The JSON-store queue is independent of the manager key, so separate
160
+ handles and custom lock-manager namespaces still cannot overlap mutations of
161
+ the same canonical file path inside one process.
147
162
 
148
163
  ## Common patterns
149
164
 
@@ -0,0 +1,196 @@
1
+ ---
2
+ title: Migrating to 0.5
3
+ description: "Ordered checklist for moving a 0.4 consumer from the Python helper to native mode and adopting the 0.5 API contracts."
4
+ ---
5
+
6
+ # Migrating from 0.4 to 0.5
7
+
8
+ Use this checklist from top to bottom. Version 0.5 replaces the Python worker,
9
+ changes the default archive mode policy, and adds explicit contracts for
10
+ publication, walking, locks, secrets, and native-only features. Nothing in this
11
+ guide requires a Rust toolchain: all supported native binaries are prebuilt and
12
+ bundled in `@openclaw/fs-safe`.
13
+
14
+ ## 1. Update the package and runtime
15
+
16
+ - Run on Node.js 22 or newer.
17
+ - Update `@openclaw/fs-safe` and regenerate every lock or shrinkwrap file your
18
+ deployment consumes.
19
+ - Keep optional dependencies enabled if you want JavaScript ZIP/TAR support.
20
+ Native loading no longer depends on optional packages because all seven
21
+ binaries ship in `@openclaw/fs-safe`. An install that omits optional packages
22
+ can still import fs-safe, but missing JS archive decoders fail with actionable
23
+ errors.
24
+
25
+ If you call `resolveRootPath()` or `resolveRootPathSync()` directly, upgrade to
26
+ 0.5: versions through 0.4.7 could approve an in-root symlink traversal that
27
+ resolved outside the root. `root()` handles were not affected. See the
28
+ [affected versions and exposure](security-model.md#affected-versions-exposure).
29
+
30
+ ## 2. Replace Python helper configuration
31
+
32
+ Change startup configuration before the first filesystem operation:
33
+
34
+ ```ts
35
+ import { configureFsSafeNative } from "@openclaw/fs-safe/config";
36
+
37
+ configureFsSafeNative({ mode: "auto" });
38
+ ```
39
+
40
+ | Remove from 0.4 | Use in 0.5 |
41
+ |---|---|
42
+ | `configureFsSafePython({ mode })` | `configureFsSafeNative({ mode })` |
43
+ | `FS_SAFE_PYTHON_MODE` | `FS_SAFE_NATIVE_MODE` |
44
+ | `OPENCLAW_FS_SAFE_PYTHON_MODE` | `OPENCLAW_FS_SAFE_NATIVE_MODE` |
45
+ | `pythonPath`, `FS_SAFE_PYTHON`, pinned-Python aliases | Nothing; bundled native binaries do not use an interpreter |
46
+
47
+ The old names warn once and map `auto`, `off`, or `require` so a shipped 0.4
48
+ deployment does not silently change policy. Interpreter paths are ignored and
49
+ Python is never spawned. Treat that warning as an upgrade diagnostic, not as a
50
+ second supported helper path.
51
+
52
+ Choose the production mode deliberately:
53
+
54
+ - `auto` keeps guarded JavaScript fallbacks when a binding is unavailable.
55
+ - `off` makes fallback testing deterministic.
56
+ - `require` fails with `helper-unavailable` instead of weakening an operation
57
+ that expected native support.
58
+
59
+ See [Native helper policy](native-helper.md) and
60
+ [Native architecture](native.md).
61
+
62
+ ## 3. Audit every archive call
63
+
64
+ The 0.5 default is `entryModes: "clamp"`. Directories become `0o755`; files
65
+ become `0o644` or `0o755` when owner-execute was archived. Set
66
+ `entryModes: "preserve"` explicitly only if your 0.4 consumer intentionally
67
+ relied on archived rwx bits. Setuid, setgid, sticky bits, and archived ownership
68
+ are never restored.
69
+
70
+ ```ts
71
+ await extractArchive({
72
+ archivePath: uploadPath,
73
+ destDir: restoreRoot,
74
+ timeoutMs: 30_000,
75
+ entryModes: "clamp",
76
+ entryFilter: (entry) =>
77
+ entry.path.startsWith("snapshot/cache/") ? "skip" : "extract",
78
+ onFiltered: "skip-entry",
79
+ limits: {
80
+ maxArchiveBytes: 256 * 1024 * 1024,
81
+ maxEntries: 50_000,
82
+ maxExtractedBytes: 512 * 1024 * 1024,
83
+ maxEntryBytes: 256 * 1024 * 1024,
84
+ maxMetaEntryBytes: 1024 * 1024,
85
+ maxEntryPathComponents: 64,
86
+ },
87
+ });
88
+ ```
89
+
90
+ Returning `"skip"` rejects the archive unless `onFiltered: "skip-entry"` is
91
+ explicit. Zstd and bzip2 TAR are native-only; ZIP, TAR, and gzip retain guarded
92
+ JavaScript implementations. Catch `ArchiveLimitError` by its code, including
93
+ `archive-entry-path-components-exceeds-limit` for deep implicit-directory
94
+ attacks. See [Archive extraction](archive.md).
95
+
96
+ ## 4. Pick a publication failure policy
97
+
98
+ `publishFileExclusive()` never replaces an existing target. Choose a strategy
99
+ and decide what a post-create directory-sync failure means to your application:
100
+
101
+ ```ts
102
+ await publishFileExclusive({
103
+ sourcePath: stagedArchive,
104
+ targetPath: finalArchive,
105
+ strategy: "link-or-copy",
106
+ onSyncFailure: "preserve",
107
+ });
108
+ ```
109
+
110
+ `rollback` is the default: an unchanged target created by this call is removed
111
+ when directory sync throws. `preserve` keeps a complete but possibly
112
+ non-durable target and reports `cleanup: "preserved"` plus
113
+ `directorySync: { status: "failed", code? }` in the typed error. Backup
114
+ archives commonly need `preserve`; transactional protocols that expose only
115
+ durably committed names usually want `rollback`. See
116
+ [Directory durability](durability.md).
117
+
118
+ ## 5. Replace recursive scans with an explicit walk policy
119
+
120
+ Use `Root.walk()` for caller-controlled relative paths. Every examined entry
121
+ consumes the budget even when filtered:
122
+
123
+ ```ts
124
+ for await (const entry of workspace.walk("memory", {
125
+ maxDepth: 12,
126
+ maxEntries: 50_000,
127
+ symlinkPolicy: "skip",
128
+ entryFilter: (entry) =>
129
+ entry.kind === "directory" && entry.relativePath.endsWith("/.git")
130
+ ? "skip-subtree"
131
+ : "include",
132
+ onDirectoryError: "skip-and-report",
133
+ })) {
134
+ if (entry.kind === "directory-error") {
135
+ reportIncompleteSubtree(entry.relativePath, entry.error);
136
+ continue;
137
+ }
138
+ indexEntry(entry);
139
+ }
140
+ ```
141
+
142
+ The default directory-error policy remains `throw`. See
143
+ [Directory walking](walk.md).
144
+
145
+ ## 6. Adopt the focused concurrency and secret APIs
146
+
147
+ - Use `acquireFileLockSync()` only in synchronous boot or migration code; retry
148
+ waits block the thread. Request-serving paths should use `withFileLock()`.
149
+ - Remove the `allowReentrant` boolean from async file-lock options. If a logical
150
+ holder intentionally nests acquisition, pass the same operation-scoped
151
+ `reentrantOwner` string to each acquisition; different or missing owners
152
+ contend normally. Never replace the boolean with a process-wide constant.
153
+ Locked and unlocked `jsonStore` mutations serialize by canonical file path
154
+ and do not opt into lock reentrancy; nested same-file mutations from an update
155
+ callback fail with `store-reentrant-update`, so return the complete value from
156
+ the outer callback instead.
157
+ - Use `createSecretFileAtomic()` for first-writer-wins credentials and catch
158
+ `secret-exists`; use `writeSecretFileAtomic()` only when replacement is the
159
+ intended protocol.
160
+ - Async `readSecretFile()` is strict; `tryReadSecretFile()` returns `undefined`
161
+ only for missing or blank content and still rejects suspicious files.
162
+ - Check `tempWorkspace.cleanup()` results when ownership matters;
163
+ `identity-mismatch` deliberately preserves a replacement path.
164
+
165
+ See [File locks](sidecar-lock.md), [Secret files](secret-file.md), and
166
+ [Temp workspaces](temp.md).
167
+
168
+ ## 7. Gate native-only features
169
+
170
+ `createPrivateDirectory()` is Windows-only and native-only because a pathname
171
+ fallback cannot promise the same creation-time DACL. Zstd/bzip2 extraction and
172
+ `strategy: "rename-noreplace"` are also native-only. Test the unavailable path
173
+ instead of assuming installation always succeeds.
174
+
175
+ ## 8. Run both behavior families in CI
176
+
177
+ For each consumer workflow that matters:
178
+
179
+ 1. Run once with `FS_SAFE_NATIVE_MODE=auto` on every supported OS.
180
+ 2. Run once with `FS_SAFE_NATIVE_MODE=off` to prove the JavaScript fallback.
181
+ 3. Run native-required or native-only cases with `FS_SAFE_NATIVE_MODE=require`.
182
+ 4. Exercise archive traversal/link/depth limits, publication sync failure, and
183
+ partial-walk reporting with production-shaped fixtures.
184
+
185
+ For downstream staging and backup consumers:
186
+
187
+ - [ ] Replace private whole-file hashing with `sha256File(path | FileHandle)`
188
+ from `durability`; native mode keeps digest work off the event loop and
189
+ the JavaScript fallback remains streaming.
190
+ - [ ] If Windows trust policy depends on exact principals, consume
191
+ `readOwnerAndDacl()` from `permissions`, reject incomplete/null/remote
192
+ descriptors as your policy requires, skip inherit-only ACEs where
193
+ appropriate, and apply the application's own SID allowlist.
194
+
195
+ The [Testing](testing.md) page documents the test hooks and mode setup used by
196
+ fs-safe itself.
@@ -0,0 +1,85 @@
1
+ ---
2
+ title: Native helper policy
3
+ description: "How fs-safe loads its bundled native filesystem primitives and how auto, require, and off affect guarded fallbacks."
4
+ ---
5
+
6
+ # Native helper policy
7
+
8
+ `@openclaw/fs-safe` itself contains seven prebuilt binaries for Linux x64/arm64 (glibc or musl), macOS x64/arm64, and Windows x64. The loader selects `dist/native/<target>/fs-safe-native.node` without optional platform packages, downloads, postinstall scripts, or a consumer Rust build. Carrying every target makes the npm tarball larger, but every installation receives the same complete artifact.
9
+
10
+ ```ts
11
+ import { configureFsSafeNative } from "@openclaw/fs-safe/config";
12
+
13
+ configureFsSafeNative({ mode: "auto" }); // default
14
+ configureFsSafeNative({ mode: "off" }); // guarded JavaScript only
15
+ configureFsSafeNative({ mode: "require" }); // fail closed when the binding is unavailable
16
+ ```
17
+
18
+ The equivalent environment variables are `FS_SAFE_NATIVE_MODE` and `OPENCLAW_FS_SAFE_NATIVE_MODE`. Accepted values are `auto`, `off`, `require`, `true`, `false`, `on`, `never`, `required`, `1`, and `0`.
19
+
20
+ ## Modes
21
+
22
+ | Mode | Behavior |
23
+ |---|---|
24
+ | `auto` | Prefer native primitives when the current bundled binary loads; otherwise silently use the guarded JavaScript path. |
25
+ | `off` | Do not load a bundled binary. Use the guarded JavaScript path deterministically. |
26
+ | `require` | Throw `FsSafeError("helper-unavailable")` instead of falling back when an operation needs the native binding and it cannot load. |
27
+
28
+ Configure the mode once during startup. Loading is lazy and cached; changing from `auto` to `require` after a failed load changes failure policy but does not repeatedly probe the binary.
29
+
30
+ ## Native boundary
31
+
32
+ The bundled native layer exposes policy-free filesystem mechanisms: beneath-root
33
+ open/mkdir/link, no-replace rename, identity reads, archive decode/execution,
34
+ clone/copy/hash workers, and Windows security descriptor calls. The TypeScript
35
+ layer owns policy, retries, filters, budgets, modes, cleanup, error
36
+ normalization, and the decision to fall back.
37
+
38
+ - Linux uses `openat2` with `RESOLVE_BENEATH | RESOLVE_NO_MAGICLINKS` and `renameat2(RENAME_NOREPLACE)`.
39
+ - macOS 15.4 and newer prefer `O_RESOLVE_BENEATH`; older kernels resolve components with `O_NOFOLLOW` and restart in-root symlinks from the pinned root descriptor. Both routes use an `F_GETPATH` post-open escape detector and report `best-effort` because directory rename races are not atomic with that check. No-replace publication uses `renameatx_np(RENAME_EXCL)`.
40
+ - Windows uses handle-relative `NtCreateFile`, rejects reparse points, and uses `FileRenameInfoEx` with replacement disabled.
41
+
42
+ Native primitives back create-only pinned writes, async sidecar creation,
43
+ guarded publication, archive acceleration, and direct Windows ACL operations.
44
+ Equivalent JavaScript paths remain available for documented fallback-capable
45
+ features. See [Native architecture](native.md#javascript-fallback-guarantees-and-delta)
46
+ for the exact difference.
47
+
48
+ `openBeneath()` returns `{ fd, containment }`. `containment` is
49
+ `"kernel-atomic"` for Linux `openat2` and `"best-effort"` for macOS and
50
+ Windows. Public JavaScript root open/read/writable results also expose the
51
+ field and report `"best-effort"`; the label reports mechanism, not policy.
52
+
53
+ ## Migration from the Python helper
54
+
55
+ Version 0.5 removes the Python worker and interpreter-path selection. The mode
56
+ contract is unchanged, so migrate startup configuration directly:
57
+
58
+ | Python helper configuration | Native replacement |
59
+ |---|---|
60
+ | `configureFsSafePython({ mode: "auto" })` | `configureFsSafeNative({ mode: "auto" })` |
61
+ | `configureFsSafePython({ mode: "off" })` | `configureFsSafeNative({ mode: "off" })` |
62
+ | `configureFsSafePython({ mode: "require" })` | `configureFsSafeNative({ mode: "require" })` |
63
+ | `FS_SAFE_PYTHON_MODE` | `FS_SAFE_NATIVE_MODE` |
64
+ | `OPENCLAW_FS_SAFE_PYTHON_MODE` | `OPENCLAW_FS_SAFE_NATIVE_MODE` |
65
+ | `pythonPath`, `FS_SAFE_PYTHON`, and the OpenClaw interpreter-path aliases | Remove; prebuilt bindings do not use an interpreter path |
66
+
67
+ In 0.5, `configureFsSafePython` and the legacy Python environment names
68
+ remain only as an upgrade bridge. On the first config read they emit one
69
+ `DeprecationWarning` with code `FS_SAFE_PYTHON_DEPRECATED`, state the mapped
70
+ native mode, and then apply that mode. A legacy interpreter path without an
71
+ explicit mode maps to `auto` and the path itself is ignored. Native config has
72
+ the normal precedence over legacy environment config.
73
+
74
+ There is no silent alias and no Python execution fallback. The bridge exists
75
+ only to make shipped 0.4 configuration visible and predictable while the
76
+ consumer performs its 0.5 upgrade.
77
+
78
+ ## Related pages
79
+
80
+ - [Config](config.md)
81
+ - [Security model](security-model.md)
82
+ - [Writing](writing.md)
83
+ - [File locks](sidecar-lock.md)
84
+ - [Durability](durability.md)
85
+ - [Migrating to 0.5](migrating-to-0.5.md)