@openclaw/openshell-sandbox 2026.10.5-beta.4 → 2026.10.10-beta.2

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 (240) hide show
  1. package/README.md +1 -1
  2. package/node_modules/@openclaw/fs-safe/CHANGELOG.md +148 -0
  3. package/node_modules/@openclaw/fs-safe/README.md +20 -4
  4. package/node_modules/@openclaw/fs-safe/dist/advanced.d.ts +2 -1
  5. package/node_modules/@openclaw/fs-safe/dist/advanced.js +1 -0
  6. package/node_modules/@openclaw/fs-safe/dist/archive-merge.js +28 -3
  7. package/node_modules/@openclaw/fs-safe/dist/archive-parser.wasm +0 -0
  8. package/node_modules/@openclaw/fs-safe/dist/archive-read.js +11 -5
  9. package/node_modules/@openclaw/fs-safe/dist/archive-zip-loader.js +4 -2
  10. package/node_modules/@openclaw/fs-safe/dist/archive.js +2 -2
  11. package/node_modules/@openclaw/fs-safe/dist/atomic-io.js +2 -2
  12. package/node_modules/@openclaw/fs-safe/dist/bounded-read.d.ts +1 -0
  13. package/node_modules/@openclaw/fs-safe/dist/bounded-read.js +9 -2
  14. package/node_modules/@openclaw/fs-safe/dist/clone-metadata.d.ts +2 -0
  15. package/node_modules/@openclaw/fs-safe/dist/clone-metadata.js +2 -1
  16. package/node_modules/@openclaw/fs-safe/dist/config.d.ts +6 -0
  17. package/node_modules/@openclaw/fs-safe/dist/config.js +5 -0
  18. package/node_modules/@openclaw/fs-safe/dist/copy-file-input.d.ts +3 -1
  19. package/node_modules/@openclaw/fs-safe/dist/copy-file-input.js +8 -7
  20. package/node_modules/@openclaw/fs-safe/dist/copy-link.d.ts +26 -0
  21. package/node_modules/@openclaw/fs-safe/dist/copy-link.js +170 -0
  22. package/node_modules/@openclaw/fs-safe/dist/copy-metadata.d.ts +9 -0
  23. package/node_modules/@openclaw/fs-safe/dist/copy-metadata.js +39 -0
  24. package/node_modules/@openclaw/fs-safe/dist/copy-path-admission.js +11 -2
  25. package/node_modules/@openclaw/fs-safe/dist/copy-root-file-sync.d.ts +2 -4
  26. package/node_modules/@openclaw/fs-safe/dist/copy-root-file-sync.js +4 -2
  27. package/node_modules/@openclaw/fs-safe/dist/copy-tree-portable.js +3 -6
  28. package/node_modules/@openclaw/fs-safe/dist/copy.js +3 -3
  29. package/node_modules/@openclaw/fs-safe/dist/create.js +1 -1
  30. package/node_modules/@openclaw/fs-safe/dist/creation-boundary.js +7 -4
  31. package/node_modules/@openclaw/fs-safe/dist/darwin-acl.d.ts +12 -0
  32. package/node_modules/@openclaw/fs-safe/dist/darwin-acl.js +29 -0
  33. package/node_modules/@openclaw/fs-safe/dist/directory-durability.d.ts +7 -4
  34. package/node_modules/@openclaw/fs-safe/dist/directory-guard.js +20 -5
  35. package/node_modules/@openclaw/fs-safe/dist/directory-mode-node.d.ts +3 -0
  36. package/node_modules/@openclaw/fs-safe/dist/directory-mode-node.js +27 -2
  37. package/node_modules/@openclaw/fs-safe/dist/directory-receipt.d.ts +7 -7
  38. package/node_modules/@openclaw/fs-safe/dist/directory-receipt.js +9 -4
  39. package/node_modules/@openclaw/fs-safe/dist/durability.d.ts +2 -1
  40. package/node_modules/@openclaw/fs-safe/dist/entry-publication.js +2 -2
  41. package/node_modules/@openclaw/fs-safe/dist/exact-identity.d.ts +17 -0
  42. package/node_modules/@openclaw/fs-safe/dist/exact-identity.js +29 -0
  43. package/node_modules/@openclaw/fs-safe/dist/file-handle-transfer.js +4 -2
  44. package/node_modules/@openclaw/fs-safe/dist/file-hash.d.ts +1 -1
  45. package/node_modules/@openclaw/fs-safe/dist/file-hash.js +11 -9
  46. package/node_modules/@openclaw/fs-safe/dist/file-identity.d.ts +7 -1
  47. package/node_modules/@openclaw/fs-safe/dist/file-identity.js +27 -5
  48. package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-admission.js +1 -2
  49. package/node_modules/@openclaw/fs-safe/dist/file-lock-sync.js +8 -1
  50. package/node_modules/@openclaw/fs-safe/dist/file-lock.d.ts +1 -0
  51. package/node_modules/@openclaw/fs-safe/dist/file-lock.js +1 -0
  52. package/node_modules/@openclaw/fs-safe/dist/file-write-lease.d.ts +9 -0
  53. package/node_modules/@openclaw/fs-safe/dist/file-write-lease.js +42 -0
  54. package/node_modules/@openclaw/fs-safe/dist/guarded-mkdir.d.ts +1 -0
  55. package/node_modules/@openclaw/fs-safe/dist/guarded-mkdir.js +22 -15
  56. package/node_modules/@openclaw/fs-safe/dist/guest-native-python.js +3 -0
  57. package/node_modules/@openclaw/fs-safe/dist/hardlink-fallback.d.ts +1 -0
  58. package/node_modules/@openclaw/fs-safe/dist/hardlink-fallback.js +5 -0
  59. package/node_modules/@openclaw/fs-safe/dist/index.d.ts +1 -0
  60. package/node_modules/@openclaw/fs-safe/dist/index.js +1 -0
  61. package/node_modules/@openclaw/fs-safe/dist/json-durable-queue-ownership.js +16 -3
  62. package/node_modules/@openclaw/fs-safe/dist/local-file-descriptor.js +24 -13
  63. package/node_modules/@openclaw/fs-safe/dist/native-binding.d.ts +85 -18
  64. package/node_modules/@openclaw/fs-safe/dist/native-config.d.ts +5 -0
  65. package/node_modules/@openclaw/fs-safe/dist/native-config.js +11 -0
  66. package/node_modules/@openclaw/fs-safe/dist/native-directory-observation.js +6 -4
  67. package/node_modules/@openclaw/fs-safe/dist/native-fallback-warning.d.ts +2 -1
  68. package/node_modules/@openclaw/fs-safe/dist/native-fallback-warning.js +26 -2
  69. package/node_modules/@openclaw/fs-safe/dist/native-guidance.d.ts +3 -0
  70. package/node_modules/@openclaw/fs-safe/dist/native-guidance.js +21 -0
  71. package/node_modules/@openclaw/fs-safe/dist/native-move-fallback.d.ts +1 -1
  72. package/node_modules/@openclaw/fs-safe/dist/native-move-fallback.js +2 -0
  73. package/node_modules/@openclaw/fs-safe/dist/native-operations.js +8 -6
  74. package/node_modules/@openclaw/fs-safe/dist/native-parent-admission.d.ts +44 -4
  75. package/node_modules/@openclaw/fs-safe/dist/native-parent-admission.js +85 -15
  76. package/node_modules/@openclaw/fs-safe/dist/native-pinned-write-windows.d.ts +3 -3
  77. package/node_modules/@openclaw/fs-safe/dist/native-pinned-write-windows.js +33 -6
  78. package/node_modules/@openclaw/fs-safe/dist/native-pinned-write.d.ts +4 -1
  79. package/node_modules/@openclaw/fs-safe/dist/native-pinned-write.js +22 -3
  80. package/node_modules/@openclaw/fs-safe/dist/native-policy-directory-observation.js +4 -3
  81. package/node_modules/@openclaw/fs-safe/dist/native-policy-parent.d.ts +1 -1
  82. package/node_modules/@openclaw/fs-safe/dist/native-policy-parent.js +3 -0
  83. package/node_modules/@openclaw/fs-safe/dist/native-staged-file-metadata.d.ts +3 -0
  84. package/node_modules/@openclaw/fs-safe/dist/native-staged-file-metadata.js +16 -0
  85. package/node_modules/@openclaw/fs-safe/dist/native-staged-file.d.ts +1 -1
  86. package/node_modules/@openclaw/fs-safe/dist/native-staged-file.js +15 -5
  87. package/node_modules/@openclaw/fs-safe/dist/native-stat-identity.d.ts +3 -0
  88. package/node_modules/@openclaw/fs-safe/dist/native-stat-identity.js +7 -0
  89. package/node_modules/@openclaw/fs-safe/dist/native-status.d.ts +17 -0
  90. package/node_modules/@openclaw/fs-safe/dist/native-status.js +30 -0
  91. package/node_modules/@openclaw/fs-safe/dist/native.d.ts +31 -4
  92. package/node_modules/@openclaw/fs-safe/dist/native.js +107 -9
  93. package/node_modules/@openclaw/fs-safe/dist/opened-realpath.js +9 -1
  94. package/node_modules/@openclaw/fs-safe/dist/overwrite-file-handle.js +4 -1
  95. package/node_modules/@openclaw/fs-safe/dist/owner-dacl-batch.js +1 -1
  96. package/node_modules/@openclaw/fs-safe/dist/owner-dacl.js +1 -1
  97. package/node_modules/@openclaw/fs-safe/dist/permission-exec.js +9 -1
  98. package/node_modules/@openclaw/fs-safe/dist/permissions-public.d.ts +1 -0
  99. package/node_modules/@openclaw/fs-safe/dist/permissions-public.js +1 -0
  100. package/node_modules/@openclaw/fs-safe/dist/permissions-windows.js +83 -47
  101. package/node_modules/@openclaw/fs-safe/dist/pinned-open.d.ts +2 -0
  102. package/node_modules/@openclaw/fs-safe/dist/pinned-open.js +8 -3
  103. package/node_modules/@openclaw/fs-safe/dist/pinned-write-input.js +13 -0
  104. package/node_modules/@openclaw/fs-safe/dist/pinned-write-staged.js +10 -8
  105. package/node_modules/@openclaw/fs-safe/dist/pinned-write-types.d.ts +2 -1
  106. package/node_modules/@openclaw/fs-safe/dist/pinned-write.js +7 -5
  107. package/node_modules/@openclaw/fs-safe/dist/pipe.d.ts +9 -0
  108. package/node_modules/@openclaw/fs-safe/dist/pipe.js +26 -0
  109. package/node_modules/@openclaw/fs-safe/dist/private-directory.js +1 -1
  110. package/node_modules/@openclaw/fs-safe/dist/private-producer-handoff.js +2 -5
  111. package/node_modules/@openclaw/fs-safe/dist/private-temp-workspace.d.ts +3 -2
  112. package/node_modules/@openclaw/fs-safe/dist/private-temp-workspace.js +38 -10
  113. package/node_modules/@openclaw/fs-safe/dist/publish-copy-stage.js +92 -10
  114. package/node_modules/@openclaw/fs-safe/dist/publish-file-failure.d.ts +3 -0
  115. package/node_modules/@openclaw/fs-safe/dist/publish-file-failure.js +3 -0
  116. package/node_modules/@openclaw/fs-safe/dist/publish-file.d.ts +5 -3
  117. package/node_modules/@openclaw/fs-safe/dist/publish-file.js +26 -20
  118. package/node_modules/@openclaw/fs-safe/dist/read-opened-file.d.ts +2 -0
  119. package/node_modules/@openclaw/fs-safe/dist/read-opened-file.js +3 -4
  120. package/node_modules/@openclaw/fs-safe/dist/realpath.js +32 -0
  121. package/node_modules/@openclaw/fs-safe/dist/regular-file.d.ts +4 -0
  122. package/node_modules/@openclaw/fs-safe/dist/regular-file.js +11 -15
  123. package/node_modules/@openclaw/fs-safe/dist/replace-file-descriptor.js +3 -2
  124. package/node_modules/@openclaw/fs-safe/dist/root-copy.d.ts +18 -0
  125. package/node_modules/@openclaw/fs-safe/dist/root-copy.js +128 -0
  126. package/node_modules/@openclaw/fs-safe/dist/root-create-native.d.ts +8 -13
  127. package/node_modules/@openclaw/fs-safe/dist/root-create-native.js +252 -27
  128. package/node_modules/@openclaw/fs-safe/dist/root-directory-creation.js +5 -1
  129. package/node_modules/@openclaw/fs-safe/dist/root-directory-list-types.d.ts +11 -8
  130. package/node_modules/@openclaw/fs-safe/dist/root-directory-list.js +35 -13
  131. package/node_modules/@openclaw/fs-safe/dist/root-file-open-batch-sync.d.ts +8 -0
  132. package/node_modules/@openclaw/fs-safe/dist/root-file-open-batch-sync.js +108 -0
  133. package/node_modules/@openclaw/fs-safe/dist/root-file.d.ts +7 -0
  134. package/node_modules/@openclaw/fs-safe/dist/root-file.js +15 -0
  135. package/node_modules/@openclaw/fs-safe/dist/root-impl.d.ts +2 -0
  136. package/node_modules/@openclaw/fs-safe/dist/root-impl.js +154 -175
  137. package/node_modules/@openclaw/fs-safe/dist/root-mkdir-native.d.ts +2 -0
  138. package/node_modules/@openclaw/fs-safe/dist/root-mkdir-native.js +239 -0
  139. package/node_modules/@openclaw/fs-safe/dist/root-move-noreplace.js +37 -15
  140. package/node_modules/@openclaw/fs-safe/dist/root-native-creation-path.d.ts +32 -0
  141. package/node_modules/@openclaw/fs-safe/dist/root-native-creation-path.js +56 -0
  142. package/node_modules/@openclaw/fs-safe/dist/root-options.d.ts +2 -0
  143. package/node_modules/@openclaw/fs-safe/dist/root-public-types.d.ts +1 -0
  144. package/node_modules/@openclaw/fs-safe/dist/root-remove-native.js +95 -16
  145. package/node_modules/@openclaw/fs-safe/dist/root-remove.js +6 -4
  146. package/node_modules/@openclaw/fs-safe/dist/root-walk.js +23 -19
  147. package/node_modules/@openclaw/fs-safe/dist/root-write-admission.js +8 -6
  148. package/node_modules/@openclaw/fs-safe/dist/root-write-publication.js +5 -1
  149. package/node_modules/@openclaw/fs-safe/dist/secure-file-windows.js +3 -1
  150. package/node_modules/@openclaw/fs-safe/dist/secure-file.d.ts +2 -0
  151. package/node_modules/@openclaw/fs-safe/dist/secure-file.js +10 -4
  152. package/node_modules/@openclaw/fs-safe/dist/sibling-staged-file.js +1 -1
  153. package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-reclaim.js +2 -2
  154. package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-retained.js +1 -1
  155. package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-target.js +8 -1
  156. package/node_modules/@openclaw/fs-safe/dist/staged-directory.d.ts +13 -0
  157. package/node_modules/@openclaw/fs-safe/dist/staged-directory.js +29 -0
  158. package/node_modules/@openclaw/fs-safe/dist/temp-workspace-child-admission.js +5 -3
  159. package/node_modules/@openclaw/fs-safe/dist/temp-workspace-descriptor.d.ts +8 -0
  160. package/node_modules/@openclaw/fs-safe/dist/temp-workspace-descriptor.js +13 -2
  161. package/node_modules/@openclaw/fs-safe/dist/temp-workspace-owner.js +71 -3
  162. package/node_modules/@openclaw/fs-safe/dist/temp-workspace-ownership.d.ts +2 -1
  163. package/node_modules/@openclaw/fs-safe/dist/temp-workspace-ownership.js +22 -0
  164. package/node_modules/@openclaw/fs-safe/dist/temp-workspace-types.d.ts +3 -0
  165. package/node_modules/@openclaw/fs-safe/dist/test-hooks.d.ts +1 -0
  166. package/node_modules/@openclaw/fs-safe/dist/test-hooks.js +1 -0
  167. package/node_modules/@openclaw/fs-safe/dist/trash.js +2 -2
  168. package/node_modules/@openclaw/fs-safe/dist/types.d.ts +4 -0
  169. package/node_modules/@openclaw/fs-safe/dist/watch-native.js +2 -2
  170. package/node_modules/@openclaw/fs-safe/dist/windows-appcontainer.d.ts +5 -0
  171. package/node_modules/@openclaw/fs-safe/dist/windows-appcontainer.js +39 -0
  172. package/node_modules/@openclaw/fs-safe/dist/windows-owner.d.ts +1 -0
  173. package/node_modules/@openclaw/fs-safe/dist/windows-owner.js +25 -36
  174. package/node_modules/@openclaw/fs-safe/dist/windows-security-command.js +24 -5
  175. package/node_modules/@openclaw/fs-safe/dist/windows-security-facts.d.ts +2 -1
  176. package/node_modules/@openclaw/fs-safe/dist/windows-security-facts.js +4 -0
  177. package/node_modules/@openclaw/fs-safe/dist/windows-security-helper.d.ts +2 -0
  178. package/node_modules/@openclaw/fs-safe/dist/windows-security-helper.exe +0 -0
  179. package/node_modules/@openclaw/fs-safe/dist/windows-security-helper.js +7 -0
  180. package/node_modules/@openclaw/fs-safe/dist/windows-test-hooks.d.ts +16 -0
  181. package/node_modules/@openclaw/fs-safe/dist/windows-test-hooks.js +51 -0
  182. package/node_modules/@openclaw/fs-safe/dist/write-file-handle.d.ts +1 -0
  183. package/node_modules/@openclaw/fs-safe/dist/write-file-handle.js +12 -10
  184. package/node_modules/@openclaw/fs-safe/docs/advanced.md +2 -1
  185. package/node_modules/@openclaw/fs-safe/docs/archive.md +16 -0
  186. package/node_modules/@openclaw/fs-safe/docs/config.md +71 -1
  187. package/node_modules/@openclaw/fs-safe/docs/contributing.md +45 -5
  188. package/node_modules/@openclaw/fs-safe/docs/copy.md +45 -5
  189. package/node_modules/@openclaw/fs-safe/docs/durability.md +34 -7
  190. package/node_modules/@openclaw/fs-safe/docs/errors.md +8 -0
  191. package/node_modules/@openclaw/fs-safe/docs/guest.md +2 -0
  192. package/node_modules/@openclaw/fs-safe/docs/install.md +47 -27
  193. package/node_modules/@openclaw/fs-safe/docs/migrating-to-0.5.md +6 -6
  194. package/node_modules/@openclaw/fs-safe/docs/migrating-to-0.6.md +1 -1
  195. package/node_modules/@openclaw/fs-safe/docs/native-helper.md +79 -6
  196. package/node_modules/@openclaw/fs-safe/docs/native.md +279 -8
  197. package/node_modules/@openclaw/fs-safe/docs/permissions.md +64 -15
  198. package/node_modules/@openclaw/fs-safe/docs/pipe.md +81 -0
  199. package/node_modules/@openclaw/fs-safe/docs/platform-capabilities.md +63 -0
  200. package/node_modules/@openclaw/fs-safe/docs/public-api.md +10 -0
  201. package/node_modules/@openclaw/fs-safe/docs/queue-link-free-design.md +237 -0
  202. package/node_modules/@openclaw/fs-safe/docs/quickstart.md +23 -3
  203. package/node_modules/@openclaw/fs-safe/docs/reading.md +56 -2
  204. package/node_modules/@openclaw/fs-safe/docs/regular-file.md +2 -0
  205. package/node_modules/@openclaw/fs-safe/docs/secret-file.md +5 -5
  206. package/node_modules/@openclaw/fs-safe/docs/secure-file.md +1 -1
  207. package/node_modules/@openclaw/fs-safe/docs/security-model.md +35 -0
  208. package/node_modules/@openclaw/fs-safe/docs/sidecar-lock.md +31 -13
  209. package/node_modules/@openclaw/fs-safe/docs/staged-file.md +18 -0
  210. package/node_modules/@openclaw/fs-safe/docs/store.md +15 -0
  211. package/node_modules/@openclaw/fs-safe/docs/temp.md +39 -0
  212. package/node_modules/@openclaw/fs-safe/docs/testing.md +13 -0
  213. package/node_modules/@openclaw/fs-safe/docs/types.md +51 -81
  214. package/node_modules/@openclaw/fs-safe/docs/walk.md +4 -1
  215. package/node_modules/@openclaw/fs-safe/docs/watch.md +3 -2
  216. package/node_modules/@openclaw/fs-safe/docs/writing.md +121 -14
  217. package/node_modules/@openclaw/fs-safe/package.json +21 -12
  218. package/node_modules/@openclaw/fs-safe-darwin-arm64/fs-safe-native.node +0 -0
  219. package/node_modules/@openclaw/fs-safe-darwin-arm64/package.json +1 -1
  220. package/node_modules/@openclaw/fs-safe-darwin-x64/fs-safe-native.node +0 -0
  221. package/node_modules/@openclaw/fs-safe-darwin-x64/package.json +1 -1
  222. package/node_modules/@openclaw/fs-safe-freebsd-arm64/fs-safe-native.node +0 -0
  223. package/node_modules/@openclaw/fs-safe-freebsd-arm64/package.json +29 -0
  224. package/node_modules/@openclaw/fs-safe-freebsd-x64/fs-safe-native.node +0 -0
  225. package/node_modules/@openclaw/fs-safe-freebsd-x64/package.json +29 -0
  226. package/node_modules/@openclaw/fs-safe-linux-arm64-gnu/fs-safe-native.node +0 -0
  227. package/node_modules/@openclaw/fs-safe-linux-arm64-gnu/package.json +1 -1
  228. package/node_modules/@openclaw/fs-safe-linux-arm64-musl/fs-safe-native.node +0 -0
  229. package/node_modules/@openclaw/fs-safe-linux-arm64-musl/package.json +1 -1
  230. package/node_modules/@openclaw/fs-safe-linux-x64-gnu/fs-safe-native.node +0 -0
  231. package/node_modules/@openclaw/fs-safe-linux-x64-gnu/package.json +1 -1
  232. package/node_modules/@openclaw/fs-safe-linux-x64-musl/fs-safe-native.node +0 -0
  233. package/node_modules/@openclaw/fs-safe-linux-x64-musl/package.json +1 -1
  234. package/node_modules/@openclaw/fs-safe-win32-arm64-msvc/fs-safe-native.node +0 -0
  235. package/node_modules/@openclaw/fs-safe-win32-arm64-msvc/package.json +29 -0
  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/package.json +5 -5
  239. package/node_modules/@openclaw/fs-safe/dist/windows-security-bridge.cs +0 -336
  240. package/node_modules/@openclaw/fs-safe/dist/windows-security-bridge.ps1 +0 -90
