@openclaw/openshell-sandbox 2026.9.7 → 2026.10.1-beta.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 (205) hide show
  1. package/dist/index.js +53 -97
  2. package/node_modules/@openclaw/fs-safe/CHANGELOG.md +95 -0
  3. package/node_modules/@openclaw/fs-safe/README.md +70 -379
  4. package/node_modules/@openclaw/fs-safe/dist/absolute-path.js +35 -51
  5. package/node_modules/@openclaw/fs-safe/dist/advanced.d.ts +2 -0
  6. package/node_modules/@openclaw/fs-safe/dist/advanced.js +1 -0
  7. package/node_modules/@openclaw/fs-safe/dist/archive-input.js +2 -0
  8. package/node_modules/@openclaw/fs-safe/dist/archive-parser.wasm +0 -0
  9. package/node_modules/@openclaw/fs-safe/dist/archive-staging.d.ts +1 -1
  10. package/node_modules/@openclaw/fs-safe/dist/archive-staging.js +9 -10
  11. package/node_modules/@openclaw/fs-safe/dist/archive.js +2 -5
  12. package/node_modules/@openclaw/fs-safe/dist/atomic-io.d.ts +51 -0
  13. package/node_modules/@openclaw/fs-safe/dist/atomic-io.js +242 -0
  14. package/node_modules/@openclaw/fs-safe/dist/config.d.ts +1 -1
  15. package/node_modules/@openclaw/fs-safe/dist/config.js +1 -1
  16. package/node_modules/@openclaw/fs-safe/dist/copy-file-input.d.ts +1 -1
  17. package/node_modules/@openclaw/fs-safe/dist/copy-file-input.js +2 -2
  18. package/node_modules/@openclaw/fs-safe/dist/copy-tree-portable.js +2 -0
  19. package/node_modules/@openclaw/fs-safe/dist/create.js +12 -5
  20. package/node_modules/@openclaw/fs-safe/dist/directory-guard.d.ts +11 -0
  21. package/node_modules/@openclaw/fs-safe/dist/directory-guard.js +4 -7
  22. package/node_modules/@openclaw/fs-safe/dist/entry-publication-types.d.ts +55 -0
  23. package/node_modules/@openclaw/fs-safe/dist/entry-publication-types.js +1 -0
  24. package/node_modules/@openclaw/fs-safe/dist/entry-publication.d.ts +8 -0
  25. package/node_modules/@openclaw/fs-safe/dist/entry-publication.js +262 -0
  26. package/node_modules/@openclaw/fs-safe/dist/exclusive-create.d.ts +3 -0
  27. package/node_modules/@openclaw/fs-safe/dist/exclusive-create.js +22 -0
  28. package/node_modules/@openclaw/fs-safe/dist/file-cleanup.d.ts +1 -6
  29. package/node_modules/@openclaw/fs-safe/dist/file-cleanup.js +3 -6
  30. package/node_modules/@openclaw/fs-safe/dist/file-identity.js +3 -6
  31. package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root-io.js +2 -2
  32. package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root-mutation.js +9 -0
  33. package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root.js +4 -6
  34. package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-stale-admission.js +1 -1
  35. package/node_modules/@openclaw/fs-safe/dist/file-lock-sync.js +8 -0
  36. package/node_modules/@openclaw/fs-safe/dist/file-store-boundary.js +2 -0
  37. package/node_modules/@openclaw/fs-safe/dist/file-store-sync-write.js +20 -18
  38. package/node_modules/@openclaw/fs-safe/dist/file-store.js +4 -10
  39. package/node_modules/@openclaw/fs-safe/dist/fs.d.ts +2 -10
  40. package/node_modules/@openclaw/fs-safe/dist/fs.js +2 -10
  41. package/node_modules/@openclaw/fs-safe/dist/guarded-mkdir.d.ts +3 -4
  42. package/node_modules/@openclaw/fs-safe/dist/guarded-mkdir.js +6 -18
  43. package/node_modules/@openclaw/fs-safe/dist/guest-dispatch-python.js +1 -1
  44. package/node_modules/@openclaw/fs-safe/dist/guest-native-python.js +0 -2
  45. package/node_modules/@openclaw/fs-safe/dist/guest.js +3 -9
  46. package/node_modules/@openclaw/fs-safe/dist/index.d.ts +1 -1
  47. package/node_modules/@openclaw/fs-safe/dist/index.js +1 -1
  48. package/node_modules/@openclaw/fs-safe/dist/json-durable-queue-directory.js +1 -5
  49. package/node_modules/@openclaw/fs-safe/dist/json.js +19 -37
  50. package/node_modules/@openclaw/fs-safe/dist/move-path.js +2 -0
  51. package/node_modules/@openclaw/fs-safe/dist/mutation-authority.d.ts +1 -0
  52. package/node_modules/@openclaw/fs-safe/dist/mutation-authority.js +9 -1
  53. package/node_modules/@openclaw/fs-safe/dist/native-binding.d.ts +47 -2
  54. package/node_modules/@openclaw/fs-safe/dist/native-config.d.ts +1 -9
  55. package/node_modules/@openclaw/fs-safe/dist/native-config.js +6 -40
  56. package/node_modules/@openclaw/fs-safe/dist/native-parent-admission.d.ts +2 -0
  57. package/node_modules/@openclaw/fs-safe/dist/native-parent-admission.js +5 -2
  58. package/node_modules/@openclaw/fs-safe/dist/native-pinned-write.js +11 -316
  59. package/node_modules/@openclaw/fs-safe/dist/native-policy-parent-windows.d.ts +7 -7
  60. package/node_modules/@openclaw/fs-safe/dist/native-policy-parent-windows.js +28 -175
  61. package/node_modules/@openclaw/fs-safe/dist/native-policy-parent.d.ts +15 -0
  62. package/node_modules/@openclaw/fs-safe/dist/native-policy-parent.js +418 -0
  63. package/node_modules/@openclaw/fs-safe/dist/native-rename-outcome.js +9 -4
  64. package/node_modules/@openclaw/fs-safe/dist/native-staged-file.js +17 -13
  65. package/node_modules/@openclaw/fs-safe/dist/native-staged-symlink.js +6 -24
  66. package/node_modules/@openclaw/fs-safe/dist/native.js +5 -1
  67. package/node_modules/@openclaw/fs-safe/dist/path-case.js +10 -9
  68. package/node_modules/@openclaw/fs-safe/dist/path-scope-lexical.js +4 -1
  69. package/node_modules/@openclaw/fs-safe/dist/path-segment-route.d.ts +1 -0
  70. package/node_modules/@openclaw/fs-safe/dist/path-segment-route.js +3 -0
  71. package/node_modules/@openclaw/fs-safe/dist/path.js +4 -1
  72. package/node_modules/@openclaw/fs-safe/dist/permissions-windows.d.ts +0 -1
  73. package/node_modules/@openclaw/fs-safe/dist/pinned-mutation-admission.js +1 -3
  74. package/node_modules/@openclaw/fs-safe/dist/pinned-write-staged.js +2 -0
  75. package/node_modules/@openclaw/fs-safe/dist/pinned-write.js +9 -10
  76. package/node_modules/@openclaw/fs-safe/dist/private-producer-handoff.js +3 -6
  77. package/node_modules/@openclaw/fs-safe/dist/publish-copy-stage.js +2 -2
  78. package/node_modules/@openclaw/fs-safe/dist/publish-file.js +4 -3
  79. package/node_modules/@openclaw/fs-safe/dist/replace-file-buffer.d.ts +3 -4
  80. package/node_modules/@openclaw/fs-safe/dist/replace-file-buffer.js +18 -32
  81. package/node_modules/@openclaw/fs-safe/dist/replace-file-copy-fallback.d.ts +5 -17
  82. package/node_modules/@openclaw/fs-safe/dist/replace-file-copy-fallback.js +92 -230
  83. package/node_modules/@openclaw/fs-safe/dist/replace-file-copy-source.d.ts +4 -15
  84. package/node_modules/@openclaw/fs-safe/dist/replace-file-copy-source.js +22 -63
  85. package/node_modules/@openclaw/fs-safe/dist/replace-file-descriptor.d.ts +9 -34
  86. package/node_modules/@openclaw/fs-safe/dist/replace-file-descriptor.js +78 -83
  87. package/node_modules/@openclaw/fs-safe/dist/replace-file-destination.d.ts +18 -20
  88. package/node_modules/@openclaw/fs-safe/dist/replace-file-destination.js +64 -98
  89. package/node_modules/@openclaw/fs-safe/dist/replace-file-temp-owner.d.ts +16 -33
  90. package/node_modules/@openclaw/fs-safe/dist/replace-file-temp-owner.js +61 -200
  91. package/node_modules/@openclaw/fs-safe/dist/replace-file-types.d.ts +1 -6
  92. package/node_modules/@openclaw/fs-safe/dist/replace-file.js +76 -216
  93. package/node_modules/@openclaw/fs-safe/dist/root-boundary.js +14 -24
  94. package/node_modules/@openclaw/fs-safe/dist/root-context.d.ts +1 -4
  95. package/node_modules/@openclaw/fs-safe/dist/root-context.js +4 -19
  96. package/node_modules/@openclaw/fs-safe/dist/root-create-native.d.ts +52 -0
  97. package/node_modules/@openclaw/fs-safe/dist/root-create-native.js +350 -0
  98. package/node_modules/@openclaw/fs-safe/dist/root-directory-list-types.d.ts +35 -0
  99. package/node_modules/@openclaw/fs-safe/dist/root-directory-list-types.js +1 -0
  100. package/node_modules/@openclaw/fs-safe/dist/root-directory-list.d.ts +2 -27
  101. package/node_modules/@openclaw/fs-safe/dist/root-directory-list.js +31 -28
  102. package/node_modules/@openclaw/fs-safe/dist/root-directory.js +1 -4
  103. package/node_modules/@openclaw/fs-safe/dist/root-errors.js +34 -0
  104. package/node_modules/@openclaw/fs-safe/dist/root-file.js +9 -21
  105. package/node_modules/@openclaw/fs-safe/dist/root-impl.js +105 -37
  106. package/node_modules/@openclaw/fs-safe/dist/root-move-noreplace.d.ts +7 -2
  107. package/node_modules/@openclaw/fs-safe/dist/root-move-noreplace.js +67 -38
  108. package/node_modules/@openclaw/fs-safe/dist/root-observed-path.d.ts +1 -1
  109. package/node_modules/@openclaw/fs-safe/dist/root-observed-path.js +0 -2
  110. package/node_modules/@openclaw/fs-safe/dist/root-options.d.ts +2 -3
  111. package/node_modules/@openclaw/fs-safe/dist/root-path-existing.js +3 -1
  112. package/node_modules/@openclaw/fs-safe/dist/root-path-stat.js +68 -85
  113. package/node_modules/@openclaw/fs-safe/dist/root-path.js +6 -1
  114. package/node_modules/@openclaw/fs-safe/dist/root-paths-lexical.d.ts +4 -0
  115. package/node_modules/@openclaw/fs-safe/dist/root-paths-lexical.js +1 -1
  116. package/node_modules/@openclaw/fs-safe/dist/root-paths.js +4 -9
  117. package/node_modules/@openclaw/fs-safe/dist/root-remove-native.d.ts +4 -0
  118. package/node_modules/@openclaw/fs-safe/dist/root-remove-native.js +311 -0
  119. package/node_modules/@openclaw/fs-safe/dist/root-remove.d.ts +11 -0
  120. package/node_modules/@openclaw/fs-safe/dist/root-remove.js +3 -1
  121. package/node_modules/@openclaw/fs-safe/dist/root-walk.d.ts +2 -2
  122. package/node_modules/@openclaw/fs-safe/dist/root-walk.js +1 -3
  123. package/node_modules/@openclaw/fs-safe/dist/root-write-admission.d.ts +3 -3
  124. package/node_modules/@openclaw/fs-safe/dist/root-write-admission.js +8 -10
  125. package/node_modules/@openclaw/fs-safe/dist/safe-path-segment.d.ts +0 -1
  126. package/node_modules/@openclaw/fs-safe/dist/safe-path-segment.js +0 -4
  127. package/node_modules/@openclaw/fs-safe/dist/secret-file.js +12 -9
  128. package/node_modules/@openclaw/fs-safe/dist/secure-temp-dir.d.ts +0 -2
  129. package/node_modules/@openclaw/fs-safe/dist/sibling-staged-file.js +2 -0
  130. package/node_modules/@openclaw/fs-safe/dist/sibling-temp.js +7 -4
  131. package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-acquire.js +13 -6
  132. package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-reclaim.d.ts +0 -3
  133. package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-reclaim.js +5 -23
  134. package/node_modules/@openclaw/fs-safe/dist/staged-file-settlement.d.ts +2 -1
  135. package/node_modules/@openclaw/fs-safe/dist/staged-file-settlement.js +11 -6
  136. package/node_modules/@openclaw/fs-safe/dist/temp-cleanup.js +1 -8
  137. package/node_modules/@openclaw/fs-safe/dist/temp-workspace-child-admission.d.ts +1 -1
  138. package/node_modules/@openclaw/fs-safe/dist/temp-workspace-child-admission.js +2 -9
  139. package/node_modules/@openclaw/fs-safe/dist/text-atomic.d.ts +1 -6
  140. package/node_modules/@openclaw/fs-safe/dist/trash.js +6 -2
  141. package/node_modules/@openclaw/fs-safe/dist/walk.js +100 -46
  142. package/node_modules/@openclaw/fs-safe/dist/watch-hints.d.ts +7 -0
  143. package/node_modules/@openclaw/fs-safe/dist/watch-hints.js +235 -19
  144. package/node_modules/@openclaw/fs-safe/dist/watch-native.d.ts +21 -2
  145. package/node_modules/@openclaw/fs-safe/dist/watch-native.js +21 -3
  146. package/node_modules/@openclaw/fs-safe/dist/watch-rescan.d.ts +6 -0
  147. package/node_modules/@openclaw/fs-safe/dist/watch-rescan.js +111 -0
  148. package/node_modules/@openclaw/fs-safe/dist/watch-scan.d.ts +13 -0
  149. package/node_modules/@openclaw/fs-safe/dist/watch-scan.js +27 -4
  150. package/node_modules/@openclaw/fs-safe/dist/watch-stream.js +2 -0
  151. package/node_modules/@openclaw/fs-safe/dist/watch.js +131 -40
  152. package/node_modules/@openclaw/fs-safe/dist/windows-path-alias.d.ts +8 -0
  153. package/node_modules/@openclaw/fs-safe/dist/windows-path-alias.js +24 -6
  154. package/node_modules/@openclaw/fs-safe/dist/windows-path-syntax.d.ts +13 -0
  155. package/node_modules/@openclaw/fs-safe/dist/windows-path-syntax.js +51 -0
  156. package/node_modules/@openclaw/fs-safe/docs/advanced.md +4 -0
  157. package/node_modules/@openclaw/fs-safe/docs/atomic.md +4 -1
  158. package/node_modules/@openclaw/fs-safe/docs/config.md +6 -14
  159. package/node_modules/@openclaw/fs-safe/docs/contributing.md +4 -14
  160. package/node_modules/@openclaw/fs-safe/docs/durability.md +6 -29
  161. package/node_modules/@openclaw/fs-safe/docs/entry-publication.md +146 -0
  162. package/node_modules/@openclaw/fs-safe/docs/errors.md +16 -0
  163. package/node_modules/@openclaw/fs-safe/docs/file-store.md +28 -5
  164. package/node_modules/@openclaw/fs-safe/docs/index.md +2 -31
  165. package/node_modules/@openclaw/fs-safe/docs/install.md +10 -31
  166. package/node_modules/@openclaw/fs-safe/docs/json.md +1 -1
  167. package/node_modules/@openclaw/fs-safe/docs/local-roots.md +0 -1
  168. package/node_modules/@openclaw/fs-safe/docs/migrating-to-0.5.md +12 -12
  169. package/node_modules/@openclaw/fs-safe/docs/native-helper.md +35 -96
  170. package/node_modules/@openclaw/fs-safe/docs/native.md +22 -20
  171. package/node_modules/@openclaw/fs-safe/docs/path.md +1 -1
  172. package/node_modules/@openclaw/fs-safe/docs/permissions.md +1 -2
  173. package/node_modules/@openclaw/fs-safe/docs/public-api.md +1 -2
  174. package/node_modules/@openclaw/fs-safe/docs/reading.md +1 -2
  175. package/node_modules/@openclaw/fs-safe/docs/root.md +26 -184
  176. package/node_modules/@openclaw/fs-safe/docs/secret-file.md +1 -1
  177. package/node_modules/@openclaw/fs-safe/docs/security-model.md +115 -3
  178. package/node_modules/@openclaw/fs-safe/docs/store.md +2 -2
  179. package/node_modules/@openclaw/fs-safe/docs/temp.md +29 -65
  180. package/node_modules/@openclaw/fs-safe/docs/testing.md +90 -58
  181. package/node_modules/@openclaw/fs-safe/docs/types.md +3 -16
  182. package/node_modules/@openclaw/fs-safe/docs/walk.md +33 -5
  183. package/node_modules/@openclaw/fs-safe/docs/watch.md +96 -14
  184. package/node_modules/@openclaw/fs-safe/docs/writing.md +172 -16
  185. package/node_modules/@openclaw/fs-safe/package.json +10 -10
  186. package/node_modules/@openclaw/fs-safe-darwin-arm64/fs-safe-native.node +0 -0
  187. package/node_modules/@openclaw/fs-safe-darwin-arm64/package.json +1 -1
  188. package/node_modules/@openclaw/fs-safe-darwin-x64/fs-safe-native.node +0 -0
  189. package/node_modules/@openclaw/fs-safe-darwin-x64/package.json +1 -1
  190. package/node_modules/@openclaw/fs-safe-linux-arm64-gnu/fs-safe-native.node +0 -0
  191. package/node_modules/@openclaw/fs-safe-linux-arm64-gnu/package.json +1 -1
  192. package/node_modules/@openclaw/fs-safe-linux-arm64-musl/fs-safe-native.node +0 -0
  193. package/node_modules/@openclaw/fs-safe-linux-arm64-musl/package.json +1 -1
  194. package/node_modules/@openclaw/fs-safe-linux-x64-gnu/fs-safe-native.node +0 -0
  195. package/node_modules/@openclaw/fs-safe-linux-x64-gnu/package.json +1 -1
  196. package/node_modules/@openclaw/fs-safe-linux-x64-musl/fs-safe-native.node +0 -0
  197. package/node_modules/@openclaw/fs-safe-linux-x64-musl/package.json +1 -1
  198. package/node_modules/@openclaw/fs-safe-win32-x64-msvc/fs-safe-native.node +0 -0
  199. package/node_modules/@openclaw/fs-safe-win32-x64-msvc/package.json +1 -1
  200. package/package.json +5 -5
  201. package/node_modules/@openclaw/fs-safe/dist/watch-alias.d.ts +0 -6
  202. package/node_modules/@openclaw/fs-safe/dist/watch-alias.js +0 -88
  203. package/node_modules/@openclaw/fs-safe/docs/mutation-policy-proof.md +0 -69
  204. package/node_modules/@openclaw/fs-safe/docs/private-file-store.md +0 -68
  205. package/node_modules/@openclaw/fs-safe/docs/test-hooks.md +0 -110
