@openclaw/openshell-sandbox 2026.10.2 → 2026.10.5-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/dist/index.js +69 -111
  2. package/node_modules/@openclaw/fs-safe/CHANGELOG.md +35 -0
  3. package/node_modules/@openclaw/fs-safe/README.md +1 -1
  4. package/node_modules/@openclaw/fs-safe/dist/advanced.d.ts +1 -1
  5. package/node_modules/@openclaw/fs-safe/dist/advanced.js +1 -1
  6. package/node_modules/@openclaw/fs-safe/dist/archive-durability.js +1 -1
  7. package/node_modules/@openclaw/fs-safe/dist/archive-merge.js +4 -4
  8. package/node_modules/@openclaw/fs-safe/dist/archive-parser.wasm +0 -0
  9. package/node_modules/@openclaw/fs-safe/dist/archive-read.js +5 -4
  10. package/node_modules/@openclaw/fs-safe/dist/archive-staging.d.ts +1 -1
  11. package/node_modules/@openclaw/fs-safe/dist/archive-staging.js +22 -38
  12. package/node_modules/@openclaw/fs-safe/dist/copy-path-admission.d.ts +17 -0
  13. package/node_modules/@openclaw/fs-safe/dist/copy-path-admission.js +117 -0
  14. package/node_modules/@openclaw/fs-safe/dist/copy-root-file-sync.d.ts +8 -5
  15. package/node_modules/@openclaw/fs-safe/dist/copy-root-file-sync.js +27 -50
  16. package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root-acquire.d.ts +3 -0
  17. package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root-acquire.js +57 -11
  18. package/node_modules/@openclaw/fs-safe/dist/guarded-mutation.js +5 -5
  19. package/node_modules/@openclaw/fs-safe/dist/guest-native-python.js +52 -5
  20. package/node_modules/@openclaw/fs-safe/dist/guest.js +1 -1
  21. package/node_modules/@openclaw/fs-safe/dist/native-binding.d.ts +1 -0
  22. package/node_modules/@openclaw/fs-safe/dist/native-operations.d.ts +0 -1
  23. package/node_modules/@openclaw/fs-safe/dist/native-operations.js +22 -26
  24. package/node_modules/@openclaw/fs-safe/dist/native-pinned-write-windows.js +5 -1
  25. package/node_modules/@openclaw/fs-safe/dist/native-staged-file.js +4 -1
  26. package/node_modules/@openclaw/fs-safe/dist/permission-exec.js +2 -1
  27. package/node_modules/@openclaw/fs-safe/dist/pinned-write-types.d.ts +6 -1
  28. package/node_modules/@openclaw/fs-safe/dist/private-temp-workspace.js +2 -2
  29. package/node_modules/@openclaw/fs-safe/dist/publish-file.js +34 -7
  30. package/node_modules/@openclaw/fs-safe/dist/replace-file-copy-fallback.js +1 -4
  31. package/node_modules/@openclaw/fs-safe/dist/replace-file-descriptor.js +6 -0
  32. package/node_modules/@openclaw/fs-safe/dist/root-create-input.d.ts +15 -1
  33. package/node_modules/@openclaw/fs-safe/dist/root-create-input.js +4 -0
  34. package/node_modules/@openclaw/fs-safe/dist/root-directory-entry.js +1 -1
  35. package/node_modules/@openclaw/fs-safe/dist/root-directory-list.d.ts +0 -1
  36. package/node_modules/@openclaw/fs-safe/dist/root-directory-list.js +6 -12
  37. package/node_modules/@openclaw/fs-safe/dist/root-impl.js +15 -15
  38. package/node_modules/@openclaw/fs-safe/dist/root-paths.js +1 -1
  39. package/node_modules/@openclaw/fs-safe/dist/root-walk.js +4 -4
  40. package/node_modules/@openclaw/fs-safe/dist/root-write-verification.d.ts +10 -1
  41. package/node_modules/@openclaw/fs-safe/dist/root-write-verification.js +20 -0
  42. package/node_modules/@openclaw/fs-safe/dist/secure-temp-dir.js +19 -7
  43. package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-acquire.js +25 -3
  44. package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-admission.d.ts +2 -0
  45. package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-retained.d.ts +14 -0
  46. package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-retained.js +187 -0
  47. package/node_modules/@openclaw/fs-safe/dist/sidecar-lock.js +15 -1
  48. package/node_modules/@openclaw/fs-safe/dist/temp-directory-diagnostics.d.ts +10 -0
  49. package/node_modules/@openclaw/fs-safe/dist/temp-directory-diagnostics.js +29 -0
  50. package/node_modules/@openclaw/fs-safe/dist/temp-target.js +1 -1
  51. package/node_modules/@openclaw/fs-safe/dist/temp-workspace-admission.js +6 -6
  52. package/node_modules/@openclaw/fs-safe/dist/temp-workspace-child-admission.d.ts +4 -4
  53. package/node_modules/@openclaw/fs-safe/dist/temp-workspace-child-admission.js +17 -16
  54. package/node_modules/@openclaw/fs-safe/dist/temp-workspace-descriptor.js +20 -28
  55. package/node_modules/@openclaw/fs-safe/dist/walk.js +2 -13
  56. package/node_modules/@openclaw/fs-safe/dist/watch-native.d.ts +2 -2
  57. package/node_modules/@openclaw/fs-safe/dist/watch-native.js +3 -1
  58. package/node_modules/@openclaw/fs-safe/dist/watch-rescan.js +3 -9
  59. package/node_modules/@openclaw/fs-safe/dist/watch.js +2 -3
  60. package/node_modules/@openclaw/fs-safe/docs/advanced.md +1 -1
  61. package/node_modules/@openclaw/fs-safe/docs/archive.md +3 -0
  62. package/node_modules/@openclaw/fs-safe/docs/atomic.md +11 -3
  63. package/node_modules/@openclaw/fs-safe/docs/contributing.md +1 -1
  64. package/node_modules/@openclaw/fs-safe/docs/copy.md +48 -0
  65. package/node_modules/@openclaw/fs-safe/docs/guest.md +21 -4
  66. package/node_modules/@openclaw/fs-safe/docs/path-scope.md +4 -0
  67. package/node_modules/@openclaw/fs-safe/docs/sidecar-lock.md +46 -8
  68. package/node_modules/@openclaw/fs-safe/docs/temp.md +12 -0
  69. package/node_modules/@openclaw/fs-safe/docs/watch.md +3 -2
  70. package/node_modules/@openclaw/fs-safe/package.json +15 -15
  71. package/node_modules/@openclaw/fs-safe-darwin-arm64/fs-safe-native.node +0 -0
  72. package/node_modules/@openclaw/fs-safe-darwin-arm64/package.json +1 -1
  73. package/node_modules/@openclaw/fs-safe-darwin-x64/fs-safe-native.node +0 -0
  74. package/node_modules/@openclaw/fs-safe-darwin-x64/package.json +1 -1
  75. package/node_modules/@openclaw/fs-safe-linux-arm64-gnu/fs-safe-native.node +0 -0
  76. package/node_modules/@openclaw/fs-safe-linux-arm64-gnu/package.json +1 -1
  77. package/node_modules/@openclaw/fs-safe-linux-arm64-musl/fs-safe-native.node +0 -0
  78. package/node_modules/@openclaw/fs-safe-linux-arm64-musl/package.json +1 -1
  79. package/node_modules/@openclaw/fs-safe-linux-x64-gnu/fs-safe-native.node +0 -0
  80. package/node_modules/@openclaw/fs-safe-linux-x64-gnu/package.json +1 -1
  81. package/node_modules/@openclaw/fs-safe-linux-x64-musl/fs-safe-native.node +0 -0
  82. package/node_modules/@openclaw/fs-safe-linux-x64-musl/package.json +1 -1
  83. package/node_modules/@openclaw/fs-safe-win32-x64-msvc/fs-safe-native.node +0 -0
  84. package/node_modules/@openclaw/fs-safe-win32-x64-msvc/package.json +1 -1
  85. package/package.json +5 -5
  86. package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root-arbitration.d.ts +0 -17
  87. package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root-arbitration.js +0 -66
  88. package/node_modules/@openclaw/fs-safe/dist/publish-file-native.d.ts +0 -4
  89. package/node_modules/@openclaw/fs-safe/dist/publish-file-native.js +0 -35
  90. package/node_modules/@openclaw/fs-safe/dist/root-move-preflight.d.ts +0 -7
  91. package/node_modules/@openclaw/fs-safe/dist/root-move-preflight.js +0 -16
