@openclaw/openshell-sandbox 2026.10.5 → 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 (123) hide show
  1. package/dist/index.js +16 -18
  2. package/node_modules/@openclaw/fs-safe/CHANGELOG.md +48 -0
  3. package/node_modules/@openclaw/fs-safe/README.md +14 -4
  4. package/node_modules/@openclaw/fs-safe/dist/archive-merge.js +28 -3
  5. package/node_modules/@openclaw/fs-safe/dist/archive-parser.wasm +0 -0
  6. package/node_modules/@openclaw/fs-safe/dist/archive-read.js +11 -5
  7. package/node_modules/@openclaw/fs-safe/dist/archive-zip-loader.js +4 -2
  8. package/node_modules/@openclaw/fs-safe/dist/archive.js +2 -2
  9. package/node_modules/@openclaw/fs-safe/dist/clone-metadata.js +1 -1
  10. package/node_modules/@openclaw/fs-safe/dist/copy-file-input.d.ts +2 -0
  11. package/node_modules/@openclaw/fs-safe/dist/copy-file-input.js +4 -3
  12. package/node_modules/@openclaw/fs-safe/dist/copy-link.d.ts +26 -0
  13. package/node_modules/@openclaw/fs-safe/dist/copy-link.js +169 -0
  14. package/node_modules/@openclaw/fs-safe/dist/copy-metadata.d.ts +9 -0
  15. package/node_modules/@openclaw/fs-safe/dist/copy-metadata.js +39 -0
  16. package/node_modules/@openclaw/fs-safe/dist/copy-root-file-sync.js +1 -1
  17. package/node_modules/@openclaw/fs-safe/dist/copy-tree-portable.js +3 -6
  18. package/node_modules/@openclaw/fs-safe/dist/copy.js +3 -3
  19. package/node_modules/@openclaw/fs-safe/dist/darwin-acl.d.ts +12 -0
  20. package/node_modules/@openclaw/fs-safe/dist/darwin-acl.js +29 -0
  21. package/node_modules/@openclaw/fs-safe/dist/directory-mode-node.d.ts +3 -0
  22. package/node_modules/@openclaw/fs-safe/dist/directory-mode-node.js +27 -2
  23. package/node_modules/@openclaw/fs-safe/dist/entry-publication.js +1 -1
  24. package/node_modules/@openclaw/fs-safe/dist/file-handle-transfer.js +4 -2
  25. package/node_modules/@openclaw/fs-safe/dist/file-hash.js +4 -3
  26. package/node_modules/@openclaw/fs-safe/dist/file-lock.d.ts +1 -0
  27. package/node_modules/@openclaw/fs-safe/dist/file-lock.js +1 -0
  28. package/node_modules/@openclaw/fs-safe/dist/file-write-lease.d.ts +9 -0
  29. package/node_modules/@openclaw/fs-safe/dist/file-write-lease.js +42 -0
  30. package/node_modules/@openclaw/fs-safe/dist/guest-native-python.js +3 -0
  31. package/node_modules/@openclaw/fs-safe/dist/hardlink-fallback.d.ts +1 -0
  32. package/node_modules/@openclaw/fs-safe/dist/hardlink-fallback.js +5 -0
  33. package/node_modules/@openclaw/fs-safe/dist/json-durable-queue-ownership.js +16 -3
  34. package/node_modules/@openclaw/fs-safe/dist/native-binding.d.ts +57 -18
  35. package/node_modules/@openclaw/fs-safe/dist/native-directory-observation.js +1 -1
  36. package/node_modules/@openclaw/fs-safe/dist/native-move-fallback.d.ts +1 -1
  37. package/node_modules/@openclaw/fs-safe/dist/native-move-fallback.js +2 -0
  38. package/node_modules/@openclaw/fs-safe/dist/native-operations.js +1 -1
  39. package/node_modules/@openclaw/fs-safe/dist/native-parent-admission.d.ts +1 -1
  40. package/node_modules/@openclaw/fs-safe/dist/native-parent-admission.js +2 -0
  41. package/node_modules/@openclaw/fs-safe/dist/native-pinned-write-windows.d.ts +1 -1
  42. package/node_modules/@openclaw/fs-safe/dist/native-pinned-write-windows.js +19 -1
  43. package/node_modules/@openclaw/fs-safe/dist/native-pinned-write.d.ts +4 -1
  44. package/node_modules/@openclaw/fs-safe/dist/native-pinned-write.js +18 -0
  45. package/node_modules/@openclaw/fs-safe/dist/native-policy-parent.d.ts +1 -1
  46. package/node_modules/@openclaw/fs-safe/dist/native-policy-parent.js +3 -0
  47. package/node_modules/@openclaw/fs-safe/dist/native-staged-file.d.ts +1 -1
  48. package/node_modules/@openclaw/fs-safe/dist/native-staged-file.js +3 -0
  49. package/node_modules/@openclaw/fs-safe/dist/native.d.ts +5 -3
  50. package/node_modules/@openclaw/fs-safe/dist/native.js +25 -6
  51. package/node_modules/@openclaw/fs-safe/dist/opened-realpath.js +9 -1
  52. package/node_modules/@openclaw/fs-safe/dist/overwrite-file-handle.js +4 -1
  53. package/node_modules/@openclaw/fs-safe/dist/permissions-public.d.ts +1 -0
  54. package/node_modules/@openclaw/fs-safe/dist/permissions-public.js +1 -0
  55. package/node_modules/@openclaw/fs-safe/dist/permissions-windows.js +36 -47
  56. package/node_modules/@openclaw/fs-safe/dist/pinned-write-input.js +2 -0
  57. package/node_modules/@openclaw/fs-safe/dist/pinned-write-staged.js +3 -0
  58. package/node_modules/@openclaw/fs-safe/dist/pinned-write-types.d.ts +2 -1
  59. package/node_modules/@openclaw/fs-safe/dist/pinned-write.js +5 -3
  60. package/node_modules/@openclaw/fs-safe/dist/pipe.d.ts +9 -0
  61. package/node_modules/@openclaw/fs-safe/dist/pipe.js +26 -0
  62. package/node_modules/@openclaw/fs-safe/dist/private-producer-handoff.js +2 -5
  63. package/node_modules/@openclaw/fs-safe/dist/publish-copy-stage.js +92 -10
  64. package/node_modules/@openclaw/fs-safe/dist/publish-file.d.ts +1 -1
  65. package/node_modules/@openclaw/fs-safe/dist/publish-file.js +12 -11
  66. package/node_modules/@openclaw/fs-safe/dist/root-copy.d.ts +18 -0
  67. package/node_modules/@openclaw/fs-safe/dist/root-copy.js +128 -0
  68. package/node_modules/@openclaw/fs-safe/dist/root-create-native.js +12 -4
  69. package/node_modules/@openclaw/fs-safe/dist/root-impl.js +6 -92
  70. package/node_modules/@openclaw/fs-safe/dist/root-move-noreplace.js +1 -1
  71. package/node_modules/@openclaw/fs-safe/dist/root-options.d.ts +2 -0
  72. package/node_modules/@openclaw/fs-safe/dist/root-remove-native.js +8 -1
  73. package/node_modules/@openclaw/fs-safe/dist/sibling-staged-file.js +1 -1
  74. package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-retained.js +1 -1
  75. package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-target.js +8 -1
  76. package/node_modules/@openclaw/fs-safe/dist/test-hooks.d.ts +1 -0
  77. package/node_modules/@openclaw/fs-safe/dist/test-hooks.js +1 -0
  78. package/node_modules/@openclaw/fs-safe/dist/watch-native.js +2 -2
  79. package/node_modules/@openclaw/fs-safe/dist/windows-security-bridge.cs +11 -5
  80. package/node_modules/@openclaw/fs-safe/dist/windows-security-command.js +9 -1
  81. package/node_modules/@openclaw/fs-safe/dist/windows-test-hooks.d.ts +16 -0
  82. package/node_modules/@openclaw/fs-safe/dist/windows-test-hooks.js +51 -0
  83. package/node_modules/@openclaw/fs-safe/docs/archive.md +16 -0
  84. package/node_modules/@openclaw/fs-safe/docs/contributing.md +35 -5
  85. package/node_modules/@openclaw/fs-safe/docs/copy.md +22 -0
  86. package/node_modules/@openclaw/fs-safe/docs/durability.md +4 -2
  87. package/node_modules/@openclaw/fs-safe/docs/errors.md +8 -0
  88. package/node_modules/@openclaw/fs-safe/docs/guest.md +2 -0
  89. package/node_modules/@openclaw/fs-safe/docs/install.md +13 -5
  90. package/node_modules/@openclaw/fs-safe/docs/migrating-to-0.5.md +5 -5
  91. package/node_modules/@openclaw/fs-safe/docs/native-helper.md +15 -5
  92. package/node_modules/@openclaw/fs-safe/docs/native.md +51 -2
  93. package/node_modules/@openclaw/fs-safe/docs/pipe.md +81 -0
  94. package/node_modules/@openclaw/fs-safe/docs/platform-capabilities.md +63 -0
  95. package/node_modules/@openclaw/fs-safe/docs/quickstart.md +23 -3
  96. package/node_modules/@openclaw/fs-safe/docs/reading.md +7 -1
  97. package/node_modules/@openclaw/fs-safe/docs/secret-file.md +5 -5
  98. package/node_modules/@openclaw/fs-safe/docs/sidecar-lock.md +31 -13
  99. package/node_modules/@openclaw/fs-safe/docs/store.md +11 -0
  100. package/node_modules/@openclaw/fs-safe/docs/types.md +45 -80
  101. package/node_modules/@openclaw/fs-safe/docs/writing.md +121 -14
  102. package/node_modules/@openclaw/fs-safe/package.json +18 -10
  103. package/node_modules/@openclaw/fs-safe-darwin-arm64/fs-safe-native.node +0 -0
  104. package/node_modules/@openclaw/fs-safe-darwin-arm64/package.json +1 -1
  105. package/node_modules/@openclaw/fs-safe-darwin-x64/fs-safe-native.node +0 -0
  106. package/node_modules/@openclaw/fs-safe-darwin-x64/package.json +1 -1
  107. package/node_modules/@openclaw/fs-safe-freebsd-arm64/fs-safe-native.node +0 -0
  108. package/node_modules/@openclaw/fs-safe-freebsd-arm64/package.json +29 -0
  109. package/node_modules/@openclaw/fs-safe-freebsd-x64/fs-safe-native.node +0 -0
  110. package/node_modules/@openclaw/fs-safe-freebsd-x64/package.json +29 -0
  111. package/node_modules/@openclaw/fs-safe-linux-arm64-gnu/fs-safe-native.node +0 -0
  112. package/node_modules/@openclaw/fs-safe-linux-arm64-gnu/package.json +1 -1
  113. package/node_modules/@openclaw/fs-safe-linux-arm64-musl/fs-safe-native.node +0 -0
  114. package/node_modules/@openclaw/fs-safe-linux-arm64-musl/package.json +1 -1
  115. package/node_modules/@openclaw/fs-safe-linux-x64-gnu/fs-safe-native.node +0 -0
  116. package/node_modules/@openclaw/fs-safe-linux-x64-gnu/package.json +1 -1
  117. package/node_modules/@openclaw/fs-safe-linux-x64-musl/fs-safe-native.node +0 -0
  118. package/node_modules/@openclaw/fs-safe-linux-x64-musl/package.json +1 -1
  119. package/node_modules/@openclaw/fs-safe-win32-arm64-msvc/fs-safe-native.node +0 -0
  120. package/node_modules/@openclaw/fs-safe-win32-arm64-msvc/package.json +29 -0
  121. package/node_modules/@openclaw/fs-safe-win32-x64-msvc/fs-safe-native.node +0 -0
  122. package/node_modules/@openclaw/fs-safe-win32-x64-msvc/package.json +1 -1
  123. package/package.json +5 -5