package/dist/index.js CHANGED
@@ -9,7 +9,8 @@ import { GUEST_FILESYSTEM_PYTHON } from "@openclaw/fs-safe/guest";
9
9
  import { createDeferred, formatPluginConfigIssue, mapPluginConfigIssues } from "openclaw/plugin-sdk/extension-shared";
10
10
  import { KeyedAsyncQueue } from "openclaw/plugin-sdk/keyed-async-queue";
11
11
  import { FsSafeError, canonicalPathFromExistingAncestor, extractErrorCode } from "openclaw/plugin-sdk/security-runtime";
12
- import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime";
12
+ import { asOptionalRecord, normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime";
13
+ import { safeParseJson } from "openclaw/plugin-sdk/text-utility-runtime";
13
14
  import { buildPluginConfigSchema } from "openclaw/plugin-sdk/core";
14
15
  import { MAX_TIMER_TIMEOUT_SECONDS } from "openclaw/plugin-sdk/number-runtime";
15
16
  import { z } from "zod";
@@ -63,9 +64,9 @@ const DEFAULT_SOURCE = "openclaw";
63
64
  const DEFAULT_REMOTE_WORKSPACE_DIR = "/sandbox";
64
65
  const DEFAULT_REMOTE_AGENT_WORKSPACE_DIR = "/agent";
65
66
  const DEFAULT_TIMEOUT_MS = 12e4;
66
- const OPEN_SHELL_MANAGED_REMOTE_ROOTS = [DEFAULT_REMOTE_WORKSPACE_DIR, DEFAULT_REMOTE_AGENT_WORKSPACE_DIR];
67
+ const OPEN_SHELL_MANAGED_REMOTE_PATH = /^\/(?:sandbox|agent)(?:\/|$)/;
67
68
  const nonEmptyTrimmedString = (message) => z.string({ error: message }).trim().min(1, { error: message });
68
- const openShellManagedRemotePath = (fieldName) => nonEmptyTrimmedString(`${fieldName} must be a non-empty string`).regex(/^\/(?:sandbox|agent)(?:\/|$)/, { error: (issue) => String(issue.input).startsWith("/") ? `OpenShell ${fieldName} must stay under /sandbox or /agent` : `OpenShell ${fieldName} must be absolute` }).refine((value) => isManagedOpenShellRemotePath(path.posix.normalize(value)), { error: `OpenShell ${fieldName} must stay under /sandbox or /agent` });
69
+ const openShellManagedRemotePath = (fieldName) => nonEmptyTrimmedString(`${fieldName} must be a non-empty string`).regex(OPEN_SHELL_MANAGED_REMOTE_PATH, { error: (issue) => String(issue.input).startsWith("/") ? `OpenShell ${fieldName} must stay under /sandbox or /agent` : `OpenShell ${fieldName} must be absolute` }).refine((value) => OPEN_SHELL_MANAGED_REMOTE_PATH.test(path.posix.normalize(value)), { error: `OpenShell ${fieldName} must stay under /sandbox or /agent` });
69
70
  const openShellWorkspaceName = z.string({ error: "workspace must be a valid OpenShell workspace name" }).trim().min(1, { error: "workspace must be a valid OpenShell workspace name" }).max(19, { error: "workspace must be at most 19 characters" }).regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, { error: "workspace must contain lowercase alphanumeric characters or single hyphens and must not start or end with a hyphen" });