@@ -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);
@@ -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) {
@@ -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) => {
@@ -6,6 +6,7 @@ import { recursiveMkdirPath } from "./recursive-mkdir-path.js";
6
6
  import { hasNodeErrorCode } from "./path.js";
7
7
  import { assertSafePathSegment } from "./safe-path-segment.js";
8
8
  import { assertNoWindowsPathAlias, pathForWindowsFilesystem } from "./windows-path-alias.js";
9
+ import { privateTempDirectoryFix, secureTempDirectoryFailure } from "./temp-directory-diagnostics.js";
9
10
  import { captureSecureTempRepairAdapter, repairSecureTempDirectory, secureTempDirectoryReceipt, } from "./secure-temp-repair.js";
10
11
  export function resolveSecureTempRoot(options) {
11
12
  const { fallbackPrefix: prefix, accessSync: suppliedAccess, lstatSync: suppliedLstat, mkdirSync: suppliedMkdir, descriptor: suppliedDescriptor, getuid: suppliedGetuid, tmpdir: suppliedTmpdir, platform: suppliedPlatform, preferredDir, skipPreferredOnWindows, warn: suppliedWarn, warningPrefix: suppliedWarningPrefix, unsafeFallbackLabel: suppliedFallbackLabel, } = options;
@@ -82,8 +83,17 @@ export function resolveSecureTempRoot(options) {
82
83
  candidate = lstatSync(candidatePath);
83
84
  }
84
85
  catch (error) {
85
- return { kind: hasNodeErrorCode(error, "ENOENT") ? "missing" : "invalid", error };
86
+ return { kind: hasNodeErrorCode(error, "ENOENT") ? "missing" : "invalid", error,
87
+ detail: `directory metadata could not be read; check path traversal/access permissions; ${privateTempDirectoryFix(uid)}` };
86
88
  }
89
+ const failureDetail = () => {
90
+ try {
91
+ return secureTempDirectoryFailure(candidate, uid, windows);
92
+ }
93
+ catch {
94
+ return `directory security facts could not be read; ${privateTempDirectoryFix(uid)}`;
95
+ }
96
+ };
87
97
  let receipt;
88
98
  try {
89
99
  if (descriptor && uid !== undefined) {
@@ -91,26 +101,28 @@ export function resolveSecureTempRoot(options) {
91
101
  }
92
102
  if (receipt ? (receipt.mode & 18n) !== 0n :
93
103
  candidate.isDirectory() !== true || candidate.isSymbolicLink() !== false || !isSecureDirForUser(candidate)) {
94
- return { kind: "invalid", receipt };
104
+ return { kind: "invalid", receipt, detail: failureDetail() };
95
105
  }
96
106
  accessSync(candidatePath, TMP_DIR_ACCESS_MODE);
97
107
  return { kind: "available", receipt };
98
108
  }
99
109
  catch (error) {
100
- return { kind: "invalid", receipt, error };
110
+ return { kind: "invalid", receipt, error, detail: failureDetail() };
101
111
  }
102
112
  };
103
113
  const tryRepair = (candidatePath, state, finalize = false) => {
104
114
  assertNoWindowsPathAlias(candidatePath, "filesystem", "temp directory uses a Windows filesystem namespace alias", admissionPlatform);
105
115
  if (!descriptor || uid === undefined || !state.receipt) {
106
- return finalize ? { kind: "invalid", error: new Error("Secure temp descriptor finalization is unavailable.") } : state;
116
+ return finalize ? { ...state, kind: "invalid", error: new Error("Secure temp descriptor finalization is unavailable."),
117
+ detail: `descriptor finalization is unavailable; ${privateTempDirectoryFix(uid)}` } : state;
107
118
  }
108
119
  try {
109
120
  repairSecureTempDirectory(candidatePath, state.receipt, uid, descriptor, () => accessSync(candidatePath, TMP_DIR_ACCESS_MODE), () => warn(`${warningPrefix} tightened permissions on temp dir: ${candidatePath}`), finalize);
110
121
  return { kind: "available" };
111
122
  }
112
123
  catch (error) {
113
- return { kind: "invalid", error };
124
+ return { kind: "invalid", error,
125
+ detail: state.detail ?? `descriptor identity, permissions, or access could not be verified during repair; ${privateTempDirectoryFix(uid)}` };
114
126
  }
115
127
  };
116
128
  const ensureTrustedFallbackDir = () => {
@@ -128,7 +140,7 @@ export function resolveSecureTempRoot(options) {
128
140
  finalize = !windows && (suppliedMkdir !== undefined || created !== undefined);
129
141
  }
130
142
  catch (cause) {
131
- throw new Error(`Unable to create fallback ${unsafeFallbackLabel}: ${fallbackPath}`, { cause });
143
+ throw new Error(`Unable to create fallback ${unsafeFallbackLabel}: ${JSON.stringify(fallbackPath)}; check parent directory write/search access; ${privateTempDirectoryFix(uid)}`, { cause });
132
144
  }
133
145
  // A concurrent mkdir winner has no implicit ownership or mode authority.
134
146
  state = resolveDirState(entryPath);
@@ -137,7 +149,7 @@ export function resolveSecureTempRoot(options) {
137
149
  }
138
150
  state = tryRepair(entryPath, state, finalize);
139
151
  if (state.kind !== "available") {
140
- throw new Error(`Unsafe fallback ${unsafeFallbackLabel}: ${fallbackPath}`, { cause: state.error });
152
+ throw new Error(`Unsafe fallback ${unsafeFallbackLabel}: ${JSON.stringify(fallbackPath)}; ${state.detail}`, { cause: state.error });
141
153
  }
142
154
  return fallbackPath;
143
155
  };
@@ -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 { fileObservation } from "./file-observation.js";
5
+ import { sameFileIdentityForCleanup } from "./file-identity.js";
5
6
  import { readFileHandleBounded } from "./bounded-read.js";
6
7
  import { openSidecarRoot } from "./sidecar-lock-root.js";
7
8
  import { createNativeExclusiveFile } from "./native-operations.js";
@@ -14,7 +15,8 @@ import { ancestryHasSidecarAdmission, captureSidecarAdmissionAncestry, createSid
14
15
  import { resolveSidecarLockPaths } from "./sidecar-lock-target.js";
15
16
  import { createSuppressedError } from "./suppressed-error.js";
16
17
  import { sleep } from "./timing.js";
17
- import { sidecarExclusiveCreate } from "./root-create-input.js";
18
+ import { sidecarRetainedCreate } from "./root-create-input.js";
19
+ import { cleanupCreatedSidecar, retainCreatedSidecar } from "./sidecar-lock-retained.js";
18
20
  import { stopSidecarLockMonitoring } from "./sidecar-lock-handle.js";
19
21
  export async function acquireSidecarLock(options, context) {
20
22
  const admissionAncestry = captureSidecarAdmissionAncestry();
@@ -166,6 +168,7 @@ export async function acquireSidecarLock(options, context) {
166
168
  continue;
167
169
  }
168
170
  let handle = null;
171
+ let retainedSidecar;
169
172
  let createdSnapshot = null, createdHeld;
170
173
  let admissionConflict = false;
171
174
  let exclusiveCreateConflict = false;
@@ -247,7 +250,13 @@ export async function acquireSidecarLock(options, context) {
247
250
  const relativeLockPath = relativeSidecarLockPath(lockRoot, lockPath);
248
251
  const observation = fileObservation();
249
252
  try {
250
- await observation.run(() => lockRoot.create(relativeLockPath, raw, { ...sidecarExclusiveCreate, mkdir: true, mode: 0o600 }));
253
+ await observation.run(() => lockRoot.create(relativeLockPath, raw, {
254
+ mkdir: true, mode: 0o600,
255
+ ...sidecarRetainedCreate({
256
+ retain(fd, parent, nativeParent) { retainedSidecar = retainCreatedSidecar(lockPath, raw, fd, parent, nativeParent); },
257
+ cleanup(fd, parent, identity) { cleanupCreatedSidecar(lockPath, raw, fd, parent, identity); },
258
+ }),
259
+ }));
251
260
  }
252
261
  catch (error) {
253
262
  // Only this invocation's failed exclusive open grants denial retry authority.
@@ -262,6 +271,7 @@ export async function acquireSidecarLock(options, context) {
262
271
  createdSnapshot = { raw, payload, ownershipToken };
263
272
  const opened = await openSidecarRoot(lockRoot, relativeLockPath, "unlinked");
264
273
  if (!opened) {
274
+ retainedSidecar?.settle(true, false);
265
275
  await waitForRetry();
266
276
  continue;
267
277
  }
@@ -299,7 +309,11 @@ export async function acquireSidecarLock(options, context) {
299
309
  }
300
310
  const snapshot = { raw, payload, stat: fsSync.fstatSync(handle.fd, { bigint: true }), ownershipToken };
301
311
  createdSnapshot = snapshot;
312
+ if (retainedSidecar && !sameFileIdentityForCleanup(retainedSidecar.identity, snapshot.stat)) {
313
+ throw new FsSafeError("path-mismatch", "created sidecar lock identity changed before admission");
314
+ }
302
315
  if (snapshot.stat.nlink === 0n) {
316
+ retainedSidecar?.settle(true, false);
303
317
  await handle.close();
304
318
  handle = null;
305
319
  await waitForRetry();
@@ -310,10 +324,15 @@ export async function acquireSidecarLock(options, context) {
310
324
  admissionConflict = true;
311
325
  throw Object.assign(new Error("sidecar lock admission changed"), { code: "EEXIST" });
312
326
  }
327
+ // An open descendant file prevents Windows directory relocation. The
328
+ // native receipt retains its parent and verifies the leaf at settlement.
329
+ if (retainedSidecar && process.platform === "win32")
330
+ await handle.close();
313
331
  createdHeld = {
314
332
  refCount: 1,
315
333
  reentrantOwner: requestedReentrantOwner,
316
334
  handle,
335
+ retainedSidecar,
317
336
  lockPath,
318
337
  snapshot,
319
338
  acquiredAt: Date.now(),
@@ -380,7 +399,10 @@ export async function acquireSidecarLock(options, context) {
380
399
  await handle.close().catch(() => undefined);
381
400
  }
382
401
  // Root records use the creator receipt; partial raw writes use fd identity.
383
- if (failedSnapshot) {
402
+ if (retainedSidecar) {
403
+ retainedSidecar.settle(true, false);
404
+ }
405
+ else if (failedSnapshot) {
384
406
  await removeSidecarLockIfUnchanged(lockPath, failedSnapshot, {
385
407
  lockRoot,
386
408
  parsePayload: conditionalSidecarLockParser(parserState, admission, () => admission.hasToken() && !context.held.has(normalizedTargetPath)),
@@ -2,10 +2,12 @@ import type { NativeFileHandle } from "./native-operations.js";
2
2
  import type { Root } from "./root-impl.js";
3
3
  import type { SidecarLockSnapshot } from "./sidecar-lock-reclaim.js";
4
4
  import type { SidecarLockHandle } from "./sidecar-lock-types.js";
5
+ import type { RetainedSidecar } from "./sidecar-lock-retained.js";
5
6
  export type HeldSidecarLock = {
6
7
  refCount: number;
7
8
  reentrantOwner?: string;
8
9
  handle: NativeFileHandle;
10
+ retainedSidecar?: RetainedSidecar;
9
11
  lockPath: string;
10
12
  snapshot: SidecarLockSnapshot;
11
13
  acquiredAt: number;
@@ -0,0 +1,14 @@
1
+ import type { AnyAsyncDirectoryGuard } from "./directory-guard.js";
2
+ import type { NativeWriteParent, PublishedWriteIdentity } from "./pinned-write-types.js";
3
+ export interface RetainedSidecar {
4
+ readonly identity: Readonly<{
5
+ dev: bigint;
6
+ ino: bigint;
7
+ }>;
8
+ /** Synchronous settlement also works in the process exit handler. Always closes. */
9
+ settle(remove: boolean, reportMismatch?: boolean): void;
10
+ }
11
+ /** Borrowed creation authority: never reopens a path or closes the writer's fds. */
12
+ export declare function cleanupCreatedSidecar(lockPath: string, raw: string, fd: number, parent: NativeWriteParent, identity: PublishedWriteIdentity): void;
13
+ /** Called only by the exclusive creator while its verified descriptor is alive. */
14
+ export declare function retainCreatedSidecar(lockPath: string, raw: string, createdFd: number, guard: AnyAsyncDirectoryGuard, nativeParent?: NativeWriteParent): RetainedSidecar | undefined;