@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.
- package/dist/index.js +69 -111
- package/node_modules/@openclaw/fs-safe/CHANGELOG.md +35 -0
- package/node_modules/@openclaw/fs-safe/README.md +1 -1
- package/node_modules/@openclaw/fs-safe/dist/advanced.d.ts +1 -1
- package/node_modules/@openclaw/fs-safe/dist/advanced.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/archive-durability.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/archive-merge.js +4 -4
- package/node_modules/@openclaw/fs-safe/dist/archive-parser.wasm +0 -0
- package/node_modules/@openclaw/fs-safe/dist/archive-read.js +5 -4
- package/node_modules/@openclaw/fs-safe/dist/archive-staging.d.ts +1 -1
- package/node_modules/@openclaw/fs-safe/dist/archive-staging.js +22 -38
- package/node_modules/@openclaw/fs-safe/dist/copy-path-admission.d.ts +17 -0
- package/node_modules/@openclaw/fs-safe/dist/copy-path-admission.js +117 -0
- package/node_modules/@openclaw/fs-safe/dist/copy-root-file-sync.d.ts +8 -5
- package/node_modules/@openclaw/fs-safe/dist/copy-root-file-sync.js +27 -50
- package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root-acquire.d.ts +3 -0
- package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root-acquire.js +57 -11
- package/node_modules/@openclaw/fs-safe/dist/guarded-mutation.js +5 -5
- package/node_modules/@openclaw/fs-safe/dist/guest-native-python.js +52 -5
- package/node_modules/@openclaw/fs-safe/dist/guest.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/native-binding.d.ts +1 -0
- package/node_modules/@openclaw/fs-safe/dist/native-operations.d.ts +0 -1
- package/node_modules/@openclaw/fs-safe/dist/native-operations.js +22 -26
- package/node_modules/@openclaw/fs-safe/dist/native-pinned-write-windows.js +5 -1
- package/node_modules/@openclaw/fs-safe/dist/native-staged-file.js +4 -1
- package/node_modules/@openclaw/fs-safe/dist/permission-exec.js +2 -1
- package/node_modules/@openclaw/fs-safe/dist/pinned-write-types.d.ts +6 -1
- package/node_modules/@openclaw/fs-safe/dist/private-temp-workspace.js +2 -2
- package/node_modules/@openclaw/fs-safe/dist/publish-file.js +34 -7
- package/node_modules/@openclaw/fs-safe/dist/replace-file-copy-fallback.js +1 -4
- package/node_modules/@openclaw/fs-safe/dist/replace-file-descriptor.js +6 -0
- package/node_modules/@openclaw/fs-safe/dist/root-create-input.d.ts +15 -1
- package/node_modules/@openclaw/fs-safe/dist/root-create-input.js +4 -0
- package/node_modules/@openclaw/fs-safe/dist/root-directory-entry.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/root-directory-list.d.ts +0 -1
- package/node_modules/@openclaw/fs-safe/dist/root-directory-list.js +6 -12
- package/node_modules/@openclaw/fs-safe/dist/root-impl.js +15 -15
- package/node_modules/@openclaw/fs-safe/dist/root-paths.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/root-walk.js +4 -4
- package/node_modules/@openclaw/fs-safe/dist/root-write-verification.d.ts +10 -1
- package/node_modules/@openclaw/fs-safe/dist/root-write-verification.js +20 -0
- package/node_modules/@openclaw/fs-safe/dist/secure-temp-dir.js +19 -7
- package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-acquire.js +25 -3
- package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-admission.d.ts +2 -0
- package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-retained.d.ts +14 -0
- package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-retained.js +187 -0
- package/node_modules/@openclaw/fs-safe/dist/sidecar-lock.js +15 -1
- package/node_modules/@openclaw/fs-safe/dist/temp-directory-diagnostics.d.ts +10 -0
- package/node_modules/@openclaw/fs-safe/dist/temp-directory-diagnostics.js +29 -0
- package/node_modules/@openclaw/fs-safe/dist/temp-target.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/temp-workspace-admission.js +6 -6
- package/node_modules/@openclaw/fs-safe/dist/temp-workspace-child-admission.d.ts +4 -4
- package/node_modules/@openclaw/fs-safe/dist/temp-workspace-child-admission.js +17 -16
- package/node_modules/@openclaw/fs-safe/dist/temp-workspace-descriptor.js +20 -28
- package/node_modules/@openclaw/fs-safe/dist/walk.js +2 -13
- package/node_modules/@openclaw/fs-safe/dist/watch-native.d.ts +2 -2
- package/node_modules/@openclaw/fs-safe/dist/watch-native.js +3 -1
- package/node_modules/@openclaw/fs-safe/dist/watch-rescan.js +3 -9
- package/node_modules/@openclaw/fs-safe/dist/watch.js +2 -3
- package/node_modules/@openclaw/fs-safe/docs/advanced.md +1 -1
- package/node_modules/@openclaw/fs-safe/docs/archive.md +3 -0
- package/node_modules/@openclaw/fs-safe/docs/atomic.md +11 -3
- package/node_modules/@openclaw/fs-safe/docs/contributing.md +1 -1
- package/node_modules/@openclaw/fs-safe/docs/copy.md +48 -0
- package/node_modules/@openclaw/fs-safe/docs/guest.md +21 -4
- package/node_modules/@openclaw/fs-safe/docs/path-scope.md +4 -0
- package/node_modules/@openclaw/fs-safe/docs/sidecar-lock.md +46 -8
- package/node_modules/@openclaw/fs-safe/docs/temp.md +12 -0
- package/node_modules/@openclaw/fs-safe/docs/watch.md +3 -2
- package/node_modules/@openclaw/fs-safe/package.json +15 -15
- package/node_modules/@openclaw/fs-safe-darwin-arm64/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-darwin-arm64/package.json +1 -1
- package/node_modules/@openclaw/fs-safe-darwin-x64/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-darwin-x64/package.json +1 -1
- package/node_modules/@openclaw/fs-safe-linux-arm64-gnu/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-linux-arm64-gnu/package.json +1 -1
- package/node_modules/@openclaw/fs-safe-linux-arm64-musl/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-linux-arm64-musl/package.json +1 -1
- package/node_modules/@openclaw/fs-safe-linux-x64-gnu/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-linux-x64-gnu/package.json +1 -1
- package/node_modules/@openclaw/fs-safe-linux-x64-musl/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-linux-x64-musl/package.json +1 -1
- package/node_modules/@openclaw/fs-safe-win32-x64-msvc/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-win32-x64-msvc/package.json +1 -1
- package/package.json +5 -5
- package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root-arbitration.d.ts +0 -17
- package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root-arbitration.js +0 -66
- package/node_modules/@openclaw/fs-safe/dist/publish-file-native.d.ts +0 -4
- package/node_modules/@openclaw/fs-safe/dist/publish-file-native.js +0 -35
- package/node_modules/@openclaw/fs-safe/dist/root-move-preflight.d.ts +0 -7
- 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:
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
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
|
|
941
|
-
|
|
942
|
-
|
|
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 =
|
|
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.")
|
|
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 {
|
|
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, {
|
|
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 (
|
|
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;
|