@@ -5,6 +5,7 @@ import { inspectDirectoryIdentity, inspectDirectoryIdentitySync } from "./direct
5
5
  import { nodeDirectorySearchOnlyFlags } from "./directory-mode-node.js";
6
6
  import { inspectNativeDirectoryObservation } from "./native-directory-observation.js";
7
7
  import { FsSafeError } from "./errors.js";
8
+ import { assertNativeCapabilities } from "./native.js";
8
9
  import { captureNativeFdClose } from "./native-binding.js";
9
10
  import { realpathSync } from "./realpath.js";
10
11
  import { describeStagedDirectory, exactIdentityMatches } from "./staged-directory.js";
@@ -22,6 +23,7 @@ function assertParentAdmissionAvailable(binding) {
22
23
  throw unavailable("native parent directory admission is unavailable");
23
24
  }
24
25
  captureNativeFdClose(binding);
26
+ assertNativeCapabilities(binding, "openBeneath");
25
27
  }
26
28
  export async function openNativeRootAdmission(binding, params) {
27
29
  assertParentAdmissionAvailable(binding);
@@ -1,6 +1,6 @@
1
1
  import type { FileHandle } from "node:fs/promises";
2
2
  import { type AnyAsyncDirectoryGuard } from "./directory-guard.js";
3
3
  import type { FileIdentityStat } from "./file-identity.js";
4
- import type { NativeBinding } from "./native.js";
4
+ import { type NativeBinding } from "./native.js";
5
5
  import type { PinnedWriteParams } from "./pinned-write-types.js";
6
6
  export declare function runPinnedWriteWindows(binding: NativeBinding, params: PinnedWriteParams, root: FileHandle, parentFd: number, parentGuard: AnyAsyncDirectoryGuard): Promise<FileIdentityStat>;
@@ -8,6 +8,8 @@ import { assertSyncDirectoryGuard } from "./directory-guard.js";
8
8
  import { FsSafeError } from "./errors.js";
9
9
  import { nativeOpenFlags, removeNativeCreatedFileIfStillPinned, } from "./native-operations.js";
10
10
  import { writePinnedInput } from "./pinned-write-input.js";
11
+ import { assertNativeCopyCompleted, createNativeCopyFile } from "./copy-file-input.js";
12
+ import { assertNativeCapabilities } from "./native.js";
11
13
  import { captureNativeFdClose } from "./native-binding.js";
12
14
  import { inspectFileIdentitySync } from "./strict-file-identity.js";
13
15
  import { assertFinalSymlinkRejected } from "./root-symlink-policy.js";
@@ -15,6 +17,7 @@ import { classifyNativeRenameFailure } from "./native-rename-outcome.js";
15
17
  import { cleanupPinnedFilePath } from "./file-cleanup.js";
16
18
  import { createStagedFileReceipt, settleStagedFile } from "./staged-file-settlement.js";
17
19
  import { exactIdentityMatches } from "./staged-directory.js";
20
+ import { restoreCopyMetadata } from "./copy-metadata.js";
18
21
  import { inspectNativeDirectoryObservation } from "./native-directory-observation.js";
19
22
  function closeWriteFd(closeFd, fd) {
20
23
  if (fd === undefined)
@@ -50,6 +53,7 @@ function assertNamedParentCurrent(binding, guard, identity) {
50
53
  assertSyncDirectoryGuard({ ...guard, stat: identity });
51
54
  }
52
55
  export async function runPinnedWriteWindows(binding, params, root, parentFd, parentGuard) {
56
+ assertNativeCapabilities(binding, "openBeneath", "renameNoReplace", "renameReplace", "fstatIdentity");
53
57
  const closeFd = captureNativeFdClose(binding);
54
58
  const parentPath = parentGuard.realPath;
55
59
  let tempFd;
@@ -78,6 +82,9 @@ export async function runPinnedWriteWindows(binding, params, root, parentFd, par
78
82
  try {
79
83
  tempName = `.fs-safe-${randomUUID()}.tmp`;
80
84
  params.assertBeforeMutation?.();
85
+ const copied = params.input.kind === "file" && !params.private
86
+ ? await createNativeCopyFile(binding, params.input, parentFd, tempName, params.maxBytes)
87
+ : undefined;
81
88
  if (params.private) {
82
89
  const parentIdentity = fsSync.fstatSync(parentFd, { bigint: true });
83
90
  privateHandle = await createFileHandle(path.join(parentPath, tempName), {
@@ -91,16 +98,24 @@ export async function runPinnedWriteWindows(binding, params, root, parentFd, par
91
98
  });
92
99
  tempFd = privateHandle.fd;
93
100
  }
101
+ else if (copied) {
102
+ tempFd = copied.fd;
103
+ }
94
104
  else {
95
105
  tempFd = binding.openBeneath(parentFd, tempName, nativeOpenFlags(fsSync.constants.O_WRONLY | fsSync.constants.O_CREAT | fsSync.constants.O_EXCL)).fd;
96
106
  }
97
107
  const verificationIdentity = inspectFileIdentitySync(() => fsSync.fstatSync(tempFd, { bigint: true }));
98
108
  tempIdentity = verificationIdentity;
109
+ if (params.input.kind === "file")
110
+ assertNativeCopyCompleted(params.input, copied);
99
111
  // Creation is requested at 0600 in the binding, but a restrictive umask
100
112
  // can remove owner access. Keep the unpublished inode private and
101
113
  // reopenable until the published name has been identity-fenced.
102
114
  fsSync.fchmodSync(tempFd, 0o600);
103
- await writePinnedInput(tempFd, params.input, params.maxBytes, params.assertBeforeMutation);
115
+ if (!copied)
116
+ await writePinnedInput(tempFd, params.input, params.maxBytes, params.assertBeforeMutation);
117
+ if (params.input.kind === "file")
118
+ restoreCopyMetadata(tempFd, params.input.metadata);
104
119
  if (params.sync !== false) {
105
120
  if (params.strictFileSync)
106
121
  fsSync.fsyncSync(tempFd);
@@ -132,6 +147,9 @@ export async function runPinnedWriteWindows(binding, params, root, parentFd, par
132
147
  }
133
148
  assertFinalSymlinkRejected(path.join(parentPath, params.basename), params.rejectFinalSymlink);
134
149
  }
150
+ // The authority callback itself may have cancelled an otherwise completed copy.
151
+ if (params.input.kind === "file")
152
+ params.input.signal?.throwIfAborted();
135
153
  try {
136
154
  if (params.overwrite === false) {
137
155
  binding.renameNoReplace(parentFd, tempName, parentFd, params.basename);
@@ -1,4 +1,7 @@
1
1
  import type { FileIdentityStat } from "./file-identity.js";
2
- import type { NativeBinding } from "./native.js";
2
+ import { type NativeBinding } from "./native.js";
3
3
  import type { PinnedWriteParams } from "./pinned-write-types.js";
4
+ type PinnedWriteCapabilitiesInput = Pick<PinnedWriteParams, "input" | "private" | "verifyPosixMode">;
5
+ export declare function getPinnedWriteNativeBinding(params: PinnedWriteCapabilitiesInput): NativeBinding | undefined;
4
6
  export declare function runPinnedWriteNative(binding: NativeBinding, params: PinnedWriteParams, fallback: (input: PinnedWriteParams["input"]) => Promise<FileIdentityStat>): Promise<FileIdentityStat>;
7
+ export {};
@@ -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;