@@ -3,7 +3,7 @@ import { closeSync, openSync, readSync, readdirSync, } from "node:fs";
3
3
  import { createRequire } from "node:module";
4
4
  import { FsSafeError } from "./errors.js";
5
5
  import { captureNativeFdClose } from "./native-binding.js";
6
- import { getFsSafeNativeConfig } from "./native-config.js";
6
+ import { getFsSafeNativeConfig, getNativeModeSetting } from "./native-config.js";
7
7
  const require = createRequire(import.meta.url);
8
8
  // Native registration creates process-lifetime housekeeping. Use the import
9
9
  // context rather than retaining the first operation's AsyncLocalStorage stores.
@@ -12,6 +12,62 @@ let binding;
12
12
  let loadError;
13
13
  let attempted = false;
14
14
  let loadBinding = loadBundledBinding;
15
+ let resolvePackage = (name) => require.resolve(name);
16
+ let loadOutcome = { reason: "not-probed" };
17
+ let selectedTarget;
18
+ let targetDescription;
19
+ function loadFailure(error, packageName) {
20
+ try {
21
+ return classifyLoadFailure(error, packageName);
22
+ }
23
+ catch {
24
+ // Diagnostic inspection must not replace an arbitrary thrown load value.
25
+ return { reason: "load-failed", code: "UNKNOWN" };
26
+ }
27
+ }
28
+ function classifyLoadFailure(error, packageName) {
29
+ const code = typeof error === "object" && error !== null && "code" in error ? error.code : undefined;
30
+ // Resolution must fail for the selected package itself. An exception inside
31
+ // an installed package (even one naming this package) is not package omission.
32
+ if (code === "MODULE_NOT_FOUND" && error instanceof Error &&
33
+ error.message.split("\n", 1)[0] === `Cannot find module '${packageName}'`) {
34
+ try {
35
+ resolvePackage(packageName);
36
+ }
37
+ catch (resolutionError) {
38
+ if (resolutionError instanceof Error && "code" in resolutionError &&
39
+ resolutionError.code === "MODULE_NOT_FOUND" &&
40
+ resolutionError.message.split("\n", 1)[0] === `Cannot find module '${packageName}'`) {
41
+ return { reason: "package-missing" };
42
+ }
43
+ }
44
+ }
45
+ // An allowlist excludes paths, secrets and arbitrary text in exception codes.
46
+ const knownCodes = ["ERR_DLOPEN_FAILED", "MODULE_NOT_FOUND", "ERR_MODULE_NOT_FOUND",
47
+ "ERR_PACKAGE_PATH_NOT_EXPORTED", "ERR_INVALID_PACKAGE_CONFIG", "EACCES", "EPERM", "ENOENT"];
48
+ return { reason: "load-failed", code: typeof code === "string" && knownCodes.includes(code) ? code : "UNKNOWN" };
49
+ }
50
+ /** Internal view of the cached load; never attempts to load an addon. */
51
+ export function getNativeFallbackDiagnostic(...capabilities) {
52
+ const setting = getNativeModeSetting();
53
+ const target = targetDescription === undefined ? bundledTarget() : selectedTarget;
54
+ const packageName = target === undefined ? undefined : nativePackageForTarget(target);
55
+ const manifest = require("../package.json");
56
+ const expected = {
57
+ target: targetDescription,
58
+ packageName,
59
+ packageVersion: packageName === undefined ? undefined : manifest.optionalDependencies[packageName],
60
+ };
61
+ if (setting.mode === "off")
62
+ return { ...expected, reason: "disabled", source: setting.source };
63
+ const loaded = binding;
64
+ if (loaded) {
65
+ const missing = capabilities.filter(name => typeof loaded[name] !== "function");
66
+ if (missing.length)
67
+ return { ...expected, reason: "capability-missing", capabilities: missing };
68
+ }
69
+ return { ...expected, ...loadOutcome };
70
+ }
15
71
  function isMuslFilename(file) {
16
72
  return file.includes("libc.musl-") || file.includes("ld-musl-");
17
73
  }
