@openclaw/openshell-sandbox 2026.9.6 → 2026.9.7

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 (244) hide show
  1. package/assets/icon.png +0 -0
  2. package/dist/index.js +75 -229
  3. package/node_modules/@openclaw/fs-safe/CHANGELOG.md +102 -0
  4. package/node_modules/@openclaw/fs-safe/README.md +24 -6
  5. package/node_modules/@openclaw/fs-safe/dist/advanced.d.ts +6 -1
  6. package/node_modules/@openclaw/fs-safe/dist/advanced.js +3 -1
  7. package/node_modules/@openclaw/fs-safe/dist/archive-durability.js +1 -1
  8. package/node_modules/@openclaw/fs-safe/dist/archive-merge.js +1 -2
  9. package/node_modules/@openclaw/fs-safe/dist/archive-plan.d.ts +2 -7
  10. package/node_modules/@openclaw/fs-safe/dist/archive-read.js +9 -18
  11. package/node_modules/@openclaw/fs-safe/dist/archive-staging.d.ts +1 -2
  12. package/node_modules/@openclaw/fs-safe/dist/archive-staging.js +3 -3
  13. package/node_modules/@openclaw/fs-safe/dist/archive-zip-directory.js +4 -0
  14. package/node_modules/@openclaw/fs-safe/dist/archive-zip-entry.d.ts +11 -11
  15. package/node_modules/@openclaw/fs-safe/dist/archive-zip-entry.js +3 -35
  16. package/node_modules/@openclaw/fs-safe/dist/archive-zip-integrity.d.ts +2 -2
  17. package/node_modules/@openclaw/fs-safe/dist/archive-zip-integrity.js +2 -12
  18. package/node_modules/@openclaw/fs-safe/dist/archive-zip-loader.d.ts +7 -3
  19. package/node_modules/@openclaw/fs-safe/dist/archive-zip-loader.js +13 -10
  20. package/node_modules/@openclaw/fs-safe/dist/archive-zip-manifest.js +3 -1
  21. package/node_modules/@openclaw/fs-safe/dist/archive-zip-preflight.d.ts +2 -3
  22. package/node_modules/@openclaw/fs-safe/dist/archive-zip-preflight.js +16 -8
  23. package/node_modules/@openclaw/fs-safe/dist/archive.d.ts +6 -3
  24. package/node_modules/@openclaw/fs-safe/dist/archive.js +22 -19
  25. package/node_modules/@openclaw/fs-safe/dist/atomic.d.ts +1 -1
  26. package/node_modules/@openclaw/fs-safe/dist/copy-publication.d.ts +1 -3
  27. package/node_modules/@openclaw/fs-safe/dist/copy-publication.js +2 -6
  28. package/node_modules/@openclaw/fs-safe/dist/deny-mutation-match.d.ts +2 -0
  29. package/node_modules/@openclaw/fs-safe/dist/deny-mutation-match.js +71 -0
  30. package/node_modules/@openclaw/fs-safe/dist/deny-mutations.js +4 -3
  31. package/node_modules/@openclaw/fs-safe/dist/directory-guard.d.ts +0 -1
  32. package/node_modules/@openclaw/fs-safe/dist/directory-guard.js +0 -3
  33. package/node_modules/@openclaw/fs-safe/dist/directory-mode-node.d.ts +20 -2
  34. package/node_modules/@openclaw/fs-safe/dist/directory-mode-node.js +77 -1
  35. package/node_modules/@openclaw/fs-safe/dist/directory-receipt.js +5 -7
  36. package/node_modules/@openclaw/fs-safe/dist/effective-uid.js +1 -4
  37. package/node_modules/@openclaw/fs-safe/dist/errors.d.ts +3 -1
  38. package/node_modules/@openclaw/fs-safe/dist/errors.js +3 -2
  39. package/node_modules/@openclaw/fs-safe/dist/file-identity.js +10 -2
  40. package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-acquisition.js +1 -1
  41. package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-admission.js +5 -1
  42. package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root-held.d.ts +4 -11
  43. package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root-held.js +1 -4
  44. package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root-io.d.ts +3 -6
  45. package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root-io.js +1 -1
  46. package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root-options.d.ts +2 -10
  47. package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root.d.ts +2 -4
  48. package/node_modules/@openclaw/fs-safe/dist/file-lock-sync.js +2 -2
  49. package/node_modules/@openclaw/fs-safe/dist/file-store-boundary.d.ts +3 -7
  50. package/node_modules/@openclaw/fs-safe/dist/file-store-boundary.js +7 -10
  51. package/node_modules/@openclaw/fs-safe/dist/file-store-prune.js +7 -6
  52. package/node_modules/@openclaw/fs-safe/dist/file-store.d.ts +4 -7
  53. package/node_modules/@openclaw/fs-safe/dist/file-store.js +19 -21
  54. package/node_modules/@openclaw/fs-safe/dist/guest-native-python.js +23 -31
  55. package/node_modules/@openclaw/fs-safe/dist/guest.js +21 -12
  56. package/node_modules/@openclaw/fs-safe/dist/json-document-store.d.ts +4 -9
  57. package/node_modules/@openclaw/fs-safe/dist/json-durable-queue-ownership.d.ts +1 -5
  58. package/node_modules/@openclaw/fs-safe/dist/json-durable-queue.d.ts +0 -1
  59. package/node_modules/@openclaw/fs-safe/dist/json-durable-queue.js +2 -7
  60. package/node_modules/@openclaw/fs-safe/dist/local-file-access.js +2 -5
  61. package/node_modules/@openclaw/fs-safe/dist/local-file-descriptor.d.ts +2 -5
  62. package/node_modules/@openclaw/fs-safe/dist/local-roots.d.ts +2 -7
  63. package/node_modules/@openclaw/fs-safe/dist/move-path-cleanup.js +4 -4
  64. package/node_modules/@openclaw/fs-safe/dist/mutation-authority.js +5 -0
  65. package/node_modules/@openclaw/fs-safe/dist/native-binding.d.ts +51 -14
  66. package/node_modules/@openclaw/fs-safe/dist/native-pinned-write.js +8 -2
  67. package/node_modules/@openclaw/fs-safe/dist/native-staged-symlink.d.ts +13 -0
  68. package/node_modules/@openclaw/fs-safe/dist/native-staged-symlink.js +303 -0
  69. package/node_modules/@openclaw/fs-safe/dist/owner-dacl-batch-worker.d.ts +1 -0
  70. package/node_modules/@openclaw/fs-safe/dist/owner-dacl-batch-worker.js +54 -0
  71. package/node_modules/@openclaw/fs-safe/dist/owner-dacl-batch.d.ts +5 -0
  72. package/node_modules/@openclaw/fs-safe/dist/owner-dacl-batch.js +64 -0
  73. package/node_modules/@openclaw/fs-safe/dist/owner-dacl.d.ts +2 -0
  74. package/node_modules/@openclaw/fs-safe/dist/owner-dacl.js +3 -0
  75. package/node_modules/@openclaw/fs-safe/dist/path.js +17 -1
  76. package/node_modules/@openclaw/fs-safe/dist/permission-exec.d.ts +6 -0
  77. package/node_modules/@openclaw/fs-safe/dist/permission-exec.js +3 -6
  78. package/node_modules/@openclaw/fs-safe/dist/permissions-public.d.ts +3 -1
  79. package/node_modules/@openclaw/fs-safe/dist/permissions-public.js +1 -0
  80. package/node_modules/@openclaw/fs-safe/dist/permissions-windows.d.ts +3 -6
  81. package/node_modules/@openclaw/fs-safe/dist/permissions.d.ts +3 -10
  82. package/node_modules/@openclaw/fs-safe/dist/permissions.js +0 -1
  83. package/node_modules/@openclaw/fs-safe/dist/pinned-mutation-admission.d.ts +0 -1
  84. package/node_modules/@openclaw/fs-safe/dist/pinned-mutation-admission.js +4 -2
  85. package/node_modules/@openclaw/fs-safe/dist/pinned-mutation-shared-route.d.ts +2 -8
  86. package/node_modules/@openclaw/fs-safe/dist/pinned-open.d.ts +1 -1
  87. package/node_modules/@openclaw/fs-safe/dist/pinned-open.js +2 -3
  88. package/node_modules/@openclaw/fs-safe/dist/publish-copy-stage.js +4 -0
  89. package/node_modules/@openclaw/fs-safe/dist/read-opened-file.d.ts +2 -5
  90. package/node_modules/@openclaw/fs-safe/dist/regular-file.js +3 -3
  91. package/node_modules/@openclaw/fs-safe/dist/replace-directory.js +5 -17
  92. package/node_modules/@openclaw/fs-safe/dist/replace-file-buffer.d.ts +4 -0
  93. package/node_modules/@openclaw/fs-safe/dist/replace-file-buffer.js +36 -0
  94. package/node_modules/@openclaw/fs-safe/dist/replace-file-copy-fallback.d.ts +4 -10
  95. package/node_modules/@openclaw/fs-safe/dist/replace-file-copy-fallback.js +66 -38
  96. package/node_modules/@openclaw/fs-safe/dist/replace-file-descriptor.d.ts +7 -11
  97. package/node_modules/@openclaw/fs-safe/dist/replace-file-descriptor.js +10 -2
  98. package/node_modules/@openclaw/fs-safe/dist/replace-file-destination.d.ts +21 -0
  99. package/node_modules/@openclaw/fs-safe/dist/replace-file-destination.js +123 -0
  100. package/node_modules/@openclaw/fs-safe/dist/replace-file-mutation.d.ts +26 -0
  101. package/node_modules/@openclaw/fs-safe/dist/replace-file-mutation.js +47 -0
  102. package/node_modules/@openclaw/fs-safe/dist/replace-file-temp-owner.d.ts +2 -2
  103. package/node_modules/@openclaw/fs-safe/dist/replace-file-temp-owner.js +16 -4
  104. package/node_modules/@openclaw/fs-safe/dist/replace-file-types.d.ts +55 -0
  105. package/node_modules/@openclaw/fs-safe/dist/replace-file-types.js +1 -0
  106. package/node_modules/@openclaw/fs-safe/dist/replace-file.d.ts +3 -55
  107. package/node_modules/@openclaw/fs-safe/dist/replace-file.js +31 -11
  108. package/node_modules/@openclaw/fs-safe/dist/retained-directory-replacement.d.ts +1 -0
  109. package/node_modules/@openclaw/fs-safe/dist/retained-directory-replacement.js +1 -1
  110. package/node_modules/@openclaw/fs-safe/dist/retained-file-types.d.ts +50 -0
  111. package/node_modules/@openclaw/fs-safe/dist/retained-file-types.js +1 -0
  112. package/node_modules/@openclaw/fs-safe/dist/retained-file.d.ts +3 -0
  113. package/node_modules/@openclaw/fs-safe/dist/retained-file.js +121 -0
  114. package/node_modules/@openclaw/fs-safe/dist/root-context.js +3 -2
  115. package/node_modules/@openclaw/fs-safe/dist/root-directory-entry.d.ts +9 -0
  116. package/node_modules/@openclaw/fs-safe/dist/root-directory-entry.js +28 -0
  117. package/node_modules/@openclaw/fs-safe/dist/root-directory-list.d.ts +9 -1
  118. package/node_modules/@openclaw/fs-safe/dist/root-directory-list.js +88 -37
  119. package/node_modules/@openclaw/fs-safe/dist/root-file-final-admission.d.ts +1 -1
  120. package/node_modules/@openclaw/fs-safe/dist/root-file-final-admission.js +1 -6
  121. package/node_modules/@openclaw/fs-safe/dist/root-file.js +2 -5
  122. package/node_modules/@openclaw/fs-safe/dist/root-handle-context.d.ts +4 -0
  123. package/node_modules/@openclaw/fs-safe/dist/root-handle-context.js +12 -0
  124. package/node_modules/@openclaw/fs-safe/dist/root-impl.d.ts +3 -3
  125. package/node_modules/@openclaw/fs-safe/dist/root-impl.js +8 -5
  126. package/node_modules/@openclaw/fs-safe/dist/root-move-noreplace.d.ts +2 -7
  127. package/node_modules/@openclaw/fs-safe/dist/root-move-noreplace.js +3 -3
  128. package/node_modules/@openclaw/fs-safe/dist/root-observed-path.d.ts +0 -1
  129. package/node_modules/@openclaw/fs-safe/dist/root-observed-path.js +0 -3
  130. package/node_modules/@openclaw/fs-safe/dist/root-path-observation.d.ts +4 -11
  131. package/node_modules/@openclaw/fs-safe/dist/root-path.js +7 -10
  132. package/node_modules/@openclaw/fs-safe/dist/root-paths.d.ts +2 -6
  133. package/node_modules/@openclaw/fs-safe/dist/root-remove-identity.d.ts +1 -3
  134. package/node_modules/@openclaw/fs-safe/dist/root-walk.d.ts +19 -12
  135. package/node_modules/@openclaw/fs-safe/dist/root-walk.js +56 -20
  136. package/node_modules/@openclaw/fs-safe/dist/root-write-admission.js +1 -6
  137. package/node_modules/@openclaw/fs-safe/dist/root-write-complete-parent.d.ts +2 -0
  138. package/node_modules/@openclaw/fs-safe/dist/root-write-complete-parent.js +1 -1
  139. package/node_modules/@openclaw/fs-safe/dist/safe-path-segment.d.ts +1 -0
  140. package/node_modules/@openclaw/fs-safe/dist/safe-path-segment.js +8 -2
  141. package/node_modules/@openclaw/fs-safe/dist/secret-file.d.ts +6 -2
  142. package/node_modules/@openclaw/fs-safe/dist/secret-file.js +12 -16
  143. package/node_modules/@openclaw/fs-safe/dist/secret-read-policy.js +1 -1
  144. package/node_modules/@openclaw/fs-safe/dist/secure-file-windows.js +5 -14
  145. package/node_modules/@openclaw/fs-safe/dist/secure-file.js +3 -2
  146. package/node_modules/@openclaw/fs-safe/dist/secure-temp-dir.js +2 -7
  147. package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-acquire.js +4 -4
  148. package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-admission-parser.d.ts +1 -2
  149. package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-handle.d.ts +2 -8
  150. package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-policy.d.ts +2 -7
  151. package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-reclaim.d.ts +2 -2
  152. package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-reclaim.js +11 -12
  153. package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-stale-admission.d.ts +1 -5
  154. package/node_modules/@openclaw/fs-safe/dist/sidecar-lock.js +9 -7
  155. package/node_modules/@openclaw/fs-safe/dist/staged-symlink-types.d.ts +39 -0
  156. package/node_modules/@openclaw/fs-safe/dist/staged-symlink-types.js +1 -0
  157. package/node_modules/@openclaw/fs-safe/dist/stat-observation.js +3 -10
  158. package/node_modules/@openclaw/fs-safe/dist/strict-file-identity.d.ts +2 -0
  159. package/node_modules/@openclaw/fs-safe/dist/strict-file-identity.js +6 -6
  160. package/node_modules/@openclaw/fs-safe/dist/symlink-parents.js +58 -7
  161. package/node_modules/@openclaw/fs-safe/dist/temp-target.js +8 -10
  162. package/node_modules/@openclaw/fs-safe/dist/temp-workspace-admission.js +28 -33
  163. package/node_modules/@openclaw/fs-safe/dist/temp-workspace-child-admission.d.ts +1 -1
  164. package/node_modules/@openclaw/fs-safe/dist/temp-workspace-child-admission.js +16 -17
  165. package/node_modules/@openclaw/fs-safe/dist/temp-workspace-descriptor.js +1 -2
  166. package/node_modules/@openclaw/fs-safe/dist/temp-workspace-owner.js +4 -9
  167. package/node_modules/@openclaw/fs-safe/dist/temp-workspace-ownership.d.ts +8 -0
  168. package/node_modules/@openclaw/fs-safe/dist/temp-workspace-ownership.js +52 -0
  169. package/node_modules/@openclaw/fs-safe/dist/test-hooks.d.ts +5 -1
  170. package/node_modules/@openclaw/fs-safe/dist/text-atomic.d.ts +2 -1
  171. package/node_modules/@openclaw/fs-safe/dist/text-atomic.js +2 -0
  172. package/node_modules/@openclaw/fs-safe/dist/trash.js +27 -1
  173. package/node_modules/@openclaw/fs-safe/dist/walk.d.ts +2 -5
  174. package/node_modules/@openclaw/fs-safe/dist/watch-alias.d.ts +6 -0
  175. package/node_modules/@openclaw/fs-safe/dist/watch-alias.js +88 -0
  176. package/node_modules/@openclaw/fs-safe/dist/watch-hints.d.ts +9 -0
  177. package/node_modules/@openclaw/fs-safe/dist/watch-hints.js +93 -0
  178. package/node_modules/@openclaw/fs-safe/dist/watch-native.d.ts +36 -0
  179. package/node_modules/@openclaw/fs-safe/dist/watch-native.js +73 -0
  180. package/node_modules/@openclaw/fs-safe/dist/watch-scan.d.ts +28 -0
  181. package/node_modules/@openclaw/fs-safe/dist/watch-scan.js +300 -0
  182. package/node_modules/@openclaw/fs-safe/dist/watch-stream.d.ts +8 -0
  183. package/node_modules/@openclaw/fs-safe/dist/watch-stream.js +32 -0
  184. package/node_modules/@openclaw/fs-safe/dist/watch-types.d.ts +60 -0
  185. package/node_modules/@openclaw/fs-safe/dist/watch-types.js +1 -0
  186. package/node_modules/@openclaw/fs-safe/dist/watch.d.ts +5 -0
  187. package/node_modules/@openclaw/fs-safe/dist/watch.js +530 -0
  188. package/node_modules/@openclaw/fs-safe/dist/windows-owner.d.ts +3 -7
  189. package/node_modules/@openclaw/fs-safe/dist/windows-owner.js +0 -1
  190. package/node_modules/@openclaw/fs-safe/dist/windows-security-bridge.cs +6 -6
  191. package/node_modules/@openclaw/fs-safe/dist/windows-security-bridge.ps1 +78 -3
  192. package/node_modules/@openclaw/fs-safe/dist/windows-security-command.d.ts +8 -0
  193. package/node_modules/@openclaw/fs-safe/dist/windows-security-command.js +66 -15
  194. package/node_modules/@openclaw/fs-safe/dist/windows-security-facts.d.ts +4 -0
  195. package/node_modules/@openclaw/fs-safe/dist/windows-security-facts.js +6 -2
  196. package/node_modules/@openclaw/fs-safe/docs/advanced.md +4 -2
  197. package/node_modules/@openclaw/fs-safe/docs/archive.md +16 -0
  198. package/node_modules/@openclaw/fs-safe/docs/atomic.md +93 -3
  199. package/node_modules/@openclaw/fs-safe/docs/contributing.md +102 -4
  200. package/node_modules/@openclaw/fs-safe/docs/copy.md +2 -0
  201. package/node_modules/@openclaw/fs-safe/docs/durability.md +7 -0
  202. package/node_modules/@openclaw/fs-safe/docs/file-store.md +5 -0
  203. package/node_modules/@openclaw/fs-safe/docs/guest.md +7 -1
  204. package/node_modules/@openclaw/fs-safe/docs/index.md +1 -0
  205. package/node_modules/@openclaw/fs-safe/docs/install.md +30 -0
  206. package/node_modules/@openclaw/fs-safe/docs/native-helper.md +20 -4
  207. package/node_modules/@openclaw/fs-safe/docs/native.md +63 -1
  208. package/node_modules/@openclaw/fs-safe/docs/permissions.md +66 -0
  209. package/node_modules/@openclaw/fs-safe/docs/public-api.md +30 -3
  210. package/node_modules/@openclaw/fs-safe/docs/retained-file.md +115 -0
  211. package/node_modules/@openclaw/fs-safe/docs/root.md +35 -9
  212. package/node_modules/@openclaw/fs-safe/docs/secret-file.md +19 -0
  213. package/node_modules/@openclaw/fs-safe/docs/security-model.md +4 -1
  214. package/node_modules/@openclaw/fs-safe/docs/sidecar-lock.md +5 -1
  215. package/node_modules/@openclaw/fs-safe/docs/staged-symlink.md +124 -0
  216. package/node_modules/@openclaw/fs-safe/docs/store.md +3 -1
  217. package/node_modules/@openclaw/fs-safe/docs/temp.md +24 -4
  218. package/node_modules/@openclaw/fs-safe/docs/testing.md +210 -0
  219. package/node_modules/@openclaw/fs-safe/docs/types.md +6 -0
  220. package/node_modules/@openclaw/fs-safe/docs/walk.md +34 -1
  221. package/node_modules/@openclaw/fs-safe/docs/watch.md +251 -0
  222. package/node_modules/@openclaw/fs-safe/docs/writing.md +21 -0
  223. package/node_modules/@openclaw/fs-safe/package.json +13 -9
  224. package/node_modules/@openclaw/fs-safe-darwin-arm64/fs-safe-native.node +0 -0
  225. package/node_modules/@openclaw/fs-safe-darwin-arm64/package.json +1 -1
  226. package/node_modules/@openclaw/fs-safe-darwin-x64/fs-safe-native.node +0 -0
  227. package/node_modules/@openclaw/fs-safe-darwin-x64/package.json +1 -1
  228. package/node_modules/@openclaw/fs-safe-linux-arm64-gnu/fs-safe-native.node +0 -0
  229. package/node_modules/@openclaw/fs-safe-linux-arm64-gnu/package.json +1 -1
  230. package/node_modules/@openclaw/fs-safe-linux-arm64-musl/fs-safe-native.node +0 -0
  231. package/node_modules/@openclaw/fs-safe-linux-arm64-musl/package.json +1 -1
  232. package/node_modules/@openclaw/fs-safe-linux-x64-gnu/fs-safe-native.node +0 -0
  233. package/node_modules/@openclaw/fs-safe-linux-x64-gnu/package.json +1 -1
  234. package/node_modules/@openclaw/fs-safe-linux-x64-musl/fs-safe-native.node +0 -0
  235. package/node_modules/@openclaw/fs-safe-linux-x64-musl/package.json +1 -1
  236. package/node_modules/@openclaw/fs-safe-win32-x64-msvc/fs-safe-native.node +0 -0
  237. package/node_modules/@openclaw/fs-safe-win32-x64-msvc/package.json +1 -1
  238. package/node_modules/p-limit/index.d.ts +2 -0
  239. package/node_modules/p-limit/index.js +17 -1
  240. package/node_modules/p-limit/package.json +1 -1
  241. package/node_modules/p-limit/readme.md +2 -0
  242. package/package.json +6 -6
  243. package/node_modules/@openclaw/fs-safe/dist/directory-mode-owner.d.ts +0 -20
  244. package/node_modules/@openclaw/fs-safe/dist/directory-mode-owner.js +0 -78
