@openclaw/openshell-sandbox 2026.10.1-beta.2 → 2026.10.5-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/dist/index.js +69 -111
  2. package/node_modules/@openclaw/fs-safe/CHANGELOG.md +91 -0
  3. package/node_modules/@openclaw/fs-safe/README.md +1 -1
  4. package/node_modules/@openclaw/fs-safe/dist/advanced.d.ts +1 -0
  5. package/node_modules/@openclaw/fs-safe/dist/advanced.js +1 -0
  6. package/node_modules/@openclaw/fs-safe/dist/archive-durability.js +1 -1
  7. package/node_modules/@openclaw/fs-safe/dist/archive-entry.js +7 -3
  8. package/node_modules/@openclaw/fs-safe/dist/archive-merge.js +4 -4
  9. package/node_modules/@openclaw/fs-safe/dist/archive-parser.wasm +0 -0
  10. package/node_modules/@openclaw/fs-safe/dist/archive-read.js +5 -4
  11. package/node_modules/@openclaw/fs-safe/dist/archive-staging.d.ts +1 -1
  12. package/node_modules/@openclaw/fs-safe/dist/archive-staging.js +22 -38
  13. package/node_modules/@openclaw/fs-safe/dist/copy-path-admission.d.ts +17 -0
  14. package/node_modules/@openclaw/fs-safe/dist/copy-path-admission.js +117 -0
  15. package/node_modules/@openclaw/fs-safe/dist/copy-root-file-sync.d.ts +38 -0
  16. package/node_modules/@openclaw/fs-safe/dist/copy-root-file-sync.js +195 -0
  17. package/node_modules/@openclaw/fs-safe/dist/create.d.ts +1 -0
  18. package/node_modules/@openclaw/fs-safe/dist/create.js +4 -1
  19. package/node_modules/@openclaw/fs-safe/dist/entry-publication.js +3 -1
  20. package/node_modules/@openclaw/fs-safe/dist/errors.d.ts +2 -0
  21. package/node_modules/@openclaw/fs-safe/dist/errors.js +5 -0
  22. package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root-acquire.d.ts +3 -0
  23. package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root-acquire.js +57 -11
  24. package/node_modules/@openclaw/fs-safe/dist/guarded-mutation.js +5 -5
  25. package/node_modules/@openclaw/fs-safe/dist/guest-native-python.js +52 -5
  26. package/node_modules/@openclaw/fs-safe/dist/guest.js +1 -1
  27. package/node_modules/@openclaw/fs-safe/dist/index.d.ts +1 -1
  28. package/node_modules/@openclaw/fs-safe/dist/index.js +1 -1
  29. package/node_modules/@openclaw/fs-safe/dist/native-binding.d.ts +4 -0
  30. package/node_modules/@openclaw/fs-safe/dist/native-move-fallback.d.ts +3 -0
  31. package/node_modules/@openclaw/fs-safe/dist/native-move-fallback.js +54 -0
  32. package/node_modules/@openclaw/fs-safe/dist/native-noreplace.d.ts +8 -0
  33. package/node_modules/@openclaw/fs-safe/dist/native-noreplace.js +55 -0
  34. package/node_modules/@openclaw/fs-safe/dist/native-operations.d.ts +0 -1
  35. package/node_modules/@openclaw/fs-safe/dist/native-operations.js +22 -26
  36. package/node_modules/@openclaw/fs-safe/dist/native-pinned-write-windows.js +5 -1
  37. package/node_modules/@openclaw/fs-safe/dist/native-pinned-write.d.ts +1 -1
  38. package/node_modules/@openclaw/fs-safe/dist/native-pinned-write.js +12 -2
  39. package/node_modules/@openclaw/fs-safe/dist/native-rename-outcome.js +3 -1
  40. package/node_modules/@openclaw/fs-safe/dist/native-staged-file.d.ts +1 -1
  41. package/node_modules/@openclaw/fs-safe/dist/native-staged-file.js +52 -6
  42. package/node_modules/@openclaw/fs-safe/dist/permission-exec.js +2 -1
  43. package/node_modules/@openclaw/fs-safe/dist/pinned-write-input.d.ts +2 -0
  44. package/node_modules/@openclaw/fs-safe/dist/pinned-write-input.js +17 -0
  45. package/node_modules/@openclaw/fs-safe/dist/pinned-write-types.d.ts +6 -1
  46. package/node_modules/@openclaw/fs-safe/dist/pinned-write.js +1 -1
  47. package/node_modules/@openclaw/fs-safe/dist/private-temp-workspace.js +2 -2
  48. package/node_modules/@openclaw/fs-safe/dist/publish-file-failure.d.ts +5 -0
  49. package/node_modules/@openclaw/fs-safe/dist/publish-file-failure.js +1 -0
  50. package/node_modules/@openclaw/fs-safe/dist/publish-file.d.ts +2 -0
  51. package/node_modules/@openclaw/fs-safe/dist/publish-file.js +35 -2
  52. package/node_modules/@openclaw/fs-safe/dist/replace-directory.js +6 -5
  53. package/node_modules/@openclaw/fs-safe/dist/replace-file-copy-fallback.js +1 -4
  54. package/node_modules/@openclaw/fs-safe/dist/replace-file-descriptor.js +6 -0
  55. package/node_modules/@openclaw/fs-safe/dist/root-create-input.d.ts +15 -1
  56. package/node_modules/@openclaw/fs-safe/dist/root-create-input.js +4 -0
  57. package/node_modules/@openclaw/fs-safe/dist/root-directory-entry.js +1 -1
  58. package/node_modules/@openclaw/fs-safe/dist/root-directory-list.d.ts +0 -1
  59. package/node_modules/@openclaw/fs-safe/dist/root-directory-list.js +6 -12
  60. package/node_modules/@openclaw/fs-safe/dist/root-errors.js +25 -4
  61. package/node_modules/@openclaw/fs-safe/dist/root-impl.js +18 -18
  62. package/node_modules/@openclaw/fs-safe/dist/root-move-noreplace.d.ts +4 -2
  63. package/node_modules/@openclaw/fs-safe/dist/root-move-noreplace.js +13 -17
  64. package/node_modules/@openclaw/fs-safe/dist/root-paths.js +1 -1
  65. package/node_modules/@openclaw/fs-safe/dist/root-walk.js +4 -4
  66. package/node_modules/@openclaw/fs-safe/dist/root-write-verification.d.ts +10 -1
  67. package/node_modules/@openclaw/fs-safe/dist/root-write-verification.js +20 -0
  68. package/node_modules/@openclaw/fs-safe/dist/secure-temp-dir.js +19 -7
  69. package/node_modules/@openclaw/fs-safe/dist/sibling-staged-file.js +18 -3
  70. package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-acquire.js +25 -3
  71. package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-admission.d.ts +2 -0
  72. package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-retained.d.ts +14 -0
  73. package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-retained.js +187 -0
  74. package/node_modules/@openclaw/fs-safe/dist/sidecar-lock.js +15 -1
  75. package/node_modules/@openclaw/fs-safe/dist/staged-file-settlement.js +4 -1
  76. package/node_modules/@openclaw/fs-safe/dist/temp-directory-diagnostics.d.ts +10 -0
  77. package/node_modules/@openclaw/fs-safe/dist/temp-directory-diagnostics.js +29 -0
  78. package/node_modules/@openclaw/fs-safe/dist/temp-target.js +1 -1
  79. package/node_modules/@openclaw/fs-safe/dist/temp-workspace-admission.js +6 -6
  80. package/node_modules/@openclaw/fs-safe/dist/temp-workspace-child-admission.d.ts +4 -4
  81. package/node_modules/@openclaw/fs-safe/dist/temp-workspace-child-admission.js +17 -16
  82. package/node_modules/@openclaw/fs-safe/dist/temp-workspace-descriptor.js +20 -28
  83. package/node_modules/@openclaw/fs-safe/dist/temp-workspace-owner.d.ts +1 -0
  84. package/node_modules/@openclaw/fs-safe/dist/temp-workspace-owner.js +27 -4
  85. package/node_modules/@openclaw/fs-safe/dist/walk.js +2 -13
  86. package/node_modules/@openclaw/fs-safe/dist/watch-native.d.ts +2 -2
  87. package/node_modules/@openclaw/fs-safe/dist/watch-native.js +3 -1
  88. package/node_modules/@openclaw/fs-safe/dist/watch-rescan.js +3 -9
  89. package/node_modules/@openclaw/fs-safe/dist/watch.js +2 -3
  90. package/node_modules/@openclaw/fs-safe/docs/advanced.md +1 -0
  91. package/node_modules/@openclaw/fs-safe/docs/archive.md +5 -1
  92. package/node_modules/@openclaw/fs-safe/docs/atomic.md +11 -3
  93. package/node_modules/@openclaw/fs-safe/docs/contributing.md +1 -1
  94. package/node_modules/@openclaw/fs-safe/docs/copy.md +109 -1
  95. package/node_modules/@openclaw/fs-safe/docs/durability.md +50 -5
  96. package/node_modules/@openclaw/fs-safe/docs/errors.md +37 -0
  97. package/node_modules/@openclaw/fs-safe/docs/guest.md +21 -4
  98. package/node_modules/@openclaw/fs-safe/docs/native-helper.md +5 -3
  99. package/node_modules/@openclaw/fs-safe/docs/native.md +39 -6
  100. package/node_modules/@openclaw/fs-safe/docs/path-scope.md +4 -0
  101. package/node_modules/@openclaw/fs-safe/docs/public-api.md +3 -1
  102. package/node_modules/@openclaw/fs-safe/docs/sidecar-lock.md +46 -8
  103. package/node_modules/@openclaw/fs-safe/docs/staged-file.md +10 -3
  104. package/node_modules/@openclaw/fs-safe/docs/temp.md +12 -0
  105. package/node_modules/@openclaw/fs-safe/docs/watch.md +6 -3
  106. package/node_modules/@openclaw/fs-safe/docs/writing.md +40 -11
  107. package/node_modules/@openclaw/fs-safe/package.json +16 -16
  108. package/node_modules/@openclaw/fs-safe-darwin-arm64/fs-safe-native.node +0 -0
  109. package/node_modules/@openclaw/fs-safe-darwin-arm64/package.json +1 -1
  110. package/node_modules/@openclaw/fs-safe-darwin-x64/fs-safe-native.node +0 -0
  111. package/node_modules/@openclaw/fs-safe-darwin-x64/package.json +1 -1
  112. package/node_modules/@openclaw/fs-safe-linux-arm64-gnu/fs-safe-native.node +0 -0
  113. package/node_modules/@openclaw/fs-safe-linux-arm64-gnu/package.json +1 -1
  114. package/node_modules/@openclaw/fs-safe-linux-arm64-musl/fs-safe-native.node +0 -0
  115. package/node_modules/@openclaw/fs-safe-linux-arm64-musl/package.json +1 -1
  116. package/node_modules/@openclaw/fs-safe-linux-x64-gnu/fs-safe-native.node +0 -0
  117. package/node_modules/@openclaw/fs-safe-linux-x64-gnu/package.json +1 -1
  118. package/node_modules/@openclaw/fs-safe-linux-x64-musl/fs-safe-native.node +0 -0
  119. package/node_modules/@openclaw/fs-safe-linux-x64-musl/package.json +1 -1
  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
  123. package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root-arbitration.d.ts +0 -17
  124. package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root-arbitration.js +0 -66
  125. package/node_modules/@openclaw/fs-safe/dist/root-move-preflight.d.ts +0 -7
  126. package/node_modules/@openclaw/fs-safe/dist/root-move-preflight.js +0 -16