@@ -127,19 +183,24 @@ function isMusl() {
127
183
  return false;
128
184
  }
129
185
  function targetFor(platform, arch, musl) {
130
- if (platform === "win32" && arch === "x64")
131
- return "win32-x64-msvc";
186
+ if (platform === "win32" && (arch === "x64" || arch === "arm64"))
187
+ return `win32-${arch}-msvc`;
132
188
  if (platform === "darwin" && arch === "x64")
133
189
  return "darwin-x64";
134
190
  if (platform === "darwin" && arch === "arm64")
135
191
  return "darwin-arm64";
192
+ if (platform === "freebsd" && (arch === "x64" || arch === "arm64"))
193
+ return `freebsd-${arch}`;
136
194
  if (platform === "linux" && (arch === "x64" || arch === "arm64")) {
137
195
  return `linux-${arch}-${musl ? "musl" : "gnu"}`;
138
196
  }
139
197
  return undefined;
140
198
  }
141
199
  function bundledTarget() {
142
- return targetFor(process.platform, process.arch, isMusl());
200
+ const musl = isMusl();
201
+ selectedTarget = targetFor(process.platform, process.arch, musl);
202
+ targetDescription = selectedTarget ?? `${process.platform}-${process.arch}${process.platform === "linux" ? `-${musl ? "musl" : "gnu"}` : ""}`;
203
+ return selectedTarget;
143
204
  }