package/assets/icon.png CHANGED
Binary file
package/dist/index.js CHANGED
@@ -5,6 +5,7 @@ import fsSync from "node:fs";
5
5
  import fsPromises from "node:fs/promises";
6
6
  import path from "node:path";
7
7
  import { movePathWithCopyFallback } from "@openclaw/fs-safe/atomic";
8
+ import { GUEST_FILESYSTEM_PYTHON } from "@openclaw/fs-safe/guest";
8
9
  import { createDeferred, formatPluginConfigIssue, mapPluginConfigIssues } from "openclaw/plugin-sdk/extension-shared";
9
10
  import { KeyedAsyncQueue } from "openclaw/plugin-sdk/keyed-async-queue";
10
11
  import { FsSafeError, canonicalPathFromExistingAncestor, extractErrorCode } from "openclaw/plugin-sdk/security-runtime";
@@ -12,7 +13,7 @@ import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coer
12
13
  import { buildPluginConfigSchema } from "openclaw/plugin-sdk/core";
13
14
  import { MAX_TIMER_TIMEOUT_SECONDS } from "openclaw/plugin-sdk/number-runtime";
14
15
  import { z } from "zod";
15
- import { assertNoSymlinkParents, canonicalPathFromExistingAncestor as canonicalPathFromExistingAncestor$1, isPathInside, root } from "openclaw/plugin-sdk/file-access-runtime";
16
+ import { assertNoSymlinkParents, isPathInside, root } from "openclaw/plugin-sdk/file-access-runtime";
16
17
  import pLimit from "p-limit";
