@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
@@ -13,6 +13,9 @@ export type PublishFileExclusiveFailureDetails = {
13
13
  targetIdentity?: FileIdentityStat;
14
14
  cleanup: PublishFileExclusiveCleanup;
15
15
  directorySync?: PublishFileExclusiveDirectorySyncFailure;
16
+ fallback?: "link-unlink";
17
+ publication?: "published";
18
+ sourceRemoval?: "not-attempted" | "still-linked" | "unverified" | "removed";
16
19
  };
17
20
  export type PublishFailureState = {
18
21
  phase: PublishFileExclusiveFailurePhase;
@@ -21,6 +24,8 @@ export type PublishFailureState = {
21
24
  targetCleanupIdentity?: FileIdentityStat;
22
25
  preserveTarget: boolean;
23
26
  directorySync?: PublishFileExclusiveDirectorySyncFailure;
27
+ fallback?: PublishFileExclusiveFailureDetails["fallback"];
28
+ sourceRemoval?: PublishFileExclusiveFailureDetails["sourceRemoval"];
24
29
  };
25
30
  export declare function rememberCreatedTarget(state: PublishFailureState, identity: FileIdentityStat, phase: PublishFileExclusiveFailurePhase): void;
26
31
  export declare function publicationFailure(error: unknown, state: PublishFailureState, cleanup: PublishFileExclusiveCleanup): FsSafeError;
@@ -12,6 +12,7 @@ export function publicationFailure(error, state, cleanup) {
12
12
  targetCreated: state.targetCreated,
13
13
  ...(state.targetIdentity ? { targetIdentity: state.targetIdentity } : {}),
14
14
  ...(state.directorySync ? { directorySync: state.directorySync } : {}),
15
+ ...(state.fallback ? { fallback: state.fallback, publication: "published", sourceRemoval: state.sourceRemoval } : {}),
15
16
  cleanup,
16
17
  };
17
18
  return new FsSafeError(error instanceof FsSafeError ? error.code : "helper-failed", `exclusive file publication failed during ${state.phase}: ${cause.message}`, { cause, details });
@@ -5,6 +5,8 @@ import { type PublishFileExclusiveSyncFailurePolicy } from "./publish-file-failu
5
5
  export type PublishFileExclusiveStrategy = "link-or-copy" | "link-required" | "rename-noreplace";
6
6
  export type PublishFileExclusiveResult = {
7
7
  method: "hardlink" | "exclusive-copy" | "rename-noreplace";
8
+ /** Linux auto fallback; exclusive and identity-checked, but not crash-atomic. */
9
+ fallback?: "link-unlink";
8
10
  identity: Stats;
9
11
  directorySync: DirectorySyncOutcome;
10
12
  };
@@ -1,3 +1,5 @@
1
+ import { nativeMoveNoReplace } from "./native-move-fallback.js";
2
+ import { getFsSafeNativeConfig } from "./native-config.js";
1
3
  import { createHash } from "node:crypto";
2
4
  import fsSync, {} from "node:fs";
3
5
  import fs, {} from "node:fs/promises";
@@ -256,7 +258,7 @@ export async function publishFileExclusive(params) {
256
258
  else
257
259
  assertSourceCurrent();
258
260
  assertPublishedTargetCurrent(targetPath, identity, "publication target changed during directory sync");
259
- return { method, identity: fsSync.fstatSync(handle.fd), directorySync };
261
+ return { method, ...(failure.fallback ? { fallback: failure.fallback } : {}), identity: fsSync.fstatSync(handle.fd), directorySync };
260
262
  };
261
263
  await parent.assertCurrent();
262
264
  assertSourceCurrent();
@@ -269,7 +271,38 @@ export async function publishFileExclusive(params) {
269
271
  }
270
272
  if (strategy === "rename-noreplace") {
271
273
  const binding = requireNativeBinding();
272
- binding.renameNoReplace(sourceNativeParent.handle.fd, sourceNativeParent.basename, targetNativeParent.handle.fd, targetNativeParent.basename);
274
+ try {
275
+ const fallback = nativeMoveNoReplace(binding, [
276
+ sourceNativeParent.handle.fd, sourceNativeParent.basename,
277
+ targetNativeParent.handle.fd, targetNativeParent.basename,
278
+ ], sourceExactIdentity, getFsSafeNativeConfig().mode === "auto", "file publication");
279
+ if (fallback) {
280
+ failure.fallback = fallback;
281
+ failure.sourceRemoval = "removed";
282
+ }
283
+ }
284
+ catch (error) {
285
+ if (error instanceof FsSafeError) {
286
+ if (error.details?.publication === "published") {
287
+ failure.targetCreated = true;
288
+ failure.preserveTarget = true;
289
+ failure.phase = "rename-verify";
290
+ failure.fallback = "link-unlink";
291
+ failure.sourceRemoval = error.details.sourceRemoval;
292
+ // Changed/unverified pairs cannot establish which inode was published.
293
+ if (failure.sourceRemoval === "still-linked")
294
+ rememberCreatedTarget(failure, sourceExactIdentity, "rename-verify");
295
+ }
296
+ throw error;
297
+ }
298
+ const code = error?.code;
299
+ if (code === "path-mismatch" || code === "hardlink") {
300
+ throw new FsSafeError(code, "publication source changed before fallback", { cause: error });
301
+ }
302
+ if (code === "ELOOP")
303
+ throw new FsSafeError("symlink", "publication source became a symlink", { cause: error });
304
+ throw error;
305
+ }
273
306
  rememberCreatedTarget(failure, sourceExactIdentity, "rename-verify");
274
307
  // A failed post-rename fence must not delete the only remaining name.
275
308
  failure.preserveTarget = true;
@@ -1,3 +1,4 @@
1
+ import { noReplaceUnavailable } from "./native-noreplace.js";
1
2
  import { randomUUID } from "node:crypto";
2
3
  import fsSync from "node:fs";
3
4
  import fs from "node:fs/promises";
@@ -19,7 +20,8 @@ function assertGuardsCurrent(guards) {
19
20
  }
20
21
  function replacementFailure(error, message, details, cause = error) {
21
22
  if (error instanceof FsSafeError) {
22
- return new FsSafeError(error.code, message, { cause, details });
23
+ const code = error.code;
24
+ return new FsSafeError(code, code === "helper-unavailable" ? `${message}: ${error.message}` : message, { cause, details });
23
25
  }
24
26
  const failure = new Error(message, { cause });
25
27
  copyOperationalCode(failure, error);
@@ -46,14 +48,13 @@ function observeOptionalDirectory(pathname) {
46
48
  return inspectDirectoryIdentitySync(pathname, undefined, initial);
47
49
  }
48
50
  function normalizeNativeRenameFailure(error) {
51
+ const unavailable = noReplaceUnavailable(error, "directory rename", true);
52
+ if (unavailable)
53
+ return unavailable;
49
54
  const code = error?.code;
50
55
  if (code === NATIVE_RENAME_SOURCE_IDENTITY_MISMATCH) {
51
56
  return new FsSafeError("path-mismatch", "directory replacement source identity changed before native rename", { cause: error });
52
57
  }
53
- if (code === "EINVAL" || code === "ENOSYS" || code === "ENOTSUP" ||
54
- code === "EOPNOTSUPP") {
55
- return new FsSafeError("helper-unavailable", "native no-replace directory rename is unavailable on this filesystem", { cause: error });
56
- }
57
58
  return error;
58
59
  }
59
60
  function renameRetainedDirectory(params) {
@@ -139,15 +139,12 @@ function* replacePinnedWithRestore(io, file, replacement, maxRestoreBytes, repla
139
139
  if (io.asynchronous)
140
140
  yield chmod;
141
141
  yield* file.sync();
142
- throw restoreFailure(writeError, "restored");
143
142
  }
144
143
  catch (restoreError) {
145
144
  mutation.rethrowRefusal();
146
- if (restoreError instanceof FsSafeError && restoreError.details?.cleanup === "restored") {
147
- throw restoreError;
148
- }
149
145
  throw restoreFailure(writeError, "restore-failed", restoreError);
150
146
  }
147
+ throw restoreFailure(writeError, "restored");
151
148
  }
152
149
  }
153
150
  export function* copyFallbackReplace(io, params) {
@@ -139,6 +139,12 @@ export function* writeTempFile(io, params) {
139
139
  const writing = file.writeFile(params.content, true);
140
140
  if (io.asynchronous)
141
141
  yield writing;
142
+ if (io.asyncFs === fs && process.platform !== "win32" && !params.sync) {
143
+ // Matching non-durable writes have no I/O left after this identity fence.
144
+ const current = inspectAtomicIdentity(io, () => file.statExact(), identity, true);
145
+ if (Number(current.mode & 4095n) === (params.mode & 0o7777))
146
+ return { file, identity };
147
+ }
142
148
  const chmod = file.chmod(params.mode);
143
149
  if (io.asynchronous)
144
150
  yield chmod;
@@ -1,14 +1,28 @@
1
- import type { PinnedWriteInput } from "./pinned-write-types.js";
1
+ import type { NativeWriteParent, PinnedWriteInput, PublishedWriteIdentity } from "./pinned-write-types.js";
2
2
  import type { RootCreateOptions, RootCreateStreamOptions, RootWriteOptions } from "./root-options.js";
3
+ import type { AnyAsyncDirectoryGuard } from "./directory-guard.js";
3
4
  declare const exclusiveSidecarCreate: unique symbol;
4
5
  export declare const sidecarExclusiveCreate: Readonly<{
5
6
  [exclusiveSidecarCreate]: true;
6
7
  }>;
8
+ export declare const retainSidecarCreated: unique symbol;
9
+ export type SidecarCreationRetention = Readonly<{
10
+ retain(fd: number, parent: AnyAsyncDirectoryGuard, nativeParent?: NativeWriteParent): void;
11
+ cleanup(fd: number, parent: NativeWriteParent, identity: PublishedWriteIdentity): void;
12
+ }>;
7
13
  export type RootWriteParams = RootWriteOptions & RootCreateOptions & RootCreateStreamOptions & {
8
14
  relativePath: string;
9
15
  data: string | Buffer | AsyncIterable<Uint8Array>;
10
16
  strictFileSync?: boolean;
11
17
  [exclusiveSidecarCreate]?: true;
18
+ [retainSidecarCreated]?: SidecarCreationRetention;
19
+ };
20
+ export declare function sidecarRetainedCreate(retain: NonNullable<RootWriteParams[typeof retainSidecarCreated]>): {
21
+ [exclusiveSidecarCreate]: true;
22
+ [retainSidecarCreated]: Readonly<{
23
+ retain(fd: number, parent: AnyAsyncDirectoryGuard, nativeParent?: NativeWriteParent): void;
24
+ cleanup(fd: number, parent: NativeWriteParent, identity: PublishedWriteIdentity): void;
25
+ }>;
12
26
  };
13
27
  export declare function rethrowCreateInputError(error: unknown): never;
14
28
  export declare function createInputOptions(data: RootWriteParams["data"], options: RootCreateOptions & RootCreateStreamOptions, defaultMaxBytes?: number): RootCreateOptions & RootCreateStreamOptions;
@@ -5,6 +5,10 @@ import { MutationAuthorityError } from "./mutation-authority.js";
5
5
  // Keep this composition hook off the public Root options and package exports.
6
6
  const exclusiveSidecarCreate = Symbol("exclusiveSidecarCreate");
7
7
  export const sidecarExclusiveCreate = Object.freeze({ [exclusiveSidecarCreate]: true });
8
+ export const retainSidecarCreated = Symbol("retainSidecarCreated");
9
+ export function sidecarRetainedCreate(retain) {
10
+ return { ...sidecarExclusiveCreate, [retainSidecarCreated]: retain };
11
+ }
8
12
  class CreateInputError extends FsSafeError {
9
13
  rejection;
10
14
  constructor(rejection) {
@@ -17,7 +17,7 @@ export async function lookupRootDirectoryEntry(root, guard, name) {
17
17
  const observed = inspectStatObservationSync(bigint => bigint
18
18
  ? fs.lstatSync(pathname, { bigint: true }) : fs.lstatSync(pathname));
19
19
  await assertRootDirectoryObservationGuard(root, guard);
20
- return { entry: { name, ...pathStatFromStats(observed.stat) }, identity: observed.identity };
20
+ return { entry: pathStatFromStats(observed.stat, name), identity: observed.identity };
21
21
  }
22
22
  catch (error) {
23
23
  await assertRootDirectoryObservationGuard(root, guard);
@@ -16,6 +16,5 @@ export declare function assertRootDirectoryObservationGuard(root: RootContext, g
16
16
  * both its identity and canonical check here.
17
17
  */
18
18
  export declare function assertRootPathObservationReceiptCurrent(root: RootContext, receipt: RootPathObservationReceipt | RootPathParentObservationReceipt, finalTarget?: Stats | BigIntStats): void;
19
- export declare function listDirectoryPath(root: RootContext, directory: string, withFileTypes: true, receipt?: RootPathObservationReceipt): Promise<DirEntry[]>;
20
19
  export declare function listDirectoryPath(root: RootContext, directory: string, withFileTypes: boolean, receipt?: RootPathObservationReceipt): Promise<string[] | DirEntry[]>;
21
20
  export declare function openRootDirectoryListing(root: RootContext, directory: string, options: RootDirectoryListingOptions, receipt?: RootPathObservationReceipt): Promise<RootDirectoryListing>;
@@ -105,10 +105,6 @@ export async function assertRootDirectoryObservationGuard(root, guard) {
105
105
  throw directoryChangedError(error);
106
106
  }
107
107
  }
108
- function sameObservationDirectory(left, right) {
109
- return left.dir === right.dir && left.realPath === right.realPath &&
110
- left.identity.dev === right.identity.dev && left.identity.ino === right.identity.ino;
111
- }
112
108
  function assertReceiptDirectoryGuardSync(root, guard, backend) {
113
109
  if (backend) {
114
110
  const observed = inspectNativeDirectoryObservation(backend, guard.dir, guard.identity);
@@ -139,7 +135,8 @@ export function assertRootPathObservationReceiptCurrent(root, receipt, finalTarg
139
135
  rootGuard.identity.dev !== root.rootIdentity.dev || rootGuard.identity.ino !== root.rootIdentity.ino) {
140
136
  throw rootPathChangedError();
141
137
  }
142
- if (sameObservationDirectory(rootGuard, directoryGuard)) {
138
+ if (rootGuard.dir === directoryGuard.dir && rootGuard.realPath === directoryGuard.realPath &&
139
+ rootGuard.identity.dev === directoryGuard.identity.dev && rootGuard.identity.ino === directoryGuard.identity.ino) {
143
140
  try {
144
141
  if (receipt.kind === "stat" && receipt.target === rootGuard && finalTarget &&
145
142
  !receipt.directoryObserver) {
@@ -176,12 +173,6 @@ function normalizeDirectoryError(error) {
176
173
  }
177
174
  return error;
178
175
  }
179
- function normalizeInitialDirectoryError(error) {
180
- if (error instanceof FsSafeError && error.code === "not-file") {
181
- return new FsSafeError("not-found", "directory not found", { cause: error });
182
- }
183
- return normalizeDirectoryError(error);
184
- }
185
176
  export async function listDirectoryPath(root, directory, withFileTypes, receipt) {
186
177
  let guard;
187
178
  if (receipt) {
@@ -192,7 +183,10 @@ export async function listDirectoryPath(root, directory, withFileTypes, receipt)
192
183
  guard = await createRootDirectoryObservationGuard(root, directory);
193
184
  }
194
185
  catch (error) {
195
- throw normalizeInitialDirectoryError(error);
186
+ if (error instanceof FsSafeError && error.code === "not-file") {
187
+ throw new FsSafeError("not-found", "directory not found", { cause: error });
188
+ }
189
+ throw normalizeDirectoryError(error);
196
190
  }
197
191
  }
198
192
  return await listGuardedDirectoryPath(root, guard, withFileTypes, receipt);
@@ -29,7 +29,7 @@ export function hardlinkedPathNotAllowedError() {
29
29
  export function isAlreadyExistsError(error) {
30
30
  return hasNodeErrorCode(error, "EEXIST") || /File exists|EEXIST/i.test(String(error));
31
31
  }
32
- function descriptorExhaustion(error) {
32
+ function resourceExhaustion(error) {
33
33
  const pending = [error];
34
34
  let details;
35
35
  let code;
@@ -45,7 +45,11 @@ function descriptorExhaustion(error) {
45
45
  const diagnostic = current;
46
46
  if (current instanceof FsSafeError && (!details || current.details?.cleanup))
47
47
  details = current.details ?? details;
48
- if (diagnostic.code === "EMFILE" || diagnostic.code === "ENFILE")
48
+ if (diagnostic.code === "EMFILE" || diagnostic.code === "ENFILE") {
49
+ if (!code || code === "ENOSPC")
50
+ code = diagnostic.code;
51
+ }
52
+ else if (diagnostic.code === "ENOSPC")
49
53
  code ??= diagnostic.code;
50
54
  if (diagnostic.name === "SuppressedError")
51
55
  pending.push(diagnostic.suppressed, diagnostic.error);
@@ -55,15 +59,32 @@ function descriptorExhaustion(error) {
55
59
  }
56
60
  if (!code)
57
61
  return;
62
+ // Disk-space diagnostics retain the published direct-write code and already-classified errors.
63
+ if (code === "ENOSPC" && error instanceof FsSafeError)
64
+ return;
58
65
  const cleanup = details?.cleanup;
59
66
  const publication = details?.publication;
60
67
  const preserved = cleanup?.status === "preserved" && publication?.status === "indeterminate";
61
- return new FsSafeError("helper-failed", `filesystem write failed: too many open files (${code})${preserved ? "; publication outcome is indeterminate; staged file preserved" : ""}`, { cause: error, details });
68
+ const message = code === "ENOSPC" ? PINNED_WRITE_ERRNO_MESSAGES.get(code) : "filesystem write failed: too many open files";
69
+ return new FsSafeError(code === "ENOSPC" ? "invalid-path" : "helper-failed", `${message} (${code})${preserved ? "; publication outcome is indeterminate; staged file preserved" : ""}`, { cause: error, details });
62
70
  }
63
71
  export function normalizePinnedWriteError(error) {
64
- const exhausted = descriptorExhaustion(error);
72
+ const exhausted = resourceExhaustion(error);
65
73
  if (exhausted)
66
74
  return exhausted;
75
+ // Disposal retains cleanup evidence, but the failed publication remains primary.
76
+ let primary = error;
77
+ const seen = new Set();
78
+ try {
79
+ while (primary instanceof Error && primary.name === "SuppressedError" && !seen.has(primary)) {
80
+ seen.add(primary);
81
+ primary = primary.suppressed;
82
+ }
83
+ if (primary !== error && primary instanceof FsSafeError && primary.details?.phase === "publish") {
84
+ return new FsSafeError(primary.code, primary.message, { cause: error, details: primary.details });
85
+ }
86
+ }
87
+ catch { /* Unreadable diagnostics retain the original failure. */ }
67
88
  if (error instanceof FsSafeError) {
68
89
  return error;
69
90
  }
@@ -86,7 +86,6 @@ import { admitPathInsideRoot } from "./root-boundary.js";
86
86
  import { listDirectoryPath, openRootDirectoryListing } from "./root-directory-list.js";
87
87
  import { statResolvedPathInRoot } from "./root-path-stat.js";
88
88
  import { entriesInRoot } from "./root-entries.js";
89
- import { assertMoveMutationAllowed } from "./root-move-preflight.js";
90
89
  import { assertRootIdentityCurrent, assertRootIdentityCurrentSync, assertValidRootDestinationPath, assertValidRootRelativePath, createRootObservationGuard, ensureTrailingSep, expandRelativePathWithHome, resolvePathInRoot, resolveRootContext, rootRelativeReadPath, } from "./root-context.js";
91
90
  import { errorCauseOptions, fileNotFoundError, hardlinkedPathNotAllowedError, isAlreadyExistsError, normalizePinnedPathError, normalizePinnedWriteError, outsideWorkspaceError, } from "./root-errors.js";
92
91
  import { getFsSafeTestHooks } from "./test-hooks.js";
@@ -96,7 +95,7 @@ import { registerTempPathForExit } from "./temp-cleanup.js";
96
95
  import { removePathInRootFallback, validateRemoveOptions } from "./root-remove.js";
97
96
  import { removePathInRootNative } from "./root-remove-native.js";
98
97
  import { serializePathWrite } from "./write-queue.js";
99
- import { verifyAtomicWriteResult } from "./root-write-verification.js";
98
+ import { verifyAtomicWriteResult, verifyRootWritePublication } from "./root-write-verification.js";
100
99
  import { assertRootWritePathSelectionSync, assertRootWriteSelectionSync, createRootWriteSelectionForFd, prepareGuardedRootWritePathSelection, resolveGuardedWritePathInRoot, resolveGuardedWriteTargetInRoot, resolvePinnedWriteTargetInRoot, refreshRetainedRootWriteAdmission, refreshRootWritePathSelection, } from "./root-write-admission.js";
101
100
  import { prepareSharedRootWriteTarget } from "./root-write-complete-parent.js";
102
101
  import { finishRootFallbackWrite } from "./root-write-publication.js";
@@ -107,7 +106,7 @@ import { admitMoveSourceStat, movePathNative } from "./root-move-noreplace.js";
107
106
  import { admitRootReadHandle, inspectOpenedPathIdentitySync } from "./root-read-admission.js";
108
107
  import { createCopyPublicationObserver, onCopyPublication, onCopySourceAdmission } from "./copy-publication.js";
109
108
  import { writeAllToFile } from "./write-file-handle.js";
110
- import { createInputOptions, rethrowCreateInputError, rootWriteInput } from "./root-create-input.js";
109
+ import { createInputOptions, rethrowCreateInputError, rootWriteInput, retainSidecarCreated } from "./root-create-input.js";
111
110
  import { assertFinalSymlinkRejected, mutationSymlinkResolution, readSymlinkResolution } from "./root-symlink-policy.js";
112
111
  import { assertExclusiveCreateLeaf } from "./exclusive-create.js";
113
112
  import { assertNoWindowsPathAlias, resolvePathPreservingWindowsRoot } from "./windows-path-alias.js";
@@ -390,11 +389,14 @@ export class RootHandle {
390
389
  const { denyMutations, mutationSymlinks } = snapshotPinnedMutationPolicy(mutationOptions.denyMutations, mutationOptions.mutationSymlinks) ?? {};
391
390
  const overwrite = options.overwrite ?? false;
392
391
  const requireNative = isFsSafeNativeRequired();
393
- await assertMoveMutationAllowed(this.context, {
394
- fromRelative,
395
- toRelative,
396
- denyMutations,
397
- });
392
+ // Deny both routes before source alias or identity admission.
393
+ for (const relativePath of [fromRelative, toRelative]) {
394
+ const { resolved } = await resolvePathInRoot(this.context, relativePath, {
395
+ aliasErrorCode: "path-alias",
396
+ allowFinalSymlink: true,
397
+ });
398
+ await assertMutationNotDenied(resolved, denyMutations, { protectAncestors: true });
399
+ }
398
400
  await movePathFallback(this.context, {
399
401
  requireNative,
400
402
  fromRelative,
@@ -934,15 +936,13 @@ async function commitPinnedWriteInRoot(root, pinned, params, input) {
934
936
  rootIdentity: root.rootIdentity,
935
937
  mutationAdmission: pinned.mutationAdmission,
936
938
  assertBeforeMutation: params.assertBeforeMutation,
937
- verifyPublished: async (fd, expectedIdentity, parentGuard) => {
939
+ verifyPublished: async (fd, expectedIdentity, parentGuard, nativeParent) => {
938
940
  verifyingPublication = true;
939
941
  try {
940
- await verifyAtomicWriteResult({
941
- root,
942
- targetPath: pinned.targetPath,
943
- fd,
944
- expectedIdentity,
945
- parentGuard,
942
+ await verifyRootWritePublication({
943
+ fd, expectedIdentity, parentGuard,
944
+ nativeParent, retention: params[retainSidecarCreated],
945
+ verify: () => verifyAtomicWriteResult({ root, targetPath: pinned.targetPath, fd, expectedIdentity, parentGuard }),
946
946
  });
947
947
  }
948
948
  catch (error) {
@@ -1167,10 +1167,10 @@ async function movePathFallback(root, params) {
1167
1167
  let sourceIdentity;
1168
1168
  try {
1169
1169
  if (params.overwrite && params.assertBeforeMutation) {
1170
- sourceIdentity = inspectFileIdentitySync(() => admitMoveSourceStat(fsSync.lstatSync(source.resolved, { bigint: true }), true));
1170
+ sourceIdentity = inspectFileIdentitySync(() => admitMoveSourceStat(fsSync.lstatSync(source.resolved, { bigint: true })));
1171
1171
  }
1172
1172
  else {
1173
- admitMoveSourceStat(fsSync.lstatSync(source.resolved), params.overwrite);
1173
+ admitMoveSourceStat(fsSync.lstatSync(source.resolved));
1174
1174
  }
1175
1175
  }
1176
1176
  catch (error) {
@@ -1234,7 +1234,7 @@ async function movePathFallback(root, params) {
1234
1234
  }
1235
1235
  }
1236
1236
  }
1237
- inspectFileIdentitySync(() => admitMoveSourceStat(fsSync.lstatSync(source.resolved, { bigint: true }), true), sourceIdentity);
1237
+ inspectFileIdentitySync(() => admitMoveSourceStat(fsSync.lstatSync(source.resolved, { bigint: true })), sourceIdentity);
1238
1238
  }
1239
1239
  await fs.rename(source.resolved, target.resolved);
1240
1240
  }
@@ -1,8 +1,10 @@
1
1
  import { type BigIntStats, type Stats } from "node:fs";
2
2
  import type { RootMoveOptions } from "./root-options.js";
3
3
  import { type RootContext } from "./root-context.js";
4
- export declare function admitMoveSourceStat<T extends Stats | BigIntStats>(stat: T, overwrite?: boolean): T;
5
- export declare function movePathNative(root: RootContext, params: Pick<RootMoveOptions, "assertBeforeMutation" | "denyMutations" | "mutationSymlinks">, paths: {
4
+ export declare function admitMoveSourceStat<T extends Stats | BigIntStats>(stat: T): T;
5
+ export declare function movePathNative(root: RootContext, params: Pick<RootMoveOptions, "assertBeforeMutation" | "denyMutations" | "mutationSymlinks"> & {
6
+ requireNative: boolean;
7
+ }, paths: {
6
8
  sourcePath: string;
7
9
  sourceParentPath: string;
8
10
  targetPath: string;
@@ -15,6 +15,7 @@ import { PATH_ALIAS_POLICIES } from "./path-policy.js";
15
15
  import { createSuppressedError } from "./suppressed-error.js";
16
16
  import { inspectFileIdentitySync } from "./strict-file-identity.js";
17
17
  import { getFsSafeTestHooks } from "./test-hooks.js";
18
+ import { nativeMoveNoReplace } from "./native-move-fallback.js";
18
19
  function nativeParentRelativePath(rootReal, parentPath) {
19
20
  const relative = path.relative(rootReal, parentPath);
20
21
  if (relative === "" || relative === ".")
@@ -57,22 +58,13 @@ function normalizeMoveError(error) {
57
58
  }
58
59
  return error;
59
60
  }
60
- function normalizeRenameNoReplaceError(error) {
61
- if (error?.code === "EINVAL") {
62
- return new FsSafeError("helper-unavailable", "native no-replace move is unavailable on this filesystem", errorCauseOptions(error));
63
- }
64
- return normalizeMoveError(error);
65
- }
66
- export function admitMoveSourceStat(stat, overwrite = false) {
61
+ export function admitMoveSourceStat(stat) {
67
62
  if (stat.isSymbolicLink()) {
68
63
  throw new FsSafeError("symlink", "symlink not allowed");
69
64
  }
70
65
  if (stat.isFile() && stat.nlink > 1) {
71
66
  throw hardlinkedPathNotAllowedError();
72
67
  }
73
- if (!overwrite && stat.isDirectory()) {
74
- throw new FsSafeError("invalid-path", "directory moves require overwrite: true");
75
- }
76
68
  return stat;
77
69
  }
78
70
  export async function movePathNative(root, params, paths, overwrite = false) {
@@ -124,10 +116,9 @@ export async function movePathNative(root, params, paths, overwrite = false) {
124
116
  await assertRootIdentityCurrent(root);
125
117
  for (const admission of parentAdmissions)
126
118
  assertSyncDirectoryGuard(admission.guard);
127
- // Overwrite publication always binds an exact source; no-clobber retains
128
- // its existing callback-dependent identity contract.
129
- const sourceStat = overwrite || params.assertBeforeMutation
130
- ? inspectFileIdentitySync(() => admitMoveSourceStat(fsSync.lstatSync(admittedSourcePath, { bigint: true }), overwrite), paths.expectedSourceIdentity)
119
+ // Linux auto fallback needs an exact source even without an authority callback.
120
+ const sourceStat = overwrite || params.assertBeforeMutation || (process.platform === "linux" && !params.requireNative)
121
+ ? inspectFileIdentitySync(() => admitMoveSourceStat(fsSync.lstatSync(admittedSourcePath, { bigint: true })), paths.expectedSourceIdentity)
131
122
  : undefined;
132
123
  if (!sourceStat)
133
124
  admitMoveSourceStat(fsSync.lstatSync(admittedSourcePath));
@@ -172,20 +163,25 @@ export async function movePathNative(root, params, paths, overwrite = false) {
172
163
  }
173
164
  }
174
165
  if (sourceStat !== undefined) {
175
- inspectFileIdentitySync(() => admitMoveSourceStat(fsSync.lstatSync(admittedSourcePath, { bigint: true }), overwrite), sourceStat);
166
+ inspectFileIdentitySync(() => admitMoveSourceStat(fsSync.lstatSync(admittedSourcePath, { bigint: true })), sourceStat);
176
167
  }
177
168
  try {
178
169
  assertFinalSymlinkRejected(admittedTargetPath, params.mutationSymlinks !== undefined);
179
170
  if (overwrite)
180
171
  binding.renameReplaceWithIdentity(sourceParent.fd, path.basename(paths.sourcePath), targetParent.fd, path.basename(paths.targetPath), sourceStat.dev, sourceStat.ino);
181
172
  else
182
- binding.renameNoReplace(sourceParent.fd, path.basename(paths.sourcePath), targetParent.fd, path.basename(paths.targetPath));
173
+ nativeMoveNoReplace(binding, [
174
+ sourceParent.fd,
175
+ path.basename(paths.sourcePath),
176
+ targetParent.fd,
177
+ path.basename(paths.targetPath),
178
+ ], sourceStat, !params.requireNative);
183
179
  }
184
180
  catch (error) {
185
181
  if (isAlreadyExistsError(error) || (!overwrite && error?.code === "ENOTEMPTY")) {
186
182
  throw new FsSafeError("already-exists", "destination exists", errorCauseOptions(error));
187
183
  }
188
- throw overwrite ? normalizeMoveError(error) : normalizeRenameNoReplaceError(error);
184
+ throw normalizeMoveError(error);
189
185
  }
190
186
  try {
191
187
  for (const admission of parentAdmissions)
@@ -244,7 +244,7 @@ async function resolveCheckedPathsWithinRoot(params, allowMissingFallback) {
244
244
  if (!root) {
245
245
  throw new FsSafeError("not-found", "root dir not found");
246
246
  }
247
- opened = await root.open(pathResult.relativePath);
247
+ opened = await root.open(`./${pathResult.relativePath}`);
248
248
  resolvedPaths.push(opened.realPath);
249
249
  }
250
250
  catch (err) {
@@ -136,7 +136,9 @@ export async function* walkRoot(root, relativePath, options) {
136
136
  let failed = false;
137
137
  let operationError;
138
138
  try {
139
- const relativeDirectory = directory.split(path.sep).join(path.posix.sep);
139
+ const relativeDirectory = path.posix.normalize(directory.split(path.sep).join(path.posix.sep));
140
+ const childPrefix = relativeDirectory === "." || relativeDirectory === "./" ? ""
141
+ : relativeDirectory.endsWith("/") ? relativeDirectory : `${relativeDirectory}/`;
140
142
  while (true) {
141
143
  let next;
142
144
  try {
@@ -150,9 +152,7 @@ export async function* walkRoot(root, relativePath, options) {
150
152
  if (next === undefined)
151
153
  return;
152
154
  const name = next.kind === "entry" ? next.entry.name : next.name;
153
- const child = directory
154
- ? path.posix.join(relativeDirectory, name)
155
- : name;
155
+ const child = childPrefix + name;
156
156
  if (next.kind === "limit") {
157
157
  yield onLimit(child);
158
158
  return;
@@ -1,6 +1,15 @@
1
1
  import { type AnyAsyncDirectoryGuard } from "./directory-guard.js";
2
- import type { PublishedWriteIdentity } from "./pinned-write-types.js";
2
+ import type { NativeWriteParent, PublishedWriteIdentity } from "./pinned-write-types.js";
3
+ import type { SidecarCreationRetention } from "./root-create-input.js";
3
4
  import { type RootContext } from "./root-context.js";
5
+ export declare function verifyRootWritePublication(params: {
6
+ fd: number;
7
+ expectedIdentity: PublishedWriteIdentity;
8
+ parentGuard: AnyAsyncDirectoryGuard;
9
+ nativeParent?: NativeWriteParent;
10
+ retention?: SidecarCreationRetention;
11
+ verify(): Promise<void>;
12
+ }): Promise<void>;
4
13
  export declare function verifyAtomicWriteResult(params: {
5
14
  root: RootContext;
6
15
  targetPath: string;
@@ -5,10 +5,30 @@ import { FsSafeError } from "./errors.js";
5
5
  import { sameFileIdentity } from "./file-identity.js";
6
6
  import { resolveOpenedFileRealPathForFd } from "./opened-realpath.js";
7
7
  import { assertNoUnsafeDeviceReadPath, hasNodeErrorCode, isNotFoundPathError, isSymlinkOpenError } from "./path.js";
8
+ import { createSuppressedError } from "./suppressed-error.js";
8
9
  import { resolveReadOpenFlags } from "./read-open-flags.js";
9
10
  import { assertRootIdentityCurrent } from "./root-context.js";
10
11
  import { fileNotFoundError, hardlinkedPathNotAllowedError, outsideWorkspaceError } from "./root-errors.js";
11
12
  import { admitPathInsideRoot } from "./root-boundary.js";
13
+ export async function verifyRootWritePublication(params) {
14
+ try {
15
+ await params.verify();
16
+ params.retention?.retain(params.fd, params.parentGuard, params.nativeParent);
17
+ }
18
+ catch (error) {
19
+ if (params.retention && params.nativeParent) {
20
+ // Publication already happened. The creator still owns these descriptors
21
+ // even if opening an additional retained handle failed.
22
+ try {
23
+ params.retention.cleanup(params.fd, params.nativeParent, params.expectedIdentity);
24
+ }
25
+ catch (cleanupError) {
26
+ throw createSuppressedError(cleanupError, error, "sidecar retention and creator cleanup both failed");
27
+ }
28
+ }
29
+ throw error;
30
+ }
31
+ }
12
32
  export async function verifyAtomicWriteResult(params) {
13
33
  let needsPathOpen = false;
14
34
  const assertFile = (stat) => {