@@ -32,6 +32,16 @@ export const GUEST_FILESYSTEM_CREATE_STAGING_PYTHON = [
32
32
  ].join("\n");
33
33
  export const GUEST_FILESYSTEM_RENAME_NO_REPLACE_PYTHON = [
34
34
  "def rename_no_replace(src_parent_fd, src_basename, dst_parent_fd, dst_basename):",
35
+ " import stat",
36
+ " source = os.lstat(src_basename, dir_fd=src_parent_fd)",
37
+ " kind = stat.S_IFMT(source.st_mode)",
38
+ " expected = (source.st_dev, source.st_ino, kind)",
39
+ " def verify(parent_fd, basename, links):",
40
+ " current = os.lstat(basename, dir_fd=parent_fd)",
41
+ " if (current.st_dev, current.st_ino, stat.S_IFMT(current.st_mode)) != expected:",
42
+ " raise OSError(errno.ESTALE, 'no-replace publication identity changed', basename)",
43
+ " if kind == stat.S_IFREG and current.st_nlink != links:",
44
+ " raise OSError(errno.ESTALE, 'no-replace publication link count changed', basename)",
35
45
  " libc = ctypes.CDLL(None, use_errno=True)",
36
46
  " is_linux = sys.platform.startswith('linux')",
37
47
  " if is_linux:",
@@ -62,6 +72,31 @@ export const GUEST_FILESYSTEM_RENAME_NO_REPLACE_PYTHON = [
62
72
  " raise OSError(error_code, os.strerror(error_code), dst_basename)",
63
73
  " elif not is_linux:",
64
74
  " raise OSError(errno.ENOSYS, 'atomic no-replace rename is unavailable')",
75
+ " # Only a definitive rejection of the fixed flags reaches this fallback.",
76
+ " if kind not in (stat.S_IFDIR, stat.S_IFREG):",
77
+ " raise OSError(errno.EINVAL, 'no-replace fallback requires a file or directory', src_basename)",
78
+ " verify(src_parent_fd, src_basename, 1)",
79
+ " try:",
80
+ " os.lstat(dst_basename, dir_fd=dst_parent_fd)",
81
+ " except FileNotFoundError:",
82
+ " pass",
83
+ " else:",
84
+ " raise FileExistsError(errno.EEXIST, os.strerror(errno.EEXIST), dst_basename)",
85
+ " if kind == stat.S_IFDIR:",
86
+ " verify(src_parent_fd, src_basename, 1)",
87
+ " # A directory rename can replace only an EMPTY directory raced into",
88
+ " # the absent target. Non-empty directories and files are refused.",
89
+ " # The slash enforces source directory type at the syscall, even if",
90
+ " # a peer substitutes a file/symlink after verification. Identity",
91
+ " # checks are not compare-and-swap; verify publication afterward.",
92
+ " try:",
93
+ " os.rename(src_basename + '/', dst_basename, src_dir_fd=src_parent_fd, dst_dir_fd=dst_parent_fd)",
94
+ " except OSError as error:",
95
+ " if error.errno in (errno.ENOTEMPTY, errno.ENOTDIR, errno.EEXIST):",
96
+ " raise FileExistsError(errno.EEXIST, os.strerror(errno.EEXIST), dst_basename) from error",
97
+ " raise",
98
+ " verify(dst_parent_fd, dst_basename, 1)",
99
+ " return",
65
100
  " os.link(",
66
101
  " src_basename,",
67
102
  " dst_basename,",
@@ -69,12 +104,19 @@ export const GUEST_FILESYSTEM_RENAME_NO_REPLACE_PYTHON = [
69
104
  " dst_dir_fd=dst_parent_fd,",
70
105
  " follow_symlinks=False,",
71
106
  " )",
107
+ " # Only this exact two-name interval permits nlink=2. Never roll back the",
108
+ " # target on failure: it may be the only remaining link to the source.",
109
+ " verify(dst_parent_fd, dst_basename, 2)",
110
+ " verify(src_parent_fd, src_basename, 2)",
111
+ " os.unlink(src_basename, dir_fd=src_parent_fd)",
112
+ " verify(dst_parent_fd, dst_basename, 1)",
72
113
  ].join("\n");
73
114
  export const GUEST_FILESYSTEM_CREATE_EXCLUSIVE_PYTHON = [
74
115
  "def create_exclusive(parent_fd, basename, stdin_buffer):",
75
116
  " staging_fd = None",
76
117
  " staging_name = None",
77
118
  " temp_fd = None",
119
+ " temp_inode = None",
78
120
  " temp_name = 'payload'",
79
121
  " try:",
80
122
  " try:",
@@ -85,6 +127,7 @@ export const GUEST_FILESYSTEM_CREATE_EXCLUSIVE_PYTHON = [
85
127
  " raise FileExistsError(errno.EEXIST, os.strerror(errno.EEXIST), basename)",
86
128
  " staging_name, staging_fd = create_staging_dir(parent_fd)",
87
129
  " temp_fd = os.open(temp_name, WRITE_FLAGS, 0o600, dir_fd=staging_fd)",
130
+ " temp_inode = inode_identity(os.fstat(temp_fd))",
88
131
  " while True:",
89
132
  " chunk = stdin_buffer.read(65536)",
90
133
  " if not chunk:",
@@ -92,8 +135,7 @@ export const GUEST_FILESYSTEM_CREATE_EXCLUSIVE_PYTHON = [
92
135
  " write_all(temp_fd, chunk)",
93
136
  " # exclusive create payload is durable before publication",
94
137
  " os.fsync(temp_fd)",
95
- " temp_inode = inode_identity(os.fstat(temp_fd))",
96
- " # Publish with a native atomic no-replace rename.",
138
+ " # Publish with no-replace rename or the checked file fallback.",
97
139
  " rename_no_replace(staging_fd, temp_name, parent_fd, basename)",
98
140
  " target_stat = os.lstat(basename, dir_fd=parent_fd)",
99
141
  " if temp_inode != inode_identity(target_stat):",
@@ -105,14 +147,19 @@ export const GUEST_FILESYSTEM_CREATE_EXCLUSIVE_PYTHON = [
105
147
  " if staging_fd is not None:",
106
148
  " # Cleanup stays relative to the private pinned directory, so a",
107
149
  " # parent-path substitution cannot redirect payload deletion.",
150
+ " payload_removed = False",
108
151
  " try:",
109
- " os.unlink(temp_name, dir_fd=staging_fd)",
152
+ " current_payload = os.lstat(temp_name, dir_fd=staging_fd)",
153
+ " # Preserve ambiguous or partially published link pairs.",
154
+ " if temp_inode == inode_identity(current_payload) and current_payload.st_nlink == 1:",
155
+ " os.unlink(temp_name, dir_fd=staging_fd)",
156
+ " payload_removed = True",
110
157
  " except FileNotFoundError:",
111
- " pass",
158
+ " payload_removed = True",
112
159
  " staging_identity = entry_identity(os.fstat(staging_fd))",
113
160
  " os.close(staging_fd)",
114
161
  " staging_removed = False",
115
- " if staging_name is not None:",
162
+ " if staging_name is not None and payload_removed:",
116
163
  " try:",
117
164
  " current_staging = os.lstat(staging_name, dir_fd=parent_fd)",
118
165
  " if same_identity(staging_identity, current_staging):",
@@ -395,7 +395,7 @@ export const GUEST_FILESYSTEM_PYTHON = [
395
395
  " break",
396
396
  " write_all(temp_fd, chunk)",
397
397
  " try:",
398
- " os.fchmod(temp_fd, stat.S_IMODE(src_stat.st_mode))",
398
+ " os.fchmod(temp_fd, stat.S_IMODE(src_file_stat.st_mode))",
399
399
  " except AttributeError:",
400
400
  " pass",
401
401
  " os.fsync(temp_fd)",
@@ -1,4 +1,4 @@
1
- export { FsSafeError, categorizeFsSafeError, } from "./errors.js";
1
+ export { FsSafeError, categorizeFsSafeError, isNoReplaceUnsupported, } from "./errors.js";
2
2
  export type * from "./errors.js";
3
3
  export { DEFAULT_ROOT_MAX_BYTES, root, type Root, } from "./root.js";
4
4
  export type * from "./root-public-types.js";
@@ -1,4 +1,4 @@
1
- export { FsSafeError, categorizeFsSafeError, } from "./errors.js";
1
+ export { FsSafeError, categorizeFsSafeError, isNoReplaceUnsupported, } from "./errors.js";
2
2
  export { DEFAULT_ROOT_MAX_BYTES, root, } from "./root.js";
3
3
  export { configureFsSafeNative, getFsSafeNativeConfig, } from "./native-config.js";
4
4
  export { writeExternalFileWithinRoot, } from "./output.js";
@@ -139,6 +139,7 @@ export interface NativeBinding {
139
139
  renameReplaceWithIdentity?(sourceParent: number, source: string, targetParent: number, target: string, dev: bigint, ino: bigint): void;
140
140
  /** Windows-only, private handle custody; no borrowed/runtime descriptors. */
141
141
  retainWindowsFile?(directory: string, basename: string, parentDev: bigint, parentIno: bigint, dev: bigint, ino: bigint, size: bigint, mtimeNs: bigint, ctimeNs: bigint, sha256: string, maxBytes: number): NativeRetainedFile;
142
+ retainWindowsSidecar?(parentFd: number, directory: string, basename: string, parentDev: bigint, parentIno: bigint, dev: bigint, ino: bigint, size: bigint, sha256: string, maxBytes: number): NativeRetainedFile;
142
143
  watchRegister?(root: string, limit: number, callback: (batch: import("./watch-native.js").NativeWatchWireBatch) => void, persistent: boolean): number;
143
144
  watchConfigure?(id: number, anchors: string[], exclusions: string[]): void;
144
145
  watchEntries?(id: number, entries: import("./watch-native.js").NativeWatchEntry[]): {
@@ -203,6 +204,7 @@ export interface NativeBinding {
203
204
  stagedFileMatches?(parentFd: number, basename: string, fileFd: number): boolean;
204
205
  removeStagedFile?(parentFd: number, basename: string, fileFd: number): "removed" | "name-absent" | "preserved";
205
206
  copyFileExclusive?(sourceFd: number, parentFd: number, basename: string, clone: CopyCloneMode, maxBytes: number | undefined, signal: AbortSignal | undefined): Promise<NativeFileCopyResult>;
207
+ copyFileExclusiveSync?(sourceFd: number, parentFd: number, basename: string, clone: CopyCloneMode, maxBytes: number | undefined): NativeFileCopyResult;
206
208
  cloneFileExclusive(sourceFd: number, targetRootFd: number, targetRelPath: string): number;
207
209
  copyFileRangeExclusive(sourceFd: number, targetRootFd: number, targetRelPath: string): Promise<NativeCopyResult>;
208
210
  createPrivateDirectory(path: string): void;
@@ -235,6 +237,8 @@ export interface NativeBinding {
235
237
  entryPublicationFilesystem?(parentFd: number): string;
236
238
  publishRetainedEntryNoReplace?(sourceParentFd: number, sourceBasename: string, sourceFd: number, targetParentFd: number, targetBasename: string): NativePublicationTransition;
237
239
  renameNoReplace(...args: NativeTwoPathArgs): void;
240
+ /** Linux auto-mode fallback through already-admitted direct-child parents. */
241
+ moveNoReplaceFallback?(...args: [...paths: NativeTwoPathArgs, expectedSourceDev: bigint, expectedSourceIno: bigint]): void;
238
242
  /** Identity-fenced retained-directory rename capability. */
239
243
  renameNoReplaceWithIdentity?(...args: [...paths: NativeTwoPathArgs, expectedSourceDev: bigint, expectedSourceIno: bigint]): void;
240
244
  renameReplace(...args: NativeTwoPathArgs): void;
@@ -0,0 +1,3 @@
1
+ import type { BigIntStats } from "node:fs";
2
+ import type { NativeBinding } from "./native-binding.js";
3
+ export declare function nativeMoveNoReplace(binding: NativeBinding, paths: Parameters<NativeBinding["renameNoReplace"]>, expected: BigIntStats | undefined, allowFallback: boolean, operation?: string): "link-unlink" | undefined;
@@ -0,0 +1,54 @@
1
+ import { FsSafeError } from "./errors.js";
2
+ import { cachedNoReplaceUnavailable, noReplaceUnavailable, rememberNoReplaceUnavailable, } from "./native-noreplace.js";
3
+ function fallbackError(error, operation) {
4
+ const code = error?.code;
5
+ const message = error instanceof Error ? error.message : "native no-replace move fallback failed";
6
+ if (code === "EEXIST") {
7
+ return new FsSafeError("already-exists", "no-replace fallback destination exists", { cause: error });
8
+ }
9
+ if (code === "FS_SAFE_INTERNAL_MOVE_LINK_UNSUPPORTED") {
10
+ return new FsSafeError("helper-unavailable", message, { cause: error,
11
+ details: { capability: "rename-noreplace", fallback: "link-unlink", fallbackCapability: "linkat" } });
12
+ }
13
+ const sourceRemoval = new Map([
14
+ ["FS_SAFE_INTERNAL_MOVE_LINK_CHANGED", "not-attempted"],
15
+ ["FS_SAFE_INTERNAL_MOVE_SOURCE_LINKED", "still-linked"],
16
+ ["FS_SAFE_INTERNAL_MOVE_UNLINK_UNVERIFIED", "unverified"],
17
+ ["FS_SAFE_INTERNAL_MOVE_PUBLISHED", "removed"],
18
+ ]).get(code ?? "");
19
+ if (sourceRemoval) {
20
+ return new FsSafeError(code === "FS_SAFE_INTERNAL_MOVE_LINK_CHANGED" ? "path-mismatch" : "helper-failed", message, { cause: error, details: { operation, fallback: "link-unlink",
21
+ publication: "published", sourceRemoval } });
22
+ }
23
+ // Keep identity errors available to the caller's normalizer.
24
+ return error;
25
+ }
26
+ export function nativeMoveNoReplace(binding, paths, expected, allowFallback, operation = "move") {
27
+ const [sourceFd, source, targetFd, target] = paths;
28
+ let unavailable = cachedNoReplaceUnavailable(binding, sourceFd, operation);
29
+ if (!unavailable) {
30
+ try {
31
+ binding.renameNoReplace(...paths);
32
+ return;
33
+ }
34
+ catch (error) {
35
+ unavailable = noReplaceUnavailable(error, operation, true);
36
+ if (!unavailable)
37
+ throw error;
38
+ // As in sibling publication/quarantine, distinct sibling names cannot
39
+ // have a directory-ancestry EINVAL. Cross-parent moves must not cache it.
40
+ if (sourceFd === targetFd && source !== target) {
41
+ rememberNoReplaceUnavailable(binding, sourceFd, unavailable);
42
+ }
43
+ }
44
+ }
45
+ if (!allowFallback || process.platform !== "linux" || !binding.moveNoReplaceFallback || !expected)
46
+ throw unavailable;
47
+ try {
48
+ binding.moveNoReplaceFallback(...paths, expected.dev, expected.ino);
49
+ return expected.isFile() ? "link-unlink" : undefined;
50
+ }
51
+ catch (error) {
52
+ throw fallbackError(error, operation);
53
+ }
54
+ }
@@ -0,0 +1,8 @@
1
+ import { FsSafeError } from "./errors.js";
2
+ import type { NativeBinding } from "./native-binding.js";
3
+ export declare const NATIVE_NOREPLACE_UNSUPPORTED = "FS_SAFE_INTERNAL_RENAME_NOREPLACE_UNSUPPORTED";
4
+ export declare function isNoReplaceUnavailable(error: unknown): error is FsSafeError;
5
+ export declare function noReplaceUnavailable(error: unknown, operation: string, syscallOnly?: boolean): FsSafeError | undefined;
6
+ export declare function rememberNoReplaceUnavailable(binding: NativeBinding, parentFd: number, error: FsSafeError): void;
7
+ export declare function cachedNoReplaceUnavailable(binding: NativeBinding, parentFd: number, operation?: string): FsSafeError | undefined;
8
+ export declare function cachedNoReplaceDevice(binding: NativeBinding, device: bigint, operation: string): FsSafeError | undefined;
@@ -0,0 +1,55 @@
1
+ import fs from "node:fs";
2
+ import { FsSafeError } from "./errors.js";
3
+ export const NATIVE_NOREPLACE_UNSUPPORTED = "FS_SAFE_INTERNAL_RENAME_NOREPLACE_UNSUPPORTED";
4
+ const unsupported = new WeakMap();
5
+ const capabilityErrors = new WeakMap();
6
+ const MAX_DEVICES = 128;
7
+ function unavailable(operation, detail, cause) {
8
+ const error = new FsSafeError("helper-unavailable", `native no-replace ${operation} is unsupported on this filesystem (${detail})`, { cause, details: { capability: "rename-noreplace" } });
9
+ capabilityErrors.set(error, detail);
10
+ return error;
11
+ }
12
+ export function isNoReplaceUnavailable(error) {
13
+ return error instanceof FsSafeError && capabilityErrors.has(error);
14
+ }
15
+ export function noReplaceUnavailable(error, operation, syscallOnly = false) {
16
+ try {
17
+ const code = error?.code;
18
+ if (code !== NATIVE_NOREPLACE_UNSUPPORTED &&
19
+ !(syscallOnly && ["EINVAL", "ENOSYS", "ENOTSUP", "EOPNOTSUPP"].includes(code ?? "")))
20
+ return;
21
+ const capability = process.platform === "darwin" ? "renameatx_np RENAME_EXCL"
22
+ : process.platform === "win32" ? "native no-replace rename" : "renameat2 RENAME_NOREPLACE";
23
+ const detail = code === NATIVE_NOREPLACE_UNSUPPORTED
24
+ ? error.message : `${capability}: ${code}`;
25
+ return unavailable(operation, detail, error);
26
+ }
27
+ catch {
28
+ // Diagnostic getters do not establish non-publication.
29
+ return;
30
+ }
31
+ }
32
+ export function rememberNoReplaceUnavailable(binding, parentFd, error) {
33
+ const detail = capabilityErrors.get(error);
34
+ if (!detail)
35
+ return;
36
+ const device = fs.fstatSync(parentFd, { bigint: true }).dev;
37
+ let devices = unsupported.get(binding);
38
+ if (!devices)
39
+ unsupported.set(binding, devices = new Map());
40
+ if (!devices.has(device) && devices.size >= MAX_DEVICES)
41
+ devices.delete(devices.keys().next().value);
42
+ devices.set(device, detail);
43
+ }
44
+ export function cachedNoReplaceUnavailable(binding, parentFd, operation = "publication") {
45
+ if (!unsupported.has(binding))
46
+ return;
47
+ return cachedNoReplaceDevice(binding, fs.fstatSync(parentFd, { bigint: true }).dev, operation);
48
+ }
49
+ export function cachedNoReplaceDevice(binding, device, operation) {
50
+ const devices = unsupported.get(binding);
51
+ if (!devices)
52
+ return;
53
+ const detail = devices.get(device);
54
+ return detail ? unavailable(operation, detail) : undefined;
55
+ }
@@ -5,7 +5,6 @@ export type NativeFileHandle = {
5
5
  writeFile(data: string | Buffer, encoding?: BufferEncoding): Promise<void>;
6
6
  };
7
7
  export declare function nativeOpenFlags(flags: number): number;
8
- export declare function writeNativeFd(fd: number, data: Buffer): void;
9
8
  export declare function removeNativeCreatedFileIfStillPinned(params: {
10
9
  parentPath: string;
11
10
  parentFd: number;
@@ -10,31 +10,6 @@ export function nativeOpenFlags(flags) {
10
10
  (closeOnExec ?? 0) |
11
11
  (typeof fsSync.constants.O_NOFOLLOW === "number" ? fsSync.constants.O_NOFOLLOW : 0));
12
12
  }
13
- export function writeNativeFd(fd, data) {
14
- let offset = 0;
15
- while (offset < data.byteLength) {
16
- const written = fsSync.writeSync(fd, data, offset, data.byteLength - offset);
17
- if (written <= 0) {
18
- throw Object.assign(new Error("native file write made no progress"), { code: "EIO" });
19
- }
20
- offset += written;
21
- }
22
- }
23
- function wrapNativeFd(fd, closeFd) {
24
- let open = true;
25
- return {
26
- fd,
27
- async close() {
28
- if (open) {
29
- open = false;
30
- closeFd(fd);
31
- }
32
- },
33
- async writeFile(data, encoding) {
34
- writeNativeFd(fd, Buffer.isBuffer(data) ? data : Buffer.from(data, encoding ?? "utf8"));
35
- },
36
- };
37
- }
38
13
  export function removeNativeCreatedFileIfStillPinned(params) {
39
14
  if (!params.created) {
40
15
  return;
@@ -86,7 +61,28 @@ export async function createNativeExclusiveFile(targetPath, mode) {
86
61
  fd = opened.fd;
87
62
  fsSync.fchmodSync(fd, mode);
88
63
  created = fsSync.fstatSync(fd, { bigint: true });
89
- return wrapNativeFd(fd, closeFd);
64
+ const ownedFd = fd;
65
+ let open = true;
66
+ return {
67
+ fd: ownedFd,
68
+ async close() {
69
+ if (open) {
70
+ open = false;
71
+ closeFd(ownedFd);
72
+ }
73
+ },
74
+ async writeFile(data, encoding) {
75
+ const buffer = Buffer.isBuffer(data) ? data : Buffer.from(data, encoding ?? "utf8");
76
+ let offset = 0;
77
+ while (offset < buffer.byteLength) {
78
+ const written = fsSync.writeSync(ownedFd, buffer, offset, buffer.byteLength - offset);
79
+ if (written <= 0) {
80
+ throw Object.assign(new Error("native file write made no progress"), { code: "EIO" });
81
+ }
82
+ offset += written;
83
+ }
84
+ },
85
+ };
90
86
  }
91
87
  catch (error) {
92
88
  if (fd !== undefined) {
@@ -188,7 +188,11 @@ export async function runPinnedWriteWindows(binding, params, root, parentFd, par
188
188
  if (params.sync !== false)
189
189
  syncFileBestEffortSync(parentFd);
190
190
  // Verification follows publication and final chmod, outside rollback handling.
191
- await params.verifyPublished?.(targetFd, verificationIdentity, parentGuard);
191
+ if (params.overwrite === false && params.input.kind === "buffer" && params.input.stageBeforePublish === false) {
192
+ await params.verifyPublished?.(targetFd, verificationIdentity, parentGuard, { fd: parentFd, binding });
193
+ }
194
+ else
195
+ await params.verifyPublished?.(targetFd, verificationIdentity, parentGuard);
192
196
  completed = true;
193
197
  return { dev: targetIdentity.dev, ino: targetIdentity.ino };
194
198
  }
@@ -1,4 +1,4 @@
1
1
  import type { FileIdentityStat } from "./file-identity.js";
2
2
  import type { NativeBinding } from "./native.js";
3
3
  import type { PinnedWriteParams } from "./pinned-write-types.js";
4
- export declare function runPinnedWriteNative(binding: NativeBinding, params: PinnedWriteParams): Promise<FileIdentityStat>;
4
+ export declare function runPinnedWriteNative(binding: NativeBinding, params: PinnedWriteParams, fallback: (input: PinnedWriteParams["input"]) => Promise<FileIdentityStat>): Promise<FileIdentityStat>;
@@ -57,7 +57,9 @@ 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
59
  import { captureNativeFdClose } from "./native-binding.js";
60
- export async function runPinnedWriteNative(binding, params) {
60
+ import { cachedNoReplaceUnavailable } from "./native-noreplace.js";
61
+ import { isFsSafeNativeRequired } from "./native-config.js";
62
+ export async function runPinnedWriteNative(binding, params, fallback) {
61
63
  const env_1 = { stack: [], error: void 0, hasError: false };
62
64
  try {
63
65
  const closeFd = captureNativeFdClose(binding);
@@ -126,9 +128,17 @@ export async function runPinnedWriteNative(binding, params) {
126
128
  windowsOwnsDirectories = true;
127
129
  return await runPinnedWriteWindows(binding, params, root, parentFd, verificationGuard);
128
130
  }
131
+ if (params.overwrite === false && !(params.input.kind === "buffer" && params.input.stageBeforePublish === false)) {
132
+ const unavailable = cachedNoReplaceUnavailable(binding, parentFd);
133
+ if (unavailable) {
134
+ if (isFsSafeNativeRequired() || (params.input.kind === "file" && params.input.clone === "always"))
135
+ throw unavailable;
136
+ return await fallback(params.input);
137
+ }
138
+ }
129
139
  const ownedParent = parentFd;
130
140
  parentFd = undefined;
131
- return await writeNativeStage(binding, ownedParent, closeFd, directory, params, verificationGuard);
141
+ return await writeNativeStage(binding, ownedParent, closeFd, directory, params, verificationGuard, fallback);
132
142
  }
133
143
  catch (error) {
134
144
  admissionFailure = { error };
@@ -1,7 +1,9 @@
1
+ import { NATIVE_NOREPLACE_UNSUPPORTED } from "./native-noreplace.js";
1
2
  export const NATIVE_RENAME_SOURCE_IDENTITY_MISMATCH = "FS_SAFE_INTERNAL_RENAME_SOURCE_IDENTITY_MISMATCH";
2
3
  export function classifyNativeRenameFailure(error) {
3
4
  try {
4
- if (error?.code === NATIVE_RENAME_SOURCE_IDENTITY_MISMATCH) {
5
+ const code = error?.code;
6
+ if (code === NATIVE_RENAME_SOURCE_IDENTITY_MISMATCH || code === NATIVE_NOREPLACE_UNSUPPORTED) {
5
7
  return "uncommitted";
6
8
  }
7
9
  }
@@ -25,7 +25,7 @@ declare class NativeStagedFile implements StagedFile {
25
25
  #private;
26
26
  constructor(owner: NativeStageOwner);
27
27
  static create(owner: NativeStageOwner, input: PinnedWriteInput, maxBytes?: number): Promise<NativeStagedFile>;
28
- static write(binding: NativeStagingBinding, parentFd: number, closeParentFd: (fd: number) => void, directory: StagedFileReceipt["directory"], params: PinnedWriteParams, parentGuard: AnyAsyncDirectoryGuard): Promise<FileIdentityStat>;
28
+ static write(binding: NativeStagingBinding, parentFd: number, closeParentFd: (fd: number) => void, directory: StagedFileReceipt["directory"], params: PinnedWriteParams, parentGuard: AnyAsyncDirectoryGuard, fallback: (input: PinnedWriteInput) => Promise<FileIdentityStat>): Promise<FileIdentityStat>;
29
29
  get receipt(): StagedFileReceipt;
30
30
  assertCurrent(): Promise<void>;
31
31
  publish(basename: string, options: {
@@ -59,12 +59,14 @@ import path from "node:path";
59
59
  import { FsSafeError } from "./errors.js";
60
60
  import { assertSynchronousCallbackResult, isMutationAuthorityError } from "./mutation-authority.js";
61
61
  import { captureNativeFdClose } from "./native-binding.js";
62
- import { writePinnedInput } from "./pinned-write-input.js";
62
+ import { replayPinnedInput, writePinnedInput } from "./pinned-write-input.js";
63
63
  import { assertNativeCopyCompleted, createNativeCopyFile } from "./copy-file-input.js";
64
64
  import { assertStagedDirectoryCurrent, openStagedDirectory } from "./staged-directory.js";
65
65
  import { assertFinalSymlinkRejected } from "./root-symlink-policy.js";
66
66
  import { classifyNativeRenameFailure } from "./native-rename-outcome.js";
67
67
  import { createStagedFileReceipt, stagedFailure } from "./staged-file-settlement.js";
68
+ import { noReplaceUnavailable, rememberNoReplaceUnavailable } from "./native-noreplace.js";
69
+ import { isFsSafeNativeRequired } from "./native-config.js";
68
70
  export function assertNativeStaging(binding) {
69
71
  if ([
70
72
  binding.closeOwnedFd,
@@ -93,6 +95,9 @@ class NativeStagedFile {
93
95
  #state = { status: "open", publication: NOT_PUBLISHED };
94
96
  #receipt;
95
97
  #rejectFinalSymlink = false;
98
+ #unsupportedNoReplace;
99
+ #fallbackCleanup;
100
+ #fallbackCleanupError;
96
101
  constructor(owner) {
97
102
  const name = owner.name ?? `.fs-safe-${randomUUID()}.tmp`;
98
103
  assertBasename(name, owner.portableNames !== false);
@@ -121,7 +126,7 @@ class NativeStagedFile {
121
126
  await staged.#prepare(input, maxBytes);
122
127
  return staged;
123
128
  }
124
- static async write(binding, parentFd, closeParentFd, directory, params, parentGuard) {
129
+ static async write(binding, parentFd, closeParentFd, directory, params, parentGuard, fallback) {
125
130
  const env_1 = { stack: [], error: void 0, hasError: false };
126
131
  try {
127
132
  const exclusive = params.overwrite === false && params.input.kind === "buffer" && params.input.stageBeforePublish === false;
@@ -143,12 +148,39 @@ class NativeStagedFile {
143
148
  if (staged.#verifyMode)
144
149
  staged.#assertCurrent();
145
150
  }
146
- const published = exclusive
147
- ? staged.#completePublication(params.basename, false, params.onPublished)
148
- : await staged.publish(params.basename, { overwrite: params.overwrite !== false }, params.onPublished);
151
+ let published;
152
+ try {
153
+ published = exclusive
154
+ ? staged.#completePublication(params.basename, false, params.onPublished)
155
+ : await staged.publish(params.basename, { overwrite: params.overwrite !== false }, params.onPublished);
156
+ }
157
+ catch (error) {
158
+ if (!staged.#unsupportedNoReplace || isFsSafeNativeRequired() ||
159
+ (params.input.kind === "file" && params.input.clone === "always"))
160
+ throw error;
161
+ // Remove only our identity-known stage before re-admitting the JS write.
162
+ // Keep its descriptor alive to replay a one-shot stream in bounded chunks.
163
+ staged.#fallbackCleanup = "failed";
164
+ let removed;
165
+ try {
166
+ removed = binding.removeStagedFile(parentFd, staged.#owner.name, staged.#file());
167
+ staged.#fallbackCleanup = removed;
168
+ }
169
+ catch (cleanupError) {
170
+ staged.#fallbackCleanupError = { error: cleanupError };
171
+ throw error;
172
+ }
173
+ if (removed !== "removed" && removed !== "name-absent")
174
+ throw error;
175
+ rememberNoReplaceUnavailable(binding, parentFd, staged.#unsupportedNoReplace);
176
+ return await fallback(replayPinnedInput(staged.#file(), params.input));
177
+ }
149
178
  const identity = published.staged.identity;
150
179
  try {
151
- await params.verifyPublished?.(staged.#file(), identity, parentGuard);
180
+ if (exclusive)
181
+ await params.verifyPublished?.(staged.#file(), identity, parentGuard, { fd: parentFd, binding });
182
+ else
183
+ await params.verifyPublished?.(staged.#file(), identity, parentGuard);
152
184
  }
153
185
  catch (error) {
154
186
  if (params.overwrite === false && params.input.kind !== "file" && params.input.stageBeforePublish === true) {
@@ -321,6 +353,15 @@ class NativeStagedFile {
321
353
  catch (error) {
322
354
  // Record before inspecting metadata, whose getters can reenter cleanup.
323
355
  state.publication = Object.freeze({ status: "indeterminate", basename, overwrite });
356
+ // Linux VFS and rename(2): unsupported flags return EINVAL; ENOSYS
357
+ // means no syscall. A regular file renamed to a distinct sibling cannot
358
+ // hit the directory-into-itself EINVAL case. Nothing was published.
359
+ const unavailable = !overwrite ? noReplaceUnavailable(error, "publication", true) : undefined;
360
+ if (unavailable && this.#state === state && state.status === "open") {
361
+ state.publication = NOT_PUBLISHED;
362
+ this.#unsupportedNoReplace = unavailable;
363
+ throw unavailable;
364
+ }
324
365
  // Only explicit pre-dispatch provenance can rule out a committed rename.
325
366
  if (classifyNativeRenameFailure(error) === "uncommitted") {
326
367
  state.publication = NOT_PUBLISHED;
@@ -377,6 +418,11 @@ class NativeStagedFile {
377
418
  if (state.publication.status === "indeterminate") {
378
419
  outcome = "preserved";
379
420
  }
421
+ else if (this.#fallbackCleanup) {
422
+ outcome = this.#fallbackCleanup;
423
+ if (this.#fallbackCleanupError)
424
+ errors.push(this.#fallbackCleanupError.error);
425
+ }
380
426
  else if (state.publication.status === "not-published" && state.fileFd !== undefined) {
381
427
  try {
382
428
  outcome = this.#owner.binding.removeStagedFile(this.#owner.parentFd, this.#owner.name, state.fileFd);
@@ -5,7 +5,8 @@ import { formatErrorDetail } from "./error-detail.js";
5
5
  const execFileAsync = promisify(execFile);
6
6
  export const DEFAULT_PERMISSION_EXEC_TIMEOUT_MS = 30_000;
7
7
  export function formatPermissionErrorDetail(value) {
8
- const formatted = formatErrorDetail(value);
8
+ // Escaping cannot shrink input; 401 code units decide the 400-character excerpt.
9
+ const formatted = formatErrorDetail(value.slice(0, 401));
9
10
  return formatted.length > 400 ? `${formatted.slice(0, 399)}…` : formatted;
10
11
  }
11
12
  const MAX_CAUGHT_FAILURE_PROTOTYPES = 4;
@@ -1,3 +1,5 @@
1
1
  import type { FileHandle } from "node:fs/promises";
2
2
  import type { PinnedWriteInput } from "./pinned-write-types.js";
3
+ /** Replay a consumed stream from an owned, readable stage descriptor. */
4
+ export declare function replayPinnedInput(fd: number, input: PinnedWriteInput): PinnedWriteInput;
3
5
  export declare function writePinnedInput(target: FileHandle | number, input: PinnedWriteInput, maxBytes?: number, assertBeforeMutation?: () => void): Promise<void>;
@@ -1,8 +1,25 @@
1
+ import { readSync } from "node:fs";
1
2
  import { types } from "node:util";
2
3
  import { snapshotByteView } from "./byte-view.js";
3
4
  import { writeCopyFileToFd } from "./copy-file-input.js";
4
5
  import { FsSafeError } from "./errors.js";
5
6
  import { writeAllToFile } from "./write-file-handle.js";
7
+ /** Replay a consumed stream from an owned, readable stage descriptor. */
8
+ export function replayPinnedInput(fd, input) {
9
+ if (input.kind !== "stream")
10
+ return input;
11
+ return { kind: "stream", stageBeforePublish: true, stream: (async function* () {
12
+ let position = 0;
13
+ const buffer = Buffer.alloc(64 * 1024);
14
+ for (;;) {
15
+ const bytes = readSync(fd, buffer, 0, buffer.length, position);
16
+ if (!bytes)
17
+ return;
18
+ position += bytes;
19
+ yield buffer.subarray(0, bytes);
20
+ }
21
+ })() };
22
+ }
6
23
  const isUint8Array = types.isUint8Array;
7
24
  function streamChunkBytes(chunk) {
8
25
  if (Buffer.isBuffer(chunk))
@@ -2,6 +2,11 @@ import type { CopyFileInput } from "./copy-file-input.js";
2
2
  import type { AnyAsyncDirectoryGuard } from "./directory-guard.js";
3
3
  import type { FileIdentityStat } from "./file-identity.js";
4
4
  import type { MutationDirectoryObservation } from "./pinned-mutation-observation.js";
5
+ import type { NativeBinding } from "./native-binding.js";
6
+ export type NativeWriteParent = Readonly<{
7
+ fd: number;
8
+ binding: NativeBinding;
9
+ }>;
5
10
  export type PinnedWriteInput = {
6
11
  kind: "buffer";
7
12
  data: string | Buffer;
@@ -64,5 +69,5 @@ export type PinnedWriteParams = {
64
69
  mutationAdmission?: PinnedWriteMutationAdmission;
65
70
  onRenameIdentityMismatch?: "verify-content";
66
71
  onPublished?: (identity: PublishedWriteIdentity) => void;
67
- verifyPublished?: (fd: number, identity: PublishedWriteIdentity, parentGuard: AnyAsyncDirectoryGuard) => Promise<void>;
72
+ verifyPublished?: (fd: number, identity: PublishedWriteIdentity, parentGuard: AnyAsyncDirectoryGuard, nativeParent?: NativeWriteParent) => Promise<void>;
68
73
  };
@@ -90,7 +90,7 @@ export async function runPinnedWriteHelper(params) {
90
90
  }
91
91
  const native = getNativeBinding();
92
92
  if (native) {
93
- return await runPinnedWriteNative(native, normalizedParams);
93
+ return await runPinnedWriteNative(native, normalizedParams, input => runPinnedWriteFallback({ ...normalizedParams, input }));
94
94
  }
95
95
  return await runPinnedWriteFallback(normalizedParams);
96
96
  }
@@ -166,7 +166,7 @@ async function createTempWorkspace(options, scopedPrefix = false) {
166
166
  else
167
167
  admission.assertCurrent();
168
168
  const stat = inspectDirectoryIdentitySync(dir);
169
- const needsModeInitialization = validateInitialTempWorkspaceChild(stat, admission.ownerUid, dirMode);
169
+ const needsModeInitialization = validateInitialTempWorkspaceChild(stat, admission.ownerUid, dirMode, dir);
170
170
  // Retain while the child still has its private creation mode so an
171
171
  // explicit dirMode such as 0 cannot make identity descriptor acquisition fail.
172
172
  retainedChild = TempWorkspaceRetainedChild.retain(dir, stat);
@@ -261,7 +261,7 @@ function createTempWorkspaceSync(options, scopedPrefix = false) {
261
261
  else {
262
262
  stat = inspectDirectoryIdentitySync(dir);
263
263
  }
264
- const needsModeInitialization = validateInitialTempWorkspaceChild(stat, admission.ownerUid, dirMode);
264
+ const needsModeInitialization = validateInitialTempWorkspaceChild(stat, admission.ownerUid, dirMode, dir);
265
265
  retainedChild ??= TempWorkspaceRetainedChild.retain(dir, stat);
266
266
  if (needsModeInitialization) {
267
267
  retainedChild.initializeModeSync(dirMode, admission.ownerUid, admission.assertCurrent);