144
205
  function nativePackageForTarget(target) {
145
206
  return `@openclaw/fs-safe-${target}`;
@@ -147,9 +208,13 @@ function nativePackageForTarget(target) {
147
208
  function loadBundledBinding() {
148
209
  const target = bundledTarget();
149
210
  if (!target) {
211
+ loadOutcome = { reason: "unsupported-target" };
150
212
  throw new Error(`Unsupported OS or architecture: ${process.platform}-${process.arch}`);
151
213
  }
152
214
  const loaded = runNativeInitialization(() => require(nativePackageForTarget(target)));
215
+ if (typeof loaded.closeOwnedFd !== "function") {
216
+ loadOutcome = { reason: "capability-missing", capabilities: ["closeOwnedFd"] };
217
+ }
153
218
  captureNativeFdClose(loaded);
154
219
  return loaded;
155
220
  }
@@ -166,7 +231,24 @@ export function __nativeLoaderDetectorsForTest() {
166
231
  export function __nativeTargetForTest(platform, arch, musl = false) {
167
232
  return targetFor(platform, arch, musl);
168
233
  }
169
- export function getNativeBinding() {
234
+ export function selectNativeBinding(native, ...capabilities) {
235
+ if (!native)
236
+ return undefined;
237
+ const missing = capabilities.filter((name) => typeof native[name] !== "function");
238
+ if (missing.length === 0)
239
+ return native;
240
+ if (getFsSafeNativeConfig().mode === "require") {
241
+ throw new FsSafeError("helper-unavailable", `native fs-safe capabilities unavailable: ${missing.join(", ")}`);
242
+ }
243
+ return undefined;
244
+ }
245
+ export function assertNativeCapabilities(native, ...capabilities) {
246
+ const missing = capabilities.filter((name) => typeof native[name] !== "function");
247
+ if (missing.length) {
248
+ throw new FsSafeError("helper-unavailable", `native fs-safe capabilities unavailable: ${missing.join(", ")}`);
249
+ }
250
+ }
251
+ export function getNativeBinding(...capabilities) {
170
252
  const { mode } = getFsSafeNativeConfig();
171
253
  if (mode === "off")
172
254
  return undefined;
@@ -174,15 +256,23 @@ export function getNativeBinding() {
174
256
  attempted = true;
175
257
  try {
176
258
  const loaded = loadBinding();
259
+ if (typeof loaded.closeOwnedFd !== "function") {
260
+ loadOutcome = { reason: "capability-missing", capabilities: ["closeOwnedFd"] };
261
+ }
177
262
  captureNativeFdClose(loaded);
178
263
  binding = loaded;
264
+ loadOutcome = { reason: "loaded" };
179
265
  }
180
266
  catch (error) {
181
267
  loadError = error;
268
+ if (loadOutcome.reason === "not-probed") {
269
+ selectedTarget ??= bundledTarget();
270
+ loadOutcome = loadFailure(error, nativePackageForTarget(selectedTarget ?? `${process.platform}-${process.arch}`));
271
+ }
182
272
  }
183
273
  }
184
274
  if (binding)
185
- return binding;
275
+ return selectNativeBinding(binding, ...capabilities);
186
276
  if (mode === "require") {
187
277
  throw new FsSafeError("helper-unavailable", "native fs-safe helper is unavailable", {
188
278
  cause: loadError,
@@ -190,8 +280,8 @@ export function getNativeBinding() {
190
280
  }
191
281
  return undefined;
192
282
  }
193
- export function requireNativeBinding() {
194
- const native = getNativeBinding();
283
+ export function requireNativeBinding(...capabilities) {
284
+ const native = getNativeBinding(...capabilities);
195
285
  if (!native) {
196
286
  throw new FsSafeError("helper-unavailable", "native fs-safe helper is unavailable", {
197
287
  cause: loadError,
@@ -199,15 +289,23 @@ export function requireNativeBinding() {
199
289
  }
200
290
  return native;
201
291
  }
202
- export function __setNativeLoaderForTest(loader) {
292
+ export function __setNativeLoaderForTest(loader, resolver) {
203
293
  binding = undefined;
204
294
  loadError = undefined;
205
295
  attempted = false;
206
296
  loadBinding = loader;
297
+ loadOutcome = { reason: "not-probed" };
298
+ selectedTarget = undefined;
299
+ targetDescription = undefined;
300
+ resolvePackage = resolver ?? ((name) => require.resolve(name));
207
301
  }
208
302
  export function __resetNativeLoaderForTest() {
209
303
  binding = undefined;
210
304
  loadError = undefined;
211
305
  attempted = false;
212
306
  loadBinding = loadBundledBinding;
307
+ loadOutcome = { reason: "not-probed" };
308
+ selectedTarget = undefined;
309
+ targetDescription = undefined;
310
+ resolvePackage = (name) => require.resolve(name);
213
311
  }
@@ -26,7 +26,15 @@ export async function resolveOpenedFileRealPathForFd(fd, handleStat, ioPath) {
26
26
  : [];
27
27
  for (const fdPath of fdCandidates) {
28
28
  try {
29
- const fdRealPath = realpathSync.native(fdPath);
29
+ // Linux procfs already reports the descriptor's physical pathname. Keep
30
+ // the fresh identity check without walking that canonical path again.
31
+ const fdLink = fdPath.startsWith("/proc/") ? fsSync.readlinkSync(fdPath) : undefined;
32
+ // The deletion suffix can also name a live entry or a symlink. Preserve
33
+ // canonicalization for that ambiguous spelling, including literal names.
34
+ const fdRealPath = fdLink !== undefined && !fdLink.endsWith(" (deleted)")
35
+ ? fdLink : realpathSync.native(fdPath);
36
+ if (!path.isAbsolute(fdRealPath))
37
+ continue;
30
38
  const fdRealStat = statOptions ? fsSync.statSync(fdRealPath, statOptions) : fsSync.statSync(fdRealPath);
31
39
  if (sameFileIdentity(handleStat, fdRealStat)) {
32
40
  return { realPath: fdRealPath, stat: fdRealStat };
@@ -1,3 +1,4 @@
1
+ import fs from "node:fs";
1
2
  import { FsSafeError } from "./errors.js";
2
3
  import { assertSynchronousCallbackResult } from "./mutation-authority.js";
3
4
  import { readFileWindowFully } from "./positional-read.js";
@@ -10,7 +11,9 @@ export async function overwriteFileHandle(handle, data, options = {}) {
10
11
  const beforeWrite = options.beforeWrite;
11
12
  const payload = data.subarray(0, data.byteLength);
12
13
  const payloadBytes = payload.byteLength;
13
- const stat = await handle.stat();
14
+ const fd = handle.fd;
15
+ // Keep FileHandle's closed-handle error while avoiding a metadata I/O hop.
16
+ const stat = fd < 0 ? await handle.stat() : fs.fstatSync(fd);
14
17
  if (!stat.isFile()) {
15
18
  throw new FsSafeError("not-file", "overwrite handle must be a regular file");
16
19
  }
@@ -57,7 +57,7 @@ export async function readOwnerAndDaclBatch(paths, options = {}) {
57
57
  if (config.mode === "require") {
58
58
  throw new FsSafeError("helper-unavailable", "Windows owner and DACL facts require an up-to-date native helper");
59
59
  }
60
- warnNativeFallback("windows-owner-dacl", "Windows owner and DACL inspection uses a slower built-in system command.");
60
+ warnNativeFallback("windows-owner-dacl", "Windows owner and DACL inspection launches the packaged .NET helper for each check.", "readOwnerAndDacl");
61
61
  }
62
62
  const facts = await readWindowsSecurityFactsBatch(selected, { timeoutMs, native: useNative, mode: config.mode });
63
63
  return facts.map(projectOwnerAndDacl);
@@ -15,7 +15,7 @@ export function readOwnerAndDacl(targetPath) {
15
15
  if (getFsSafeNativeConfig().mode === "require") {
16
16
  throw new FsSafeError("helper-unavailable", "Windows owner and DACL facts require an up-to-date native helper");
17
17
  }
18
- warnNativeFallback("windows-owner-dacl", "Windows owner and DACL inspection uses a slower built-in system command.");
18
+ warnNativeFallback("windows-owner-dacl", "Windows owner and DACL inspection launches the packaged .NET helper for each check.", "readOwnerAndDacl");
19
19
  }
20
20
  const facts = typeof inspect === "function" ? inspect.call(native, targetPath) : readWindowsSecurityFactsCommand(targetPath);
21
21
  return projectOwnerAndDacl(facts);
@@ -1,7 +1,9 @@
1
+ import { assertWindowsHelperAllowed } from "./windows-appcontainer.js";
1
2
  import { execFile } from "node:child_process";
2
3
  import path from "node:path";
3
4
  import { promisify, types as utilTypes } from "node:util";
4
5
  import { formatErrorDetail } from "./error-detail.js";
6
+ import { windowsSecurityHelperPath, explainWindowsSecurityHelperFailure } from "./windows-security-helper.js";
5
7
  const execFileAsync = promisify(execFile);
6
8
  export const DEFAULT_PERMISSION_EXEC_TIMEOUT_MS = 30_000;
7
9
  export function formatPermissionErrorDetail(value) {
@@ -179,6 +181,7 @@ export function getPermissionCommandFailure(error, command, durationMs) {
179
181
  return { command, durationMs: Math.round(durationMs), ...detail };
180
182
  }
181
183
  export async function executePermissionCommand(command, args, timeoutMs = DEFAULT_PERMISSION_EXEC_TIMEOUT_MS) {
184
+ assertWindowsHelperAllowed();
182
185
  const startedAt = performance.now();
183
186
  try {
184
187
  return (await execFileAsync(command, args, {
@@ -190,6 +193,11 @@ export async function executePermissionCommand(command, args, timeoutMs = DEFAUL
190
193
  }));
191
194
  }
192
195
  catch (err) {
193
- throw new PermissionCommandError(command, performance.now() - startedAt, err, timeoutMs);
196
+ const error = new PermissionCommandError(command, performance.now() - startedAt, err, timeoutMs);
197
+ if (command === windowsSecurityHelperPath() &&
198
+ ["ENOENT", "EACCES", "ENOEXEC"].includes(dataProperty(err, "code")?.value)) {
199
+ explainWindowsSecurityHelperFailure(error);
200
+ }
201
+ throw error;
194
202
  }
195
203
  }
@@ -3,3 +3,4 @@ export type { PermissionCommandFailure } from "./permission-exec.js";
3
3
  export { createPrivateDirectory, type CreatePrivateDirectoryOptions, } from "./private-directory.js";
4
4
  export { readOwnerAndDacl, type OwnerAndDaclResult, type WindowsAccessControlEntry, type WindowsAceFlags, } from "./owner-dacl.js";
5
5
  export { readOwnerAndDaclBatch } from "./owner-dacl-batch.js";
6
+ export { inspectDarwinAcl, type DarwinAclInspection } from "./darwin-acl.js";
@@ -2,3 +2,4 @@ export { formatOctal, formatPermissionDetail, formatPermissionRemediation, inspe
2
2
  export { createPrivateDirectory, } from "./private-directory.js";
3
3
  export { readOwnerAndDacl, } from "./owner-dacl.js";
4
4
  export { readOwnerAndDaclBatch } from "./owner-dacl-batch.js";
5
+ export { inspectDarwinAcl } from "./darwin-acl.js";
@@ -1,10 +1,12 @@
1
1
  import os from "node:os";
2
+ import { FsSafeError } from "./errors.js";
2
3
  import { getNativeBinding } from "./native.js";
3
4
  import { executePermissionCommand } from "./permission-exec.js";
4
5
  import { safeStat } from "./permissions.js";
5
6
  import { normalizeLowercaseStringOrEmpty } from "./string-coerce.js";
6
7
  import { resolveWindowsSystemCommand } from "./windows-command.js";
7
8
  import { inspectWindowsOwner } from "./windows-owner.js";
9
+ import { assertWindowsAclRemediationAllowed } from "./windows-appcontainer.js";
8
10
  import { hasWindowsPathAlias } from "./windows-path-alias.js";
9
11
  const INHERIT_FLAGS = new Set(["I", "OI", "CI", "IO", "NP"]);
10
12
  const WORLD_PRINCIPALS = new Set(["everyone", "users", "builtin\\users", "authenticated users", "nt authority\\authenticated users", "anonymous logon", "nt authority\\anonymous logon", "guests", "builtin\\guests", "interactive", "nt authority\\interactive", "network", "nt authority\\network", "local"]);
@@ -40,30 +42,29 @@ const normalize = (value) => normalizeLowercaseStringOrEmpty(value);
40
42
  const defaultWindowsUserInfo = () => os.userInfo();
41
43
  const defaultPermissionExec = executePermissionCommand;
42
44
  function inspectWindowsPermissionsNative(targetPath, unverified) {
43
- const native = getNativeBinding();
45
+ const native = getNativeBinding("readOwnerAndDacl");
44
46
  if (!native)
45
47
  return undefined;
46
- try {
47
- const facts = native.readOwnerAndDacl(targetPath);
48
- if (facts.fallbackRequired)
49
- return undefined;
50
- return {
51
- ...unverified,
52
- source: "windows-acl",
53
- worldWritable: facts.worldWritable,
54
- groupWritable: facts.groupWritable,
55
- worldReadable: facts.worldReadable,
56
- groupReadable: facts.groupReadable,
57
- ownerSid: facts.ownerSid,
58
- ownerTrusted: facts.ownerClass !== "foreign",
59
- aclSummary: `native owner=${facts.ownerClass} world=` +
60
- `${facts.worldReadable ? "r" : "-"}${facts.worldWritable ? "w" : "-"} ` +
61
- `group=${facts.groupReadable ? "r" : "-"}${facts.groupWritable ? "w" : "-"}`,
62
- };
63
- }
64
- catch {
48
+ const facts = native.readOwnerAndDacl(targetPath);
49
+ if (facts.fallbackRequired) {
50
+ if (facts.currentAppContainerSid) {
51
+ throw new FsSafeError("helper-unavailable", "AppContainer ACL facts require an unavailable external helper");
52
+ }
65
53
  return undefined;
66
54
  }
55
+ return {
56
+ ...unverified,
57
+ source: "windows-acl",
58
+ worldWritable: facts.worldWritable,
59
+ groupWritable: facts.groupWritable,
60
+ worldReadable: facts.worldReadable,
61
+ groupReadable: facts.groupReadable,
62
+ ownerSid: facts.ownerSid,
63
+ ownerTrusted: facts.ownerClass !== "foreign",
64
+ aclSummary: `native owner=${facts.ownerClass} world=` +
65
+ `${facts.worldReadable ? "r" : "-"}${facts.worldWritable ? "w" : "-"} ` +
66
+ `group=${facts.groupReadable ? "r" : "-"}${facts.groupWritable ? "w" : "-"}`,
67
+ };
67
68
  }
68
69
  export async function inspectWindowsPermissions(targetPath, unverified, opts) {
69
70
  if (hasWindowsPathAlias(targetPath, "filesystem", "win32")) {
@@ -250,7 +251,38 @@ function summarizeWindowsOwnerAcl(owner) {
250
251
  canWrite: (ace.mask & 0x500d_0156) !== 0,
251
252
  };
252
253
  });
253
- return { ok: true, entries, ...summarizeWindowsAcl(entries, { USERSID: owner.currentUserSid }) };
254
+ const summary = summarizeWindowsAcl(entries, { USERSID: owner.currentUserSid });
255
+ if (owner.currentAppContainerSid) {
256
+ const own = summary.untrustedGroup.filter(entry => entry.sid === owner.currentAppContainerSid);
257
+ summary.trusted.push(...own);
258
+ summary.untrustedGroup = summary.untrustedGroup.filter(entry => entry.sid !== owner.currentAppContainerSid);
259
+ }
260
+ return { ok: true, entries, ...summary };
261
+ }
262
+ function hasUnmergedAppContainerAces(facts) {
263
+ // These unique full-control allow entries cannot be merged or have their masks
264
+ // rewritten by .NET's ACL normalization. SID trust stays with the summary owner.
265
+ const inheritance = facts.aces[0]?.flags.raw;
266
+ return !!facts.currentAppContainerSid && (inheritance === 0 || inheritance === 3) &&
267
+ new Set(facts.aces.map(ace => ace.sid)).size === facts.aces.length &&
268
+ facts.aces.every(ace => ace.aceType === "allow" && ace.mask === 0x001f01ff &&
269
+ ace.flags.raw === inheritance);
270
+ }
271
+ function compareWindowsSids(left, right) {
272
+ const a = left.split("-").slice(2).map(Number);
273
+ const b = right.split("-").slice(2).map(Number);
274
+ // Match .NET Framework SecurityIdentifier.CompareTo: authority, subauthority
275
+ // count, then signed 32-bit subtraction for each subauthority.
276
+ if (a[0] !== b[0])
277
+ return a[0] < b[0] ? -1 : 1;
278
+ if (a.length !== b.length)
279
+ return a.length - b.length;
280
+ for (let index = 1; index < a.length; index++) {
281
+ const difference = ((a[index] | 0) - (b[index] | 0)) | 0;
282
+ if (difference !== 0)
283
+ return difference;
284
+ }
285
+ return 0;
254
286
  }
255
287
  export async function inspectWindowsAcl(targetPath, opts) {
256
288
  if (opts?.principalTranslationFailed) {
@@ -262,33 +294,35 @@ export async function inspectWindowsAcl(targetPath, opts) {
262
294
  return summarizeWindowsOwnerAcl({ error: String(error), errorCause: error });
263
295
  }
264
296
  let owner;
265
- if (process.platform === "win32" && opts?.env === undefined && opts?.exec === undefined) {
266
- try {
267
- const native = getNativeBinding();
268
- const stat = native && await safeStat(targetPath);
269
- // Keep the named-query behavior for leaf links; the native reader follows them.
270
- const facts = stat?.ok && !stat.isSymlink ? native?.readOwnerAndDacl(targetPath) : undefined;
271
- if (facts && !facts.fallbackRequired && facts.isLocal && facts.aceListComplete && facts.unsupportedAceTypes.length === 0 &&
272
- facts.aces.every(ace => ace.flags.inherited && ace.mask !== 0)) {
273
- // .NET normalizes explicit ACEs, but retains nonzero inherited ACEs in
274
- // their original order without merging them. Other forms keep its query.
275
- // Adapt descriptor facts only: the public summary owns its SID and rights
276
- // classification, which differs from the native secure-read policy.
277
- owner = {
278
- daclPresent: facts.daclPresent,
279
- currentUserSid: facts.currentUserSid,
280
- aces: facts.aces.map(ace => ({
281
- sid: ace.sid,
282
- mask: ace.mask,
283
- deny: ace.aceType === "deny",
284
- inheritOnly: ace.flags.inheritOnly,
285
- })),
286
- };
287
- }
297
+ if (process.platform === "win32" && opts?.exec === undefined) {
298
+ const native = getNativeBinding("readOwnerAndDacl");
299
+ const stat = native && await safeStat(targetPath);
300
+ // Keep the named-query behavior for leaf links; the native reader follows them.
301
+ const facts = stat?.ok && !stat.isSymlink ? native?.readOwnerAndDacl(targetPath) : undefined;
302
+ const explicitAppContainer = facts && hasUnmergedAppContainerAces(facts);
303
+ if (facts && !facts.fallbackRequired && facts.isLocal && facts.aceListComplete && facts.unsupportedAceTypes.length === 0 &&
304
+ (facts.aces.every(ace => ace.flags.inherited && ace.mask !== 0) || explicitAppContainer)) {
305
+ // .NET normalizes explicit ACEs, but retains nonzero inherited ACEs in
306
+ // their original order without merging them. Other forms keep its query.
307
+ // Adapt descriptor facts only: the public summary owns its SID and rights
308
+ // classification, which differs from the native secure-read policy.
309
+ const aces = explicitAppContainer
310
+ ? [...facts.aces].sort((a, b) => compareWindowsSids(a.sid, b.sid)) : facts.aces;
311
+ owner = {
312
+ daclPresent: facts.daclPresent,
313
+ currentUserSid: facts.currentUserSid,
314
+ currentAppContainerSid: facts.currentAppContainerSid ?? undefined,
315
+ sid: facts.ownerSid,
316
+ aces: aces.map(ace => ({
317
+ sid: ace.sid,
318
+ mask: ace.mask,
319
+ deny: ace.aceType === "deny",
320
+ inheritOnly: ace.flags.inheritOnly,
321
+ })),
322
+ };
288
323
  }
289
- catch {
290
- // Preserve this inspector's structured fallback and command diagnostics
291
- // when the optional helper or native descriptor query is unavailable.
324
+ else if (facts?.currentAppContainerSid) {
325
+ throw new FsSafeError("helper-unavailable", "AppContainer ACL normalization requires an unavailable external helper");
292
326
  }
293
327
  }
294
328
  owner ??= await inspectWindowsOwner({ targetPath, env: opts?.env, exec: opts?.exec ?? defaultPermissionExec });
@@ -304,6 +338,8 @@ export function formatWindowsAclSummary(summary) {
304
338
  return untrusted.length === 0 ? "trusted-only" : untrusted.map((entry) => `${entry.principal}:${entry.rawRights}`).join(", ");
305
339
  }
306
340
  export function formatIcaclsResetCommand(targetPath, opts) {
341
+ // Removing inherited ACEs can revoke the container's own access.
342
+ assertWindowsAclRemediationAllowed();
307
343
  const command = resolveWindowsSystemCommand("icacls.exe", opts.env);
308
344
  const user = resolveWindowsUserPrincipal(opts.env, opts.userInfo) ?? "%USERNAME%";
309
345
  const grant = opts.isDir ? "(OI)(CI)F" : "F";
@@ -22,9 +22,11 @@ export declare function openPinnedFileSync(params: {
22
22
  filePath: string;
23
23
  resolvedPath?: string;
24
24
  rejectPathSymlink?: boolean;
25
+ rejectLeafSymlinks?: boolean;
25
26
  rejectHardlinks?: boolean;
26
27
  maxBytes?: number;
27
28
  allowedType?: PinnedOpenSyncAllowedType;
28
29
  ioFs?: PinnedOpenSyncFs;
30
+ admittedFd?: number;
29
31
  finalAdmission?: PinnedOpenSyncFinalAdmission;
30
32
  }): PinnedOpenSyncResult;
@@ -1,3 +1,4 @@
1
+ import { numberStats } from "./exact-identity.js";
1
2
  import fs from "node:fs";
2
3
  import { isUnsafeDeviceReadPath } from "./device-path.js";
3
4
  import { FsSafeError } from "./errors.js";
@@ -17,10 +18,11 @@ export function openPinnedFileSync(params) {
17
18
  const rejectPathSymlink = params.rejectPathSymlink === true;
18
19
  const statPolicy = {
19
20
  rejectHardlinks: params.rejectHardlinks,
21
+ rejectLeafSymlinks: params.rejectLeafSymlinks,
20
22
  maxBytes: params.maxBytes,
21
23
  };
22
24
  const openReadFlags = resolveReadOpenFlags({ constants: ioFs.constants });
23
- let fd = null;
25
+ let fd = params.admittedFd ?? null;
24
26
  try {
25
27
  assertNoWindowsPathAlias(filePath, "filesystem", "file path uses a Windows filesystem namespace alias");
26
28
  if (resolvedPath !== undefined) {
@@ -46,8 +48,7 @@ export function openPinnedFileSync(params) {
46
48
  assertAllowedStat(stat, allowedType, statPolicy);
47
49
  return stat;
48
50
  });
49
- fd = ioFs.openSync(realPath, openReadFlags);
50
- const openedStat = ioFs.fstatSync(fd);
51
+ fd ??= ioFs.openSync(realPath, openReadFlags);
51
52
  const identity = inspectFileIdentitySync(() => {
52
53
  const stat = ioFs.fstatSync(fd, { bigint: true });
53
54
  assertAllowedStat(stat, allowedType, statPolicy);
@@ -62,6 +63,7 @@ export function openPinnedFileSync(params) {
62
63
  path: realPath,
63
64
  descriptorIdentity: identity,
64
65
  }) ?? realPath;
66
+ const openedStat = numberStats(identity);
65
67
  const opened = { ok: true, path: admittedPath, fd, stat: openedStat, identity };
66
68
  fd = null;
67
69
  return opened;
@@ -86,6 +88,9 @@ export function openPinnedFileSync(params) {
86
88
  }
87
89
  }
88
90
  function assertAllowedStat(stat, allowedType, params) {
91
+ if (stat.isSymbolicLink() && params.rejectLeafSymlinks) {
92
+ throw new FsSafeError("symlink", "symlink not allowed");
93
+ }
89
94
  if (stat.isSymbolicLink() || !(allowedType === "directory" ? stat.isDirectory() : stat.isFile())) {
90
95
  throw new FsSafeError("not-file", "path does not have the required file type");
91
96
  }
@@ -21,6 +21,7 @@ export function replayPinnedInput(fd, input) {
21
21
  })() };
22
22
  }
23
23
  const isUint8Array = types.isUint8Array;
24
+ const SMALL_BUFFER_BYTES = 64 * 1024;
24
25
  function streamChunkBytes(chunk) {
25
26
  if (Buffer.isBuffer(chunk))
26
27
  return chunk;
@@ -29,10 +30,22 @@ function streamChunkBytes(chunk) {
29
30
  return snapshotByteView(chunk);
30
31
  }
31
32
  export async function writePinnedInput(target, input, maxBytes, assertBeforeMutation) {
33
+ if (input.kind === "link")
34
+ throw new FsSafeError("helper-failed", "link copying requires the link publication path");
32
35
  if (input.kind === "file") {
33
36
  await writeCopyFileToFd(typeof target === "number" ? target : target.fd, input, maxBytes, assertBeforeMutation);
34
37
  return;
35
38
  }
39
+ if (typeof target === "number" && input.kind === "buffer" &&
40
+ input.stageBeforePublish !== false && !assertBeforeMutation) {
41
+ const data = typeof input.data === "string" ? Buffer.from(input.data, input.encoding ?? "utf8") : input.data;
42
+ if (data.byteLength <= SMALL_BUFFER_BYTES) {
43
+ if (maxBytes !== undefined && data.byteLength > maxBytes) {
44
+ throw new FsSafeError("too-large", `file exceeds limit of ${maxBytes} bytes (got at least ${data.byteLength})`);
45
+ }
46
+ return writeAllToFile(target, data, { synchronous: true });
47
+ }
48
+ }
36
49
  let bytes = 0;
37
50
  const write = async (data) => {
38
51
  bytes += data.byteLength;
@@ -3,6 +3,7 @@ import { assertExclusiveCreateLeaf } from "./exclusive-create.js";
3
3
  import fsSync, {} from "node:fs";
4
4
  import fs, {} from "node:fs/promises";
5
5
  import path from "node:path";
6
+ import { readFileHandleBounded } from "./bounded-read.js";
6
7
  import { createFileHandle } from "./create.js";
7
8
  import { hasPreservedCreationArtifacts, creationAdmissionFromParent } from "./creation-boundary.js";
8
9
  import { syncDirectoryBestEffort } from "./directory-durability.js";
@@ -18,6 +19,7 @@ import { cleanupPinnedFilePath } from "./file-cleanup.js";
18
19
  import { assertFinalSymlinkRejected } from "./root-symlink-policy.js";
19
20
  import { createStagedFileReceipt, settleStagedFile } from "./staged-file-settlement.js";
20
21
  import { getFsSafeTestHooks } from "./test-hooks.js";
22
+ import { restoreCopyMetadata } from "./copy-metadata.js";
21
23
  export async function runPinnedStagedWrite(params, parentPath, parentGuard) {
22
24
  const verifyPosixMode = process.platform !== "win32" &&
23
25
  (params.verifyPosixMode === true || params.private === true);
@@ -31,7 +33,6 @@ export async function runPinnedStagedWrite(params, parentPath, parentGuard) {
31
33
  ? fsSync.constants.O_NOFOLLOW
32
34
  : 0);
33
35
  let handle;
34
- let tempStat;
35
36
  let tempIdentity;
36
37
  let readHandle;
37
38
  let renamed = false;
@@ -59,13 +60,15 @@ export async function runPinnedStagedWrite(params, parentPath, parentGuard) {
59
60
  await writePinnedInput(handle, params.input, params.maxBytes, assertBeforeWrite);
60
61
  if (verifyPosixMode)
61
62
  assertPinnedWriteMode(handle.fd, 0o600, params.private);
62
- tempStat = fsSync.fstatSync(handle.fd);
63
- const tempPathStat = fsSync.lstatSync(tempPath);
63
+ const tempStat = fsSync.fstatSync(handle.fd, { bigint: true });
64
+ const tempPathStat = fsSync.lstatSync(tempPath, { bigint: true });
64
65
  if (tempPathStat.isSymbolicLink() || !sameFileIdentity(tempPathStat, tempStat)) {
65
66
  throw new FsSafeError("path-mismatch", "fallback temp path changed during write");
66
67
  }
67
68
  const expectedTempStat = tempStat;
68
- await handle.chmod(params.mode);
69
+ fsSync.fchmodSync(handle.fd, params.mode);
70
+ if (params.input.kind === "file")
71
+ restoreCopyMetadata(handle.fd, params.input.metadata);
69
72
  if (verifyPosixMode) {
70
73
  assertPinnedWriteMode(handle.fd, params.mode, params.private);
71
74
  assertBeforeMutation = pinnedWriteModeAssertion(handle.fd, params.mode, params.assertBeforeMutation, params.private);
@@ -115,7 +118,7 @@ export async function runPinnedStagedWrite(params, parentPath, parentGuard) {
115
118
  await getFsSafeTestHooks()?.afterPinnedWriteFallbackRename?.(targetPath);
116
119
  if (params.sync !== false)
117
120
  await syncDirectoryBestEffort(parentPath);
118
- const targetStat = fsSync.lstatSync(targetPath);
121
+ const targetStat = fsSync.lstatSync(targetPath, { bigint: true });
119
122
  if (targetStat.isSymbolicLink()) {
120
123
  throw new FsSafeError("path-mismatch", "fallback target changed during write");
121
124
  }
@@ -133,14 +136,13 @@ export async function runPinnedStagedWrite(params, parentPath, parentGuard) {
133
136
  const expectedHash = sha256Hex(params.input.data, params.input.encoding);
134
137
  readHandle = await fs.open(targetPath, resolveReadOpenFlags());
135
138
  const readHandleStat = fsSync.fstatSync(readHandle.fd, { bigint: true });
136
- const actualHash = sha256Hex(await readHandle.readFile());
139
+ const actualHash = sha256Hex(await readFileHandleBounded(readHandle, Infinity));
137
140
  if (actualHash !== expectedHash) {
138
141
  throw new FsSafeError("path-mismatch", "fallback target changed during write");
139
142
  }
140
143
  // The content-verified destination, not the old temp inode, is now pinned.
141
144
  verificationIdentity = readHandleStat;
142
- // Preserve the helper's legacy numeric return facts, not the private proof.
143
- verifiedIdentity = { dev: Number(readHandleStat.dev), ino: Number(readHandleStat.ino) };
145
+ verifiedIdentity = readHandleStat;
144
146
  }
145
147
  });
146
148
  await params.verifyPublished?.((readHandle ?? handle).fd, verificationIdentity, parentGuard);
@@ -1,4 +1,5 @@
1
1
  import type { CopyFileInput } from "./copy-file-input.js";
2
+ import type { CopyLinkInput } from "./copy-link.js";
2
3
  import type { AnyAsyncDirectoryGuard } from "./directory-guard.js";
3
4
  import type { FileIdentityStat } from "./file-identity.js";
4
5
  import type { MutationDirectoryObservation } from "./pinned-mutation-observation.js";
@@ -16,7 +17,7 @@ export type PinnedWriteInput = {
16
17
  kind: "stream";
17
18
  stream: AsyncIterable<Uint8Array | string>;
18
19
  stageBeforePublish?: boolean;
19
- } | CopyFileInput;
20
+ } | CopyFileInput | CopyLinkInput;
20
21
  export type RenameIdentityPolicy = "strict" | "verify-content-with-lock";
21
22
  export type PublishedWriteIdentity = Readonly<{
22
23
  dev: bigint;