70
71
  const OpenShellPluginConfigSchema = z.strictObject({
71
72
  mode: z.enum(["mirror", "remote"], { error: "mode must be one of mirror, remote" }).optional(),
@@ -82,14 +83,9 @@ const OpenShellPluginConfigSchema = z.strictObject({
82
83
  remoteAgentWorkspaceDir: openShellManagedRemotePath("remoteAgentWorkspaceDir").optional(),
83
84
  timeoutSeconds: z.number({ error: `timeoutSeconds must be a number between 1 and ${MAX_TIMER_TIMEOUT_SECONDS}` }).min(1, { error: "timeoutSeconds must be a number >= 1" }).max(MAX_TIMER_TIMEOUT_SECONDS, { error: `timeoutSeconds must be a number <= ${MAX_TIMER_TIMEOUT_SECONDS}` }).optional()
84
85
  });
85
- function isManagedOpenShellRemotePath(value) {
86
- return OPEN_SHELL_MANAGED_REMOTE_ROOTS.some((root) => value === root || value.startsWith(`${root}/`));
87
- }
88
- function normalizeOpenShellRemotePath(value, fallback, fieldName = "remote path") {
89
- const candidate = value ?? fallback;
90
- const normalized = path.posix.normalize(candidate.trim() || fallback);
91
- if (!normalized.startsWith("/")) throw new Error(`OpenShell ${fieldName} must be absolute: ${candidate}`);
92
- if (!isManagedOpenShellRemotePath(normalized)) throw new Error(`OpenShell ${fieldName} must stay under ${OPEN_SHELL_MANAGED_REMOTE_ROOTS.join(" or ")}: ${candidate}`);
86
+ function normalizeOpenShellRemotePath(value) {
87
+ const normalized = path.posix.normalize(value);
88
+ if (!OPEN_SHELL_MANAGED_REMOTE_PATH.test(normalized)) throw new Error(`OpenShell remote path must stay under /sandbox or /agent: ${value}`);
93
89
  return normalized;
94
90
  }
95
91
  function createOpenShellPluginConfigSchema() {
@@ -127,8 +123,8 @@ function resolveOpenShellPluginConfig(value) {
127
123
  providers: [...new Set(cfg.providers ?? [])],
128
124
  gpu: cfg.gpu ?? false,
129
125
  autoProviders: cfg.autoProviders ?? true,
130
- remoteWorkspaceDir: normalizeOpenShellRemotePath(cfg.remoteWorkspaceDir, DEFAULT_REMOTE_WORKSPACE_DIR, "remoteWorkspaceDir"),
131
- remoteAgentWorkspaceDir: normalizeOpenShellRemotePath(cfg.remoteAgentWorkspaceDir, DEFAULT_REMOTE_AGENT_WORKSPACE_DIR, "remoteAgentWorkspaceDir"),
126
+ remoteWorkspaceDir: normalizeOpenShellRemotePath(cfg.remoteWorkspaceDir ?? DEFAULT_REMOTE_WORKSPACE_DIR),
127
+ remoteAgentWorkspaceDir: normalizeOpenShellRemotePath(cfg.remoteAgentWorkspaceDir ?? DEFAULT_REMOTE_AGENT_WORKSPACE_DIR),
132
128
  timeoutMs: typeof cfg.timeoutSeconds === "number" ? Math.floor(cfg.timeoutSeconds * 1e3) : DEFAULT_TIMEOUT_MS
133
129
  };
134
130
  }
@@ -223,21 +219,17 @@ var OpenShellFsBridge = class {
223
219
  }
224
220
  async mkdirp(params) {
225
221
  const target = this.resolveTarget(params);
226
- const hostPath = target.hostPath;
227
222
  this.ensureWritable(target, "create directories");
228
223
  await assertLocalPathSafety({
229
224
  target,
230
225
  allowFinalSymlinkForUnlink: false
231
226
  });
232
227
  await this.backend.mkdirpRemotePath(target.containerPath, params.signal);
233
- await mkdirLocalRootPath({
234
- hostPath,
235
- target
236
- });
228
+ const relativePath = relativeToRoot(target, target.hostPath);
229
+ if (relativePath) await (await root(target.mountHostRoot)).mkdir(relativePath);
237
230
  }
238
231
  async remove(params) {
239
232
  const target = this.resolveTarget(params);
240
- const hostPath = target.hostPath;
241
233
  this.ensureWritable(target, "remove files", params.recursive);
242
234
  await assertLocalPathSafety({
243
235
  target,
@@ -250,15 +242,12 @@ var OpenShellFsBridge = class {
250
242
  });
251
243
  await removeLocalRootPath({
252
244
  force: params.force,
253
- hostPath,
254
245
  recursive: params.recursive,
255
246
  target
256
247
  });
257
248
  }
258
249
  async rename(params) {
259
250
  const { from, to } = this.resolveRenameTargets(params);
260
- const fromHostPath = from.hostPath;
261
- const toHostPath = to.hostPath;
262
251
  await assertLocalPathSafety({
263
252
  target: from,
264
253
  allowFinalSymlinkForUnlink: true
@@ -267,20 +256,20 @@ var OpenShellFsBridge = class {
267
256
  target: to,
268
257
  allowFinalSymlinkForUnlink: false
269
258
  });
270
- await assertRenameSourceSupported(fromHostPath);
259
+ await assertRenameSourceSupported(from.hostPath);
271
260
  if (from.mountHostRoot !== to.mountHostRoot) throw new Error("OpenShell cross-root mirror renames require pinned fs-safe support");
272
261
  await assertSameDeviceRenameSupported({
273
- fromHostPath,
262
+ fromHostPath: from.hostPath,
274
263
  root: from.mountHostRoot,
275
- toHostPath
264
+ toHostPath: to.hostPath
276
265
  });
277
266
  await this.backend.renameRemotePath(from.containerPath, to.containerPath, params.signal);
278
- await moveLocalRootPath({
279
- from,
280
- fromHostPath,
281
- to,
282
- toHostPath
283
- });
267
+ const root$3 = await root(from.mountHostRoot);
268
+ const fromRelativePath = relativeToRoot(from, from.hostPath);
269
+ const toRelativePath = relativeToRoot(to, to.hostPath);
270
+ const parentPath = path.dirname(toRelativePath);
271
+ if (parentPath !== "." && parentPath !== "") await root$3.mkdir(parentPath);
272
+ await root$3.move(fromRelativePath, toRelativePath, { overwrite: true });
284
273
  }
285
274
  async stat(params) {
286
275
  const target = this.resolveTarget(params);
@@ -396,18 +385,13 @@ function expectResolvedContainerTarget(target, input) {
396
385
  if (!target) throw new Error(`Sandbox path escapes allowed mounts; cannot access: ${input}`);
397
386
  return target;
398
387
  }
399
- async function mkdirLocalRootPath(params) {
400
- const relativePath = relativeToRoot(params.target, params.hostPath);
401
- if (!relativePath) return;
402
- await (await root(params.target.mountHostRoot)).mkdir(relativePath);
403
- }
404
388
  async function removeLocalRootPath(params) {
405
- const root$3 = await root(params.target.mountHostRoot);
406
- const relativePath = relativeToRoot(params.target, params.hostPath);
389
+ const root$4 = await root(params.target.mountHostRoot);
390
+ const relativePath = relativeToRoot(params.target, params.target.hostPath);
407
391
  try {
408
- if (params.force === false) await fsPromises.lstat(params.hostPath);
409
- const targets = params.recursive && !relativePath ? (await root$3.list("")).map((name) => relativeToRoot(params.target, path.join(params.hostPath, name))) : [relativePath];
410
- for (const target of targets) await root$3.remove(target, {
392
+ if (params.force === false) await fsPromises.lstat(params.target.hostPath);
393
+ const targets = params.recursive && !relativePath ? (await root$4.list("")).map((name) => relativeToRoot(params.target, path.join(params.target.hostPath, name))) : [relativePath];
394
+ for (const target of targets) await root$4.remove(target, {
411
395
  force: params.force !== false,
412
396
  ...params.recursive ? {
413
397
  recursive: true,
@@ -421,18 +405,6 @@ async function removeLocalRootPath(params) {
421
405
  throw err;
422
406
  }
423
407
  }
424
- async function moveLocalRootPath(params) {
425
- const root$4 = await root(params.from.mountHostRoot);
426
- const fromRelativePath = relativeToRoot(params.from, params.fromHostPath);
427
- const toRelativePath = relativeToRoot(params.to, params.toHostPath);
428
- await mkdirParentPath(root$4, toRelativePath);
429
- await root$4.move(fromRelativePath, toRelativePath, { overwrite: true });
430
- }
431
- async function mkdirParentPath(root, relativePath) {
432
- const parentPath = path.dirname(relativePath);
433
- if (parentPath === "." || parentPath === "") return;
434
- await root.mkdir(parentPath);
435
- }
436
408
  function relativeToRoot(target, hostPath) {
437
409
  const relativePath = path.relative(target.mountHostRoot, hostPath);
438
410
  return relativePath ? `.${path.sep}${relativePath}` : "";
@@ -638,10 +610,23 @@ const ENSURE_OPEN_SHELL_REMOTE_REAL_DIRECTORY_SCRIPT = [
638
610
  "done"
639
611
  ].join("\n");
640
612
  function createOpenShellSandboxBackendFactory(params) {
641
- return async (createParams) => await createOpenShellSandboxBackend({
642
- ...params,
643
- createParams
644
- });
613
+ return async (createParams) => {
614
+ if ((createParams.cfg.docker.binds?.length ?? 0) > 0) throw new Error("OpenShell sandbox backend does not support sandbox.docker.binds.");
615
+ const { sandboxName, legacyRuntimeAdopted } = resolveOpenShellSandboxName({
616
+ scopeKey: createParams.scopeKey,
617
+ registeredRuntimeIds: createParams.registeredRuntimeIds
618
+ });
619
+ return new OpenShellSandboxBackendImpl({
620
+ createParams,
621
+ execContext: {
622
+ config: params.pluginConfig,
623
+ sandboxName
624
+ },
625
+ legacyRuntimeAdopted,
626
+ remoteWorkspaceDir: params.pluginConfig.remoteWorkspaceDir,
627
+ remoteAgentWorkspaceDir: params.pluginConfig.remoteAgentWorkspaceDir
628
+ }).asHandle();
629
+ };
645
630
  }
646
631
  function createOpenShellSandboxBackendManager(params) {
647
632
  return {
@@ -683,25 +668,6 @@ function createOpenShellSandboxBackendManager(params) {
683
668
  }
684
669
  };
685
670
  }
686
- async function createOpenShellSandboxBackend(params) {
687
- if ((params.createParams.cfg.docker.binds?.length ?? 0) > 0) throw new Error("OpenShell sandbox backend does not support sandbox.docker.binds.");
688
- const resolvedSandboxName = resolveOpenShellSandboxName({
689
- scopeKey: params.createParams.scopeKey,
690
- registeredRuntimeIds: params.createParams.registeredRuntimeIds
691
- });
692
- const sandboxName = resolvedSandboxName.sandboxName;
693
- const execContext = {
694
- config: params.pluginConfig,
695
- sandboxName
696
- };
697
- return new OpenShellSandboxBackendImpl({
698
- createParams: params.createParams,
699
- execContext,
700
- legacyRuntimeAdopted: resolvedSandboxName.legacyRuntimeAdopted,
701
- remoteWorkspaceDir: params.pluginConfig.remoteWorkspaceDir,
702
- remoteAgentWorkspaceDir: params.pluginConfig.remoteAgentWorkspaceDir
703
- }).asHandle();
704
- }
705
671
  var OpenShellSandboxBackendImpl = class {
706
672
  constructor(params) {
707
673
  this.params = params;
@@ -1378,30 +1344,20 @@ function resolveOpenShellSandboxName(params) {
1378
1344
  };
1379
1345
  }
1380
1346
  function parseOpenShellSandboxPhasePage(stdout, sandboxName) {
1381
- try {
1382
- const parsed = JSON.parse(stdout);
1383
- if (!Array.isArray(parsed)) return;
1384
- for (const entry of parsed) {
1385
- if (!entry || typeof entry !== "object") continue;
1386
- const record = entry;
1387
- if (record.name === sandboxName && typeof record.phase === "string") return {
1388
- count: parsed.length,
1389
- phase: record.phase
1390
- };
1391
- }
1392
- return { count: parsed.length };
1393
- } catch {
1394
- return;
1347
+ const parsed = safeParseJson(stdout);
1348
+ if (!Array.isArray(parsed)) return;
1349
+ for (const entry of parsed) {
1350
+ const record = asOptionalRecord(entry);
1351
+ if (record?.name === sandboxName && typeof record.phase === "string") return {
1352
+ count: parsed.length,
1353
+ phase: record.phase
1354
+ };
1395
1355
  }
1356
+ return { count: parsed.length };
1396
1357
  }
1397
1358
  function parseOpenShellSandboxPhase(stdout) {
1398
- try {
1399
- const parsed = JSON.parse(stdout);
1400
- if (typeof parsed !== "object" || parsed === null || !("phase" in parsed)) return;
1401
- return typeof parsed.phase === "string" ? parsed.phase : void 0;
1402
- } catch {
1403
- return;
1404
- }
1359
+ const parsed = asOptionalRecord(safeParseJson(stdout));
1360
+ return typeof parsed?.phase === "string" ? parsed.phase : void 0;
1405
1361
  }
1406
1362
  function resolveRemoteMaterializedSkillsWorkspaceDir(remoteWorkspaceDir) {
1407
1363
  const root = remoteWorkspaceDir.replace(/\\/g, "/").replace(/\/+$/, "") || "/";
@@ -2,6 +2,101 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.23.0 - 2026-10-02
6
+
7
+ ### Highlights
8
+
9
+ - **BREAKING: Python bridge removed:** remove the Python bridge and its compatibility options; migrate to `configureFsSafeNative` / `FS_SAFE_NATIVE_MODE`. ([#815](https://github.com/openclaw/fs-safe/pull/815))
10
+ - **Watch overflow:** fold busy hints so unselected sibling churn beside a missing tree no longer overflows the watch. ([#813](https://github.com/openclaw/fs-safe/pull/813))
11
+ - **Windows exclusive creation:** no longer follows unchanged dangling file symlinks, including standalone creators, unrooted locks and cross-volume copy publication. ([#823](https://github.com/openclaw/fs-safe/pull/823), [#824](https://github.com/openclaw/fs-safe/pull/824))
12
+ - **Faster metadata listings:** reduce path normalization and per-entry allocation when listing Root directory metadata. ([#818](https://github.com/openclaw/fs-safe/pull/818), [#819](https://github.com/openclaw/fs-safe/pull/819))
13
+
14
+ ### Fixes
15
+
16
+ - **Windows directory preparation:** retain exact file identities in `ensureAbsoluteDirectory()` so a replaced directory cannot compare equal after large Windows file IDs are rounded to JavaScript numbers. ([#825](https://github.com/openclaw/fs-safe/pull/825))
17
+ - **Windows writable creation:** in native `auto`/`off` mode, `openWritable()`, `append()`, and create-only writes could create a dangling file symlink's missing in-Root target before rejecting the link. Check final leaves before fallback creation; outside-Root targets were already rejected. ([#823](https://github.com/openclaw/fs-safe/pull/823))
18
+ - **Windows symlink creation escape:** unchanged dangling file symlinks could redirect standalone exclusive creation outside the caller-selected directory, including source contents during cross-volume copy publication and records from unrooted fallback locks. Check final leaves before creation, including internal staging files; concurrent replacement remains best-effort. ([#824](https://github.com/openclaw/fs-safe/pull/824))
19
+ - **Atomic writes:** preserve both parent-directory preparation and descriptor-close failures, in operation-first order, for synchronous and asynchronous replacements. ([#820](https://github.com/openclaw/fs-safe/pull/820))
20
+ - **Watch overflow:** fold busy pending hints into guarded subtree reconciliation so unselected sibling churn beside a missing tree no longer exhausts the hint budget. ([#813](https://github.com/openclaw/fs-safe/pull/813))
21
+ - **Native initialization:** lazily load the addon in fs-safe's import context so its housekeeping does not retain the first operation's `AsyncLocalStorage` stores when fs-safe is imported outside caller scopes. ([#797](https://github.com/openclaw/fs-safe/pull/797))
22
+
23
+ ### Performance
24
+
25
+ - **Root metadata path assembly:** avoid renormalizing each literal child beneath its canonical directory, cutting 1,000-entry listing time by a further 19–23% and sampled allocations by 33–34% on Linux x64. ([#819](https://github.com/openclaw/fs-safe/pull/819))
26
+ - **Root metadata listings:** construct named metadata once per entry, reducing 1,000-entry listing time by 16% and sampled allocations by 27–28% on Linux x64, with all filesystem and identity checks retained. ([#818](https://github.com/openclaw/fs-safe/pull/818))
27
+
28
+ ### Compatibility
29
+
30
+ - **Removed compatibility options:** remove `configureFsSafePython`, `FsSafePythonConfig`, `pythonPath`, and the six Python environment variables; use `configureFsSafeNative`, `FsSafeNativeConfig`, and `FS_SAFE_NATIVE_MODE` (or `OPENCLAW_FS_SAFE_NATIVE_MODE`). The Python variables are now ignored, so a deployment that still sets `FS_SAFE_PYTHON_MODE=require` or `off` runs in `auto` until it sets `FS_SAFE_NATIVE_MODE`. Remove secure-temp `chmodSync`, atomic adapter `chmod`/`chmodSync`, Root `nonBlockingRead`, and ACL `principalSids`; use descriptor-bound permission adapters (`descriptor` for secure-temp, `FileHandle.chmod` / `fchmodSync` for atomic writes), automatic nonblocking read admission, and direct SID classification. Callers passing removed options get TypeScript errors. ([#815](https://github.com/openclaw/fs-safe/pull/815))
31
+
32
+ ## 0.22.0 - 2026-10-01
33
+
34
+ ### Highlights
35
+
36
+ - **One-way publication:** `retainEntryForPublication` on `advanced` provides no-replace export for POSIX directories, regular files and direct symlinks, plus Windows NTFS files, directories, symbolic links and junctions. ([#776](https://github.com/openclaw/fs-safe/pull/776), [#778](https://github.com/openclaw/fs-safe/pull/778), [#779](https://github.com/openclaw/fs-safe/pull/779))
37
+ - **Windows path hardening:** reject foreign UNC shares and device namespaces before filesystem probes, blocking attacker-chosen SMB lookups, NTLM exposure and stalls. ([#781](https://github.com/openclaw/fs-safe/pull/781), [#784](https://github.com/openclaw/fs-safe/pull/784))
38
+ - **Watch overflow fixes:** Linux watch-retirement echoes no longer overflow unrelated subscriptions; Linux and macOS classify undecodable names, Linux preserves nameless lifecycle events, and macOS cleans up failed startup correctly. ([#782](https://github.com/openclaw/fs-safe/pull/782), [#783](https://github.com/openclaw/fs-safe/pull/783), [#769](https://github.com/openclaw/fs-safe/pull/769))
39
+
40
+ ### Features
41
+
42
+ - **One-way publication:** `retainEntryForPublication` retains source and parent identities for native no-replace export on supported local POSIX filesystems and Windows NTFS. It supports directories, regular files and direct symbolic links, plus NTFS junctions; link targets stay opaque, and POSIX relative and dangling targets retain their exact bytes. Commit, indeterminate and close failures remain distinct. Requires caller-exclusive source namespaces, with no source-CAS or automatic rollback guarantee; disposal only closes retained resources and never deletes either location. ([#776](https://github.com/openclaw/fs-safe/pull/776), [#778](https://github.com/openclaw/fs-safe/pull/778), [#779](https://github.com/openclaw/fs-safe/pull/779))
43
+
44
+ ### Fixes
45
+
46
+ - **Watch name handling:** ignore undecodable unselected siblings, fail closed for selected tree names on Linux and macOS, and preserve Linux directory lifecycle detail without spurious overflow. ([#783](https://github.com/openclaw/fs-safe/pull/783))
47
+ - **Windows UNC admission:** Root, root-file readers, `pathScope`, `isPathInside`, secret-file writers, sibling-temp output, trash admission, and archive output preparation reject foreign UNC shares and device namespaces before probing them, comparing hosts without Unicode case folding, preventing attacker-chosen SMB lookups, NTLM exposure, and stalls. Existing containment errors are preserved; absolute paths on another share no longer resolve through aliases back into the boundary. ([#781](https://github.com/openclaw/fs-safe/pull/781), [#784](https://github.com/openclaw/fs-safe/pull/784))
48
+ - **Linux watch isolation:** closing or replacing a watch, including after a scan failure, no longer overflows unrelated subscriptions. ([#782](https://github.com/openclaw/fs-safe/pull/782))
49
+ - **macOS watch startup:** clean up rejected FSEvents configuration and failed stream startup with the correct lifecycle calls, keeping the registration available for retry. ([#769](https://github.com/openclaw/fs-safe/pull/769))
50
+
51
+ ### Performance
52
+
53
+ - **Bounded temporary-directory cleanup:** avoid a redundant descriptor metadata read for each descendant directory on Linux and macOS, while retaining fresh name identity checks before removal. ([#760](https://github.com/openclaw/fs-safe/pull/760))
54
+ - **Atomic temp cleanup:** use the file identity captured after creation, avoiding an unnecessary metadata lookup and preserving preexisting temporary paths. ([#772](https://github.com/openclaw/fs-safe/pull/772))
55
+
56
+ ### Compatibility
57
+
58
+ - Windows now rejects foreign UNC shares and device namespaces before probing them. Absolute paths on another share no longer resolve through aliases back into the boundary; callers that relied on that behavior must pass in-boundary paths. ([#781](https://github.com/openclaw/fs-safe/pull/781), [#784](https://github.com/openclaw/fs-safe/pull/784))
59
+
60
+ ## 0.21.3 - 2026-09-30
61
+
62
+ ### Highlights
63
+
64
+ - **Watch transient files:** excluded files and atomic-save temp files no longer cause spurious overflow when they disappear between guarded passes. ([#773](https://github.com/openclaw/fs-safe/pull/773))
65
+ - **Faster watch rescans:** unrelated native hints skip rescans, and relevant hints reconcile affected directories, reducing scan work and allocation. ([#774](https://github.com/openclaw/fs-safe/pull/774))
66
+
67
+ ### Fixes
68
+
69
+ - **Watch:** no longer reports overflow when an excluded or atomic-save temp file appears and disappears between guarded passes under the same observed parent directory. ([#773](https://github.com/openclaw/fs-safe/pull/773))
70
+ - **Windows removal:** nonrecursive `Root.remove()` reports `not-empty` for a nonempty directory in required native mode, including with `force: true`, instead of `not-removable`. ([#771](https://github.com/openclaw/fs-safe/pull/771))
71
+ - **Copy and staging failures:** preserve publication callback errors with unreadable metadata, retain uncertain native rename outcomes, and prevent repeated or reentrant cleanup from reusing consumed descriptors. ([#763](https://github.com/openclaw/fs-safe/pull/763))
72
+
73
+ ### Performance
74
+
75
+ - **Watch:** skip guarded unrelated native hints and reconcile affected directories, reducing a 502-file memory-watch fixture from 513 to 55 visits and 8.16 MB to 1.33 MB sampled allocation per relevant event pass; unrelated detailed hints at two edits/second no longer trigger 120 full scans/minute, while periodic and uncertain-event reconciliation remain full. ([#774](https://github.com/openclaw/fs-safe/pull/774))
76
+ - **Linux fallback resolution:** reuse the file type from each component's checked descriptor observation, avoiding a duplicate metadata read while retaining fresh identity, symlink-target, and mount-policy checks. ([#757](https://github.com/openclaw/fs-safe/pull/757))
77
+ - **Native parent validation:** reuse the retained descriptor's captured identity during ordinary POSIX policy checks, avoiding a duplicate metadata read while keeping the final pathname inspection fresh. ([#735](https://github.com/openclaw/fs-safe/pull/735))
78
+
79
+ ## 0.21.2 - 2026-09-28
80
+
81
+ ### Highlights
82
+
83
+ - **`require` mode confines mutations under hostile concurrency:** with `FS_SAFE_NATIVE_MODE=require` (or `configureFsSafeNative({ mode: "require" })`), remove, recursive remove, `mkdir`, writable-open creation and overwrite `move` now run through retained, identity-checked native parents: descriptor-relative `unlinkat`, `mkdirat` and `renameat2` on POSIX, and handle-relative operations on Windows. An adversarial race fuzzer observed no outside or denied-path effects across about 2.8 million racing calls on Linux (with and without `openat2`), macOS and Windows. `auto` keeps its documented best-effort paths and performance. See the [operation/platform matrix](docs/security-model.md#native-root-mutation-capabilities). ([#744](https://github.com/openclaw/fs-safe/pull/744), [#751](https://github.com/openclaw/fs-safe/pull/751), [#750](https://github.com/openclaw/fs-safe/pull/750), [#752](https://github.com/openclaw/fs-safe/pull/752))
84
+ - **Watch on macOS:** entry scopes use identity-checked kqueue descriptors instead of recursive FSEvents streams, ending overflow storms for shallow ancestor scopes above busy trees. ([#753](https://github.com/openclaw/fs-safe/pull/753))
85
+ - **Faster watch scans:** watch readiness and reconciliation are 27–34% faster on large trees. ([#747](https://github.com/openclaw/fs-safe/pull/747))
86
+
87
+ ### Fixes
88
+
89
+ - **Watch cancellation:** a subscription's `signal` closes it even when another abort listener stops event propagation. ([#738](https://github.com/openclaw/fs-safe/pull/738))
90
+ - **Walk cancellation:** walks stop without yielding the current entry when a synchronous filter aborts the signal. ([#739](https://github.com/openclaw/fs-safe/pull/739))
91
+ - **Writes with `mkdir: false`:** the JavaScript fallback writes through existing in-root parent aliases using the same guarded walk, instead of rejecting them. ([#740](https://github.com/openclaw/fs-safe/pull/740))
92
+ - **Descriptor exhaustion:** native writes that fail with `EMFILE`/`ENFILE` report `helper-failed` with the exhaustion code, the indeterminate publication outcome and the preserved stage, instead of a misleading path error. ([#742](https://github.com/openclaw/fs-safe/pull/742))
93
+ - **Walk memory:** `walkDirectory()` and `walkDirectorySync()` stop reading a directory once `maxEntries` is reached instead of enumerating it completely first. `Root.walk()` sorted mode stays deterministic. ([#743](https://github.com/openclaw/fs-safe/pull/743))
94
+
95
+ ### Compatibility
96
+
97
+ - In `require` mode, the operations above now reject with `helper-unavailable` where no confining native primitive exists, instead of silently falling back to JavaScript. Examples are recursive removal on Linux without `openat2` and recursive directory removal on Windows. Use `auto` for the best-effort fallback. These `require`-mode mutations also do more identity-checked work and are slower than before, while `auto` is unchanged. ([#744](https://github.com/openclaw/fs-safe/pull/744), [#751](https://github.com/openclaw/fs-safe/pull/751), [#750](https://github.com/openclaw/fs-safe/pull/750))
98
+ - Create-only writes to an existing directory report `already-exists` in every implementation (previously `not-file` on some Windows paths). With `mutationSymlinks` omitted, Windows native and legacy JavaScript writes still differ for final links and parent junctions; set `mutationSymlinks: "reject"` for uniform behavior. ([#745](https://github.com/openclaw/fs-safe/pull/745))
99
+
5
100
  ## 0.21.1 - 2026-09-27
6
101
 
7
102
  ### Fixes