17
18
  //#region extensions/openshell/src/cli.ts
18
19
  function buildOpenShellBaseArgv(config) {
@@ -63,17 +64,6 @@ const DEFAULT_REMOTE_WORKSPACE_DIR = "/sandbox";
63
64
  const DEFAULT_REMOTE_AGENT_WORKSPACE_DIR = "/agent";
64
65
  const DEFAULT_TIMEOUT_MS = 12e4;
65
66
  const OPEN_SHELL_MANAGED_REMOTE_ROOTS = [DEFAULT_REMOTE_WORKSPACE_DIR, DEFAULT_REMOTE_AGENT_WORKSPACE_DIR];
66
- function normalizeProviders(value) {
67
- const seen = /* @__PURE__ */ new Set();
68
- const providers = [];
69
- for (const entry of value ?? []) {
70
- const normalized = entry.trim();
71
- if (seen.has(normalized)) continue;
72
- seen.add(normalized);
73
- providers.push(normalized);
74
- }
75
- return providers;
76
- }
77
67
  const nonEmptyTrimmedString = (message) => z.string({ error: message }).trim().min(1, { error: message });
78
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` });
79
69
  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" });
@@ -120,22 +110,7 @@ function createOpenShellPluginConfigSchema() {
120
110
  } });
121
111
  }
122
112
  function resolveOpenShellPluginConfig(value) {
123
- if (value === void 0) return {
124
- mode: DEFAULT_MODE,
125
- command: DEFAULT_COMMAND,
126
- gateway: void 0,
127
- gatewayEndpoint: void 0,
128
- workspace: void 0,
129
- from: DEFAULT_SOURCE,
130
- policy: void 0,
131
- providers: [],
132
- gpu: false,
133
- autoProviders: true,
134
- remoteWorkspaceDir: DEFAULT_REMOTE_WORKSPACE_DIR,
135
- remoteAgentWorkspaceDir: DEFAULT_REMOTE_AGENT_WORKSPACE_DIR,
136
- timeoutMs: DEFAULT_TIMEOUT_MS
137
- };
138
- const parsed = OpenShellPluginConfigSchema.safeParse(value);
113
+ const parsed = OpenShellPluginConfigSchema.safeParse(value === void 0 ? {} : value);
139
114
  if (!parsed.success) {
140
115
  const message = formatPluginConfigIssue(parsed.error.issues[0]);
141
116
  throw new Error(`Invalid openshell plugin config: ${message}`);
@@ -149,7 +124,7 @@ function resolveOpenShellPluginConfig(value) {
149
124
  workspace: cfg.workspace,
150
125
  from: cfg.from ?? DEFAULT_SOURCE,
151
126
  policy: cfg.policy,
152
- providers: normalizeProviders(cfg.providers),
127
+ providers: [...new Set(cfg.providers ?? [])],
153
128
  gpu: cfg.gpu ?? false,
154
129
  autoProviders: cfg.autoProviders ?? true,
155
130
  remoteWorkspaceDir: normalizeOpenShellRemotePath(cfg.remoteWorkspaceDir, DEFAULT_REMOTE_WORKSPACE_DIR, "remoteWorkspaceDir"),
@@ -196,64 +171,49 @@ var OpenShellFsBridge = class {
196
171
  }
197
172
  async readFile(params) {
198
173
  const target = this.resolveTarget(params);
199
- const hostPath = this.requireHostPath(target);
200
- let opened;
174
+ const hostPath = target.hostPath;
201
175
  try {
202
- await assertLocalPathSafety({
203
- target,
204
- allowFinalSymlinkForUnlink: false
205
- });
206
- const root$1 = await root(target.mountHostRoot);
207
- if (params.maxBytes !== void 0) return (await root$1.read(path.relative(target.mountHostRoot, hostPath), {
176
+ return (await (await root(target.mountHostRoot)).read(relativeToRoot(target, hostPath), {
208
177
  hardlinks: "reject",
209
- maxBytes: params.maxBytes
178
+ maxBytes: params.maxBytes ?? Infinity
210
179
  })).buffer;
211
- opened = await root$1.open(path.relative(target.mountHostRoot, hostPath), { hardlinks: "reject" });
212
- try {
213
- return await opened.handle.readFile();
214
- } finally {
215
- await opened.handle.close();
216
- }
217
180
  } catch (err) {
218
181
  throw new Error(`Sandbox boundary checks failed; cannot read files: ${target.containerPath}`, { cause: err });
219
182
  }
220
183
  }
221
184
  async readDirectory(params) {
222
185
  const target = this.resolveTarget(params);
223
- const hostPath = this.requireHostPath(target);
224
- await assertLocalPathSafety({
225
- target,
226
- allowFinalSymlinkForUnlink: false
227
- });
228
- return (await (await root(target.mountHostRoot)).list(relativeToRoot(target, hostPath), { withFileTypes: true })).map(({ name, isDirectory }) => ({
186
+ const hostPath = target.hostPath;
187
+ const root$1 = await root(target.mountHostRoot);
188
+ const entries = [];
189
+ for await (const { name, isDirectory } of root$1.entries(relativeToRoot(target, hostPath), { order: "sorted" })) entries.push({
229
190
  name,
230
191
  isDirectory
231
- }));
192
+ });
193
+ return entries;
232
194
  }
233
195
  async writeFile(params) {
234
196
  const target = this.resolveTarget(params);
235
- const hostPath = this.requireHostPath(target);
197
+ const hostPath = target.hostPath;
236
198
  this.ensureWritable(target, "write files");
237
- await assertLocalPathSafety({
238
- target,
239
- allowFinalSymlinkForUnlink: false
240
- });
241
199
  const buffer = Buffer.isBuffer(params.data) ? params.data : Buffer.from(params.data, params.encoding ?? "utf8");
242
- await (await root(target.mountHostRoot)).write(path.relative(target.mountHostRoot, hostPath), buffer, { mkdir: params.mkdir });
200
+ await (await root(target.mountHostRoot)).write(relativeToRoot(target, hostPath), buffer, {
201
+ mkdir: params.mkdir,
202
+ mutationSymlinks: "reject"
203
+ });
243
204
  await this.backend.syncLocalPathToRemote(hostPath, target.containerPath);
244
205
  }
245
206
  async createFileExclusive(params) {
246
207
  const target = this.resolveTarget(params);
247
- const hostPath = this.requireHostPath(target);
208
+ const hostPath = target.hostPath;
248
209
  this.ensureWritable(target, "create files");
249
- await assertLocalPathSafety({
250
- target,
251
- allowFinalSymlinkForUnlink: false
252
- });
253
210
  const buffer = Buffer.isBuffer(params.data) ? params.data : Buffer.from(params.data, params.encoding ?? "utf8");
254
211
  const root$2 = await root(target.mountHostRoot);
255
212
  try {
256
- await root$2.create(path.relative(target.mountHostRoot, hostPath), buffer, { mkdir: params.mkdir !== false });
213
+ await root$2.create(relativeToRoot(target, hostPath), buffer, {
214
+ mkdir: params.mkdir !== false,
215
+ mutationSymlinks: "reject"
216
+ });
257
217
  } catch (error) {
258
218
  if (error instanceof FsSafeError && error.code === "already-exists") return "exists";
259
219
  throw error;
@@ -263,7 +223,7 @@ var OpenShellFsBridge = class {
263
223
  }
264
224
  async mkdirp(params) {
265
225
  const target = this.resolveTarget(params);
266
- const hostPath = this.requireHostPath(target);
226
+ const hostPath = target.hostPath;
267
227
  this.ensureWritable(target, "create directories");
268
228
  await assertLocalPathSafety({
269
229
  target,
@@ -277,7 +237,7 @@ var OpenShellFsBridge = class {
277
237
  }
278
238
  async remove(params) {
279
239
  const target = this.resolveTarget(params);
280
- const hostPath = this.requireHostPath(target);
240
+ const hostPath = target.hostPath;
281
241
  this.ensureWritable(target, "remove files", params.recursive);
282
242
  await assertLocalPathSafety({
283
243
  target,
@@ -297,8 +257,8 @@ var OpenShellFsBridge = class {
297
257
  }
298
258
  async rename(params) {
299
259
  const { from, to } = this.resolveRenameTargets(params);
300
- const fromHostPath = this.requireHostPath(from);
301
- const toHostPath = this.requireHostPath(to);
260
+ const fromHostPath = from.hostPath;
261
+ const toHostPath = to.hostPath;
302
262
  await assertLocalPathSafety({
303
263
  target: from,
304
264
  allowFinalSymlinkForUnlink: true
@@ -324,7 +284,7 @@ var OpenShellFsBridge = class {
324
284
  }
325
285
  async stat(params) {
326
286
  const target = this.resolveTarget(params);
327
- const hostPath = this.requireHostPath(target);
287
+ const hostPath = target.hostPath;
328
288
  const stats = await fsPromises.lstat(hostPath).catch(() => null);
329
289
  if (!stats) return null;
330
290
  await assertLocalPathSafety({
@@ -348,10 +308,6 @@ var OpenShellFsBridge = class {
348
308
  workdir
349
309
  })), ...this.sandbox.readOnlyResourceMounts ?? []];
350
310
  }
351
- requireHostPath(target) {
352
- if (!target.hostPath) throw new Error(`OpenShell mirror bridge requires a local host path: ${target.containerPath}`);
353
- return target.hostPath;
354
- }
355
311
  containerMounts(readOnlyMounts = this.readOnlyMounts()) {
356
312
  const workspaceRoot = path.resolve(this.sandbox.workspaceDir);
357
313
  const agentRoot = path.resolve(this.sandbox.agentWorkspaceDir);
@@ -450,7 +406,7 @@ async function removeLocalRootPath(params) {
450
406
  const relativePath = relativeToRoot(params.target, params.hostPath);
451
407
  try {
452
408
  if (params.force === false) await fsPromises.lstat(params.hostPath);
453
- const targets = params.recursive && !relativePath ? await root$3.list("") : [relativePath];
409
+ const targets = params.recursive && !relativePath ? (await root$3.list("")).map((name) => relativeToRoot(params.target, path.join(params.hostPath, name))) : [relativePath];
454
410
  for (const target of targets) await root$3.remove(target, {
455
411
  force: params.force !== false,
456
412
  ...params.recursive ? {
@@ -479,7 +435,7 @@ async function mkdirParentPath(root, relativePath) {
479
435
  }
480
436
  function relativeToRoot(target, hostPath) {
481
437
  const relativePath = path.relative(target.mountHostRoot, hostPath);
482
- return relativePath === "." ? "" : relativePath;
438
+ return relativePath ? `.${path.sep}${relativePath}` : "";
483
439
  }
484
440
  async function assertRenameSourceSupported(fromHostPath) {
485
441
  const stats = await fsPromises.lstat(fromHostPath);
@@ -513,21 +469,14 @@ async function nearestExistingDirectoryStats(params) {
513
469
  return await fsPromises.lstat(rootPath);
514
470
  }
515
471
  function isNotFoundError(err) {
516
- return err instanceof FsSafeError && err.code === "not-found" || typeof err === "object" && err !== null && "code" in err && err.code === "ENOENT";
472
+ return err instanceof FsSafeError && err.code === "not-found" || extractErrorCode(err) === "ENOENT";
517
473
  }
518
474
  async function assertLocalPathSafety(params) {
519
475
  const { hostPath, mountHostRoot } = params.target;
520
- if (!hostPath) throw new Error(`Missing local host path for ${params.target.containerPath}`);
521
- const canonicalRoot = await fsPromises.realpath(mountHostRoot).catch((error) => {
522
- if (isNotFoundError(error)) return path.resolve(mountHostRoot);
523
- throw error;
524
- });
525
476
  const unlinkSymlink = params.allowFinalSymlinkForUnlink && hostPath !== mountHostRoot && (await fsPromises.lstat(hostPath).catch((error) => {
526
477
  if (isNotFoundError(error)) return null;
527
478
  throw error;
528
479
  }))?.isSymbolicLink();
529
- const candidate = unlinkSymlink ? path.resolve(canonicalRoot, path.relative(mountHostRoot, hostPath)) : await canonicalPathFromExistingAncestor$1(hostPath);
530
- if (!isPathInside(canonicalRoot, candidate)) throw new Error(`Sandbox path escapes allowed mounts; cannot access: ${params.target.containerPath}`);
531
480
  await assertNoSymlinkParents({
532
481
  rootDir: mountHostRoot,
533
482
  targetPath: unlinkSymlink ? path.dirname(hostPath) : hostPath,
@@ -619,107 +568,28 @@ function buildOpenShellDirectoryUploadArgs(params) {
619
568
  ];
620
569
  }
621
570
  const REMOTE_MANAGED_ROOTS_EMPTY_SCRIPT = "for root in \"$@\"; do if [ -d \"$root\" ] && [ -n \"$(ls -A \"$root\")\" ]; then printf \"1\\n\"; exit 0; fi; done; printf \"0\\n\"";
622
- const PINNED_REMOTE_PATH_MUTATION_SCRIPT = [
623
- "set -eu",
624
- "die() { echo \"$1\" >&2; exit 1; }",
625
- "validate_basename() {",
626
- " case \"$1\" in \"\"|\".\"|\"..\"|*/*) die \"unsafe remote basename: $1\" ;; esac",
627
- "}",
628
- "pin_dir() {",
629
- " root=\"$1\"",
630
- " relative=\"$2\"",
631
- " create=\"$3\"",
632
- " case \"$root\" in /*) ;; *) die \"remote root must be absolute: $root\" ;; esac",
633
- " root=\"${root%/}\"",
634
- " [ -n \"$root\" ] || root=\"/\"",
635
- " if [ -L \"$root\" ]; then die \"unsafe remote root symlink: $root\"; fi",
636
- " mkdir -p -- \"$root\"",
637
- " canonical_root=\"$(cd \"$root\" && pwd -P)\"",
638
- " current=\"$canonical_root\"",
639
- " relative=\"${relative#/}\"",
640
- " while [ -n \"$relative\" ]; do",
641
- " part=\"${relative%%/*}\"",
642
- " if [ \"$part\" = \"$relative\" ]; then relative=\"\"; else relative=\"${relative#*/}\"; fi",
643
- " [ -n \"$part\" ] || continue",
644
- " case \"$part\" in \".\"|\"..\") die \"unsafe remote directory component: $part\" ;; esac",
645
- " if [ \"$current\" = \"/\" ]; then next=\"/$part\"; else next=\"$current/$part\"; fi",
646
- " if [ -L \"$next\" ]; then die \"unsafe remote directory symlink: $next\"; fi",
647
- " if [ -e \"$next\" ]; then",
648
- " if [ ! -d \"$next\" ]; then die \"unsafe remote directory component: $next\"; fi",
649
- " else",
650
- " if [ \"$create\" != \"1\" ]; then die \"remote directory not found: $next\"; fi",
651
- " mkdir -- \"$next\"",
652
- " fi",
653
- " current=\"$next\"",
654
- " done",
655
- " printf \"%s\\n\" \"$current\"",
656
- "}",
657
- "pin_dir_or_missing() {",
658
- " root=\"$1\"",
659
- " relative=\"$2\"",
660
- " missing_ok=\"$3\"",
661
- " case \"$root\" in /*) ;; *) die \"remote root must be absolute: $root\" ;; esac",
662
- " root=\"${root%/}\"",
663
- " [ -n \"$root\" ] || root=\"/\"",
664
- " if [ -L \"$root\" ]; then die \"unsafe remote root symlink: $root\"; fi",
665
- " if [ ! -d \"$root\" ]; then",
666
- " if [ -e \"$root\" ]; then die \"unsafe remote root component: $root\"; fi",
667
- " if [ \"$missing_ok\" = \"1\" ]; then printf \"\\n\"; return 0; fi",
668
- " die \"remote directory not found: $root\"",
669
- " fi",
670
- " canonical_root=\"$(cd \"$root\" && pwd -P)\"",
671
- " current=\"$canonical_root\"",
672
- " relative=\"${relative#/}\"",
673
- " while [ -n \"$relative\" ]; do",
674
- " part=\"${relative%%/*}\"",
675
- " if [ \"$part\" = \"$relative\" ]; then relative=\"\"; else relative=\"${relative#*/}\"; fi",
676
- " [ -n \"$part\" ] || continue",
677
- " case \"$part\" in \".\"|\"..\") die \"unsafe remote directory component: $part\" ;; esac",
678
- " if [ \"$current\" = \"/\" ]; then next=\"/$part\"; else next=\"$current/$part\"; fi",
679
- " if [ -L \"$next\" ]; then die \"unsafe remote directory symlink: $next\"; fi",
680
- " if [ -e \"$next\" ]; then",
681
- " if [ ! -d \"$next\" ]; then die \"unsafe remote directory component: $next\"; fi",
682
- " else",
683
- " if [ \"$missing_ok\" = \"1\" ]; then printf \"\\n\"; return 0; fi",
684
- " die \"remote directory not found: $next\"",
685
- " fi",
686
- " current=\"$next\"",
687
- " done",
688
- " printf \"%s\\n\" \"$current\"",
689
- "}",
690
- "operation=\"$1\"",
691
- "case \"$operation\" in",
692
- " mkdirp)",
693
- " pin_dir \"$2\" \"$3\" 1 >/dev/null",
694
- " ;;",
695
- " remove)",
696
- " validate_basename \"$4\"",
697
- " parent=\"$(pin_dir_or_missing \"$2\" \"$3\" \"${5:-0}\")\"",
698
- " [ -n \"$parent\" ] || exit 0",
699
- " target=\"$parent/$4\"",
700
- " if [ -d \"$target\" ] && [ ! -L \"$target\" ]; then rm -rf -- \"$target\"; elif [ -e \"$target\" ] || [ -L \"$target\" ]; then rm -f -- \"$target\"; fi",
701
- " ;;",
702
- " removefile)",
703
- " validate_basename \"$4\"",
704
- " parent=\"$(pin_dir_or_missing \"$2\" \"$3\" \"${5:-0}\")\"",
705
- " [ -n \"$parent\" ] || exit 0",
706
- " target=\"$parent/$4\"",
707
- " if [ -d \"$target\" ] && [ ! -L \"$target\" ]; then rmdir -- \"$target\"; elif [ -e \"$target\" ] || [ -L \"$target\" ]; then rm -f -- \"$target\"; fi",
708
- " ;;",
709
- " rename)",
710
- " src_parent=\"$(pin_dir \"$2\" \"$3\" 0)\"",
711
- " validate_basename \"$4\"",
712
- " dst_parent=\"$(pin_dir \"$5\" \"$6\" 1)\"",
713
- " validate_basename \"$7\"",
714
- " if [ -L \"$dst_parent/$7\" ]; then die \"unsafe remote rename target symlink: $dst_parent/$7\"; fi",
715
- " if [ -d \"$dst_parent/$7\" ]; then die \"unsafe remote rename target directory: $dst_parent/$7\"; fi",
716
- " mv -- \"$src_parent/$4\" \"$dst_parent/$7\"",
717
- " ;;",
718
- " *)",
719
- " die \"unknown remote path mutation: $operation\"",
720
- " ;;",
721
- "esac"
722
- ].join("\n");
571
+ const OPEN_SHELL_GUEST_MUTATION_PYTHON = `
572
+ import os, sys
573
+ guest = sys.argv.pop(1)
574
+ ignore_missing_parent = sys.argv.pop(1) == '1'
575
+ operation = sys.argv[1]
576
+ for index in ((2, 5) if operation == 'rename' else (2,)):
577
+ root = sys.argv[index].rstrip('/') or '/'
578
+ sys.argv[index] = root
579
+ if not os.path.isabs(root) or os.path.islink(root):
580
+ raise OSError('unsafe remote root: ' + root)
581
+ if operation != 'remove':
582
+ os.makedirs(root, exist_ok=True)
583
+ if operation == 'rename':
584
+ target = os.path.join(sys.argv[5], sys.argv[6], sys.argv[7])
585
+ if os.path.islink(target) or os.path.isdir(target):
586
+ raise OSError('unsafe remote rename target: ' + target)
587
+ try:
588
+ exec(guest)
589
+ except FileNotFoundError:
590
+ if operation != 'remove' or not ignore_missing_parent:
591
+ raise
592
+ `;
723
593
  const ENSURE_OPEN_SHELL_REMOTE_REAL_DIRECTORY_SCRIPT = [
724
594
  "set -e",
725
595
  "target=\"$1\"",
@@ -767,9 +637,6 @@ const ENSURE_OPEN_SHELL_REMOTE_REAL_DIRECTORY_SCRIPT = [
767
637
  " current=\"$next\"",
768
638
  "done"
769
639
  ].join("\n");
770
- function buildOpenShellSshExecEnv() {
771
- return sanitizeEnvVars(process.env).allowed;
772
- }
773
640
  function createOpenShellSandboxBackendFactory(params) {
774
641
  return async (createParams) => await createOpenShellSandboxBackend({
775
642
  ...params,
@@ -868,7 +735,7 @@ var OpenShellSandboxBackendImpl = class {
868
735
  });
869
736
  return {
870
737
  argv: pending.argv,
871
- env: buildOpenShellSshExecEnv(),
738
+ env: sanitizeEnvVars(process.env).allowed,
872
739
  stdinMode: "pipe-open",
873
740
  finalizeToken: pending.token
874
741
  };
@@ -1004,16 +871,7 @@ var OpenShellSandboxBackendImpl = class {
1004
871
  }
1005
872
  resolveWorkdirValidationRoot(workdir) {
1006
873
  try {
1007
- const normalized = normalizeRemotePath(workdir);
1008
- return resolveOpenShellWorkspaceRoot([{
1009
- remote: normalizeRemotePath(this.params.remoteWorkspaceDir),
1010
- owner: "workspace",
1011
- value: void 0
1012
- }, {
1013
- remote: normalizeRemotePath(this.params.remoteAgentWorkspaceDir),
1014
- owner: "agent",
1015
- value: void 0
1016
- }], normalized)?.remote ?? this.params.remoteWorkspaceDir;
874
+ return this.resolveRemoteTarget(workdir).root;
1017
875
  } catch {
1018
876
  return this.params.remoteWorkspaceDir;
1019
877
  }
@@ -1080,12 +938,14 @@ var OpenShellSandboxBackendImpl = class {
1080
938
  const target = this.resolveRemoteTarget(remotePath);
1081
939
  await this.runPinnedRemotePathMutation({
1082
940
  args: [
1083
- params?.recursive ? "remove" : "removefile",
941
+ "remove",
1084
942
  target.root,
1085
943
  path.posix.dirname(target.relativePath) === "." ? "" : path.posix.dirname(target.relativePath),
1086
944
  path.posix.basename(target.relativePath),
1087
- params?.ignoreMissing ? "1" : "0"
945
+ params?.recursive ? "1" : "0",
946
+ "1"
1088
947
  ],
948
+ ignoreMissingParent: params?.ignoreMissing,
1089
949
  signal: params?.signal
1090
950
  });
1091
951
  }
@@ -1100,7 +960,8 @@ var OpenShellSandboxBackendImpl = class {
1100
960
  path.posix.basename(from.relativePath),
1101
961
  to.root,
1102
962
  path.posix.dirname(to.relativePath) === "." ? "" : path.posix.dirname(to.relativePath),
1103
- path.posix.basename(to.relativePath)
963
+ path.posix.basename(to.relativePath),
964
+ "1"
1104
965
  ],
1105
966
  signal
1106
967
  });
@@ -1130,24 +991,11 @@ var OpenShellSandboxBackendImpl = class {
1130
991
  await this.maybeSeedRemoteWorkspace();
1131
992
  const target = this.resolveRemoteTarget(remotePath);
1132
993
  const stats = await fsPromises.lstat(localPath).catch(() => null);
1133
- if (!stats) {
1134
- await this.runPinnedRemotePathMutation({ args: [
1135
- "remove",
1136
- target.root,
1137
- path.posix.dirname(target.relativePath) === "." ? "" : path.posix.dirname(target.relativePath),
1138
- path.posix.basename(target.relativePath),
1139
- "1"
1140
- ] });
1141
- return;
1142
- }
1143
- if (stats.isSymbolicLink()) {
1144
- await this.runPinnedRemotePathMutation({ args: [
1145
- "remove",
1146
- target.root,
1147
- path.posix.dirname(target.relativePath) === "." ? "" : path.posix.dirname(target.relativePath),
1148
- path.posix.basename(target.relativePath),
1149
- "1"
1150
- ] });
994
+ if (!stats || stats.isSymbolicLink()) {
995
+ await this.removeRemotePath(remotePath, {
996
+ recursive: true,
997
+ ignoreMissing: true
998
+ });
1151
999
  return;
1152
1000
  }
1153
1001
  if (stats.isDirectory()) {
@@ -1175,8 +1023,13 @@ var OpenShellSandboxBackendImpl = class {
1175
1023
  }
1176
1024
  async runPinnedRemotePathMutation(params) {
1177
1025
  return await this.runRemoteShellScript({
1178
- script: PINNED_REMOTE_PATH_MUTATION_SCRIPT,
1179
- args: params.args,
1026
+ script: "python_script=\"$1\"; shift; python3 -c \"$python_script\" \"$@\"",
1027
+ args: [
1028
+ OPEN_SHELL_GUEST_MUTATION_PYTHON,
1029
+ GUEST_FILESYSTEM_PYTHON,
1030
+ params.ignoreMissingParent ? "1" : "0",
1031
+ ...params.args
1032
+ ],
1180
1033
  signal: params.signal
1181
1034
  });
1182
1035
  }
@@ -1412,7 +1265,7 @@ var OpenShellSandboxBackendImpl = class {
1412
1265
  }
1413
1266
  const relativeSkillsPath = path.posix.relative(root.remote, remoteSkillsWorkspaceDir);
1414
1267
  if (relativeSkillsPath === "" || !relativeSkillsPath.startsWith("../") && !path.posix.isAbsolute(relativeSkillsPath)) await removeDownloadedWorkspacePath(tmpDir, relativeSkillsPath.split("/").filter(Boolean));
1415
- if (root.owner === "workspace") await moveLocalShadowAside({
1268
+ await moveLocalShadowAside({
1416
1269
  workspaceDir: root.local,
1417
1270
  tmpDir,
1418
1271
  relativeParts: MATERIALIZED_SKILLS_REMOTE_PARTS,
@@ -1567,14 +1420,7 @@ async function removeDownloadedWorkspacePath(tmpDir, parts) {
1567
1420
  const next = path.join(cursor, part);
1568
1421
  const stats = await fsPromises.lstat(next).catch(() => null);
1569
1422
  if (!stats) return;
1570
- if (index === parts.length - 1) {
1571
- await fsPromises.rm(next, {
1572
- recursive: true,
1573
- force: true
1574
- });
1575
- return;
1576
- }
1577
- if (stats.isSymbolicLink() || !stats.isDirectory()) {
1423
+ if (index === parts.length - 1 || stats.isSymbolicLink() || !stats.isDirectory()) {
1578
1424
  await fsPromises.rm(next, {
1579
1425
  recursive: true,
1580
1426
  force: true
@@ -2,6 +2,108 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.21.1 - 2026-09-27
6
+
7
+ ### Fixes
8
+
9
+ - **Linux without `openat2`:** native opens now follow in-root relative symlinks exactly as `RESOLVE_BENEATH` does, rejecting absolute links, `..` escapes, procfs and `nosymfollow` links, and chains longer than 40. Denial and symlink policies report the same errors as the `openat2` path. Links inside sticky world-writable directories are refused. ([#717](https://github.com/openclaw/fs-safe/pull/717))
10
+ - **Deny policies on case-insensitive filesystems:** `denyMutations` now denies creating a not-yet-existing case or Unicode-normalization alias of a denied path (for example `case.txt` when `Case.txt` is denied on APFS, NTFS or casefold ext4). Where a filesystem's sensitivity cannot be proven, Unicode-equivalent names are denied conservatively. ([#720](https://github.com/openclaw/fs-safe/pull/720))
11
+ - **Directory listings:** `list()`, `entries()` and `walk()` reject names that are not valid UTF-8 with `invalid-path` instead of decoding them lossily, which could collide with a real `U+FFFD` name and report another entry's metadata. ([#715](https://github.com/openclaw/fs-safe/pull/715))
12
+ - **ZIP admission:** entries that differ only by case or Unicode normalization are rejected during preflight and member reads, matching extraction's collision policy. ([#716](https://github.com/openclaw/fs-safe/pull/716))
13
+ - **Atomic restore:** when a metadata call fails with `EIO` after the destination was truncated, restore-original writes the original bytes back through the retained, identity-verified descriptor instead of leaving the file empty. ([#718](https://github.com/openclaw/fs-safe/pull/718))
14
+ - **Lock and prune cleanup:** cleanup decisions compare exact bigint file identities. Legacy numeric receipts with IDs above 2^53 are refused instead of risking removal of the wrong file. ([#719](https://github.com/openclaw/fs-safe/pull/719))
15
+ - **Watch overflow:** subscriptions no longer publish spurious whole-scope overflow invalidations for excluded or unselected paths or for slow reconcile passes. macOS streams use OS-level exclusion paths and anchor-scoped streams, and Windows uses a 1 MiB change buffer on local volumes. Genuine OS event drops still invalidate. ([#724](https://github.com/openclaw/fs-safe/pull/724))
16
+ - **Mutation authority:** reject synchronous and asynchronous generator callback results before mutation, so lazy authority checks cannot be skipped. ([#711](https://github.com/openclaw/fs-safe/pull/711))
17
+ - **Synchronous atomic writes:** reject deferred `beforeRename` hooks before publication and clean up the owned stage. ([#728](https://github.com/openclaw/fs-safe/pull/728))
18
+ - **Copy publication callbacks:** reject deferred generator observers in `Root.copyIn()` while preserving the completed destination and source bytes. ([#726](https://github.com/openclaw/fs-safe/pull/726))
19
+ - **Native move diagnostics:** no-clobber `Root.move()` keeps the original native loader error as its cause, including missing libraries or incompatible glibc versions. ([#709](https://github.com/openclaw/fs-safe/pull/709))
20
+
21
+ ### Features
22
+
23
+ - **Watch polling interval:** `pollIntervalMs` sets the scan interval whenever polling is the selected transport, including `mode: "auto"` falling back to polling, without changing the events reconcile interval. ([#723](https://github.com/openclaw/fs-safe/pull/723))
24
+
25
+ ### Performance
26
+
27
+ - **Linux directory cloning:** reuse the admitted filesystem type when creating clone sources and cloning trees, avoiding a duplicate filesystem probe and allocation per operation. ([#732](https://github.com/openclaw/fs-safe/pull/732))
28
+
29
+ ## 0.21.0 - 2026-09-26
30
+
31
+ ### Highlights
32
+
33
+ - **Guarded filesystem observation:** `@openclaw/fs-safe/watch` observes literal entry and tree scopes beneath an admitted Root and delivers bounded advisory invalidations; guarded scans stay authoritative. One shared Rust event thread per process uses inotify, FSEvents and ReadDirectoryChangesW on Linux, macOS and Windows, sleeps while idle, and falls back to portable polling under `mode: "auto"`. Subscriptions become ready after one baseline scan, stay available under sustained writes, fence stale generations on `setScopes()`, join native work on `close()`, and support `persistent: false` so one-shot Node commands can exit. Thanks @vincentkoc. ([#690](https://github.com/openclaw/fs-safe/pull/690), [#695](https://github.com/openclaw/fs-safe/pull/695), [#697](https://github.com/openclaw/fs-safe/pull/697), [#703](https://github.com/openclaw/fs-safe/pull/703), [#707](https://github.com/openclaw/fs-safe/pull/707))
34
+ - **Linux user namespaces:** private temp workspaces now work beneath unmapped ancestors, including systemd user services with `PrivateUsers=true`, while UID/GID mappings and ancestor permissions are still rechecked. ([#693](https://github.com/openclaw/fs-safe/pull/693))
35
+
36
+ ### Features
37
+
38
+ - **Revocable atomic writes:** atomic replacement can recheck caller authority before new effects and report retained destination identities for partial writes and publication, without treating receipts as permission to roll back. ([#694](https://github.com/openclaw/fs-safe/pull/694))
39
+ - **Root walking:** `symlinkPolicy: "include"` reports links without following their targets, preserving sorted traversal, entry budgets, and the existing skip/follow result types. Directory-to-symlink substitutions fail before descent. ([#692](https://github.com/openclaw/fs-safe/pull/692))
40
+ - **Windows file retirement:** the advanced surface can retire existing NTFS files through a retained handle, with exact producer identity, current authority, and separate disposition and settlement facts. There is no persistence guarantee. ([#706](https://github.com/openclaw/fs-safe/pull/706))
41
+
42
+ ### Performance
43
+
44
+ - **Temporary filename sanitization:** skip duplicate normalization after reserved-device suffixing while preserving filename admission and fallback rules. ([#689](https://github.com/openclaw/fs-safe/pull/689))
45
+
46
+ ### Compatibility
47
+
48
+ - Supplied temp workspace roots must be owned by the effective user and must not be group- or world-writable; use a private per-user root instead of shared `/tmp`. A one-time warning reports host ownership that cannot be verified. ([#693](https://github.com/openclaw/fs-safe/pull/693))
49
+ - On Windows, an events-mode watch holds one handle on each watched Root, so the Root's own ancestor directories cannot be renamed while it is open; renaming inside the Root is unaffected. Use `mode: "poll"` where that matters. ([#697](https://github.com/openclaw/fs-safe/pull/697))
50
+ - `watch()` requires an explicit `mode` (`"auto"`, `"events"` or `"poll"`); `"events"` fails readiness with `helper-unavailable` when no native backend is available, and Bun currently selects polling under `"auto"`. ([#690](https://github.com/openclaw/fs-safe/pull/690))
51
+
52
+ ## 0.20.0 - 2026-09-25
53
+
54
+ ### Highlights
55
+
56
+ - **Older Linux systems:** when `openat2` is absent (Linux before 5.6) or blocked by seccomp, native operations fall back to a guarded no-follow walk instead of failing with `ENOSYS`. Nested no-clobber moves keep working, and GNU x64/arm64 bindings now target glibc 2.28, so native operations load on RHEL 8-family systems. ([#686](https://github.com/openclaw/fs-safe/pull/686), [#685](https://github.com/openclaw/fs-safe/pull/685); fixes [#572](https://github.com/openclaw/fs-safe/issues/572), [#511](https://github.com/openclaw/fs-safe/issues/511), [#548](https://github.com/openclaw/fs-safe/issues/548))
57
+ - **Path hardening:** trash moves stay inside allowed roots, symlink-parent checks inspect raw segments before dotdot normalization, relative-escape checks recognize either Windows separator and nested escapes, and safe path segments reject Windows reserved device names. Thanks @SebTardif. ([#611](https://github.com/openclaw/fs-safe/pull/611), [#615](https://github.com/openclaw/fs-safe/pull/615), [#614](https://github.com/openclaw/fs-safe/pull/614), [#612](https://github.com/openclaw/fs-safe/pull/612))
58
+ - **ZIP admission per operation:** portable ZIP reads and extraction bind entry selection to the admitted archive, name, and decoder object, and verify payloads against the admitted CRC and size. Substituted, renamed, or replaced decoder entries are rejected. ([#660](https://github.com/openclaw/fs-safe/pull/660))
59
+
60
+ ### Features
61
+
62
+ - **Retained symlink publication:** `retainSymlinkInDirectory()` on the advanced surface holds an explicitly identified POSIX symlink through exact-slot no-replace publication and explicit recovery, preserving observed foreign replacements and uncertain outcomes. ([#682](https://github.com/openclaw/fs-safe/pull/682))
63
+ - **Batched Windows ACL facts:** `readOwnerAndDaclBatch()` inspects ordered paths in one isolated native worker or one PowerShell process, with a configurable whole-batch timeout and bounded output. Results are bounded during collection; oversized batches reject with `too-large` before later paths are queried. ([#669](https://github.com/openclaw/fs-safe/pull/669), [#680](https://github.com/openclaw/fs-safe/pull/680))
64
+ - **Atomic text options:** `writeTextAtomic()` forwards the existing `beforeRename` hook and `tempPrefix` option to atomic replacement, keeping its validation and identity checks. ([#580](https://github.com/openclaw/fs-safe/pull/580))
65
+
66
+ ### Fixes
67
+
68
+ - **Lock exit cleanup:** leave raw sidecars in place when Windows reports an unknown device or inode, while keeping token-owned cleanup working when the descriptor and path identities legitimately differ (for example on VirtioFS). Thanks @SebTardif. ([#617](https://github.com/openclaw/fs-safe/pull/617))
69
+ - **Retained symlink errors:** preserve uncertain publication outcomes and cached cleanup/recovery failures when inspecting thrown error metadata fails, retaining the original cause without retrying mutations, callbacks, or descriptor closes. ([#684](https://github.com/openclaw/fs-safe/pull/684))
70
+ - **Create collision cleanup:** atomic and streamed creates remove their private stage when the JavaScript fallback observes a competing destination before publication; failures after a link attempt retain their existing recovery evidence. ([#683](https://github.com/openclaw/fs-safe/pull/683))
71
+
72
+ ### Compatibility
73
+
74
+ - Safe path segments now reject Windows reserved device names (`CON`, `NUL`, `COM1`, `CON.json`, …) on every platform, because segments are portable identifiers. Temporary filename sanitization still suffixes them (`CON.txt` becomes `CON_.txt`). ([#612](https://github.com/openclaw/fs-safe/pull/612))
75
+ - `assertNoSymlinkParents()` and guarded appends with `rejectSymlinkParents` reject raw spellings whose dotdot segments would cancel a symlink or re-enter the root after leaving it. ([#615](https://github.com/openclaw/fs-safe/pull/615))
76
+ - Without `openat2`, beneath opens report `best-effort` containment instead of `kernel-atomic`, anonymous `O_TMPFILE` opens report `ENOTSUP`, and bounded tree cleanup fails closed with `helper-unavailable` because it requires `RESOLVE_NO_XDEV`. ([#686](https://github.com/openclaw/fs-safe/pull/686))
77
+ - The Linux GNU binding floor is glibc 2.28 for both x64 and arm64 (arm64 was previously built against 2.17), matching Node 22's own Linux baseline. ([#685](https://github.com/openclaw/fs-safe/pull/685))
78
+
79
+ ## 0.19.0 - 2026-09-24
80
+
81
+ ### Highlights
82
+
83
+ - **Strict secret-file durability:** `createSecretFileAtomic()` accepts `durable: "file"` to require every file flush to succeed, including on `EPERM`; parent-directory synchronization remains best effort. ([#644](https://github.com/openclaw/fs-safe/pull/644))
84
+ - **Literal `~` names:** keep FileStore keys, absolute Root reads, discovered walk entries, and ZIP/TAR entries literal instead of treating them as home-directory shorthand. Reads, writes, removal, pruning, extraction, and durable publication select the intended entry. ([#633](https://github.com/openclaw/fs-safe/pull/633))
85
+ - **Native Unix descriptor safety:** reject negative descriptors in low-level root, query, hash, copy, clone, staging, and cleanup calls without Rust panics or working-directory operations. Modern macOS beneath opens report `EBADF` for negative roots instead of `EIO`. ([#640](https://github.com/openclaw/fs-safe/pull/640), [#643](https://github.com/openclaw/fs-safe/pull/643), [#646](https://github.com/openclaw/fs-safe/pull/646))
86
+
87
+ ### Fixes
88
+
89
+ - **Guest directory permissions:** cross-device moves preserve mode `000`, including nested directories, without changing other modes' umask behavior. Restore top-level permissions through the retained directory descriptor and preserve published entries on failure. Thanks @SebTardif. ([#616](https://github.com/openclaw/fs-safe/pull/616))
90
+ - **ReFS clone cleanup:** remove ordinary partial output when Windows rejects the ignore-readonly deletion flag. Keep readonly attributes intact and include cleanup failures in the original clone error; readonly files or other processes' open handles can still leave output behind. ([#648](https://github.com/openclaw/fs-safe/pull/648))
91
+
92
+ ### Compatibility and documentation
93
+
94
+ - Secret creation's default and boolean durability options retain their behavior, and `writeSecretFileAtomic()` remains boolean-only. Strict file synchronization preserves existing publication and cleanup semantics: a failure after publication can leave a complete file present. ([#644](https://github.com/openclaw/fs-safe/pull/644))
95
+ - `Root.walk("~")` and `Root.walk("~/dir")` expand home shorthand when iteration starts and return admitted canonical paths relative to the Root. Use `./~/dir` for a literal tilde directory, and prefix returned entry paths with `./` when passing them to another Root method. Relative Root `~/name` inputs still expand home; FileStore keys do not. ([#633](https://github.com/openclaw/fs-safe/pull/633))
96
+ - Guest moves still require OS permission to read mode-000 source directories; they do not widen source permissions. If permission restoration fails after publication, the source and published copy remain for caller reconciliation. ([#616](https://github.com/openclaw/fs-safe/pull/616))
97
+ - Clarify that `Root.append()` and `openWritable()` creation modes remain subject to the process umask and do not chmod existing files. This documents existing behavior. ([#636](https://github.com/openclaw/fs-safe/pull/636))
98
+
99
+ ## 0.18.2 - 2026-09-22
100
+
101
+ ### Fixes
102
+
103
+ - **Lock errors:** preserve Root stale-removal rejections and `null` or `undefined` retry failures instead of masking them with timeouts or `TypeError`s. ([#624](https://github.com/openclaw/fs-safe/pull/624))
104
+ - **Secret-file reads:** report caught `null` or `undefined` inspection and read failures as structured errors instead of internal `TypeError`s. ([#623](https://github.com/openclaw/fs-safe/pull/623))
105
+ - **Secret-file creation:** preserve `null` or `undefined` thrown by parameter getters instead of masking them with an internal `TypeError`. ([#625](https://github.com/openclaw/fs-safe/pull/625))
106
+
5
107
  ## 0.18.1 - 2026-09-22
6
108
 
7
109
  ### Fixes