@openclaw/openshell-sandbox 2026.10.6 → 2026.10.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 (122) hide show
  1. package/node_modules/@openclaw/fs-safe/CHANGELOG.md +48 -0
  2. package/node_modules/@openclaw/fs-safe/README.md +14 -4
  3. package/node_modules/@openclaw/fs-safe/dist/archive-merge.js +28 -3
  4. package/node_modules/@openclaw/fs-safe/dist/archive-parser.wasm +0 -0
  5. package/node_modules/@openclaw/fs-safe/dist/archive-read.js +11 -5
  6. package/node_modules/@openclaw/fs-safe/dist/archive-zip-loader.js +4 -2
  7. package/node_modules/@openclaw/fs-safe/dist/archive.js +2 -2
  8. package/node_modules/@openclaw/fs-safe/dist/clone-metadata.js +1 -1
  9. package/node_modules/@openclaw/fs-safe/dist/copy-file-input.d.ts +2 -0
  10. package/node_modules/@openclaw/fs-safe/dist/copy-file-input.js +4 -3
  11. package/node_modules/@openclaw/fs-safe/dist/copy-link.d.ts +26 -0
  12. package/node_modules/@openclaw/fs-safe/dist/copy-link.js +169 -0
  13. package/node_modules/@openclaw/fs-safe/dist/copy-metadata.d.ts +9 -0
  14. package/node_modules/@openclaw/fs-safe/dist/copy-metadata.js +39 -0
  15. package/node_modules/@openclaw/fs-safe/dist/copy-root-file-sync.js +1 -1
  16. package/node_modules/@openclaw/fs-safe/dist/copy-tree-portable.js +3 -6
  17. package/node_modules/@openclaw/fs-safe/dist/copy.js +3 -3
  18. package/node_modules/@openclaw/fs-safe/dist/darwin-acl.d.ts +12 -0
  19. package/node_modules/@openclaw/fs-safe/dist/darwin-acl.js +29 -0
  20. package/node_modules/@openclaw/fs-safe/dist/directory-mode-node.d.ts +3 -0
  21. package/node_modules/@openclaw/fs-safe/dist/directory-mode-node.js +27 -2
  22. package/node_modules/@openclaw/fs-safe/dist/entry-publication.js +1 -1
  23. package/node_modules/@openclaw/fs-safe/dist/file-handle-transfer.js +4 -2
  24. package/node_modules/@openclaw/fs-safe/dist/file-hash.js +4 -3
  25. package/node_modules/@openclaw/fs-safe/dist/file-lock.d.ts +1 -0
  26. package/node_modules/@openclaw/fs-safe/dist/file-lock.js +1 -0
  27. package/node_modules/@openclaw/fs-safe/dist/file-write-lease.d.ts +9 -0
  28. package/node_modules/@openclaw/fs-safe/dist/file-write-lease.js +42 -0
  29. package/node_modules/@openclaw/fs-safe/dist/guest-native-python.js +3 -0
  30. package/node_modules/@openclaw/fs-safe/dist/hardlink-fallback.d.ts +1 -0
  31. package/node_modules/@openclaw/fs-safe/dist/hardlink-fallback.js +5 -0
  32. package/node_modules/@openclaw/fs-safe/dist/json-durable-queue-ownership.js +16 -3
  33. package/node_modules/@openclaw/fs-safe/dist/native-binding.d.ts +57 -18
  34. package/node_modules/@openclaw/fs-safe/dist/native-directory-observation.js +1 -1
  35. package/node_modules/@openclaw/fs-safe/dist/native-move-fallback.d.ts +1 -1
  36. package/node_modules/@openclaw/fs-safe/dist/native-move-fallback.js +2 -0
  37. package/node_modules/@openclaw/fs-safe/dist/native-operations.js +1 -1
  38. package/node_modules/@openclaw/fs-safe/dist/native-parent-admission.d.ts +1 -1
  39. package/node_modules/@openclaw/fs-safe/dist/native-parent-admission.js +2 -0
  40. package/node_modules/@openclaw/fs-safe/dist/native-pinned-write-windows.d.ts +1 -1
  41. package/node_modules/@openclaw/fs-safe/dist/native-pinned-write-windows.js +19 -1
  42. package/node_modules/@openclaw/fs-safe/dist/native-pinned-write.d.ts +4 -1
  43. package/node_modules/@openclaw/fs-safe/dist/native-pinned-write.js +18 -0
  44. package/node_modules/@openclaw/fs-safe/dist/native-policy-parent.d.ts +1 -1
  45. package/node_modules/@openclaw/fs-safe/dist/native-policy-parent.js +3 -0
  46. package/node_modules/@openclaw/fs-safe/dist/native-staged-file.d.ts +1 -1
  47. package/node_modules/@openclaw/fs-safe/dist/native-staged-file.js +3 -0
  48. package/node_modules/@openclaw/fs-safe/dist/native.d.ts +5 -3
  49. package/node_modules/@openclaw/fs-safe/dist/native.js +25 -6
  50. package/node_modules/@openclaw/fs-safe/dist/opened-realpath.js +9 -1
  51. package/node_modules/@openclaw/fs-safe/dist/overwrite-file-handle.js +4 -1
  52. package/node_modules/@openclaw/fs-safe/dist/permissions-public.d.ts +1 -0
  53. package/node_modules/@openclaw/fs-safe/dist/permissions-public.js +1 -0
  54. package/node_modules/@openclaw/fs-safe/dist/permissions-windows.js +36 -47
  55. package/node_modules/@openclaw/fs-safe/dist/pinned-write-input.js +2 -0
  56. package/node_modules/@openclaw/fs-safe/dist/pinned-write-staged.js +3 -0
  57. package/node_modules/@openclaw/fs-safe/dist/pinned-write-types.d.ts +2 -1
  58. package/node_modules/@openclaw/fs-safe/dist/pinned-write.js +5 -3
  59. package/node_modules/@openclaw/fs-safe/dist/pipe.d.ts +9 -0
  60. package/node_modules/@openclaw/fs-safe/dist/pipe.js +26 -0
  61. package/node_modules/@openclaw/fs-safe/dist/private-producer-handoff.js +2 -5
  62. package/node_modules/@openclaw/fs-safe/dist/publish-copy-stage.js +92 -10
  63. package/node_modules/@openclaw/fs-safe/dist/publish-file.d.ts +1 -1
  64. package/node_modules/@openclaw/fs-safe/dist/publish-file.js +12 -11
  65. package/node_modules/@openclaw/fs-safe/dist/root-copy.d.ts +18 -0
  66. package/node_modules/@openclaw/fs-safe/dist/root-copy.js +128 -0
  67. package/node_modules/@openclaw/fs-safe/dist/root-create-native.js +12 -4
  68. package/node_modules/@openclaw/fs-safe/dist/root-impl.js +6 -92
  69. package/node_modules/@openclaw/fs-safe/dist/root-move-noreplace.js +1 -1
  70. package/node_modules/@openclaw/fs-safe/dist/root-options.d.ts +2 -0
  71. package/node_modules/@openclaw/fs-safe/dist/root-remove-native.js +8 -1
  72. package/node_modules/@openclaw/fs-safe/dist/sibling-staged-file.js +1 -1
  73. package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-retained.js +1 -1
  74. package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-target.js +8 -1
  75. package/node_modules/@openclaw/fs-safe/dist/test-hooks.d.ts +1 -0
  76. package/node_modules/@openclaw/fs-safe/dist/test-hooks.js +1 -0
  77. package/node_modules/@openclaw/fs-safe/dist/watch-native.js +2 -2
  78. package/node_modules/@openclaw/fs-safe/dist/windows-security-bridge.cs +11 -5
  79. package/node_modules/@openclaw/fs-safe/dist/windows-security-command.js +9 -1
  80. package/node_modules/@openclaw/fs-safe/dist/windows-test-hooks.d.ts +16 -0
  81. package/node_modules/@openclaw/fs-safe/dist/windows-test-hooks.js +51 -0
  82. package/node_modules/@openclaw/fs-safe/docs/archive.md +16 -0
  83. package/node_modules/@openclaw/fs-safe/docs/contributing.md +35 -5
  84. package/node_modules/@openclaw/fs-safe/docs/copy.md +22 -0
  85. package/node_modules/@openclaw/fs-safe/docs/durability.md +4 -2
  86. package/node_modules/@openclaw/fs-safe/docs/errors.md +8 -0
  87. package/node_modules/@openclaw/fs-safe/docs/guest.md +2 -0
  88. package/node_modules/@openclaw/fs-safe/docs/install.md +13 -5
  89. package/node_modules/@openclaw/fs-safe/docs/migrating-to-0.5.md +5 -5
  90. package/node_modules/@openclaw/fs-safe/docs/native-helper.md +15 -5
  91. package/node_modules/@openclaw/fs-safe/docs/native.md +51 -2
  92. package/node_modules/@openclaw/fs-safe/docs/pipe.md +81 -0
  93. package/node_modules/@openclaw/fs-safe/docs/platform-capabilities.md +63 -0
  94. package/node_modules/@openclaw/fs-safe/docs/quickstart.md +23 -3
  95. package/node_modules/@openclaw/fs-safe/docs/reading.md +7 -1
  96. package/node_modules/@openclaw/fs-safe/docs/secret-file.md +5 -5
  97. package/node_modules/@openclaw/fs-safe/docs/sidecar-lock.md +31 -13
  98. package/node_modules/@openclaw/fs-safe/docs/store.md +11 -0
  99. package/node_modules/@openclaw/fs-safe/docs/types.md +45 -80
  100. package/node_modules/@openclaw/fs-safe/docs/writing.md +121 -14
  101. package/node_modules/@openclaw/fs-safe/package.json +18 -10
  102. package/node_modules/@openclaw/fs-safe-darwin-arm64/fs-safe-native.node +0 -0
  103. package/node_modules/@openclaw/fs-safe-darwin-arm64/package.json +1 -1
  104. package/node_modules/@openclaw/fs-safe-darwin-x64/fs-safe-native.node +0 -0
  105. package/node_modules/@openclaw/fs-safe-darwin-x64/package.json +1 -1
  106. package/node_modules/@openclaw/fs-safe-freebsd-arm64/fs-safe-native.node +0 -0
  107. package/node_modules/@openclaw/fs-safe-freebsd-arm64/package.json +29 -0
  108. package/node_modules/@openclaw/fs-safe-freebsd-x64/fs-safe-native.node +0 -0
  109. package/node_modules/@openclaw/fs-safe-freebsd-x64/package.json +29 -0
  110. package/node_modules/@openclaw/fs-safe-linux-arm64-gnu/fs-safe-native.node +0 -0
  111. package/node_modules/@openclaw/fs-safe-linux-arm64-gnu/package.json +1 -1
  112. package/node_modules/@openclaw/fs-safe-linux-arm64-musl/fs-safe-native.node +0 -0
  113. package/node_modules/@openclaw/fs-safe-linux-arm64-musl/package.json +1 -1
  114. package/node_modules/@openclaw/fs-safe-linux-x64-gnu/fs-safe-native.node +0 -0
  115. package/node_modules/@openclaw/fs-safe-linux-x64-gnu/package.json +1 -1
  116. package/node_modules/@openclaw/fs-safe-linux-x64-musl/fs-safe-native.node +0 -0
  117. package/node_modules/@openclaw/fs-safe-linux-x64-musl/package.json +1 -1
  118. package/node_modules/@openclaw/fs-safe-win32-arm64-msvc/fs-safe-native.node +0 -0
  119. package/node_modules/@openclaw/fs-safe-win32-arm64-msvc/package.json +29 -0
  120. package/node_modules/@openclaw/fs-safe-win32-x64-msvc/fs-safe-native.node +0 -0
  121. package/node_modules/@openclaw/fs-safe-win32-x64-msvc/package.json +1 -1
  122. package/package.json +5 -5
@@ -56,12 +56,27 @@ import { runPinnedWriteWindows } from "./native-pinned-write-windows.js";
56
56
  import { capturePolicyAwareNativeParent } from "./native-policy-parent.js";
57
57
  import { openNativeParentAdmission, openNativeRootAdmission } from "./native-parent-admission.js";
58
58
  import { assertNativeStaging, writeNativeStage } from "./native-staged-file.js";
59
+ import { assertNativeCapabilities, getNativeBinding } from "./native.js";
59
60
  import { captureNativeFdClose } from "./native-binding.js";
60
61
  import { cachedNoReplaceUnavailable } from "./native-noreplace.js";
61
62
  import { isFsSafeNativeRequired } from "./native-config.js";
63
+ import { copyLinkAtParent } from "./copy-link.js";
64
+ const writeCapabilities = ["openBeneath", "mkdirBeneath", "renameNoReplace", "renameReplace"];
65
+ const posixWriteCapabilities = [...writeCapabilities, "createStagedFile", "stagedFileMatches", "removeStagedFile"];
66
+ function pinnedWriteCapabilities(params) {
67
+ const windows = process.platform === "win32";
68
+ const capabilities = windows ? [...writeCapabilities, "fstatIdentity"] : posixWriteCapabilities;
69
+ const nativeCopy = params.input.kind === "file" && !params.private &&
70
+ (windows || !params.verifyPosixMode);
71
+ return nativeCopy ? [...capabilities, "copyFileExclusive"] : capabilities;
72
+ }
73
+ export function getPinnedWriteNativeBinding(params) {
74
+ return getNativeBinding(...pinnedWriteCapabilities(params));
75
+ }
62
76
  export async function runPinnedWriteNative(binding, params, fallback) {
63
77
  const env_1 = { stack: [], error: void 0, hasError: false };
64
78
  try {
79
+ assertNativeCapabilities(binding, ...pinnedWriteCapabilities(params));
65
80
  const closeFd = captureNativeFdClose(binding);
66
81
  const windows = process.platform === "win32";
67
82
  if (!windows) {
@@ -112,6 +127,9 @@ export async function runPinnedWriteNative(binding, params, fallback) {
112
127
  parentPathStat = admitted.guard.stat;
113
128
  }
114
129
  const verificationGuard = { dir: parentPath, realPath: parentPath, stat: parentPathStat };
130
+ if (params.input.kind === "link") {
131
+ return await copyLinkAtParent(params, params.input, verificationGuard, { binding, fd: parentFd });
132
+ }
115
133
  if (params.overwrite === false) {
116
134
  try {
117
135
  fsSync.lstatSync(path.join(parentPath, params.basename));
@@ -1,6 +1,6 @@
1
1
  import { type BigIntStats } from "node:fs";
2
2
  import { type AsyncDirectoryGuard } from "./directory-guard.js";
3
- import type { NativeBinding } from "./native.js";
3
+ import { type NativeBinding } from "./native.js";
4
4
  import type { NativeRootAdmission } from "./native-parent-admission.js";
5
5
  import type { PinnedWriteParams } from "./pinned-write-types.js";
6
6
  import { describeStagedDirectory, type PolicyStagedDirectory } from "./staged-directory.js";
@@ -54,6 +54,7 @@ import fsSync, {} from "node:fs";
54
54
  import path from "node:path";
55
55
  import { inspectDirectoryIdentity, inspectDirectoryIdentitySync, } from "./directory-guard.js";
56
56
  import { FsSafeError } from "./errors.js";
57
+ import { assertNativeCapabilities } from "./native.js";
57
58
  import { captureNativeFdClose } from "./native-binding.js";
58
59
  import { NativePolicyDirectoryMismatch } from "./native-policy-directory-observation.js";
59
60
  import { assertWindowsPolicyParentCurrent, closeWindowsPolicyParentAfterFailure, openWindowsPolicyParent, windowsParentObservation, } from "./native-policy-parent-windows.js";
@@ -76,6 +77,7 @@ function mkdirPosixPolicyChild(binding, parentFd, basename) {
76
77
  if (typeof created === "boolean")
77
78
  return created;
78
79
  }
80
+ assertNativeCapabilities(binding, "mkdirBeneath");
79
81
  binding.mkdirBeneath(parentFd, basename, 0o777);
80
82
  return false;
81
83
  }
@@ -122,6 +124,7 @@ function tryDescribePolicyPosixParent(binding, fd, pathname) {
122
124
  export async function capturePolicyAwareNativeParent(binding, params, rootAdmission, windows, directoryFlags, fuseChildCreation = false) {
123
125
  const env_1 = { stack: [], error: void 0, hasError: false };
124
126
  try {
127
+ assertNativeCapabilities(binding, "openBeneath");
125
128
  const rootFd = rootAdmission.root.fd;
126
129
  const closeFd = captureNativeFdClose(binding);
127
130
  const observationDisposers = windows ? undefined : new Map();
@@ -5,7 +5,7 @@ import type { FileIdentityStat } from "./file-identity.js";
5
5
  import { type NativeBinding } from "./native-binding.js";
6
6
  import type { PinnedWriteInput, PinnedWriteParams } from "./pinned-write-types.js";
7
7
  import type { PublishedFileReceipt, StagedFile, StagedFileCleanupReceipt, StagedFileReceipt } from "./staged-file-types.js";
8
- export type NativeStagingBinding = NativeBinding & Required<Pick<NativeBinding, "createStagedFile" | "stagedFileMatches" | "removeStagedFile">>;
8
+ export type NativeStagingBinding = NativeBinding & Required<Pick<NativeBinding, "createStagedFile" | "stagedFileMatches" | "removeStagedFile" | "renameReplace" | "renameNoReplace">>;
9
9
  export declare function assertNativeStaging(binding: NativeBinding): asserts binding is NativeStagingBinding;
10
10
  type StagedPermissionPolicy = "private-creation" | "mode-only";
11
11
  type NativeStageOwner = Readonly<{
@@ -67,6 +67,7 @@ import { classifyNativeRenameFailure } from "./native-rename-outcome.js";
67
67
  import { createStagedFileReceipt, stagedFailure } from "./staged-file-settlement.js";
68
68
  import { noReplaceUnavailable, rememberNoReplaceUnavailable } from "./native-noreplace.js";
69
69
  import { isFsSafeNativeRequired } from "./native-config.js";
70
+ import { restoreCopyMetadata } from "./copy-metadata.js";
70
71
  export function assertNativeStaging(binding) {
71
72
  if ([
72
73
  binding.closeOwnedFd,
@@ -299,6 +300,8 @@ class NativeStagedFile {
299
300
  } : this.#assertBeforeMutation;
300
301
  if (!copied)
301
302
  await writePinnedInput(fd, input, maxBytes, assertBeforeMutation);
303
+ if (input.kind === "file")
304
+ restoreCopyMetadata(fd, input.metadata);
302
305
  if (this.#verifyMode)
303
306
  this.#assertStagePermissions(fd);
304
307
  if (this.#owner.sync !== false) {
@@ -1,4 +1,4 @@
1
- import { type NativeBinding } from "./native-binding.js";
1
+ import { type NativeBinding, type CapableNativeBinding } from "./native-binding.js";
2
2
  export type { NativeBinding } from "./native-binding.js";
3
3
  export declare function __loadBundledNativeForTest(): NativeBinding;
4
4
  export declare function __nativeLoaderDetectorsForTest(): {
@@ -7,7 +7,9 @@ export declare function __nativeLoaderDetectorsForTest(): {
7
7
  elfInterpreter: boolean | undefined;
8
8
  };
9
9
  export declare function __nativeTargetForTest(platform: NodeJS.Platform, arch: string, musl?: boolean): string | undefined;
10
- export declare function getNativeBinding(): NativeBinding | undefined;
11
- export declare function requireNativeBinding(): NativeBinding;
10
+ export declare function selectNativeBinding<K extends keyof NativeBinding>(native: NativeBinding | undefined, ...capabilities: K[]): CapableNativeBinding<K> | undefined;
11
+ export declare function assertNativeCapabilities<K extends keyof NativeBinding>(native: NativeBinding, ...capabilities: K[]): asserts native is CapableNativeBinding<K>;
12
+ export declare function getNativeBinding<K extends keyof NativeBinding = never>(...capabilities: K[]): CapableNativeBinding<K> | undefined;
13
+ export declare function requireNativeBinding<K extends keyof NativeBinding = never>(...capabilities: K[]): CapableNativeBinding<K>;
12
14
  export declare function __setNativeLoaderForTest(loader: () => NativeBinding): void;
13
15
  export declare function __resetNativeLoaderForTest(): void;
@@ -127,12 +127,14 @@ function isMusl() {
127
127
  return false;
128
128
  }
129
129
  function targetFor(platform, arch, musl) {
130
- if (platform === "win32" && arch === "x64")
131
- return "win32-x64-msvc";
130
+ if (platform === "win32" && (arch === "x64" || arch === "arm64"))
131
+ return `win32-${arch}-msvc`;
132
132
  if (platform === "darwin" && arch === "x64")
133
133
  return "darwin-x64";
134
134
  if (platform === "darwin" && arch === "arm64")
135
135
  return "darwin-arm64";
136
+ if (platform === "freebsd" && (arch === "x64" || arch === "arm64"))
137
+ return `freebsd-${arch}`;
136
138
  if (platform === "linux" && (arch === "x64" || arch === "arm64")) {
137
139
  return `linux-${arch}-${musl ? "musl" : "gnu"}`;
138
140
  }
@@ -166,7 +168,24 @@ export function __nativeLoaderDetectorsForTest() {
166
168
  export function __nativeTargetForTest(platform, arch, musl = false) {
167
169
  return targetFor(platform, arch, musl);
168
170
  }
169
- export function getNativeBinding() {
171
+ export function selectNativeBinding(native, ...capabilities) {
172
+ if (!native)
173
+ return undefined;
174
+ const missing = capabilities.filter((name) => typeof native[name] !== "function");
175
+ if (missing.length === 0)
176
+ return native;
177
+ if (getFsSafeNativeConfig().mode === "require") {
178
+ throw new FsSafeError("helper-unavailable", `native fs-safe capabilities unavailable: ${missing.join(", ")}`);
179
+ }
180
+ return undefined;
181
+ }
182
+ export function assertNativeCapabilities(native, ...capabilities) {
183
+ const missing = capabilities.filter((name) => typeof native[name] !== "function");
184
+ if (missing.length) {
185
+ throw new FsSafeError("helper-unavailable", `native fs-safe capabilities unavailable: ${missing.join(", ")}`);
186
+ }
187
+ }
188
+ export function getNativeBinding(...capabilities) {
170
189
  const { mode } = getFsSafeNativeConfig();
171
190
  if (mode === "off")
172
191
  return undefined;
@@ -182,7 +201,7 @@ export function getNativeBinding() {
182
201
  }
183
202
  }
184
203
  if (binding)
185
- return binding;
204
+ return selectNativeBinding(binding, ...capabilities);
186
205
  if (mode === "require") {
187
206
  throw new FsSafeError("helper-unavailable", "native fs-safe helper is unavailable", {
188
207
  cause: loadError,
@@ -190,8 +209,8 @@ export function getNativeBinding() {
190
209
  }
191
210
  return undefined;
192
211
  }
193
- export function requireNativeBinding() {
194
- const native = getNativeBinding();
212
+ export function requireNativeBinding(...capabilities) {
213
+ const native = getNativeBinding(...capabilities);
195
214
  if (!native) {
196
215
  throw new FsSafeError("helper-unavailable", "native fs-safe helper is unavailable", {
197
216
  cause: loadError,
@@ -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
  }
@@ -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";
@@ -40,30 +40,25 @@ const normalize = (value) => normalizeLowercaseStringOrEmpty(value);
40
40
  const defaultWindowsUserInfo = () => os.userInfo();
41
41
  const defaultPermissionExec = executePermissionCommand;
42
42
  function inspectWindowsPermissionsNative(targetPath, unverified) {
43
- const native = getNativeBinding();
43
+ const native = getNativeBinding("readOwnerAndDacl");
44
44
  if (!native)
45
45
  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 {
46
+ const facts = native.readOwnerAndDacl(targetPath);
47
+ if (facts.fallbackRequired)
65
48
  return undefined;
66
- }
49
+ return {
50
+ ...unverified,
51
+ source: "windows-acl",
52
+ worldWritable: facts.worldWritable,
53
+ groupWritable: facts.groupWritable,
54
+ worldReadable: facts.worldReadable,
55
+ groupReadable: facts.groupReadable,
56
+ ownerSid: facts.ownerSid,
57
+ ownerTrusted: facts.ownerClass !== "foreign",
58
+ aclSummary: `native owner=${facts.ownerClass} world=` +
59
+ `${facts.worldReadable ? "r" : "-"}${facts.worldWritable ? "w" : "-"} ` +
60
+ `group=${facts.groupReadable ? "r" : "-"}${facts.groupWritable ? "w" : "-"}`,
61
+ };
67
62
  }
68
63
  export async function inspectWindowsPermissions(targetPath, unverified, opts) {
69
64
  if (hasWindowsPathAlias(targetPath, "filesystem", "win32")) {
@@ -263,32 +258,26 @@ export async function inspectWindowsAcl(targetPath, opts) {
263
258
  }
264
259
  let owner;
265
260
  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
- }
288
- }
289
- catch {
290
- // Preserve this inspector's structured fallback and command diagnostics
291
- // when the optional helper or native descriptor query is unavailable.
261
+ const native = getNativeBinding("readOwnerAndDacl");
262
+ const stat = native && await safeStat(targetPath);
263
+ // Keep the named-query behavior for leaf links; the native reader follows them.
264
+ const facts = stat?.ok && !stat.isSymlink ? native?.readOwnerAndDacl(targetPath) : undefined;
265
+ if (facts && !facts.fallbackRequired && facts.isLocal && facts.aceListComplete && facts.unsupportedAceTypes.length === 0 &&
266
+ facts.aces.every(ace => ace.flags.inherited && ace.mask !== 0)) {
267
+ // .NET normalizes explicit ACEs, but retains nonzero inherited ACEs in
268
+ // their original order without merging them. Other forms keep its query.
269
+ // Adapt descriptor facts only: the public summary owns its SID and rights
270
+ // classification, which differs from the native secure-read policy.
271
+ owner = {
272
+ daclPresent: facts.daclPresent,
273
+ currentUserSid: facts.currentUserSid,
274
+ aces: facts.aces.map(ace => ({
275
+ sid: ace.sid,
276
+ mask: ace.mask,
277
+ deny: ace.aceType === "deny",
278
+ inheritOnly: ace.flags.inheritOnly,
279
+ })),
280
+ };
292
281
  }
293
282
  }
294
283
  owner ??= await inspectWindowsOwner({ targetPath, env: opts?.env, exec: opts?.exec ?? defaultPermissionExec });
@@ -29,6 +29,8 @@ function streamChunkBytes(chunk) {
29
29
  return snapshotByteView(chunk);
30
30
  }
31
31
  export async function writePinnedInput(target, input, maxBytes, assertBeforeMutation) {
32
+ if (input.kind === "link")
33
+ throw new FsSafeError("helper-failed", "link copying requires the link publication path");
32
34
  if (input.kind === "file") {
33
35
  await writeCopyFileToFd(typeof target === "number" ? target : target.fd, input, maxBytes, assertBeforeMutation);
34
36
  return;
@@ -18,6 +18,7 @@ import { cleanupPinnedFilePath } from "./file-cleanup.js";
18
18
  import { assertFinalSymlinkRejected } from "./root-symlink-policy.js";
19
19
  import { createStagedFileReceipt, settleStagedFile } from "./staged-file-settlement.js";
20
20
  import { getFsSafeTestHooks } from "./test-hooks.js";
21
+ import { restoreCopyMetadata } from "./copy-metadata.js";
21
22
  export async function runPinnedStagedWrite(params, parentPath, parentGuard) {
22
23
  const verifyPosixMode = process.platform !== "win32" &&
23
24
  (params.verifyPosixMode === true || params.private === true);
@@ -66,6 +67,8 @@ export async function runPinnedStagedWrite(params, parentPath, parentGuard) {
66
67
  }
67
68
  const expectedTempStat = tempStat;
68
69
  await handle.chmod(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);
@@ -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;
@@ -12,8 +12,7 @@ import { syncDirectoryBestEffort } from "./directory-durability.js";
12
12
  import { sha256Hex } from "./file-identity.js";
13
13
  import { withAsyncDirectoryGuards } from "./guarded-mutation.js";
14
14
  import { mkdirPathComponentsWithGuards } from "./guarded-mkdir.js";
15
- import { runPinnedWriteNative } from "./native-pinned-write.js";
16
- import { getNativeBinding } from "./native.js";
15
+ import { getPinnedWriteNativeBinding, runPinnedWriteNative } from "./native-pinned-write.js";
17
16
  import { validatePinnedRelativePath } from "./pinned-operation.js";
18
17
  import { cleanupPinnedFilePath } from "./file-cleanup.js";
19
18
  import { withSidecarLock } from "./sidecar-lock.js";
@@ -23,6 +22,7 @@ import { assertPinnedWriteMode, preparePinnedWriteMode } from "./pinned-write-mo
23
22
  import { runPinnedStagedWrite } from "./pinned-write-staged.js";
24
23
  import { assertFinalSymlinkRejected } from "./root-symlink-policy.js";
25
24
  import { assertNoWindowsPathAlias } from "./windows-path-alias.js";
25
+ import { copyLinkAtParent } from "./copy-link.js";
26
26
  function assertSafeBasename(basename) {
27
27
  if (!basename ||
28
28
  basename === "." ||
@@ -88,7 +88,7 @@ export async function runPinnedWriteHelper(params) {
88
88
  if (normalizedParams.onRenameIdentityMismatch === "verify-content") {
89
89
  return await runPinnedWriteFallback(normalizedParams);
90
90
  }
91
- const native = getNativeBinding();
91
+ const native = getPinnedWriteNativeBinding(normalizedParams);
92
92
  if (native) {
93
93
  return await runPinnedWriteNative(native, normalizedParams, input => runPinnedWriteFallback({ ...normalizedParams, input }));
94
94
  }
@@ -225,6 +225,8 @@ async function runPinnedWriteFallback(params) {
225
225
  }));
226
226
  await assertAsyncDirectoryGuard(parentGuard);
227
227
  }
228
+ if (params.input.kind === "link")
229
+ return await copyLinkAtParent(params, params.input, parentGuard);
228
230
  if (params.overwrite === false && params.input.kind !== "file" && !params.input.stageBeforePublish) {
229
231
  const assertBeforeMutation = () => {
230
232
  assertFinalSymlinkRejected(targetPath, params.rejectFinalSymlink);
@@ -0,0 +1,9 @@
1
+ import { type OwnedFileDescriptorSync } from "./create-owned-file.js";
2
+ export type OwnedPipeSync = {
3
+ readonly reader: OwnedFileDescriptorSync;
4
+ readonly writer: OwnedFileDescriptorSync;
5
+ /** True when close-on-exec was set atomically at creation. */
6
+ readonly atomicCloseOnExec: boolean;
7
+ };
8
+ /** Creates a POSIX anonymous pipe with two native-owned, close-on-exec ends. */
9
+ export declare function createPipe(): OwnedPipeSync;
@@ -0,0 +1,26 @@
1
+ import { ownFileDescriptorSync } from "./create-owned-file.js";
2
+ import { FsSafeError } from "./errors.js";
3
+ import { captureNativeFdClose } from "./native-binding.js";
4
+ import { requireNativeBinding } from "./native.js";
5
+ /** Creates a POSIX anonymous pipe with two native-owned, close-on-exec ends. */
6
+ export function createPipe() {
7
+ if (!["linux", "darwin", "freebsd"].includes(process.platform)) {
8
+ throw new FsSafeError("unsupported-platform", "anonymous pipes require Linux, Darwin, or FreeBSD");
9
+ }
10
+ const native = requireNativeBinding();
11
+ if (!native.createPipe) {
12
+ throw new FsSafeError("helper-unavailable", "native anonymous pipes are unavailable");
13
+ }
14
+ const close = captureNativeFdClose(native);
15
+ try {
16
+ const { reader, writer, atomicCloseOnExec } = native.createPipe();
17
+ return Object.freeze({
18
+ reader: ownFileDescriptorSync(reader, close),
19
+ writer: ownFileDescriptorSync(writer, close),
20
+ atomicCloseOnExec,
21
+ });
22
+ }
23
+ catch (cause) {
24
+ throw new FsSafeError("helper-failed", "anonymous pipe creation failed", { cause });
25
+ }
26
+ }
@@ -2,6 +2,7 @@ import fsSync, {} from "node:fs";
2
2
  import fs, {} from "node:fs/promises";
3
3
  import { FsSafeError } from "./errors.js";
4
4
  import { sameFileIdentityForCleanup } from "./file-identity.js";
5
+ import { isHardlinkFallbackError } from "./hardlink-fallback.js";
5
6
  import { assertSynchronousCallbackResult } from "./mutation-authority.js";
6
7
  import { resolveReadOpenFlags } from "./read-open-flags.js";
7
8
  import { errorCauseOptions } from "./root-errors.js";
@@ -39,11 +40,7 @@ export function normalizeLinkError(error) {
39
40
  if (code === "EEXIST") {
40
41
  return new FsSafeError("already-exists", "isolated producer sibling already exists", errorCauseOptions(error));
41
42
  }
42
- if (code === "EXDEV" ||
43
- code === "ENOSYS" ||
44
- code === "ENOTSUP" ||
45
- code === "EOPNOTSUPP" ||
46
- code === "EPERM") {
43
+ if (isHardlinkFallbackError(error)) {
47
44
  return new FsSafeError("helper-unavailable", "atomic isolated producer handoff is unavailable", errorCauseOptions(error));
48
45
  }
49
46
  return error;
@@ -2,7 +2,10 @@ import fs, {} from "node:fs";
2
2
  import path from "node:path";
3
3
  import { assertSyncDirectoryGuard } from "./directory-guard.js";
4
4
  import { FsSafeError } from "./errors.js";
5
+ import { assertExclusiveCreateLeaf } from "./exclusive-create.js";
5
6
  import { sameFileIdentityForCleanup } from "./file-identity.js";
7
+ import { hasErrorCode } from "./file-cleanup.js";
8
+ import { isHardlinkFallbackError } from "./hardlink-fallback.js";
6
9
  // Node has no portable no-replace rename. Publish the completed stage by link,
7
10
  // then remove its verified temporary name without yielding.
8
11
  export function publishCopyStage(params) {
@@ -27,7 +30,16 @@ export function publishCopyStage(params) {
27
30
  throw new FsSafeError("already-exists", "destination already exists");
28
31
  }
29
32
  params.onPublicationAttempt?.();
30
- fs.linkSync(temporaryPath, targetPath);
33
+ let renamed = false;
34
+ try {
35
+ fs.linkSync(temporaryPath, targetPath);
36
+ }
37
+ catch (error) {
38
+ if (!isHardlinkFallbackError(error))
39
+ throw error;
40
+ publishThroughPlaceholder(params, assertCurrent);
41
+ renamed = true;
42
+ }
31
43
  let observerRejected = false;
32
44
  let observerError;
33
45
  try {
@@ -38,16 +50,29 @@ export function publishCopyStage(params) {
38
50
  observerError = error;
39
51
  }
40
52
  try {
41
- const parent = fs.lstatSync(path.dirname(temporaryPath), { bigint: true });
42
- const current = fs.lstatSync(temporaryPath, { bigint: true });
43
- if (parent.isSymbolicLink() || !parent.isDirectory() ||
44
- !sameFileIdentityForCleanup(parent, params.parentGuard.stat) ||
45
- current.isSymbolicLink() || !current.isFile() ||
46
- !sameFileIdentityForCleanup(current, identity)) {
47
- throw new FsSafeError("path-mismatch", "published copy temporary name changed before cleanup");
53
+ if (renamed) {
54
+ assertSyncDirectoryGuard(params.parentGuard);
55
+ if (fs.lstatSync(temporaryPath, { throwIfNoEntry: false })) {
56
+ throw new FsSafeError("path-mismatch", "renamed copy stage still exists");
57
+ }
58
+ const target = fs.lstatSync(targetPath, { bigint: true });
59
+ if (!target.isFile() || target.isSymbolicLink() || target.nlink !== 1n ||
60
+ !sameFileIdentityForCleanup(target, identity)) {
61
+ throw new FsSafeError("path-mismatch", "renamed copy target changed after publication");
62
+ }
63
+ }
64
+ else {
65
+ const parent = fs.lstatSync(path.dirname(temporaryPath), { bigint: true });
66
+ const current = fs.lstatSync(temporaryPath, { bigint: true });
67
+ if (parent.isSymbolicLink() || !parent.isDirectory() ||
68
+ !sameFileIdentityForCleanup(parent, params.parentGuard.stat) ||
69
+ current.isSymbolicLink() || !current.isFile() ||
70
+ !sameFileIdentityForCleanup(current, identity)) {
71
+ throw new FsSafeError("path-mismatch", "published copy temporary name changed before cleanup");
72
+ }
73
+ // This removes the stage's name, never the published destination or source.
74
+ fs.unlinkSync(temporaryPath);
48
75
  }
49
- // This removes the stage's name, never the published destination or source.
50
- fs.unlinkSync(temporaryPath);
51
76
  }
52
77
  catch (error) {
53
78
  throw new FsSafeError("helper-failed", "published copy temporary cleanup failed", {
@@ -58,3 +83,60 @@ export function publishCopyStage(params) {
58
83
  if (observerRejected)
59
84
  throw observerError;
60
85
  }
86
+ function isEmptyPlaceholder(current, identity) {
87
+ return current.isFile() && !current.isSymbolicLink() && current.nlink === 1n &&
88
+ current.size === 0n && sameFileIdentityForCleanup(current, identity);
89
+ }
90
+ function publishThroughPlaceholder(params, assertCurrent) {
91
+ const { targetPath, temporaryPath } = params;
92
+ // An empty placeholder can be observed before the completed stage atomically replaces it.
93
+ // The stage proved directory write access; a real permission denial still fails O_EXCL.
94
+ assertCurrent();
95
+ let fd;
96
+ try {
97
+ assertExclusiveCreateLeaf(targetPath);
98
+ fd = fs.openSync(targetPath, fs.constants.O_WRONLY | fs.constants.O_CREAT |
99
+ fs.constants.O_EXCL | (fs.constants.O_NOFOLLOW ?? 0), 0o600);
100
+ }
101
+ catch (error) {
102
+ if (hasErrorCode(error, "EEXIST")) {
103
+ throw new FsSafeError("already-exists", "destination already exists", { cause: error });
104
+ }
105
+ throw error;
106
+ }
107
+ let placeholder;
108
+ try {
109
+ try {
110
+ placeholder = fs.fstatSync(fd, { bigint: true });
111
+ }
112
+ finally {
113
+ fs.closeSync(fd);
114
+ }
115
+ assertCurrent();
116
+ const current = fs.lstatSync(targetPath, { bigint: true, throwIfNoEntry: false });
117
+ if (!current || !isEmptyPlaceholder(current, placeholder)) {
118
+ throw new FsSafeError("path-mismatch", "exclusive copy placeholder changed before publication");
119
+ }
120
+ fs.renameSync(temporaryPath, targetPath);
121
+ }
122
+ catch (error) {
123
+ try {
124
+ assertSyncDirectoryGuard(params.parentGuard);
125
+ const current = fs.lstatSync(targetPath, { bigint: true, throwIfNoEntry: false });
126
+ if (current) {
127
+ if (!placeholder)
128
+ throw new FsSafeError("path-mismatch", "copy placeholder identity unavailable for cleanup");
129
+ if (isEmptyPlaceholder(current, placeholder))
130
+ fs.unlinkSync(targetPath);
131
+ }
132
+ }
133
+ catch (cleanupError) {
134
+ throw new FsSafeError("helper-failed", "unpublished copy placeholder cleanup failed", {
135
+ cause: new AggregateError([error, cleanupError], "publication and cleanup failed"),
136
+ details: { publication: "not-published", path: targetPath, cleanup: "failed",
137
+ ...(placeholder ? { dev: placeholder.dev, ino: placeholder.ino } : {}) },
138
+ });
139
+ }
140
+ throw error;
141
+ }
142
+ }
@@ -2,6 +2,7 @@ import { type BigIntStats, type Stats } from "node:fs";
2
2
  import { type DirectoryReceipt, type DirectorySyncOutcome } from "./directory-durability.js";
3
3
  import { type FileIdentityStat } from "./file-identity.js";
4
4
  import { type PublishFileExclusiveSyncFailurePolicy } from "./publish-file-failure.js";
5
+ export { isHardlinkFallbackError } from "./hardlink-fallback.js";
5
6
  export type PublishFileExclusiveStrategy = "link-or-copy" | "link-required" | "rename-noreplace";
6
7
  export type PublishFileExclusiveResult = {
7
8
  method: "hardlink" | "exclusive-copy" | "rename-noreplace";
@@ -10,7 +11,6 @@ export type PublishFileExclusiveResult = {
10
11
  identity: Stats;
11
12
  directorySync: DirectorySyncOutcome;
12
13
  };
13
- export declare function isHardlinkFallbackError(error: unknown): boolean;
14
14
  export declare function publishFileExclusive(params: {
15
15
  sourcePath: string;
16
16
  targetPath: string;