@lousy-agents/agent-shell 5.21.4 → 5.21.5
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 +1961 -1732
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { createRequire as __rspack_createRequire } from "node:module";
|
|
3
3
|
const __rspack_createRequire_require = __rspack_createRequire(import.meta.url);
|
|
4
4
|
var __webpack_modules__ = ({
|
|
5
|
-
|
|
5
|
+
461(__unused_rspack_module, __unused_rspack___webpack_exports__, __webpack_require__) {
|
|
6
6
|
|
|
7
7
|
;// CONCATENATED MODULE: external "node:child_process"
|
|
8
8
|
const external_node_child_process_namespaceObject = __rspack_createRequire_require("node:child_process");
|
|
@@ -154,6 +154,85 @@ function sameFileIdentityForCleanup(left, right, platform = process.platform) {
|
|
|
154
154
|
return sameStatValue(left.dev, right.dev) && sameStatValue(left.ino, right.ino);
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
+
;// CONCATENATED MODULE: external "node:async_hooks"
|
|
158
|
+
const external_node_async_hooks_namespaceObject = __rspack_createRequire_require("node:async_hooks");
|
|
159
|
+
;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/file-observation.js
|
|
160
|
+
|
|
161
|
+
const active = new external_node_async_hooks_namespaceObject.AsyncLocalStorage();
|
|
162
|
+
function recordFileObservationFailure(error, kind) {
|
|
163
|
+
const failures = active.getStore();
|
|
164
|
+
if (!failures)
|
|
165
|
+
return;
|
|
166
|
+
let kinds = failures.get(error);
|
|
167
|
+
if (!kinds)
|
|
168
|
+
failures.set(error, kinds = new Set());
|
|
169
|
+
kinds.add(kind);
|
|
170
|
+
}
|
|
171
|
+
function isFileObservationFailure(error, kind) {
|
|
172
|
+
return active.getStore()?.get(error)?.has(kind) === true;
|
|
173
|
+
}
|
|
174
|
+
// Each Root observation owns its receipts, including nested and concurrent reads.
|
|
175
|
+
function fileObservation() {
|
|
176
|
+
const failures = new Map();
|
|
177
|
+
return {
|
|
178
|
+
run(operation) { return active.run(failures, operation); },
|
|
179
|
+
has(error, kind) { return failures.get(error)?.has(kind) === true; },
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/strict-file-identity.js
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
function identityMismatch() {
|
|
187
|
+
const error = new errors_FsSafeError("path-mismatch", "file identity changed or could not be verified");
|
|
188
|
+
recordFileObservationFailure(error, "identity");
|
|
189
|
+
return error;
|
|
190
|
+
}
|
|
191
|
+
function identityCheck(expected, platform) {
|
|
192
|
+
const known = {};
|
|
193
|
+
const check = (stat) => {
|
|
194
|
+
let complete = true;
|
|
195
|
+
for (const field of ["dev", "ino"]) {
|
|
196
|
+
const value = stat[field];
|
|
197
|
+
// Numeric receipts cannot recover identity bits already lost to rounding.
|
|
198
|
+
if (typeof value !== "bigint")
|
|
199
|
+
throw identityMismatch();
|
|
200
|
+
if (platform === "win32" && value === 0n) {
|
|
201
|
+
complete = false;
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
if (known[field] !== undefined && known[field] !== value)
|
|
205
|
+
throw identityMismatch();
|
|
206
|
+
known[field] = value;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return complete;
|
|
210
|
+
};
|
|
211
|
+
if (expected && !check(expected))
|
|
212
|
+
throw identityMismatch();
|
|
213
|
+
return check;
|
|
214
|
+
}
|
|
215
|
+
// Retry only unknown Windows identities, retaining every known component so a
|
|
216
|
+
// later observation cannot erase a definite mismatch. Never reopen the file.
|
|
217
|
+
async function inspectFileIdentity(inspect, expected, platform = process.platform) {
|
|
218
|
+
const check = identityCheck(expected, platform);
|
|
219
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
220
|
+
const stat = await inspect();
|
|
221
|
+
if (check(stat))
|
|
222
|
+
return stat;
|
|
223
|
+
}
|
|
224
|
+
throw identityMismatch();
|
|
225
|
+
}
|
|
226
|
+
function inspectFileIdentitySync(inspect, expected, platform = process.platform) {
|
|
227
|
+
const check = identityCheck(expected, platform);
|
|
228
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
229
|
+
const stat = inspect();
|
|
230
|
+
if (check(stat))
|
|
231
|
+
return stat;
|
|
232
|
+
}
|
|
233
|
+
throw identityMismatch();
|
|
234
|
+
}
|
|
235
|
+
|
|
157
236
|
;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/path.js
|
|
158
237
|
|
|
159
238
|
|
|
@@ -368,6 +447,7 @@ function normalizeRemovePathError(error) {
|
|
|
368
447
|
|
|
369
448
|
|
|
370
449
|
|
|
450
|
+
|
|
371
451
|
async function directory_guard_createAsyncDirectoryGuard(dir) {
|
|
372
452
|
const stat = await promises_namespaceObject.lstat(dir);
|
|
373
453
|
if (stat.isSymbolicLink() || !stat.isDirectory()) {
|
|
@@ -432,6 +512,15 @@ function directory_guard_createNearestExistingSyncDirectoryGuard(rootReal, targe
|
|
|
432
512
|
}
|
|
433
513
|
return directory_guard_createSyncDirectoryGuard(root);
|
|
434
514
|
}
|
|
515
|
+
// Recovery receipts must retain every identity bit, including on Windows.
|
|
516
|
+
async function inspectDirectoryIdentity(dir, expected) {
|
|
517
|
+
return await inspectFileIdentity(async () => {
|
|
518
|
+
const stat = await promises_namespaceObject.lstat(dir, { bigint: true });
|
|
519
|
+
if (stat.isSymbolicLink() || !stat.isDirectory())
|
|
520
|
+
throw root_errors_directoryComponentNotDirectoryError();
|
|
521
|
+
return stat;
|
|
522
|
+
}, expected);
|
|
523
|
+
}
|
|
435
524
|
|
|
436
525
|
;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/directory-durability.js
|
|
437
526
|
|
|
@@ -873,6 +962,8 @@ async function guardedRename(params) {
|
|
|
873
962
|
? await createNearestExistingDirectoryGuard(params.targetRoot, path.dirname(params.to))
|
|
874
963
|
: await createAsyncDirectoryGuard(path.dirname(params.to));
|
|
875
964
|
await withAsyncDirectoryGuards([sourceGuard, targetGuard], async () => {
|
|
965
|
+
// Authority must survive all awaited guards; do not yield before rename dispatch.
|
|
966
|
+
params.assertBeforeRename?.();
|
|
876
967
|
await fs.rename(params.from, params.to);
|
|
877
968
|
}, { verifyAfter: params.verifyAfter });
|
|
878
969
|
}
|
|
@@ -1042,6 +1133,49 @@ function mergeDenyMutationPolicies(defaultPolicy, callPolicy) {
|
|
|
1042
1133
|
};
|
|
1043
1134
|
}
|
|
1044
1135
|
|
|
1136
|
+
;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/opened-file-failure.js
|
|
1137
|
+
|
|
1138
|
+
|
|
1139
|
+
|
|
1140
|
+
function recordFileOpenFailure(error, filePath) {
|
|
1141
|
+
recordFileObservationFailure(error, `open:${filePath}`);
|
|
1142
|
+
throw error;
|
|
1143
|
+
}
|
|
1144
|
+
function openedPathResolutionError(error = new errors_FsSafeError("path-mismatch", "unable to resolve opened file path")) {
|
|
1145
|
+
recordFileObservationFailure(error, "resolution");
|
|
1146
|
+
return error;
|
|
1147
|
+
}
|
|
1148
|
+
async function recordPreOpenFileChange(error, handle, filePath, before, opened) {
|
|
1149
|
+
if (!isFileObservationFailure(error, "identity") || !before?.isFile() || !opened?.isFile() ||
|
|
1150
|
+
![0n, 1n].includes(before.nlink) || ![0n, 1n].includes(opened.nlink))
|
|
1151
|
+
return;
|
|
1152
|
+
try {
|
|
1153
|
+
await inspectFileIdentity(async () => before);
|
|
1154
|
+
const current = await inspectFileIdentity(() => handle.stat({ bigint: true }), opened);
|
|
1155
|
+
if (current.isFile() && [0n, 1n].includes(current.nlink) &&
|
|
1156
|
+
(before.dev !== current.dev || before.ino !== current.ino)) {
|
|
1157
|
+
// A stale pathname sample is not proof that its former inode was unlinked.
|
|
1158
|
+
recordFileObservationFailure(error, `changed:${filePath}`);
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
catch {
|
|
1162
|
+
// Unknown, changed, or closed descriptor evidence never permits a discard.
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
async function recordOpenedFileFailure(error, handle, filePath, identity) {
|
|
1166
|
+
if ((!isFileObservationFailure(error, "resolution") && !isFileObservationFailure(error, "identity")) ||
|
|
1167
|
+
!identity.isFile() || identity.nlink > 1n)
|
|
1168
|
+
return;
|
|
1169
|
+
try {
|
|
1170
|
+
const current = await inspectFileIdentity(() => handle.stat({ bigint: true }), identity);
|
|
1171
|
+
if (current.isFile() && current.nlink === 0n)
|
|
1172
|
+
recordFileObservationFailure(error, `unlinked:${filePath}`);
|
|
1173
|
+
}
|
|
1174
|
+
catch {
|
|
1175
|
+
// A closed, changed, or unknown descriptor provides no retry evidence.
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1045
1179
|
;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/opened-realpath.js
|
|
1046
1180
|
|
|
1047
1181
|
|
|
@@ -1081,6 +1215,12 @@ async function resolveOpenedFileRealPathForFd(fd, handleStat, ioPath) {
|
|
|
1081
1215
|
}
|
|
1082
1216
|
catch (err) {
|
|
1083
1217
|
if (!path_isNotFoundPathError(err)) {
|
|
1218
|
+
// Windows can fail here on a deleted-but-open file. Brand only this
|
|
1219
|
+
// resolver operation; the caller must still prove unlink on the same fd.
|
|
1220
|
+
if (process.platform === "win32" && err instanceof Error &&
|
|
1221
|
+
["EPERM", "EBADF"].includes(err.code ?? "")) {
|
|
1222
|
+
throw openedPathResolutionError(err);
|
|
1223
|
+
}
|
|
1084
1224
|
throw err;
|
|
1085
1225
|
}
|
|
1086
1226
|
}
|
|
@@ -1088,7 +1228,7 @@ async function resolveOpenedFileRealPathForFd(fd, handleStat, ioPath) {
|
|
|
1088
1228
|
if (parentResolved) {
|
|
1089
1229
|
return parentResolved;
|
|
1090
1230
|
}
|
|
1091
|
-
throw
|
|
1231
|
+
throw openedPathResolutionError();
|
|
1092
1232
|
}
|
|
1093
1233
|
async function resolveOpenedFileRealPathFromParent(handleStat, ioPath, statOptions) {
|
|
1094
1234
|
let parentReal;
|
|
@@ -1582,7 +1722,7 @@ async function runPinnedWriteWindows(binding, params, root, parentFd, parentGuar
|
|
|
1582
1722
|
let renamed = false;
|
|
1583
1723
|
let completed = false;
|
|
1584
1724
|
try {
|
|
1585
|
-
tempName =
|
|
1725
|
+
tempName = `.fs-safe-${(0,external_node_crypto_namespaceObject.randomUUID)()}.tmp`;
|
|
1586
1726
|
tempFd = binding.openBeneath(parentFd, tempName, nativeOpenFlags(external_node_fs_namespaceObject.constants.O_WRONLY | external_node_fs_namespaceObject.constants.O_CREAT | external_node_fs_namespaceObject.constants.O_EXCL)).fd;
|
|
1587
1727
|
tempIdentity = external_node_fs_namespaceObject.fstatSync(tempFd);
|
|
1588
1728
|
const verificationIdentity = external_node_fs_namespaceObject.fstatSync(tempFd, { bigint: true });
|
|
@@ -1792,7 +1932,7 @@ const UNCOMMITTED_RENAME_ERRORS = new Set([
|
|
|
1792
1932
|
"ENAMETOOLONG", "ENOENT", "ENOSPC", "ENOSYS", "ENOTDIR", "ENOTEMPTY", "ENOTSUP",
|
|
1793
1933
|
"EPERM", "EROFS", "ETXTBSY", "EXDEV",
|
|
1794
1934
|
]);
|
|
1795
|
-
function
|
|
1935
|
+
function native_staged_file_failure(error, details) {
|
|
1796
1936
|
const code = error instanceof errors_FsSafeError
|
|
1797
1937
|
? error.code
|
|
1798
1938
|
: error?.code === "EEXIST" ? "already-exists" : "helper-failed";
|
|
@@ -1924,10 +2064,10 @@ class NativeStagedFile {
|
|
|
1924
2064
|
catch (error) {
|
|
1925
2065
|
const { receipt: cleanup, error: cleanupError } = this.#finalize();
|
|
1926
2066
|
if (cleanupError) {
|
|
1927
|
-
throw
|
|
2067
|
+
throw native_staged_file_failure(new AggregateError([error, cleanupError], "preparation and cleanup failed"), { phase: "prepare", publication: cleanup.publication, cleanup });
|
|
1928
2068
|
}
|
|
1929
2069
|
if (cleanup.status === "preserved") {
|
|
1930
|
-
throw
|
|
2070
|
+
throw native_staged_file_failure(error, { phase: "prepare", publication: cleanup.publication, cleanup });
|
|
1931
2071
|
}
|
|
1932
2072
|
throw error;
|
|
1933
2073
|
}
|
|
@@ -1986,7 +2126,7 @@ class NativeStagedFile {
|
|
|
1986
2126
|
const publication = this.#state.status === "closed"
|
|
1987
2127
|
? this.#state.receipt.publication
|
|
1988
2128
|
: this.#state.publication;
|
|
1989
|
-
throw
|
|
2129
|
+
throw native_staged_file_failure(error, { phase: "publish", publication });
|
|
1990
2130
|
}
|
|
1991
2131
|
}
|
|
1992
2132
|
#finalize() {
|
|
@@ -2027,7 +2167,7 @@ class NativeStagedFile {
|
|
|
2027
2167
|
status: outcome,
|
|
2028
2168
|
resources,
|
|
2029
2169
|
});
|
|
2030
|
-
const error = errors.length ?
|
|
2170
|
+
const error = errors.length ? native_staged_file_failure(errors.length === 1 ? errors[0] : new AggregateError(errors, "staged cleanup failed"), { phase: "cleanup", publication: state.publication, cleanup: receipt }) : undefined;
|
|
2031
2171
|
this.#state = { status: "closed", receipt, error };
|
|
2032
2172
|
return this.#state;
|
|
2033
2173
|
}
|
|
@@ -2336,974 +2476,756 @@ function bounded_read_readFileDescriptorBoundedSync(fd, maxBytes) {
|
|
|
2336
2476
|
}
|
|
2337
2477
|
}
|
|
2338
2478
|
|
|
2339
|
-
;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/
|
|
2340
|
-
|
|
2341
|
-
function
|
|
2342
|
-
return
|
|
2343
|
-
}
|
|
2344
|
-
function getFsSafeTestHooks() {
|
|
2345
|
-
return test_hooks_fsSafeTestHooks;
|
|
2346
|
-
}
|
|
2347
|
-
function __setFsSafeTestHooksForTest(hooks) {
|
|
2348
|
-
if (hooks && !allowFsSafeTestHooks()) {
|
|
2349
|
-
throw new Error("__setFsSafeTestHooksForTest is only available in tests");
|
|
2350
|
-
}
|
|
2351
|
-
test_hooks_fsSafeTestHooks = hooks;
|
|
2479
|
+
;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/error-detail.js
|
|
2480
|
+
const UNSAFE_ERROR_DETAIL_CHARACTERS = /[\u0000-\u001f\u007f-\u009f\u2028\u2029]/gu;
|
|
2481
|
+
function formatErrorDetail(value) {
|
|
2482
|
+
return value.replace(UNSAFE_ERROR_DETAIL_CHARACTERS, (character) => `\\u${character.charCodeAt(0).toString(16).padStart(4, "0")}`);
|
|
2352
2483
|
}
|
|
2353
2484
|
|
|
2354
|
-
;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2485
|
+
;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/root-path-symlink.js
|
|
2358
2486
|
|
|
2359
2487
|
|
|
2360
2488
|
|
|
2361
2489
|
|
|
2362
2490
|
|
|
2363
2491
|
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
function createSidecarLockOwnershipToken() {
|
|
2370
|
-
let token = SIDECAR_LOCK_OWNERSHIP_TOKEN_PREFIX;
|
|
2371
|
-
for (const byte of (0,external_node_crypto_namespaceObject.randomBytes)(SIDECAR_LOCK_OWNERSHIP_TOKEN_BYTES)) {
|
|
2372
|
-
for (let bit = 7; bit >= 0; bit -= 1) {
|
|
2373
|
-
token += byte & (1 << bit) ? "\t" : " ";
|
|
2374
|
-
}
|
|
2375
|
-
}
|
|
2376
|
-
return token;
|
|
2377
|
-
}
|
|
2378
|
-
function readSidecarLockOwnershipToken(raw) {
|
|
2379
|
-
return SIDECAR_LOCK_OWNERSHIP_TOKEN_PATTERN.exec(raw)?.[1];
|
|
2380
|
-
}
|
|
2381
|
-
function serializeSidecarLockPayload(payload) {
|
|
2382
|
-
const ownershipToken = createSidecarLockOwnershipToken();
|
|
2383
|
-
return {
|
|
2384
|
-
raw: `${JSON.stringify(payload, null, 2)}\n${ownershipToken}\n`,
|
|
2385
|
-
ownershipToken,
|
|
2386
|
-
};
|
|
2387
|
-
}
|
|
2388
|
-
function relativeSidecarLockPath(lockRoot, lockPath) {
|
|
2389
|
-
const resolved = external_node_path_namespaceObject.resolve(lockPath);
|
|
2390
|
-
const lexicalRelative = external_node_path_namespaceObject.relative(lockRoot.rootDir, resolved);
|
|
2391
|
-
const relative = lexicalRelative !== ".." &&
|
|
2392
|
-
!lexicalRelative.startsWith(`..${external_node_path_namespaceObject.sep}`) &&
|
|
2393
|
-
!external_node_path_namespaceObject.isAbsolute(lexicalRelative)
|
|
2394
|
-
? lexicalRelative
|
|
2395
|
-
: external_node_path_namespaceObject.relative(lockRoot.rootReal, resolved);
|
|
2396
|
-
if (!relative || relative === ".." || relative.startsWith(`..${external_node_path_namespaceObject.sep}`) || external_node_path_namespaceObject.isAbsolute(relative)) {
|
|
2397
|
-
throw new errors_FsSafeError("outside-workspace", "sidecar lock path is outside lockRoot");
|
|
2492
|
+
function normalizeSymlinkResolutionError(error) {
|
|
2493
|
+
if (isSymlinkOpenError(error)) {
|
|
2494
|
+
throw new errors_FsSafeError("symlink", "symlink path could not be resolved", {
|
|
2495
|
+
cause: error instanceof Error ? error : undefined,
|
|
2496
|
+
});
|
|
2398
2497
|
}
|
|
2399
|
-
|
|
2498
|
+
if (!path_isNotFoundPathError(error))
|
|
2499
|
+
throw error;
|
|
2400
2500
|
}
|
|
2401
|
-
function
|
|
2402
|
-
if (parser) {
|
|
2403
|
-
return parser(raw);
|
|
2404
|
-
}
|
|
2501
|
+
async function resolveSymlinkHopPath(symlinkPath) {
|
|
2405
2502
|
try {
|
|
2406
|
-
|
|
2407
|
-
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
2503
|
+
return external_node_path_namespaceObject.resolve(await promises_namespaceObject.realpath(symlinkPath));
|
|
2408
2504
|
}
|
|
2409
|
-
catch {
|
|
2410
|
-
|
|
2505
|
+
catch (error) {
|
|
2506
|
+
normalizeSymlinkResolutionError(error);
|
|
2507
|
+
const linkTarget = await promises_namespaceObject.readlink(symlinkPath);
|
|
2508
|
+
return resolvePathViaExistingAncestor(external_node_path_namespaceObject.resolve(external_node_path_namespaceObject.dirname(symlinkPath), linkTarget));
|
|
2411
2509
|
}
|
|
2412
2510
|
}
|
|
2413
|
-
|
|
2414
|
-
let handle;
|
|
2511
|
+
function root_path_symlink_resolveSymlinkHopPathSync(symlinkPath) {
|
|
2415
2512
|
try {
|
|
2416
|
-
|
|
2417
|
-
const lockRoot = options.lockRoot;
|
|
2418
|
-
const relative = relativeSidecarLockPath(lockRoot, lockPath);
|
|
2419
|
-
const opened = await lockRoot.open(relative).catch(async (error) => {
|
|
2420
|
-
if (error instanceof errors_FsSafeError && error.code === "path-mismatch") {
|
|
2421
|
-
// An owner can unlink after open. Prove absence through the same root;
|
|
2422
|
-
// acquisition retries create-only, so a later replacement stays guarded.
|
|
2423
|
-
try {
|
|
2424
|
-
await lockRoot.stat(relative);
|
|
2425
|
-
}
|
|
2426
|
-
catch (probeError) {
|
|
2427
|
-
if (probeError instanceof errors_FsSafeError && probeError.code === "not-found") {
|
|
2428
|
-
return null;
|
|
2429
|
-
}
|
|
2430
|
-
}
|
|
2431
|
-
}
|
|
2432
|
-
throw error;
|
|
2433
|
-
});
|
|
2434
|
-
if (!opened)
|
|
2435
|
-
return null;
|
|
2436
|
-
try {
|
|
2437
|
-
const raw = (await readFileHandleBounded(opened.handle, MAX_LOCK_PAYLOAD_BYTES)).toString("utf8");
|
|
2438
|
-
return {
|
|
2439
|
-
raw,
|
|
2440
|
-
payload: parseSidecarLockPayload(raw, options.parsePayload),
|
|
2441
|
-
stat: opened.stat,
|
|
2442
|
-
};
|
|
2443
|
-
}
|
|
2444
|
-
finally {
|
|
2445
|
-
await opened.handle.close().catch(() => undefined);
|
|
2446
|
-
}
|
|
2447
|
-
}
|
|
2448
|
-
const before = await promises_namespaceObject.lstat(lockPath);
|
|
2449
|
-
if (!before.isFile() || before.isSymbolicLink()) {
|
|
2450
|
-
if (options.rejectNonFile) {
|
|
2451
|
-
throw new errors_FsSafeError("not-file", `sidecar lock is not a regular file: ${lockPath}`);
|
|
2452
|
-
}
|
|
2453
|
-
return null;
|
|
2454
|
-
}
|
|
2455
|
-
await getFsSafeTestHooks()?.beforeSidecarLockSnapshotOpen?.(lockPath);
|
|
2456
|
-
const noFollow = process.platform !== "win32" && typeof external_node_fs_namespaceObject.constants.O_NOFOLLOW === "number"
|
|
2457
|
-
? external_node_fs_namespaceObject.constants.O_NOFOLLOW
|
|
2458
|
-
: 0;
|
|
2459
|
-
try {
|
|
2460
|
-
handle = await promises_namespaceObject.open(lockPath, external_node_fs_namespaceObject.constants.O_RDONLY |
|
|
2461
|
-
noFollow |
|
|
2462
|
-
(typeof external_node_fs_namespaceObject.constants.O_NONBLOCK === "number" ? external_node_fs_namespaceObject.constants.O_NONBLOCK : 0));
|
|
2463
|
-
}
|
|
2464
|
-
catch (error) {
|
|
2465
|
-
if (options.rejectNonFile && error.code === "ELOOP") {
|
|
2466
|
-
throw new errors_FsSafeError("not-file", `sidecar lock is not a regular file: ${lockPath}`, {
|
|
2467
|
-
cause: error,
|
|
2468
|
-
});
|
|
2469
|
-
}
|
|
2470
|
-
throw error;
|
|
2471
|
-
}
|
|
2472
|
-
const opened = await handle.stat();
|
|
2473
|
-
if (!opened.isFile()) {
|
|
2474
|
-
if (options.rejectNonFile) {
|
|
2475
|
-
throw new errors_FsSafeError("not-file", `sidecar lock is not a regular file: ${lockPath}`);
|
|
2476
|
-
}
|
|
2477
|
-
return null;
|
|
2478
|
-
}
|
|
2479
|
-
if (!options.allowDescriptorIdentityDrift && !file_identity_sameFileIdentity(before, opened))
|
|
2480
|
-
return null;
|
|
2481
|
-
const raw = (await readFileHandleBounded(handle, MAX_LOCK_PAYLOAD_BYTES)).toString("utf8");
|
|
2482
|
-
const after = await promises_namespaceObject.lstat(lockPath);
|
|
2483
|
-
if (!after.isFile() || !file_identity_sameFileIdentity(before, after))
|
|
2484
|
-
return null;
|
|
2485
|
-
return { raw, payload: parseSidecarLockPayload(raw, options.parsePayload), stat: after };
|
|
2513
|
+
return path.resolve(fs.realpathSync(symlinkPath));
|
|
2486
2514
|
}
|
|
2487
|
-
catch (
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
}
|
|
2492
|
-
throw err;
|
|
2515
|
+
catch (error) {
|
|
2516
|
+
normalizeSymlinkResolutionError(error);
|
|
2517
|
+
const linkTarget = fs.readlinkSync(symlinkPath);
|
|
2518
|
+
return resolvePathViaExistingAncestorSync(path.resolve(path.dirname(symlinkPath), linkTarget));
|
|
2493
2519
|
}
|
|
2494
|
-
|
|
2495
|
-
|
|
2520
|
+
}
|
|
2521
|
+
|
|
2522
|
+
;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/safe-path-segment.js
|
|
2523
|
+
|
|
2524
|
+
const SAFE_PATH_SEGMENT_PATTERN = /^[A-Za-z0-9_-][A-Za-z0-9._-]*$/;
|
|
2525
|
+
const SAFE_DOT_PREFIX_PATH_SEGMENT_PATTERN = /^[A-Za-z0-9._-]+$/;
|
|
2526
|
+
// Windows treats "C:name" as relative to the drive's current directory even
|
|
2527
|
+
// though path.win32.isAbsolute() reports false.
|
|
2528
|
+
const DRIVE_RELATIVE_PREFIX = /^[A-Za-z]:(?![\\/])/;
|
|
2529
|
+
const HYPHEN_CHAR_CODE = 0x2d;
|
|
2530
|
+
function safe_path_segment_isDriveRelativePath(value) {
|
|
2531
|
+
return DRIVE_RELATIVE_PREFIX.test(value);
|
|
2532
|
+
}
|
|
2533
|
+
function assertNoDriveRelativePathSegments(value, label) {
|
|
2534
|
+
if (value.split("/").some(safe_path_segment_isDriveRelativePath)) {
|
|
2535
|
+
throw new errors_FsSafeError("invalid-path", `${label} must not contain a drive letter`);
|
|
2496
2536
|
}
|
|
2537
|
+
return value;
|
|
2497
2538
|
}
|
|
2498
|
-
function
|
|
2499
|
-
let
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
if (options.rejectNonFile) {
|
|
2504
|
-
throw new FsSafeError("not-file", `sidecar lock is not a regular file: ${lockPath}`);
|
|
2505
|
-
}
|
|
2506
|
-
return null;
|
|
2507
|
-
}
|
|
2508
|
-
fd = fsSync.openSync(lockPath, resolveReadOpenFlags());
|
|
2509
|
-
const opened = fsSync.fstatSync(fd);
|
|
2510
|
-
if (!opened.isFile()) {
|
|
2511
|
-
if (options.rejectNonFile) {
|
|
2512
|
-
throw new FsSafeError("not-file", `sidecar lock is not a regular file: ${lockPath}`);
|
|
2513
|
-
}
|
|
2514
|
-
return null;
|
|
2515
|
-
}
|
|
2516
|
-
const raw = readFileDescriptorBoundedSync(fd, MAX_LOCK_PAYLOAD_BYTES).toString("utf8");
|
|
2517
|
-
const after = fsSync.lstatSync(lockPath);
|
|
2518
|
-
if (!sameFileIdentity(before, opened) || !sameFileIdentity(opened, after))
|
|
2519
|
-
return null;
|
|
2520
|
-
return {
|
|
2521
|
-
raw,
|
|
2522
|
-
payload: parseSidecarLockPayload(raw, parsePayload),
|
|
2523
|
-
stat: after,
|
|
2524
|
-
ownershipToken: readSidecarLockOwnershipToken(raw),
|
|
2525
|
-
};
|
|
2526
|
-
}
|
|
2527
|
-
catch (error) {
|
|
2528
|
-
if (error.code === "ENOENT")
|
|
2529
|
-
return null;
|
|
2530
|
-
throw error;
|
|
2539
|
+
function trimHyphenEdges(value) {
|
|
2540
|
+
let start = 0;
|
|
2541
|
+
let end = value.length;
|
|
2542
|
+
while (start < end && value.charCodeAt(start) === HYPHEN_CHAR_CODE) {
|
|
2543
|
+
start += 1;
|
|
2531
2544
|
}
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
fsSync.closeSync(fd);
|
|
2545
|
+
while (end > start && value.charCodeAt(end - 1) === HYPHEN_CHAR_CODE) {
|
|
2546
|
+
end -= 1;
|
|
2535
2547
|
}
|
|
2548
|
+
return start === 0 && end === value.length ? value : value.slice(start, end);
|
|
2536
2549
|
}
|
|
2537
|
-
function
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2550
|
+
function isSafePathSegment(segment, options = {}) {
|
|
2551
|
+
return (segment !== "" &&
|
|
2552
|
+
segment !== "." &&
|
|
2553
|
+
segment !== ".." &&
|
|
2554
|
+
!segment.includes("/") &&
|
|
2555
|
+
!segment.includes("\\") &&
|
|
2556
|
+
!segment.includes("\0") &&
|
|
2557
|
+
(options.allowDotPrefix === true || !segment.startsWith(".")) &&
|
|
2558
|
+
(options.allowDotPrefix === true
|
|
2559
|
+
? SAFE_DOT_PREFIX_PATH_SEGMENT_PATTERN.test(segment)
|
|
2560
|
+
: SAFE_PATH_SEGMENT_PATTERN.test(segment)));
|
|
2543
2561
|
}
|
|
2544
|
-
function
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
readSidecarLockOwnershipToken(current.raw) === observed.ownershipToken &&
|
|
2550
|
-
readSidecarLockOwnershipToken(observed.raw) === observed.ownershipToken &&
|
|
2551
|
-
current.raw === observed.raw);
|
|
2552
|
-
}
|
|
2553
|
-
if (observed.stat && current.stat && !file_identity_sameFileIdentity(observed.stat, current.stat)) {
|
|
2554
|
-
return false;
|
|
2562
|
+
function assertSafePathSegment(segment, options = {}) {
|
|
2563
|
+
// Validate the exact value callers will later join into paths; trimming here
|
|
2564
|
+
// would let whitespace-padded ids pass and then be used verbatim.
|
|
2565
|
+
if (!isSafePathSegment(segment, options)) {
|
|
2566
|
+
throw new FsSafeError("invalid-path", `${options.label ?? "path segment"} must be a safe path segment`);
|
|
2555
2567
|
}
|
|
2556
|
-
|
|
2557
|
-
|
|
2568
|
+
return segment;
|
|
2569
|
+
}
|
|
2570
|
+
function sanitizeSafePathSegment(value, fallback, options = {}) {
|
|
2571
|
+
const sanitized = value
|
|
2572
|
+
.trim()
|
|
2573
|
+
.replace(/[\\/]+/g, "-")
|
|
2574
|
+
.replace(/\0/g, "")
|
|
2575
|
+
.replace(/[^A-Za-z0-9._-]+/g, "-");
|
|
2576
|
+
const trimmed = trimHyphenEdges(sanitized);
|
|
2577
|
+
if (isSafePathSegment(trimmed, options)) {
|
|
2578
|
+
return trimmed;
|
|
2558
2579
|
}
|
|
2559
|
-
return
|
|
2580
|
+
return assertSafePathSegment(fallback, { ...options, label: "fallback path segment" });
|
|
2560
2581
|
}
|
|
2561
|
-
|
|
2562
|
-
|
|
2582
|
+
function assertSafePathPrefix(prefix, options = {}) {
|
|
2583
|
+
// Prefixes are often derived from safe filenames. Normalize harmless
|
|
2584
|
+
// filename characters first, but still reject real path-control bytes.
|
|
2585
|
+
if (prefix.includes("/") || prefix.includes("\\") || prefix.includes("\0")) {
|
|
2586
|
+
return assertSafePathSegment(prefix, {
|
|
2587
|
+
allowDotPrefix: true,
|
|
2588
|
+
...options,
|
|
2589
|
+
label: options.label ?? "path prefix",
|
|
2590
|
+
});
|
|
2591
|
+
}
|
|
2592
|
+
return assertSafePathSegment(prefix.replace(/[^A-Za-z0-9._-]+/g, "-"), {
|
|
2593
|
+
allowDotPrefix: true,
|
|
2563
2594
|
...options,
|
|
2564
|
-
|
|
2595
|
+
label: options.label ?? "path prefix",
|
|
2565
2596
|
});
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2597
|
+
}
|
|
2598
|
+
|
|
2599
|
+
;// CONCATENATED MODULE: external "node:os"
|
|
2600
|
+
const external_node_os_namespaceObject = __rspack_createRequire_require("node:os");
|
|
2601
|
+
;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/short-path.js
|
|
2602
|
+
|
|
2603
|
+
|
|
2604
|
+
function shortPath(value) {
|
|
2605
|
+
const home = external_node_os_namespaceObject.homedir();
|
|
2606
|
+
const shortened = value.startsWith(home) ? `~${value.slice(home.length)}` : value;
|
|
2607
|
+
return formatErrorDetail(shortened);
|
|
2608
|
+
}
|
|
2609
|
+
|
|
2610
|
+
;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/root-path.js
|
|
2611
|
+
|
|
2612
|
+
|
|
2613
|
+
|
|
2614
|
+
|
|
2615
|
+
|
|
2616
|
+
|
|
2617
|
+
|
|
2618
|
+
|
|
2619
|
+
|
|
2620
|
+
|
|
2621
|
+
|
|
2622
|
+
const ROOT_PATH_ALIAS_POLICIES = {
|
|
2623
|
+
strict: Object.freeze({
|
|
2624
|
+
allowFinalSymlinkForUnlink: false,
|
|
2625
|
+
allowFinalHardlinkForUnlink: false,
|
|
2626
|
+
}),
|
|
2627
|
+
unlinkTarget: Object.freeze({
|
|
2628
|
+
allowFinalSymlinkForUnlink: true,
|
|
2629
|
+
allowFinalHardlinkForUnlink: true,
|
|
2630
|
+
}),
|
|
2631
|
+
};
|
|
2632
|
+
async function resolveRootPath(params) {
|
|
2633
|
+
try {
|
|
2634
|
+
return await resolveRootPathInternal(params);
|
|
2571
2635
|
}
|
|
2572
|
-
|
|
2573
|
-
|
|
2636
|
+
catch (error) {
|
|
2637
|
+
throw sanitizeRootPathError(error);
|
|
2574
2638
|
}
|
|
2575
|
-
return true;
|
|
2576
2639
|
}
|
|
2577
|
-
async function
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2640
|
+
async function resolveRootPathInternal(params) {
|
|
2641
|
+
assertValidRootPathInputs(params);
|
|
2642
|
+
const rootPath = external_node_path_namespaceObject.resolve(params.rootPath);
|
|
2643
|
+
const absolutePath = external_node_path_namespaceObject.resolve(params.absolutePath);
|
|
2644
|
+
const rootCanonicalPath = params.rootCanonicalPath
|
|
2645
|
+
? external_node_path_namespaceObject.resolve(params.rootCanonicalPath)
|
|
2646
|
+
: await resolvePathViaExistingAncestor(rootPath);
|
|
2647
|
+
const context = createBoundaryResolutionContext({
|
|
2648
|
+
resolveParams: params,
|
|
2649
|
+
rootPath,
|
|
2650
|
+
absolutePath,
|
|
2651
|
+
rootCanonicalPath,
|
|
2652
|
+
outsideLexicalCanonicalPath: await resolveOutsideLexicalCanonicalPathAsync({
|
|
2653
|
+
rootPath,
|
|
2654
|
+
absolutePath,
|
|
2655
|
+
}),
|
|
2656
|
+
});
|
|
2657
|
+
const outsideResult = await resolveOutsideRootPathAsync({
|
|
2658
|
+
boundaryLabel: params.boundaryLabel,
|
|
2659
|
+
context,
|
|
2660
|
+
});
|
|
2661
|
+
if (outsideResult) {
|
|
2662
|
+
return outsideResult;
|
|
2663
|
+
}
|
|
2664
|
+
return resolveRootPathLexicalAsync({
|
|
2665
|
+
params,
|
|
2666
|
+
absolutePath: context.absolutePath,
|
|
2667
|
+
rootPath: context.rootPath,
|
|
2668
|
+
rootCanonicalPath: context.rootCanonicalPath,
|
|
2581
2669
|
});
|
|
2582
|
-
return !!current && !!observed && sidecarLockSnapshotMatches(current, observed);
|
|
2583
2670
|
}
|
|
2584
|
-
|
|
2671
|
+
function resolveRootPathSync(params) {
|
|
2585
2672
|
try {
|
|
2586
|
-
|
|
2587
|
-
return true;
|
|
2673
|
+
return resolveRootPathSyncInternal(params);
|
|
2588
2674
|
}
|
|
2589
|
-
catch (
|
|
2590
|
-
|
|
2591
|
-
return false;
|
|
2592
|
-
}
|
|
2593
|
-
throw err;
|
|
2675
|
+
catch (error) {
|
|
2676
|
+
throw sanitizeRootPathError(error);
|
|
2594
2677
|
}
|
|
2595
2678
|
}
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2679
|
+
function resolveRootPathSyncInternal(params) {
|
|
2680
|
+
assertValidRootPathInputs(params);
|
|
2681
|
+
const rootPath = path.resolve(params.rootPath);
|
|
2682
|
+
const absolutePath = path.resolve(params.absolutePath);
|
|
2683
|
+
const rootCanonicalPath = params.rootCanonicalPath
|
|
2684
|
+
? path.resolve(params.rootCanonicalPath)
|
|
2685
|
+
: resolvePathViaExistingAncestorSync(rootPath);
|
|
2686
|
+
const context = createBoundaryResolutionContext({
|
|
2687
|
+
resolveParams: params,
|
|
2688
|
+
rootPath,
|
|
2689
|
+
absolutePath,
|
|
2690
|
+
rootCanonicalPath,
|
|
2691
|
+
outsideLexicalCanonicalPath: resolveOutsideLexicalCanonicalPathSync({
|
|
2692
|
+
rootPath,
|
|
2693
|
+
absolutePath,
|
|
2694
|
+
}),
|
|
2695
|
+
});
|
|
2696
|
+
const outsideResult = resolveOutsideRootPathSync({
|
|
2697
|
+
boundaryLabel: params.boundaryLabel,
|
|
2698
|
+
context,
|
|
2699
|
+
});
|
|
2700
|
+
if (outsideResult) {
|
|
2701
|
+
return outsideResult;
|
|
2601
2702
|
}
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2703
|
+
return resolveRootPathLexicalSync({
|
|
2704
|
+
params,
|
|
2705
|
+
absolutePath: context.absolutePath,
|
|
2706
|
+
rootPath: context.rootPath,
|
|
2707
|
+
rootCanonicalPath: context.rootCanonicalPath,
|
|
2708
|
+
});
|
|
2709
|
+
}
|
|
2710
|
+
function sanitizeRootPathError(error) {
|
|
2711
|
+
if (error instanceof Error) {
|
|
2712
|
+
error.message = formatErrorDetail(error.message);
|
|
2607
2713
|
}
|
|
2714
|
+
return error;
|
|
2608
2715
|
}
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2716
|
+
function assertValidRootPathInputs(params) {
|
|
2717
|
+
path_assertNoNulPathInput(params.rootPath, "root path contains a NUL byte");
|
|
2718
|
+
path_assertNoNulPathInput(params.absolutePath, "absolute path contains a NUL byte");
|
|
2719
|
+
assertNoEmbeddedDriveRelativeSegment(params.rootPath, "root path");
|
|
2720
|
+
assertNoEmbeddedDriveRelativeSegment(params.absolutePath, "absolute path");
|
|
2721
|
+
if (params.rootCanonicalPath !== undefined) {
|
|
2722
|
+
path_assertNoNulPathInput(params.rootCanonicalPath, "canonical root path contains a NUL byte");
|
|
2723
|
+
assertNoEmbeddedDriveRelativeSegment(params.rootCanonicalPath, "canonical root path");
|
|
2724
|
+
}
|
|
2612
2725
|
}
|
|
2613
|
-
|
|
2614
|
-
if (
|
|
2615
|
-
return
|
|
2726
|
+
function assertNoEmbeddedDriveRelativeSegment(filePath, label) {
|
|
2727
|
+
if (process.platform !== "win32") {
|
|
2728
|
+
return;
|
|
2616
2729
|
}
|
|
2617
|
-
const
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2730
|
+
const root = external_node_path_namespaceObject.parse(filePath).root;
|
|
2731
|
+
assertNoDriveRelativePathSegments(filePath.slice(root.length).replaceAll("\\", "/"), label);
|
|
2732
|
+
}
|
|
2733
|
+
function isPromiseLike(value) {
|
|
2734
|
+
return Boolean(value &&
|
|
2735
|
+
(typeof value === "object" || typeof value === "function") &&
|
|
2736
|
+
"then" in value &&
|
|
2737
|
+
typeof value.then === "function");
|
|
2738
|
+
}
|
|
2739
|
+
function createLexicalTraversalState(params) {
|
|
2740
|
+
const rawAbsolutePath = params.params.absolutePath;
|
|
2741
|
+
const rawRelativePath = rawPathRelativeToRoot(params.rootPath, rawAbsolutePath);
|
|
2742
|
+
const relative = rawRelativePath ?? external_node_path_namespaceObject.relative(params.rootPath, params.absolutePath);
|
|
2743
|
+
return {
|
|
2744
|
+
segments: splitTraversalSegments(relative),
|
|
2745
|
+
allowFinalSymlink: params.params.policy?.allowFinalSymlinkForUnlink === true,
|
|
2746
|
+
canonicalCursor: params.rootCanonicalPath,
|
|
2747
|
+
lexicalCursor: params.rootPath,
|
|
2748
|
+
preserveFinalSymlink: false,
|
|
2749
|
+
};
|
|
2750
|
+
}
|
|
2751
|
+
function createLexicalTraversalContext(params) {
|
|
2752
|
+
return {
|
|
2753
|
+
state: createLexicalTraversalState(params),
|
|
2754
|
+
resolveParams: params.params,
|
|
2755
|
+
rootPath: params.rootPath,
|
|
2756
|
+
rootCanonicalPath: params.rootCanonicalPath,
|
|
2757
|
+
absolutePath: params.absolutePath,
|
|
2758
|
+
};
|
|
2759
|
+
}
|
|
2760
|
+
function splitTraversalSegments(value) {
|
|
2761
|
+
return value
|
|
2762
|
+
.split(process.platform === "win32" ? /[\\/]+/ : /\/+/)
|
|
2763
|
+
.filter((segment) => Boolean(segment) && segment !== ".");
|
|
2764
|
+
}
|
|
2765
|
+
function rawPathRelativeToRoot(rootPath, candidatePath) {
|
|
2766
|
+
if (!external_node_path_namespaceObject.isAbsolute(candidatePath)) {
|
|
2767
|
+
return undefined;
|
|
2631
2768
|
}
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
}
|
|
2639
|
-
return "removed";
|
|
2769
|
+
const root = external_node_path_namespaceObject.resolve(rootPath);
|
|
2770
|
+
const candidate = process.platform === "win32"
|
|
2771
|
+
? candidatePath.replaceAll("/", external_node_path_namespaceObject.sep)
|
|
2772
|
+
: candidatePath;
|
|
2773
|
+
if (candidate === root) {
|
|
2774
|
+
return "";
|
|
2640
2775
|
}
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2776
|
+
const rootWithSep = root.endsWith(external_node_path_namespaceObject.sep) ? root : `${root}${external_node_path_namespaceObject.sep}`;
|
|
2777
|
+
const candidatePrefix = candidate.slice(0, rootWithSep.length);
|
|
2778
|
+
const prefixMatches = process.platform === "win32"
|
|
2779
|
+
? candidatePrefix.toLowerCase() === rootWithSep.toLowerCase()
|
|
2780
|
+
: candidatePrefix === rootWithSep;
|
|
2781
|
+
return prefixMatches ? candidate.slice(rootWithSep.length) : undefined;
|
|
2782
|
+
}
|
|
2783
|
+
function assertLexicalCursorInsideBoundary(context, candidatePath) {
|
|
2784
|
+
assertInsideBoundary({
|
|
2785
|
+
boundaryLabel: context.resolveParams.boundaryLabel,
|
|
2786
|
+
rootCanonicalPath: context.rootCanonicalPath,
|
|
2787
|
+
candidatePath,
|
|
2788
|
+
absolutePath: context.absolutePath,
|
|
2789
|
+
});
|
|
2790
|
+
}
|
|
2791
|
+
function applyMissingSuffixToCanonicalCursor(context, missingFromIndex) {
|
|
2792
|
+
const missingSuffix = context.state.segments.slice(missingFromIndex);
|
|
2793
|
+
for (const segment of missingSuffix) {
|
|
2794
|
+
advanceCanonicalCursorForSegment(context, segment);
|
|
2646
2795
|
}
|
|
2647
2796
|
}
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2797
|
+
function advanceCanonicalCursorForSegment(context, segment) {
|
|
2798
|
+
context.state.canonicalCursor = external_node_path_namespaceObject.resolve(context.state.canonicalCursor, segment);
|
|
2799
|
+
assertLexicalCursorInsideBoundary(context, context.state.canonicalCursor);
|
|
2800
|
+
}
|
|
2801
|
+
function finalizeLexicalResolution(context, kind) {
|
|
2802
|
+
assertLexicalCursorInsideBoundary(context, context.state.canonicalCursor);
|
|
2803
|
+
return buildResolvedRootPath({
|
|
2804
|
+
absolutePath: context.absolutePath,
|
|
2805
|
+
canonicalPath: context.state.canonicalCursor,
|
|
2806
|
+
rootPath: context.rootPath,
|
|
2807
|
+
rootCanonicalPath: context.rootCanonicalPath,
|
|
2808
|
+
kind,
|
|
2809
|
+
});
|
|
2810
|
+
}
|
|
2811
|
+
function handleLexicalLstatFailure(context, error, missingFromIndex) {
|
|
2812
|
+
if (!path_isNotFoundPathError(error)) {
|
|
2813
|
+
return false;
|
|
2654
2814
|
}
|
|
2815
|
+
applyMissingSuffixToCanonicalCursor(context, missingFromIndex);
|
|
2816
|
+
return true;
|
|
2655
2817
|
}
|
|
2656
|
-
function
|
|
2657
|
-
if (
|
|
2658
|
-
|
|
2818
|
+
function handleLexicalStatReadFailure(context, error, missingFromIndex) {
|
|
2819
|
+
if (handleLexicalLstatFailure(context, error, missingFromIndex)) {
|
|
2820
|
+
return null;
|
|
2659
2821
|
}
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2822
|
+
throw error;
|
|
2823
|
+
}
|
|
2824
|
+
function handleLexicalStatDisposition(context, params) {
|
|
2825
|
+
if (!params.isSymbolicLink) {
|
|
2826
|
+
advanceCanonicalCursorForSegment(context, params.segment);
|
|
2827
|
+
return "continue";
|
|
2664
2828
|
}
|
|
2665
|
-
if (
|
|
2666
|
-
|
|
2829
|
+
if (context.resolveParams.rejectSymlinks === true && params.isLast) {
|
|
2830
|
+
throw new errors_FsSafeError("symlink", "symlink path component not allowed");
|
|
2667
2831
|
}
|
|
2668
|
-
if (
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2832
|
+
if (context.state.allowFinalSymlink && params.isLast) {
|
|
2833
|
+
context.state.preserveFinalSymlink = true;
|
|
2834
|
+
advanceCanonicalCursorForSegment(context, params.segment);
|
|
2835
|
+
return "break";
|
|
2672
2836
|
}
|
|
2837
|
+
return "resolve-link";
|
|
2673
2838
|
}
|
|
2674
|
-
function
|
|
2675
|
-
if (
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
const base = Math.min(maxTimeout, Math.max(minTimeout, minTimeout * factor ** attempt));
|
|
2685
|
-
const jitter = retry.randomize ? 1 + Math.random() : 1;
|
|
2686
|
-
return Math.min(maxTimeout, Math.round(base * jitter));
|
|
2687
|
-
}
|
|
2688
|
-
// Windows denies access to a lock file while a just-unlinked directory entry
|
|
2689
|
-
// is still being torn down, so a contended acquire sees EPERM on a name that is
|
|
2690
|
-
// already gone -- both when creating it exclusively and when reading the
|
|
2691
|
-
// holder's snapshot. The next attempt succeeds, so this is contention rather
|
|
2692
|
-
// than a permission failure. The error must name the lock file itself: the
|
|
2693
|
-
// exclusive-create helper opens the parent directory first, and a denial from
|
|
2694
|
-
// that setup step carries no teardown evidence and has to reach the caller.
|
|
2695
|
-
const maxTransientLockDenials = 8;
|
|
2696
|
-
function isTransientLockFileDenial(error, lockPath) {
|
|
2697
|
-
const denial = error;
|
|
2698
|
-
return process.platform === "win32" && denial?.code === "EPERM" && denial.path === lockPath;
|
|
2699
|
-
}
|
|
2700
|
-
function sidecarLockPayloadIsStale(payload, staleMs, nowMs) {
|
|
2701
|
-
const createdAtMs = sidecarLockPayloadCreatedAtMs(payload);
|
|
2702
|
-
return createdAtMs !== null && nowMs - createdAtMs > staleMs;
|
|
2703
|
-
}
|
|
2704
|
-
function sidecarLockPayloadCreatedAtMs(payload) {
|
|
2705
|
-
const createdAt = payload &&
|
|
2706
|
-
typeof payload === "object" &&
|
|
2707
|
-
"createdAt" in payload &&
|
|
2708
|
-
typeof payload.createdAt === "string"
|
|
2709
|
-
? payload.createdAt
|
|
2710
|
-
: "";
|
|
2711
|
-
const createdAtMs = Date.parse(createdAt);
|
|
2712
|
-
return Number.isFinite(createdAtMs) ? createdAtMs : null;
|
|
2839
|
+
function applyResolvedSymlinkHop(context, linkCanonical) {
|
|
2840
|
+
if (!path_isPathInside(context.rootCanonicalPath, linkCanonical)) {
|
|
2841
|
+
throw symlinkEscapeError({
|
|
2842
|
+
boundaryLabel: context.resolveParams.boundaryLabel,
|
|
2843
|
+
rootCanonicalPath: context.rootCanonicalPath,
|
|
2844
|
+
symlinkPath: context.state.lexicalCursor,
|
|
2845
|
+
});
|
|
2846
|
+
}
|
|
2847
|
+
context.state.canonicalCursor = linkCanonical;
|
|
2848
|
+
context.state.lexicalCursor = linkCanonical;
|
|
2713
2849
|
}
|
|
2714
|
-
|
|
2715
|
-
const createdAtMs = sidecarLockPayloadCreatedAtMs(params.payload);
|
|
2716
|
-
if (createdAtMs !== null)
|
|
2717
|
-
return params.nowMs - createdAtMs > params.staleMs;
|
|
2850
|
+
function readLexicalStat(context, params) {
|
|
2718
2851
|
try {
|
|
2719
|
-
|
|
2852
|
+
const stat = params.read(context.state.lexicalCursor);
|
|
2853
|
+
if (isPromiseLike(stat)) {
|
|
2854
|
+
return Promise.resolve(stat).catch((error) => handleLexicalStatReadFailure(context, error, params.missingFromIndex));
|
|
2855
|
+
}
|
|
2856
|
+
return stat;
|
|
2720
2857
|
}
|
|
2721
|
-
catch {
|
|
2722
|
-
return
|
|
2858
|
+
catch (error) {
|
|
2859
|
+
return handleLexicalStatReadFailure(context, error, params.missingFromIndex);
|
|
2723
2860
|
}
|
|
2724
2861
|
}
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2862
|
+
function resolveAndApplySymlinkHop(context, params) {
|
|
2863
|
+
const linkCanonical = params.resolveLinkCanonical(context.state.lexicalCursor);
|
|
2864
|
+
if (isPromiseLike(linkCanonical)) {
|
|
2865
|
+
return Promise.resolve(linkCanonical).then((value) => {
|
|
2866
|
+
applyResolvedSymlinkHop(context, value);
|
|
2867
|
+
});
|
|
2731
2868
|
}
|
|
2732
|
-
|
|
2733
|
-
cause: error instanceof Error ? error : undefined,
|
|
2734
|
-
});
|
|
2735
|
-
combined.name = "SuppressedError";
|
|
2736
|
-
Object.defineProperties(combined, {
|
|
2737
|
-
error: { configurable: true, value: error },
|
|
2738
|
-
suppressed: { configurable: true, value: suppressed },
|
|
2739
|
-
});
|
|
2740
|
-
return combined;
|
|
2869
|
+
applyResolvedSymlinkHop(context, linkCanonical);
|
|
2741
2870
|
}
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
async function resolveNormalizedTargetPath(targetPath) {
|
|
2752
|
-
const resolved = external_node_path_namespaceObject.resolve(targetPath);
|
|
2753
|
-
const dir = external_node_path_namespaceObject.dirname(resolved);
|
|
2754
|
-
await promises_namespaceObject.mkdir(dir, { recursive: true });
|
|
2755
|
-
try {
|
|
2756
|
-
return external_node_path_namespaceObject.join(await promises_namespaceObject.realpath(dir), external_node_path_namespaceObject.basename(resolved));
|
|
2757
|
-
}
|
|
2758
|
-
catch {
|
|
2759
|
-
return resolved;
|
|
2871
|
+
function applyParentTraversalStep(context) {
|
|
2872
|
+
context.state.lexicalCursor = external_node_path_namespaceObject.resolve(context.state.lexicalCursor, "..");
|
|
2873
|
+
advanceCanonicalCursorForSegment(context, "..");
|
|
2874
|
+
}
|
|
2875
|
+
function* iterateLexicalTraversal(state) {
|
|
2876
|
+
for (let idx = 0; idx < state.segments.length; idx += 1) {
|
|
2877
|
+
const segment = state.segments[idx] ?? "";
|
|
2878
|
+
const isLast = idx === state.segments.length - 1;
|
|
2879
|
+
yield { idx, segment, isLast };
|
|
2760
2880
|
}
|
|
2761
2881
|
}
|
|
2762
|
-
async function
|
|
2763
|
-
const
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
let held = context.held.get(normalizedTargetPath);
|
|
2770
|
-
if (held &&
|
|
2771
|
-
options.reentrantOwner !== undefined &&
|
|
2772
|
-
held.reentrantOwner !== undefined &&
|
|
2773
|
-
options.reentrantOwner === held.reentrantOwner) {
|
|
2774
|
-
// A final release may already have decremented the count to zero and be
|
|
2775
|
-
// removing the sidecar. Do not admit a new reentrant handle until that
|
|
2776
|
-
// cleanup settles: successful cleanup requires a fresh acquisition, while
|
|
2777
|
-
// failed cleanup leaves the existing sidecar held for a retry.
|
|
2778
|
-
if (held.releasePromise) {
|
|
2779
|
-
await held.releasePromise.catch(() => undefined);
|
|
2780
|
-
held = context.held.get(normalizedTargetPath);
|
|
2781
|
-
}
|
|
2782
|
-
if (held &&
|
|
2783
|
-
options.reentrantOwner !== undefined &&
|
|
2784
|
-
held.reentrantOwner !== undefined &&
|
|
2785
|
-
options.reentrantOwner === held.reentrantOwner) {
|
|
2786
|
-
held.refCount += 1;
|
|
2787
|
-
return context.handleForHeldLock(normalizedTargetPath, held);
|
|
2788
|
-
}
|
|
2789
|
-
}
|
|
2790
|
-
const startedAt = Date.now();
|
|
2791
|
-
const maxRetries = options.timeoutMs === Number.POSITIVE_INFINITY ? undefined : retry.retries;
|
|
2792
|
-
const reclaimGuardPath = `${lockPath}.reclaim`;
|
|
2793
|
-
let ownsReclaimGuard = false;
|
|
2794
|
-
let attempt = 0;
|
|
2795
|
-
// Bounded so a genuine denial still surfaces as EPERM, not a lock timeout.
|
|
2796
|
-
let transientDenials = 0;
|
|
2797
|
-
const withinDenialBudget = () => ++transientDenials <= (/* inlined export .maxTransientLockDenials */8);
|
|
2798
|
-
const waitForRetry = async () => {
|
|
2799
|
-
const elapsed = Date.now() - startedAt;
|
|
2800
|
-
if ((options.timeoutMs !== undefined &&
|
|
2801
|
-
options.timeoutMs !== Number.POSITIVE_INFINITY &&
|
|
2802
|
-
elapsed >= options.timeoutMs) ||
|
|
2803
|
-
(maxRetries !== undefined && attempt >= maxRetries)) {
|
|
2804
|
-
throw Object.assign(new Error(`file lock timeout for ${normalizedTargetPath}`), {
|
|
2805
|
-
code: "file_lock_timeout",
|
|
2806
|
-
lockPath,
|
|
2807
|
-
normalizedTargetPath,
|
|
2808
|
-
});
|
|
2809
|
-
}
|
|
2810
|
-
const remaining = options.timeoutMs === undefined || options.timeoutMs === Number.POSITIVE_INFINITY
|
|
2811
|
-
? Number.POSITIVE_INFINITY
|
|
2812
|
-
: Math.max(0, options.timeoutMs - elapsed);
|
|
2813
|
-
const delay = Math.min(computeSidecarLockDelayMs(retry, attempt), remaining);
|
|
2814
|
-
attempt += 1;
|
|
2815
|
-
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
2816
|
-
};
|
|
2817
|
-
// Waiting can fail on the caller's own retry or deadline limits. Classifying
|
|
2818
|
-
// a denial as contention must not cost them the original diagnosis, so hand
|
|
2819
|
-
// the denial back when no further attempt can be scheduled.
|
|
2820
|
-
const retryOrRethrowDenial = async (denial) => {
|
|
2821
|
-
try {
|
|
2822
|
-
await waitForRetry();
|
|
2823
|
-
}
|
|
2824
|
-
catch (waitError) {
|
|
2825
|
-
if (waitError.code === "file_lock_timeout")
|
|
2826
|
-
throw denial;
|
|
2827
|
-
throw waitError;
|
|
2828
|
-
}
|
|
2829
|
-
};
|
|
2830
|
-
try {
|
|
2831
|
-
while (true) {
|
|
2832
|
-
if (!ownsReclaimGuard && (await sidecarReclaimGuardExists(reclaimGuardPath))) {
|
|
2833
|
-
await waitForRetry();
|
|
2834
|
-
continue;
|
|
2835
|
-
}
|
|
2836
|
-
let handle = null;
|
|
2837
|
-
let createdSnapshot = null;
|
|
2838
|
-
let lockFileCreateDenied = false;
|
|
2839
|
-
try {
|
|
2840
|
-
const payload = await options.payload();
|
|
2841
|
-
const { raw, ownershipToken } = serializeSidecarLockPayload(payload);
|
|
2842
|
-
if (options.lockRoot) {
|
|
2843
|
-
const relativeLockPath = relativeSidecarLockPath(options.lockRoot, lockPath);
|
|
2844
|
-
try {
|
|
2845
|
-
await options.lockRoot.create(relativeLockPath, raw, { mkdir: true, mode: 0o600 });
|
|
2846
|
-
}
|
|
2847
|
-
catch (error) {
|
|
2848
|
-
if (error instanceof errors_FsSafeError && error.code === "already-exists") {
|
|
2849
|
-
throw Object.assign(new Error("sidecar lock exists"), { code: "EEXIST" });
|
|
2850
|
-
}
|
|
2851
|
-
throw error;
|
|
2852
|
-
}
|
|
2853
|
-
createdSnapshot = { raw, payload, ownershipToken };
|
|
2854
|
-
handle = (await options.lockRoot.open(relativeLockPath)).handle;
|
|
2855
|
-
}
|
|
2856
|
-
else {
|
|
2857
|
-
try {
|
|
2858
|
-
handle =
|
|
2859
|
-
(await createNativeExclusiveFile(lockPath, 0o600)) ??
|
|
2860
|
-
(await promises_namespaceObject.open(lockPath, "wx"));
|
|
2861
|
-
}
|
|
2862
|
-
catch (createError) {
|
|
2863
|
-
lockFileCreateDenied = isTransientLockFileDenial(createError, lockPath);
|
|
2864
|
-
throw createError;
|
|
2865
|
-
}
|
|
2866
|
-
await handle.writeFile(raw, "utf8");
|
|
2867
|
-
}
|
|
2868
|
-
const snapshot = { raw, payload, stat: await handle.stat(), ownershipToken };
|
|
2869
|
-
const createdHeld = {
|
|
2870
|
-
refCount: 1,
|
|
2871
|
-
reentrantOwner: options.reentrantOwner,
|
|
2872
|
-
handle,
|
|
2873
|
-
lockPath,
|
|
2874
|
-
snapshot,
|
|
2875
|
-
acquiredAt: Date.now(),
|
|
2876
|
-
metadata: options.metadata ?? {},
|
|
2877
|
-
lockRoot: options.lockRoot,
|
|
2878
|
-
parsePayload: options.parsePayload,
|
|
2879
|
-
};
|
|
2880
|
-
context.held.set(normalizedTargetPath, createdHeld);
|
|
2881
|
-
if (ownsReclaimGuard) {
|
|
2882
|
-
try {
|
|
2883
|
-
await releaseSidecarReclaimGuard(context.reclaimGuards, reclaimGuardPath);
|
|
2884
|
-
ownsReclaimGuard = false;
|
|
2885
|
-
}
|
|
2886
|
-
catch (err) {
|
|
2887
|
-
await context.releaseHeldLock(normalizedTargetPath, createdHeld, { force: true });
|
|
2888
|
-
throw err;
|
|
2889
|
-
}
|
|
2890
|
-
}
|
|
2891
|
-
const returnedHandle = context.handleForHeldLock(normalizedTargetPath, createdHeld);
|
|
2892
|
-
const interval = options.compromiseCheckIntervalMs;
|
|
2893
|
-
if (options.onCompromised && interval !== undefined && interval > 0) {
|
|
2894
|
-
createdHeld.compromiseTimer = setInterval(() => {
|
|
2895
|
-
void returnedHandle
|
|
2896
|
-
.verifyStillHeld()
|
|
2897
|
-
.catch(() => false)
|
|
2898
|
-
.then((stillHeld) => {
|
|
2899
|
-
if (!stillHeld && createdHeld.compromiseTimer) {
|
|
2900
|
-
clearInterval(createdHeld.compromiseTimer);
|
|
2901
|
-
createdHeld.compromiseTimer = undefined;
|
|
2902
|
-
options.onCompromised?.({ lockPath, normalizedTargetPath });
|
|
2903
|
-
}
|
|
2904
|
-
});
|
|
2905
|
-
}, interval);
|
|
2906
|
-
createdHeld.compromiseTimer.unref();
|
|
2907
|
-
}
|
|
2908
|
-
return returnedHandle;
|
|
2909
|
-
}
|
|
2910
|
-
catch (err) {
|
|
2911
|
-
try {
|
|
2912
|
-
if (handle) {
|
|
2913
|
-
const failedSnapshot = { payload: null };
|
|
2914
|
-
try {
|
|
2915
|
-
failedSnapshot.stat = await handle.stat();
|
|
2916
|
-
}
|
|
2917
|
-
catch {
|
|
2918
|
-
// Best-effort cleanup of a failed exclusive create.
|
|
2919
|
-
}
|
|
2920
|
-
const current = context.held.get(normalizedTargetPath);
|
|
2921
|
-
if (current?.handle === handle) {
|
|
2922
|
-
context.held.delete(normalizedTargetPath);
|
|
2923
|
-
}
|
|
2924
|
-
await handle.close().catch(() => undefined);
|
|
2925
|
-
// The file may be empty or partial JSON, so remove by the identity
|
|
2926
|
-
// captured from our exclusive handle rather than by pathname alone.
|
|
2927
|
-
await removeSidecarLockIfUnchanged(lockPath, failedSnapshot, {
|
|
2928
|
-
lockRoot: options.lockRoot,
|
|
2929
|
-
parsePayload: options.parsePayload,
|
|
2930
|
-
});
|
|
2931
|
-
}
|
|
2932
|
-
else if (createdSnapshot) {
|
|
2933
|
-
await removeSidecarLockIfUnchanged(lockPath, createdSnapshot, {
|
|
2934
|
-
lockRoot: options.lockRoot,
|
|
2935
|
-
parsePayload: options.parsePayload,
|
|
2936
|
-
});
|
|
2937
|
-
}
|
|
2938
|
-
}
|
|
2939
|
-
catch (cleanupError) {
|
|
2940
|
-
throw createSuppressedError(cleanupError, err, "file lock acquisition and cleanup both failed");
|
|
2941
|
-
}
|
|
2942
|
-
if (lockFileCreateDenied && withinDenialBudget()) {
|
|
2943
|
-
await retryOrRethrowDenial(err);
|
|
2944
|
-
continue;
|
|
2945
|
-
}
|
|
2946
|
-
if (err.code !== "EEXIST") {
|
|
2947
|
-
throw err;
|
|
2948
|
-
}
|
|
2949
|
-
if (ownsReclaimGuard) {
|
|
2950
|
-
await releaseSidecarReclaimGuard(context.reclaimGuards, reclaimGuardPath);
|
|
2951
|
-
ownsReclaimGuard = false;
|
|
2952
|
-
continue;
|
|
2953
|
-
}
|
|
2954
|
-
const nowMs = Date.now();
|
|
2955
|
-
let snapshot;
|
|
2956
|
-
try {
|
|
2957
|
-
snapshot = await readSidecarLockSnapshot(lockPath, {
|
|
2958
|
-
lockRoot: options.lockRoot,
|
|
2959
|
-
parsePayload: options.parsePayload,
|
|
2960
|
-
rejectNonFile: true,
|
|
2961
|
-
});
|
|
2962
|
-
}
|
|
2963
|
-
catch (readErr) {
|
|
2964
|
-
if (!isTransientLockFileDenial(readErr, lockPath) || !withinDenialBudget())
|
|
2965
|
-
throw readErr;
|
|
2966
|
-
await retryOrRethrowDenial(readErr);
|
|
2967
|
-
continue;
|
|
2968
|
-
}
|
|
2969
|
-
if (!snapshot) {
|
|
2970
|
-
continue;
|
|
2971
|
-
}
|
|
2972
|
-
if (context.held.has(normalizedTargetPath)) {
|
|
2973
|
-
await waitForRetry();
|
|
2974
|
-
continue;
|
|
2975
|
-
}
|
|
2976
|
-
const shouldReclaim = options.shouldReclaim ?? defaultSidecarLockShouldReclaim;
|
|
2977
|
-
if (await shouldReclaim({
|
|
2978
|
-
lockPath,
|
|
2979
|
-
normalizedTargetPath,
|
|
2980
|
-
payload: snapshot?.payload ?? null,
|
|
2981
|
-
staleMs: options.staleMs,
|
|
2982
|
-
nowMs,
|
|
2983
|
-
heldByThisProcess: context.held.has(normalizedTargetPath),
|
|
2984
|
-
})) {
|
|
2985
|
-
if (!(await sidecarLockSnapshotStillPresent(lockPath, snapshot, {
|
|
2986
|
-
lockRoot: options.lockRoot,
|
|
2987
|
-
parsePayload: options.parsePayload,
|
|
2988
|
-
}))) {
|
|
2989
|
-
continue;
|
|
2990
|
-
}
|
|
2991
|
-
const staleRecovery = options.staleRecovery ?? "fail-closed";
|
|
2992
|
-
if (staleRecovery === "remove-if-unchanged") {
|
|
2993
|
-
if (!(await tryAcquireSidecarReclaimGuard(context.reclaimGuards, reclaimGuardPath))) {
|
|
2994
|
-
await waitForRetry();
|
|
2995
|
-
continue;
|
|
2996
|
-
}
|
|
2997
|
-
ownsReclaimGuard = true;
|
|
2998
|
-
const removal = await removeStaleSidecarLockIfAllowed({
|
|
2999
|
-
lockPath,
|
|
3000
|
-
normalizedTargetPath,
|
|
3001
|
-
snapshot,
|
|
3002
|
-
shouldRemoveStaleLock: options.shouldRemoveStaleLock,
|
|
3003
|
-
lockRoot: options.lockRoot,
|
|
3004
|
-
parsePayload: options.parsePayload,
|
|
3005
|
-
});
|
|
3006
|
-
if (removal === "removed" || removal === "changed") {
|
|
3007
|
-
continue;
|
|
3008
|
-
}
|
|
3009
|
-
await releaseSidecarReclaimGuard(context.reclaimGuards, reclaimGuardPath);
|
|
3010
|
-
ownsReclaimGuard = false;
|
|
3011
|
-
}
|
|
3012
|
-
throw Object.assign(new Error(`file lock stale for ${normalizedTargetPath}`), {
|
|
3013
|
-
code: "file_lock_stale",
|
|
3014
|
-
lockPath,
|
|
3015
|
-
normalizedTargetPath,
|
|
3016
|
-
});
|
|
3017
|
-
}
|
|
3018
|
-
await waitForRetry();
|
|
3019
|
-
}
|
|
3020
|
-
}
|
|
3021
|
-
}
|
|
3022
|
-
finally {
|
|
3023
|
-
if (ownsReclaimGuard) {
|
|
3024
|
-
await releaseSidecarReclaimGuard(context.reclaimGuards, reclaimGuardPath).catch(() => undefined);
|
|
3025
|
-
}
|
|
3026
|
-
}
|
|
3027
|
-
}
|
|
3028
|
-
|
|
3029
|
-
;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/sidecar-lock-handle.js
|
|
3030
|
-
|
|
3031
|
-
function createSidecarLockHandle(params) {
|
|
3032
|
-
let released = false;
|
|
3033
|
-
let releasePromise;
|
|
3034
|
-
let releaseStarted = false;
|
|
3035
|
-
const release = async () => {
|
|
3036
|
-
if (released)
|
|
3037
|
-
return;
|
|
3038
|
-
if (!releasePromise) {
|
|
3039
|
-
const retry = releaseStarted;
|
|
3040
|
-
releaseStarted = true;
|
|
3041
|
-
releasePromise = (async () => {
|
|
3042
|
-
await params.release({ retry });
|
|
3043
|
-
released = true;
|
|
3044
|
-
})();
|
|
3045
|
-
}
|
|
3046
|
-
try {
|
|
3047
|
-
await releasePromise;
|
|
3048
|
-
}
|
|
3049
|
-
finally {
|
|
3050
|
-
releasePromise = undefined;
|
|
3051
|
-
}
|
|
3052
|
-
};
|
|
3053
|
-
return {
|
|
3054
|
-
lockPath: params.lockPath,
|
|
3055
|
-
normalizedTargetPath: params.normalizedTargetPath,
|
|
3056
|
-
verifyStillHeld: params.verifyStillHeld,
|
|
3057
|
-
release,
|
|
3058
|
-
[Symbol.asyncDispose]: release,
|
|
3059
|
-
};
|
|
3060
|
-
}
|
|
3061
|
-
function createHeldSidecarLockHandle(params) {
|
|
3062
|
-
return createSidecarLockHandle({
|
|
3063
|
-
lockPath: params.held.lockPath,
|
|
3064
|
-
normalizedTargetPath: params.normalizedTargetPath,
|
|
3065
|
-
verifyStillHeld: async () => await sidecarLockSnapshotStillPresent(params.held.lockPath, params.held.snapshot, {
|
|
3066
|
-
lockRoot: params.held.lockRoot,
|
|
3067
|
-
parsePayload: params.held.parsePayload,
|
|
3068
|
-
}),
|
|
3069
|
-
release: (options) => params.release(options),
|
|
3070
|
-
});
|
|
3071
|
-
}
|
|
3072
|
-
|
|
3073
|
-
;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/sidecar-lock.js
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
const GLOBAL_STATE_KEY = Symbol.for("fsSafe.sidecarLockManagers");
|
|
3081
|
-
const GLOBAL_CLEANUP_KEY = Symbol.for("fsSafe.sidecarLockCleanupRegistered");
|
|
3082
|
-
const GLOBAL_CLEANUP_HANDLER_KEY = Symbol.for("fsSafe.sidecarLockCleanupHandler");
|
|
3083
|
-
function getGlobalManagers() {
|
|
3084
|
-
const globalWithState = globalThis;
|
|
3085
|
-
if (!globalWithState[GLOBAL_STATE_KEY]) {
|
|
3086
|
-
globalWithState[GLOBAL_STATE_KEY] = new Map();
|
|
3087
|
-
}
|
|
3088
|
-
return globalWithState[GLOBAL_STATE_KEY];
|
|
3089
|
-
}
|
|
3090
|
-
function resolveManagerState(key) {
|
|
3091
|
-
const managers = getGlobalManagers();
|
|
3092
|
-
let state = managers.get(key);
|
|
3093
|
-
if (!state) {
|
|
3094
|
-
state = {
|
|
3095
|
-
cleanupRegistered: false,
|
|
3096
|
-
held: new Map(),
|
|
3097
|
-
reclaimCleanupRegistered: false,
|
|
3098
|
-
reclaimGuards: new Set(),
|
|
3099
|
-
};
|
|
3100
|
-
managers.set(key, state);
|
|
3101
|
-
}
|
|
3102
|
-
else {
|
|
3103
|
-
// The global manager symbol is shared across package copies and hot reloads.
|
|
3104
|
-
// Backfill state created by fs-safe versions that predate reclaim guards.
|
|
3105
|
-
state.reclaimCleanupRegistered ??= false;
|
|
3106
|
-
state.reclaimGuards ??= new Set();
|
|
3107
|
-
for (const held of state.held.values()) {
|
|
3108
|
-
held.refCount ??= 1;
|
|
3109
|
-
}
|
|
3110
|
-
}
|
|
3111
|
-
return state;
|
|
3112
|
-
}
|
|
3113
|
-
function snapshotMatchesSync(lockPath, observed) {
|
|
3114
|
-
let fd;
|
|
3115
|
-
try {
|
|
3116
|
-
const beforeStat = external_node_fs_namespaceObject.lstatSync(lockPath);
|
|
3117
|
-
if (!beforeStat.isFile()) {
|
|
3118
|
-
return false;
|
|
2882
|
+
async function resolveRootPathLexicalAsync(params) {
|
|
2883
|
+
const context = createLexicalTraversalContext(params);
|
|
2884
|
+
const { state } = context;
|
|
2885
|
+
for (const { idx, segment, isLast } of iterateLexicalTraversal(state)) {
|
|
2886
|
+
if (segment === "..") {
|
|
2887
|
+
applyParentTraversalStep(context);
|
|
2888
|
+
continue;
|
|
3119
2889
|
}
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
if (!openedStat.isFile()) {
|
|
3128
|
-
return false;
|
|
2890
|
+
state.lexicalCursor = external_node_path_namespaceObject.join(state.lexicalCursor, segment);
|
|
2891
|
+
const stat = await readLexicalStat(context, {
|
|
2892
|
+
missingFromIndex: idx,
|
|
2893
|
+
read: (cursor) => promises_namespaceObject.lstat(cursor),
|
|
2894
|
+
});
|
|
2895
|
+
if (!stat) {
|
|
2896
|
+
break;
|
|
3129
2897
|
}
|
|
3130
|
-
|
|
3131
|
-
|
|
2898
|
+
const disposition = handleLexicalStatDisposition(context, {
|
|
2899
|
+
isSymbolicLink: stat.isSymbolicLink(),
|
|
2900
|
+
segment,
|
|
2901
|
+
isLast,
|
|
2902
|
+
});
|
|
2903
|
+
if (disposition === "continue") {
|
|
2904
|
+
continue;
|
|
3132
2905
|
}
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
if (!afterStat.isFile() || !file_identity_sameFileIdentity(beforeStat, afterStat)) {
|
|
3136
|
-
return false;
|
|
2906
|
+
if (disposition === "break") {
|
|
2907
|
+
break;
|
|
3137
2908
|
}
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
finally {
|
|
3144
|
-
if (fd !== undefined) {
|
|
3145
|
-
try {
|
|
3146
|
-
external_node_fs_namespaceObject.closeSync(fd);
|
|
3147
|
-
}
|
|
3148
|
-
catch {
|
|
3149
|
-
// Best-effort process-exit cleanup.
|
|
3150
|
-
}
|
|
2909
|
+
await resolveAndApplySymlinkHop(context, {
|
|
2910
|
+
resolveLinkCanonical: (cursor) => resolveSymlinkHopPath(cursor),
|
|
2911
|
+
});
|
|
2912
|
+
if (context.resolveParams.rejectSymlinks === true) {
|
|
2913
|
+
throw new errors_FsSafeError("symlink", "symlink path component not allowed");
|
|
3151
2914
|
}
|
|
3152
2915
|
}
|
|
2916
|
+
const kind = await getPathKind(state.canonicalCursor, state.preserveFinalSymlink);
|
|
2917
|
+
return finalizeLexicalResolution(context, kind);
|
|
3153
2918
|
}
|
|
3154
|
-
function
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
2919
|
+
function resolveRootPathLexicalSync(params) {
|
|
2920
|
+
const context = createLexicalTraversalContext(params);
|
|
2921
|
+
const { state } = context;
|
|
2922
|
+
for (let idx = 0; idx < state.segments.length; idx += 1) {
|
|
2923
|
+
const segment = state.segments[idx] ?? "";
|
|
2924
|
+
const isLast = idx === state.segments.length - 1;
|
|
2925
|
+
if (segment === "..") {
|
|
2926
|
+
applyParentTraversalStep(context);
|
|
2927
|
+
continue;
|
|
3159
2928
|
}
|
|
3160
|
-
|
|
3161
|
-
|
|
2929
|
+
state.lexicalCursor = path.join(state.lexicalCursor, segment);
|
|
2930
|
+
const maybeStat = readLexicalStat(context, {
|
|
2931
|
+
missingFromIndex: idx,
|
|
2932
|
+
read: (cursor) => fs.lstatSync(cursor),
|
|
2933
|
+
});
|
|
2934
|
+
if (isPromiseLike(maybeStat)) {
|
|
2935
|
+
throw new Error("Unexpected async lexical stat");
|
|
3162
2936
|
}
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
for (const [normalizedTargetPath, held] of state.held) {
|
|
3167
|
-
void held.handle.close().catch(() => undefined);
|
|
3168
|
-
try {
|
|
3169
|
-
if (!held.lockRoot && snapshotMatchesSync(held.lockPath, held.snapshot)) {
|
|
3170
|
-
external_node_fs_namespaceObject.rmSync(held.lockPath, { force: true });
|
|
3171
|
-
}
|
|
2937
|
+
const stat = maybeStat;
|
|
2938
|
+
if (!stat) {
|
|
2939
|
+
break;
|
|
3172
2940
|
}
|
|
3173
|
-
|
|
3174
|
-
|
|
2941
|
+
const disposition = handleLexicalStatDisposition(context, {
|
|
2942
|
+
isSymbolicLink: stat.isSymbolicLink(),
|
|
2943
|
+
segment,
|
|
2944
|
+
isLast,
|
|
2945
|
+
});
|
|
2946
|
+
if (disposition === "continue") {
|
|
2947
|
+
continue;
|
|
2948
|
+
}
|
|
2949
|
+
if (disposition === "break") {
|
|
2950
|
+
break;
|
|
2951
|
+
}
|
|
2952
|
+
const maybeApplied = resolveAndApplySymlinkHop(context, {
|
|
2953
|
+
resolveLinkCanonical: (cursor) => resolveSymlinkHopPathSync(cursor),
|
|
2954
|
+
});
|
|
2955
|
+
if (isPromiseLike(maybeApplied)) {
|
|
2956
|
+
throw new Error("Unexpected async symlink resolution");
|
|
2957
|
+
}
|
|
2958
|
+
if (context.resolveParams.rejectSymlinks === true) {
|
|
2959
|
+
throw new FsSafeError("symlink", "symlink path component not allowed");
|
|
3175
2960
|
}
|
|
3176
|
-
state.held.delete(normalizedTargetPath);
|
|
3177
2961
|
}
|
|
3178
|
-
|
|
2962
|
+
const kind = getPathKindSync(state.canonicalCursor, state.preserveFinalSymlink);
|
|
2963
|
+
return finalizeLexicalResolution(context, kind);
|
|
3179
2964
|
}
|
|
3180
|
-
function
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
const
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
2965
|
+
function resolveCanonicalOutsideLexicalPath(params) {
|
|
2966
|
+
return params.outsideLexicalCanonicalPath ?? params.absolutePath;
|
|
2967
|
+
}
|
|
2968
|
+
function createBoundaryResolutionContext(params) {
|
|
2969
|
+
const lexicalInside = path_isPathInside(params.rootPath, params.absolutePath);
|
|
2970
|
+
const canonicalOutsideLexicalPath = resolveCanonicalOutsideLexicalPath({
|
|
2971
|
+
absolutePath: params.absolutePath,
|
|
2972
|
+
outsideLexicalCanonicalPath: params.outsideLexicalCanonicalPath,
|
|
2973
|
+
});
|
|
2974
|
+
assertLexicalBoundaryOrCanonicalAlias({
|
|
2975
|
+
skipLexicalRootCheck: params.resolveParams.skipLexicalRootCheck,
|
|
2976
|
+
lexicalInside,
|
|
2977
|
+
canonicalOutsideLexicalPath,
|
|
2978
|
+
rootCanonicalPath: params.rootCanonicalPath,
|
|
2979
|
+
boundaryLabel: params.resolveParams.boundaryLabel,
|
|
2980
|
+
rootPath: params.rootPath,
|
|
2981
|
+
absolutePath: params.absolutePath,
|
|
2982
|
+
});
|
|
2983
|
+
return {
|
|
2984
|
+
rootPath: params.rootPath,
|
|
2985
|
+
absolutePath: params.absolutePath,
|
|
2986
|
+
rootCanonicalPath: params.rootCanonicalPath,
|
|
2987
|
+
lexicalInside,
|
|
2988
|
+
canonicalOutsideLexicalPath,
|
|
3189
2989
|
};
|
|
3190
|
-
globalWithCleanup[GLOBAL_CLEANUP_HANDLER_KEY] = cleanup;
|
|
3191
|
-
process.on("exit", cleanup);
|
|
3192
2990
|
}
|
|
3193
|
-
async function
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
return false;
|
|
2991
|
+
async function resolveOutsideRootPathAsync(params) {
|
|
2992
|
+
if (params.context.lexicalInside) {
|
|
2993
|
+
return null;
|
|
3197
2994
|
}
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
2995
|
+
const kind = await getPathKind(params.context.absolutePath, false);
|
|
2996
|
+
return buildOutsideRootPathFromContext({
|
|
2997
|
+
boundaryLabel: params.boundaryLabel,
|
|
2998
|
+
context: params.context,
|
|
2999
|
+
kind,
|
|
3000
|
+
});
|
|
3001
|
+
}
|
|
3002
|
+
function resolveOutsideRootPathSync(params) {
|
|
3003
|
+
if (params.context.lexicalInside) {
|
|
3004
|
+
return null;
|
|
3201
3005
|
}
|
|
3202
|
-
|
|
3203
|
-
|
|
3006
|
+
const kind = getPathKindSync(params.context.absolutePath, false);
|
|
3007
|
+
return buildOutsideRootPathFromContext({
|
|
3008
|
+
boundaryLabel: params.boundaryLabel,
|
|
3009
|
+
context: params.context,
|
|
3010
|
+
kind,
|
|
3011
|
+
});
|
|
3012
|
+
}
|
|
3013
|
+
function buildOutsideRootPathFromContext(params) {
|
|
3014
|
+
return buildOutsideLexicalRootPath({
|
|
3015
|
+
boundaryLabel: params.boundaryLabel,
|
|
3016
|
+
rootCanonicalPath: params.context.rootCanonicalPath,
|
|
3017
|
+
absolutePath: params.context.absolutePath,
|
|
3018
|
+
canonicalOutsideLexicalPath: params.context.canonicalOutsideLexicalPath,
|
|
3019
|
+
rootPath: params.context.rootPath,
|
|
3020
|
+
kind: params.kind,
|
|
3021
|
+
});
|
|
3022
|
+
}
|
|
3023
|
+
async function resolveOutsideLexicalCanonicalPathAsync(params) {
|
|
3024
|
+
if (path_isPathInside(params.rootPath, params.absolutePath)) {
|
|
3025
|
+
return undefined;
|
|
3204
3026
|
}
|
|
3205
|
-
|
|
3206
|
-
|
|
3027
|
+
return await resolvePathViaExistingAncestor(params.absolutePath);
|
|
3028
|
+
}
|
|
3029
|
+
function resolveOutsideLexicalCanonicalPathSync(params) {
|
|
3030
|
+
if (isPathInside(params.rootPath, params.absolutePath)) {
|
|
3031
|
+
return undefined;
|
|
3207
3032
|
}
|
|
3208
|
-
|
|
3209
|
-
|
|
3033
|
+
return resolvePathViaExistingAncestorSync(params.absolutePath);
|
|
3034
|
+
}
|
|
3035
|
+
function buildOutsideLexicalRootPath(params) {
|
|
3036
|
+
assertInsideBoundary({
|
|
3037
|
+
boundaryLabel: params.boundaryLabel,
|
|
3038
|
+
rootCanonicalPath: params.rootCanonicalPath,
|
|
3039
|
+
candidatePath: params.canonicalOutsideLexicalPath,
|
|
3040
|
+
absolutePath: params.absolutePath,
|
|
3041
|
+
});
|
|
3042
|
+
return buildResolvedRootPath({
|
|
3043
|
+
absolutePath: params.absolutePath,
|
|
3044
|
+
canonicalPath: params.canonicalOutsideLexicalPath,
|
|
3045
|
+
rootPath: params.rootPath,
|
|
3046
|
+
rootCanonicalPath: params.rootCanonicalPath,
|
|
3047
|
+
kind: params.kind,
|
|
3048
|
+
});
|
|
3049
|
+
}
|
|
3050
|
+
function assertLexicalBoundaryOrCanonicalAlias(params) {
|
|
3051
|
+
if (params.skipLexicalRootCheck || params.lexicalInside) {
|
|
3052
|
+
return;
|
|
3053
|
+
}
|
|
3054
|
+
if (path_isPathInside(params.rootCanonicalPath, params.canonicalOutsideLexicalPath)) {
|
|
3055
|
+
return;
|
|
3056
|
+
}
|
|
3057
|
+
throw pathEscapeError({
|
|
3058
|
+
boundaryLabel: params.boundaryLabel,
|
|
3059
|
+
rootPath: params.rootPath,
|
|
3060
|
+
absolutePath: params.absolutePath,
|
|
3061
|
+
});
|
|
3062
|
+
}
|
|
3063
|
+
function buildResolvedRootPath(params) {
|
|
3064
|
+
return {
|
|
3065
|
+
absolutePath: params.absolutePath,
|
|
3066
|
+
canonicalPath: params.canonicalPath,
|
|
3067
|
+
rootPath: params.rootPath,
|
|
3068
|
+
rootCanonicalPath: params.rootCanonicalPath,
|
|
3069
|
+
relativePath: relativeInsideRoot(params.rootCanonicalPath, params.canonicalPath),
|
|
3070
|
+
exists: params.kind.exists,
|
|
3071
|
+
kind: params.kind.kind,
|
|
3072
|
+
};
|
|
3073
|
+
}
|
|
3074
|
+
async function getPathKind(absolutePath, preserveFinalSymlink) {
|
|
3075
|
+
try {
|
|
3076
|
+
const stat = preserveFinalSymlink
|
|
3077
|
+
? await promises_namespaceObject.lstat(absolutePath)
|
|
3078
|
+
: await promises_namespaceObject.stat(absolutePath);
|
|
3079
|
+
return { exists: true, kind: toResolvedKind(stat) };
|
|
3210
3080
|
}
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
lockRoot: held.lockRoot,
|
|
3215
|
-
parsePayload: held.parsePayload,
|
|
3216
|
-
});
|
|
3217
|
-
if (state.held.get(normalizedTargetPath) === held) {
|
|
3218
|
-
state.held.delete(normalizedTargetPath);
|
|
3219
|
-
}
|
|
3220
|
-
if (held.compromiseTimer) {
|
|
3221
|
-
clearInterval(held.compromiseTimer);
|
|
3222
|
-
held.compromiseTimer = undefined;
|
|
3081
|
+
catch (error) {
|
|
3082
|
+
if (path_isNotFoundPathError(error)) {
|
|
3083
|
+
return { exists: false, kind: "missing" };
|
|
3223
3084
|
}
|
|
3224
|
-
|
|
3085
|
+
throw error;
|
|
3086
|
+
}
|
|
3087
|
+
}
|
|
3088
|
+
function getPathKindSync(absolutePath, preserveFinalSymlink) {
|
|
3225
3089
|
try {
|
|
3226
|
-
|
|
3227
|
-
return true;
|
|
3090
|
+
const stat = preserveFinalSymlink ? fs.lstatSync(absolutePath) : fs.statSync(absolutePath);
|
|
3091
|
+
return { exists: true, kind: toResolvedKind(stat) };
|
|
3228
3092
|
}
|
|
3229
|
-
|
|
3230
|
-
|
|
3093
|
+
catch (error) {
|
|
3094
|
+
if (isNotFoundPathError(error)) {
|
|
3095
|
+
return { exists: false, kind: "missing" };
|
|
3096
|
+
}
|
|
3097
|
+
throw error;
|
|
3231
3098
|
}
|
|
3232
3099
|
}
|
|
3233
|
-
function
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
held,
|
|
3237
|
-
release: async (options) => await releaseHeldLock(state, normalizedTargetPath, held, { retry: options?.retry }),
|
|
3238
|
-
});
|
|
3239
|
-
}
|
|
3240
|
-
function createSidecarLockManager(key) {
|
|
3241
|
-
const state = resolveManagerState(key);
|
|
3242
|
-
function ensureExitCleanupRegistered() {
|
|
3243
|
-
state.cleanupRegistered = true;
|
|
3244
|
-
state.reclaimCleanupRegistered = true;
|
|
3245
|
-
ensureGlobalExitCleanupRegistered();
|
|
3100
|
+
function toResolvedKind(stat) {
|
|
3101
|
+
if (stat.isFile()) {
|
|
3102
|
+
return "file";
|
|
3246
3103
|
}
|
|
3247
|
-
|
|
3248
|
-
return
|
|
3249
|
-
held: state.held,
|
|
3250
|
-
reclaimGuards: state.reclaimGuards,
|
|
3251
|
-
ensureExitCleanupRegistered,
|
|
3252
|
-
handleForHeldLock: (normalizedTargetPath, held) => handleForHeldLock(state, normalizedTargetPath, held),
|
|
3253
|
-
releaseHeldLock: async (normalizedTargetPath, held, releaseOptions) => await releaseHeldLock(state, normalizedTargetPath, held, releaseOptions),
|
|
3254
|
-
});
|
|
3104
|
+
if (stat.isDirectory()) {
|
|
3105
|
+
return "directory";
|
|
3255
3106
|
}
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
let result;
|
|
3259
|
-
try {
|
|
3260
|
-
result = await fn();
|
|
3261
|
-
}
|
|
3262
|
-
catch (bodyError) {
|
|
3263
|
-
try {
|
|
3264
|
-
await lock.release();
|
|
3265
|
-
}
|
|
3266
|
-
catch (releaseError) {
|
|
3267
|
-
throw createSuppressedError(releaseError, bodyError, "file lock callback and release both failed");
|
|
3268
|
-
}
|
|
3269
|
-
throw bodyError;
|
|
3270
|
-
}
|
|
3271
|
-
await lock.release();
|
|
3272
|
-
return result;
|
|
3107
|
+
if (stat.isSymbolicLink()) {
|
|
3108
|
+
return "symlink";
|
|
3273
3109
|
}
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
3110
|
+
return "other";
|
|
3111
|
+
}
|
|
3112
|
+
function relativeInsideRoot(rootPath, targetPath) {
|
|
3113
|
+
const relative = external_node_path_namespaceObject.relative(external_node_path_namespaceObject.resolve(rootPath), external_node_path_namespaceObject.resolve(targetPath));
|
|
3114
|
+
if (!relative || relative === ".") {
|
|
3115
|
+
return "";
|
|
3278
3116
|
}
|
|
3279
|
-
|
|
3280
|
-
|
|
3117
|
+
if (isPathRelativeEscape(relative)) {
|
|
3118
|
+
return "";
|
|
3281
3119
|
}
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
metadata: held.metadata,
|
|
3288
|
-
forceRelease: () => releaseHeldLock(state, normalizedTargetPath, held, { force: true }),
|
|
3289
|
-
}));
|
|
3120
|
+
return relative;
|
|
3121
|
+
}
|
|
3122
|
+
function assertInsideBoundary(params) {
|
|
3123
|
+
if (path_isPathInside(params.rootCanonicalPath, params.candidatePath)) {
|
|
3124
|
+
return;
|
|
3290
3125
|
}
|
|
3291
|
-
|
|
3126
|
+
throw new Error(`Path resolves outside ${params.boundaryLabel} (${shortPath(params.rootCanonicalPath)}): ${shortPath(params.absolutePath)}`);
|
|
3292
3127
|
}
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3128
|
+
function pathEscapeError(params) {
|
|
3129
|
+
return new Error(`Path escapes ${params.boundaryLabel} (${shortPath(params.rootPath)}): ${shortPath(params.absolutePath)}`);
|
|
3130
|
+
}
|
|
3131
|
+
function symlinkEscapeError(params) {
|
|
3132
|
+
return new Error(`Symlink escapes ${params.boundaryLabel} (${shortPath(params.rootCanonicalPath)}): ${shortPath(params.symlinkPath)}`);
|
|
3297
3133
|
}
|
|
3298
3134
|
|
|
3299
|
-
;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/
|
|
3135
|
+
;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/sidecar-lock-root.js
|
|
3300
3136
|
|
|
3301
3137
|
|
|
3302
3138
|
|
|
3303
3139
|
|
|
3304
3140
|
|
|
3305
3141
|
|
|
3142
|
+
async function openSidecarRoot(lockRoot, relative, discardObservation, onOpenFailure) {
|
|
3143
|
+
const resolved = await lockRoot.resolve(relative);
|
|
3144
|
+
const canonicalPath = async () => (await resolveRootPath({
|
|
3145
|
+
absolutePath: resolved,
|
|
3146
|
+
rootPath: lockRoot.rootReal,
|
|
3147
|
+
rootCanonicalPath: lockRoot.rootReal,
|
|
3148
|
+
boundaryLabel: "sidecar lock root",
|
|
3149
|
+
})).canonicalPath;
|
|
3150
|
+
const expectedRealPath = await canonicalPath();
|
|
3151
|
+
if (expectedRealPath === lockRoot.rootReal)
|
|
3152
|
+
throw new errors_FsSafeError("not-file", "sidecar lock is a directory");
|
|
3153
|
+
const parents = [];
|
|
3154
|
+
let completeParents = true;
|
|
3155
|
+
if (discardObservation) {
|
|
3156
|
+
// Follow permitted in-root aliases first; receipts cover canonical ancestry.
|
|
3157
|
+
for (let dir = external_node_path_namespaceObject.dirname(expectedRealPath);; dir = external_node_path_namespaceObject.dirname(dir)) {
|
|
3158
|
+
try {
|
|
3159
|
+
parents.push({ dir, stat: await inspectDirectoryIdentity(dir) });
|
|
3160
|
+
}
|
|
3161
|
+
catch (error) {
|
|
3162
|
+
if (!path_isNotFoundPathError(error))
|
|
3163
|
+
throw error;
|
|
3164
|
+
completeParents = false;
|
|
3165
|
+
}
|
|
3166
|
+
if (dir === lockRoot.rootReal)
|
|
3167
|
+
break;
|
|
3168
|
+
}
|
|
3169
|
+
}
|
|
3170
|
+
const observation = fileObservation();
|
|
3171
|
+
try {
|
|
3172
|
+
const opened = await observation.run(() => lockRoot.open(relative));
|
|
3173
|
+
if (opened.realPath !== expectedRealPath) {
|
|
3174
|
+
await opened.handle.close().catch(() => undefined);
|
|
3175
|
+
throw new errors_FsSafeError("path-mismatch", "sidecar lock path changed during open");
|
|
3176
|
+
}
|
|
3177
|
+
return opened;
|
|
3178
|
+
}
|
|
3179
|
+
catch (error) {
|
|
3180
|
+
const openFailed = observation.has(error, `open:${resolved}`);
|
|
3181
|
+
if (openFailed)
|
|
3182
|
+
onOpenFailure?.(error);
|
|
3183
|
+
const missing = openFailed && error instanceof errors_FsSafeError && error.code === "not-found";
|
|
3184
|
+
if (missing && !discardObservation)
|
|
3185
|
+
return null;
|
|
3186
|
+
const discardable = observation.has(error, `unlinked:${resolved}`) ||
|
|
3187
|
+
(discardObservation === "changed" && observation.has(error, `changed:${resolved}`));
|
|
3188
|
+
if (!discardObservation || (!missing && (!completeParents || !discardable)))
|
|
3189
|
+
throw error;
|
|
3190
|
+
try {
|
|
3191
|
+
try {
|
|
3192
|
+
const current = await lockRoot.stat(relative);
|
|
3193
|
+
if (!current.isFile || current.isSymbolicLink || current.nlink !== 1)
|
|
3194
|
+
throw error;
|
|
3195
|
+
}
|
|
3196
|
+
catch (probeError) {
|
|
3197
|
+
if (!(probeError instanceof errors_FsSafeError && probeError.code === "not-found"))
|
|
3198
|
+
throw probeError;
|
|
3199
|
+
}
|
|
3200
|
+
for (const { dir, stat } of parents)
|
|
3201
|
+
await inspectDirectoryIdentity(dir, stat);
|
|
3202
|
+
await lockRoot.resolve(relative);
|
|
3203
|
+
if (await canonicalPath() === expectedRealPath)
|
|
3204
|
+
return null;
|
|
3205
|
+
}
|
|
3206
|
+
catch {
|
|
3207
|
+
// Keep the original failure when directory or confinement proof fails.
|
|
3208
|
+
}
|
|
3209
|
+
throw error;
|
|
3210
|
+
}
|
|
3211
|
+
}
|
|
3212
|
+
|
|
3213
|
+
;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/test-hooks.js
|
|
3214
|
+
let test_hooks_fsSafeTestHooks;
|
|
3215
|
+
function allowFsSafeTestHooks() {
|
|
3216
|
+
return false || process.env.VITEST === "true";
|
|
3217
|
+
}
|
|
3218
|
+
function getFsSafeTestHooks() {
|
|
3219
|
+
return test_hooks_fsSafeTestHooks;
|
|
3220
|
+
}
|
|
3221
|
+
function __setFsSafeTestHooksForTest(hooks) {
|
|
3222
|
+
if (hooks && !allowFsSafeTestHooks()) {
|
|
3223
|
+
throw new Error("__setFsSafeTestHooksForTest is only available in tests");
|
|
3224
|
+
}
|
|
3225
|
+
test_hooks_fsSafeTestHooks = hooks;
|
|
3226
|
+
}
|
|
3306
3227
|
|
|
3228
|
+
;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/sidecar-lock-reclaim.js
|
|
3307
3229
|
|
|
3308
3230
|
|
|
3309
3231
|
|
|
@@ -3314,361 +3236,395 @@ async function withSidecarLock(targetPath, options, fn) {
|
|
|
3314
3236
|
|
|
3315
3237
|
|
|
3316
3238
|
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3239
|
+
const MAX_LOCK_PAYLOAD_BYTES = 1024 * 1024;
|
|
3240
|
+
const SIDECAR_LOCK_OWNERSHIP_TOKEN_BYTES = 16;
|
|
3241
|
+
const SIDECAR_LOCK_OWNERSHIP_TOKEN_BITS = SIDECAR_LOCK_OWNERSHIP_TOKEN_BYTES * 8;
|
|
3242
|
+
const SIDECAR_LOCK_OWNERSHIP_TOKEN_PREFIX = "\t".repeat(8);
|
|
3243
|
+
const SIDECAR_LOCK_OWNERSHIP_TOKEN_PATTERN = new RegExp(`\\n(${SIDECAR_LOCK_OWNERSHIP_TOKEN_PREFIX}[ \\t]{${SIDECAR_LOCK_OWNERSHIP_TOKEN_BITS}})\\n$`);
|
|
3244
|
+
function parseSidecarLockSnapshot(snapshot, parser) {
|
|
3245
|
+
return snapshot && { ...snapshot, payload: parseSidecarLockPayload(snapshot.raw, parser) };
|
|
3321
3246
|
}
|
|
3322
|
-
function
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
throw new errors_FsSafeError("invalid-path", "invalid target path");
|
|
3247
|
+
function createSidecarLockOwnershipToken() {
|
|
3248
|
+
let token = SIDECAR_LOCK_OWNERSHIP_TOKEN_PREFIX;
|
|
3249
|
+
for (const byte of (0,external_node_crypto_namespaceObject.randomBytes)(SIDECAR_LOCK_OWNERSHIP_TOKEN_BYTES)) {
|
|
3250
|
+
for (let bit = 7; bit >= 0; bit -= 1) {
|
|
3251
|
+
token += byte & (1 << bit) ? "\t" : " ";
|
|
3252
|
+
}
|
|
3329
3253
|
}
|
|
3254
|
+
return token;
|
|
3330
3255
|
}
|
|
3331
|
-
function
|
|
3332
|
-
|
|
3333
|
-
|
|
3256
|
+
function readSidecarLockOwnershipToken(raw) {
|
|
3257
|
+
return SIDECAR_LOCK_OWNERSHIP_TOKEN_PATTERN.exec(raw)?.[1];
|
|
3258
|
+
}
|
|
3259
|
+
function serializeSidecarLockPayload(payload) {
|
|
3260
|
+
const ownershipToken = createSidecarLockOwnershipToken();
|
|
3261
|
+
return {
|
|
3262
|
+
raw: `${JSON.stringify(payload, null, 2)}\n${ownershipToken}\n`,
|
|
3263
|
+
ownershipToken,
|
|
3264
|
+
};
|
|
3265
|
+
}
|
|
3266
|
+
function relativeSidecarLockPath(lockRoot, lockPath) {
|
|
3267
|
+
const resolved = external_node_path_namespaceObject.resolve(lockPath);
|
|
3268
|
+
const lexicalRelative = external_node_path_namespaceObject.relative(lockRoot.rootDir, resolved);
|
|
3269
|
+
const relative = lexicalRelative !== ".." &&
|
|
3270
|
+
!lexicalRelative.startsWith(`..${external_node_path_namespaceObject.sep}`) &&
|
|
3271
|
+
!external_node_path_namespaceObject.isAbsolute(lexicalRelative)
|
|
3272
|
+
? lexicalRelative
|
|
3273
|
+
: external_node_path_namespaceObject.relative(lockRoot.rootReal, resolved);
|
|
3274
|
+
if (!relative || relative === ".." || relative.startsWith(`..${external_node_path_namespaceObject.sep}`) || external_node_path_namespaceObject.isAbsolute(relative)) {
|
|
3275
|
+
throw new errors_FsSafeError("outside-workspace", "sidecar lock path is outside lockRoot");
|
|
3334
3276
|
}
|
|
3277
|
+
return relative.split(external_node_path_namespaceObject.sep).join(external_node_path_namespaceObject.posix.sep);
|
|
3335
3278
|
}
|
|
3336
|
-
|
|
3279
|
+
function parseSidecarLockPayload(raw, parser) {
|
|
3280
|
+
if (parser) {
|
|
3281
|
+
return parser(raw);
|
|
3282
|
+
}
|
|
3337
3283
|
try {
|
|
3338
|
-
|
|
3284
|
+
const parsed = JSON.parse(raw);
|
|
3285
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
3339
3286
|
}
|
|
3340
|
-
catch
|
|
3341
|
-
|
|
3287
|
+
catch {
|
|
3288
|
+
return null;
|
|
3289
|
+
}
|
|
3290
|
+
}
|
|
3291
|
+
function missingSnapshotPath(error) {
|
|
3292
|
+
if (error.code === "ENOENT")
|
|
3293
|
+
return null;
|
|
3294
|
+
throw error;
|
|
3295
|
+
}
|
|
3296
|
+
async function readSidecarLockSnapshot(lockPath, options = {}) {
|
|
3297
|
+
return parseSidecarLockSnapshot(await readSidecarLockRawSnapshot(lockPath, options), options.parsePayload);
|
|
3298
|
+
}
|
|
3299
|
+
async function readSidecarLockRawSnapshot(lockPath, options = {}) {
|
|
3300
|
+
let handle;
|
|
3301
|
+
try {
|
|
3302
|
+
if (options.lockRoot) {
|
|
3303
|
+
const lockRoot = options.lockRoot;
|
|
3304
|
+
const relative = relativeSidecarLockPath(lockRoot, lockPath);
|
|
3305
|
+
const opened = await openSidecarRoot(lockRoot, relative, options.discardObservation, options.onOpenFailure);
|
|
3306
|
+
if (!opened)
|
|
3307
|
+
return null;
|
|
3308
|
+
try {
|
|
3309
|
+
const raw = (await readFileHandleBounded(opened.handle, MAX_LOCK_PAYLOAD_BYTES)).toString("utf8");
|
|
3310
|
+
return {
|
|
3311
|
+
raw,
|
|
3312
|
+
stat: opened.stat,
|
|
3313
|
+
};
|
|
3314
|
+
}
|
|
3315
|
+
finally {
|
|
3316
|
+
await opened.handle.close().catch(() => undefined);
|
|
3317
|
+
}
|
|
3318
|
+
}
|
|
3319
|
+
const before = await promises_namespaceObject.lstat(lockPath).catch(missingSnapshotPath);
|
|
3320
|
+
if (!before)
|
|
3321
|
+
return null;
|
|
3322
|
+
if (!before.isFile() || before.isSymbolicLink()) {
|
|
3323
|
+
if (options.rejectNonFile) {
|
|
3324
|
+
throw new errors_FsSafeError("not-file", `sidecar lock is not a regular file: ${lockPath}`);
|
|
3325
|
+
}
|
|
3326
|
+
return null;
|
|
3327
|
+
}
|
|
3328
|
+
await getFsSafeTestHooks()?.beforeSidecarLockSnapshotOpen?.(lockPath);
|
|
3329
|
+
const noFollow = process.platform !== "win32" && typeof external_node_fs_namespaceObject.constants.O_NOFOLLOW === "number"
|
|
3330
|
+
? external_node_fs_namespaceObject.constants.O_NOFOLLOW
|
|
3331
|
+
: 0;
|
|
3332
|
+
try {
|
|
3333
|
+
handle = await promises_namespaceObject.open(lockPath, external_node_fs_namespaceObject.constants.O_RDONLY |
|
|
3334
|
+
noFollow |
|
|
3335
|
+
(typeof external_node_fs_namespaceObject.constants.O_NONBLOCK === "number" ? external_node_fs_namespaceObject.constants.O_NONBLOCK : 0));
|
|
3336
|
+
}
|
|
3337
|
+
catch (error) {
|
|
3338
|
+
if (error.code === "ENOENT")
|
|
3339
|
+
return null;
|
|
3340
|
+
if (options.rejectNonFile && error.code === "ELOOP") {
|
|
3341
|
+
throw new errors_FsSafeError("not-file", `sidecar lock is not a regular file: ${lockPath}`, {
|
|
3342
|
+
cause: error,
|
|
3343
|
+
});
|
|
3344
|
+
}
|
|
3345
|
+
options.onOpenFailure?.(error);
|
|
3342
3346
|
throw error;
|
|
3343
3347
|
}
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
for await (const chunk of stream) {
|
|
3349
|
-
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
3350
|
-
bytes += buffer.byteLength;
|
|
3351
|
-
assertWithinMaxBytes(bytes, maxBytes);
|
|
3352
|
-
let offset = 0;
|
|
3353
|
-
while (offset < buffer.byteLength) {
|
|
3354
|
-
const { bytesWritten } = await handle.write(buffer, offset, buffer.byteLength - offset);
|
|
3355
|
-
if (bytesWritten <= 0) {
|
|
3356
|
-
throw new errors_FsSafeError("helper-failed", "fallback stream write made no progress");
|
|
3348
|
+
const opened = await handle.stat();
|
|
3349
|
+
if (!opened.isFile()) {
|
|
3350
|
+
if (options.rejectNonFile) {
|
|
3351
|
+
throw new errors_FsSafeError("not-file", `sidecar lock is not a regular file: ${lockPath}`);
|
|
3357
3352
|
}
|
|
3358
|
-
|
|
3353
|
+
return null;
|
|
3359
3354
|
}
|
|
3355
|
+
if (!options.allowDescriptorIdentityDrift && !file_identity_sameFileIdentity(before, opened))
|
|
3356
|
+
return null;
|
|
3357
|
+
const raw = (await readFileHandleBounded(handle, MAX_LOCK_PAYLOAD_BYTES)).toString("utf8");
|
|
3358
|
+
const after = await promises_namespaceObject.lstat(lockPath).catch(missingSnapshotPath);
|
|
3359
|
+
if (!after || !after.isFile() || !file_identity_sameFileIdentity(before, after))
|
|
3360
|
+
return null;
|
|
3361
|
+
return { raw, stat: after };
|
|
3362
|
+
}
|
|
3363
|
+
finally {
|
|
3364
|
+
await handle?.close().catch(() => undefined);
|
|
3360
3365
|
}
|
|
3361
3366
|
}
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
validatePinnedOperationPayload({
|
|
3366
|
-
relativeParentPath: params.relativeParentPath,
|
|
3367
|
-
});
|
|
3368
|
-
// The explicit compatibility policy uses the guarded Node fallback, where
|
|
3369
|
-
// content verification can replace the strict post-rename inode check.
|
|
3370
|
-
if (normalizedParams.onRenameIdentityMismatch === "verify-content") {
|
|
3371
|
-
return await runPinnedWriteFallback(normalizedParams);
|
|
3367
|
+
function lstatSidecarLockSync(lockPath) {
|
|
3368
|
+
try {
|
|
3369
|
+
return fsSync.lstatSync(lockPath);
|
|
3372
3370
|
}
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
return await runPinnedWriteNative(native, normalizedParams);
|
|
3371
|
+
catch (error) {
|
|
3372
|
+
return missingSnapshotPath(error);
|
|
3376
3373
|
}
|
|
3377
|
-
return await runPinnedWriteFallback(normalizedParams);
|
|
3378
3374
|
}
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
if (renameIdentity !== "verify-content-with-lock") {
|
|
3382
|
-
return await runPinnedWriteHelper(writeParams);
|
|
3383
|
-
}
|
|
3384
|
-
const relativeTargetPath = writeParams.relativeParentPath
|
|
3385
|
-
? `${writeParams.relativeParentPath}/${writeParams.basename}`
|
|
3386
|
-
: writeParams.basename;
|
|
3387
|
-
const lockPath = external_node_path_namespaceObject.join(writeParams.rootPath, `.fs-safe-write-${sha256Hex(relativeTargetPath)}.lock`);
|
|
3388
|
-
return await withSidecarLock(writeParams.rootPath, {
|
|
3389
|
-
managerKey: `fs-safe.write:${targetPath}`,
|
|
3390
|
-
lockPath,
|
|
3391
|
-
staleMs: 30_000,
|
|
3392
|
-
timeoutMs: 5_000,
|
|
3393
|
-
payload: () => ({ pid: process.pid, createdAt: new Date().toISOString() }),
|
|
3394
|
-
retry: { retries: 5, minTimeout: 100, maxTimeout: 2_000, factor: 2 },
|
|
3395
|
-
}, async () => await runPinnedWriteHelper({
|
|
3396
|
-
...writeParams,
|
|
3397
|
-
onRenameIdentityMismatch: "verify-content",
|
|
3398
|
-
}));
|
|
3375
|
+
function readSidecarLockSnapshotSync(lockPath, parsePayload, options = {}) {
|
|
3376
|
+
return parseSidecarLockSnapshot(readSidecarLockRawSnapshotSync(lockPath, options), parsePayload);
|
|
3399
3377
|
}
|
|
3400
|
-
|
|
3401
|
-
let
|
|
3402
|
-
? external_node_path_namespaceObject.join(params.rootPath, ...params.relativeParentPath.split("/"))
|
|
3403
|
-
: params.rootPath;
|
|
3404
|
-
if (params.mkdir) {
|
|
3405
|
-
// mkdirPathComponentsWithGuards may resolve the final component through
|
|
3406
|
-
// an in-root symlink (e.g. a skill-bank layout). Use its returned real
|
|
3407
|
-
// path for the subsequent guard and target path so we don't re-check the
|
|
3408
|
-
// original, possibly-symlinked, lexical path and reject it outright.
|
|
3409
|
-
parentPath = await mkdirPathComponentsWithGuards({
|
|
3410
|
-
rootReal: params.rootPath,
|
|
3411
|
-
targetPath: parentPath,
|
|
3412
|
-
beforeComponent: async (componentPath) => await getFsSafeTestHooks()?.beforeRootFallbackMutation?.("mkdir", componentPath),
|
|
3413
|
-
});
|
|
3414
|
-
}
|
|
3415
|
-
const parentGuard = params.mkdir
|
|
3416
|
-
? await directory_guard_createAsyncDirectoryGuard(parentPath)
|
|
3417
|
-
: await directory_guard_createNearestExistingDirectoryGuard(params.rootPath, parentPath);
|
|
3418
|
-
const targetPath = external_node_path_namespaceObject.join(parentPath, params.basename);
|
|
3419
|
-
if (params.overwrite === false) {
|
|
3420
|
-
const handle = await withAsyncDirectoryGuards([parentGuard], async () => await promises_namespaceObject.open(targetPath, external_node_fs_namespaceObject.constants.O_WRONLY | external_node_fs_namespaceObject.constants.O_CREAT | external_node_fs_namespaceObject.constants.O_EXCL, params.mode), {
|
|
3421
|
-
onPostGuardFailure: async (openedHandle) => {
|
|
3422
|
-
// The parent failed verification, so targetPath may now resolve
|
|
3423
|
-
// somewhere else. Close the fd, but do not clean up by path.
|
|
3424
|
-
await openedHandle.close().catch(() => undefined);
|
|
3425
|
-
},
|
|
3426
|
-
});
|
|
3427
|
-
let created = true;
|
|
3428
|
-
try {
|
|
3429
|
-
const verificationIdentity = await handle.stat({ bigint: true });
|
|
3430
|
-
await handle.chmod(params.mode);
|
|
3431
|
-
if (params.input.kind === "buffer") {
|
|
3432
|
-
assertWithinMaxBytes(byteLength(params.input.data, params.input.encoding), params.maxBytes);
|
|
3433
|
-
if (typeof params.input.data === "string") {
|
|
3434
|
-
await handle.writeFile(params.input.data, params.input.encoding ?? "utf8");
|
|
3435
|
-
}
|
|
3436
|
-
else {
|
|
3437
|
-
await handle.writeFile(params.input.data);
|
|
3438
|
-
}
|
|
3439
|
-
}
|
|
3440
|
-
else {
|
|
3441
|
-
await writeStreamToHandle(params.input.stream, handle, params.maxBytes);
|
|
3442
|
-
}
|
|
3443
|
-
await syncFileBestEffort(handle);
|
|
3444
|
-
const stat = await handle.stat();
|
|
3445
|
-
await syncDirectoryBestEffort(parentPath);
|
|
3446
|
-
// Publication is complete. A failed outer check must not remove its target.
|
|
3447
|
-
created = false;
|
|
3448
|
-
await params.verifyPublished?.(handle.fd, verificationIdentity, parentGuard);
|
|
3449
|
-
return { dev: stat.dev, ino: stat.ino };
|
|
3450
|
-
}
|
|
3451
|
-
finally {
|
|
3452
|
-
await handle.close().catch(() => undefined);
|
|
3453
|
-
if (created) {
|
|
3454
|
-
await promises_namespaceObject.rm(targetPath, { force: true }).catch(() => undefined);
|
|
3455
|
-
}
|
|
3456
|
-
}
|
|
3457
|
-
}
|
|
3458
|
-
const tempPath = external_node_path_namespaceObject.join(parentPath, `.${params.basename}.${(0,external_node_crypto_namespaceObject.randomUUID)()}.fallback.tmp`);
|
|
3459
|
-
const tempFlags = external_node_fs_namespaceObject.constants.O_WRONLY |
|
|
3460
|
-
external_node_fs_namespaceObject.constants.O_CREAT |
|
|
3461
|
-
external_node_fs_namespaceObject.constants.O_EXCL |
|
|
3462
|
-
(process.platform !== "win32" && "O_NOFOLLOW" in external_node_fs_namespaceObject.constants
|
|
3463
|
-
? external_node_fs_namespaceObject.constants.O_NOFOLLOW
|
|
3464
|
-
: 0);
|
|
3465
|
-
let handle;
|
|
3466
|
-
let tempStat;
|
|
3467
|
-
let readHandle;
|
|
3468
|
-
let renamed = false;
|
|
3378
|
+
function readSidecarLockRawSnapshotSync(lockPath, options = {}) {
|
|
3379
|
+
let fd;
|
|
3469
3380
|
try {
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
if (
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
await handle.writeFile(params.input.data, params.input.encoding ?? "utf8");
|
|
3477
|
-
}
|
|
3478
|
-
else {
|
|
3479
|
-
await handle.writeFile(params.input.data);
|
|
3381
|
+
const before = lstatSidecarLockSync(lockPath);
|
|
3382
|
+
if (!before)
|
|
3383
|
+
return null;
|
|
3384
|
+
if (!before.isFile() || before.isSymbolicLink()) {
|
|
3385
|
+
if (options.rejectNonFile) {
|
|
3386
|
+
throw new FsSafeError("not-file", `sidecar lock is not a regular file: ${lockPath}`);
|
|
3480
3387
|
}
|
|
3388
|
+
return null;
|
|
3481
3389
|
}
|
|
3482
|
-
|
|
3483
|
-
|
|
3390
|
+
try {
|
|
3391
|
+
fd = fsSync.openSync(lockPath, resolveReadOpenFlags());
|
|
3484
3392
|
}
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
|
|
3393
|
+
catch (error) {
|
|
3394
|
+
if (error.code === "ENOENT")
|
|
3395
|
+
return null;
|
|
3396
|
+
options.onOpenFailure?.(error);
|
|
3397
|
+
throw error;
|
|
3489
3398
|
}
|
|
3490
|
-
const
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
|
|
3494
|
-
await promises_namespaceObject.rename(tempPath, targetPath);
|
|
3495
|
-
renamed = true;
|
|
3496
|
-
await getFsSafeTestHooks()?.afterPinnedWriteFallbackRename?.(targetPath);
|
|
3497
|
-
await syncDirectoryBestEffort(parentPath);
|
|
3498
|
-
const targetStat = await promises_namespaceObject.lstat(targetPath);
|
|
3499
|
-
if (targetStat.isSymbolicLink()) {
|
|
3500
|
-
throw new errors_FsSafeError("path-mismatch", "fallback target changed during write");
|
|
3501
|
-
}
|
|
3502
|
-
if (!file_identity_sameFileIdentity(targetStat, expectedTempStat)) {
|
|
3503
|
-
// On filesystems like rclone FUSE, rename(2) can give the destination a
|
|
3504
|
-
// different inode from the source temp fd even with zero concurrency. The
|
|
3505
|
-
// caller must ensure mutual exclusion before passing "verify-content";
|
|
3506
|
-
// fall back to a content hash for this rename-boundary check only.
|
|
3507
|
-
if (params.onRenameIdentityMismatch !== "verify-content") {
|
|
3508
|
-
throw new errors_FsSafeError("path-mismatch", "fallback target changed during write");
|
|
3509
|
-
}
|
|
3510
|
-
if (params.input.kind !== "buffer") {
|
|
3511
|
-
throw new errors_FsSafeError("path-mismatch", "fallback target changed during write");
|
|
3512
|
-
}
|
|
3513
|
-
const expectedHash = sha256Hex(params.input.data, params.input.encoding);
|
|
3514
|
-
readHandle = await promises_namespaceObject.open(targetPath, read_open_flags_resolveReadOpenFlags());
|
|
3515
|
-
const readHandleStat = await readHandle.stat({ bigint: true });
|
|
3516
|
-
const actualHash = sha256Hex(await readHandle.readFile());
|
|
3517
|
-
if (actualHash !== expectedHash) {
|
|
3518
|
-
throw new errors_FsSafeError("path-mismatch", "fallback target changed during write");
|
|
3519
|
-
}
|
|
3520
|
-
// The content-verified destination, not the old temp inode, is now pinned.
|
|
3521
|
-
verificationIdentity = readHandleStat;
|
|
3522
|
-
// Preserve the helper's legacy numeric return facts, not the private proof.
|
|
3523
|
-
verifiedIdentity = { dev: Number(readHandleStat.dev), ino: Number(readHandleStat.ino) };
|
|
3399
|
+
const opened = fsSync.fstatSync(fd);
|
|
3400
|
+
if (!opened.isFile()) {
|
|
3401
|
+
if (options.rejectNonFile) {
|
|
3402
|
+
throw new FsSafeError("not-file", `sidecar lock is not a regular file: ${lockPath}`);
|
|
3524
3403
|
}
|
|
3525
|
-
|
|
3526
|
-
|
|
3527
|
-
|
|
3404
|
+
return null;
|
|
3405
|
+
}
|
|
3406
|
+
const raw = readFileDescriptorBoundedSync(fd, MAX_LOCK_PAYLOAD_BYTES).toString("utf8");
|
|
3407
|
+
const after = lstatSidecarLockSync(lockPath);
|
|
3408
|
+
if (!after || !sameFileIdentity(before, opened) || !sameFileIdentity(opened, after))
|
|
3409
|
+
return null;
|
|
3410
|
+
return {
|
|
3411
|
+
raw,
|
|
3412
|
+
stat: after,
|
|
3413
|
+
ownershipToken: readSidecarLockOwnershipToken(raw),
|
|
3414
|
+
};
|
|
3528
3415
|
}
|
|
3529
3416
|
finally {
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
|
|
3533
|
-
|
|
3534
|
-
|
|
3417
|
+
if (fd !== undefined)
|
|
3418
|
+
fsSync.closeSync(fd);
|
|
3419
|
+
}
|
|
3420
|
+
}
|
|
3421
|
+
function removeSidecarLockIfUnchangedSync(lockPath, observed) {
|
|
3422
|
+
const current = readSidecarLockSnapshotSync(lockPath);
|
|
3423
|
+
if (!current || !sidecarLockSnapshotMatches(current, observed))
|
|
3424
|
+
return false;
|
|
3425
|
+
fsSync.rmSync(lockPath);
|
|
3426
|
+
return true;
|
|
3427
|
+
}
|
|
3428
|
+
function sidecarLockSnapshotMatches(current, observed) {
|
|
3429
|
+
if (observed.ownershipToken !== undefined) {
|
|
3430
|
+
return (current.stat?.isFile() === true &&
|
|
3431
|
+
current.raw !== undefined &&
|
|
3432
|
+
observed.raw !== undefined &&
|
|
3433
|
+
readSidecarLockOwnershipToken(current.raw) === observed.ownershipToken &&
|
|
3434
|
+
readSidecarLockOwnershipToken(observed.raw) === observed.ownershipToken &&
|
|
3435
|
+
current.raw === observed.raw);
|
|
3436
|
+
}
|
|
3437
|
+
if (observed.stat && current.stat && !file_identity_sameFileIdentity(observed.stat, current.stat)) {
|
|
3438
|
+
return false;
|
|
3439
|
+
}
|
|
3440
|
+
if (observed.raw !== undefined) {
|
|
3441
|
+
return current.raw === observed.raw;
|
|
3442
|
+
}
|
|
3443
|
+
return observed.stat !== undefined && current.stat !== undefined;
|
|
3444
|
+
}
|
|
3445
|
+
async function removeSidecarLockIfUnchanged(lockPath, observed, options = {}) {
|
|
3446
|
+
const current = await readSidecarLockSnapshot(lockPath, {
|
|
3447
|
+
...options,
|
|
3448
|
+
allowDescriptorIdentityDrift: observed?.ownershipToken !== undefined,
|
|
3449
|
+
});
|
|
3450
|
+
if (!current || !observed || !sidecarLockSnapshotMatches(current, observed)) {
|
|
3451
|
+
return false;
|
|
3535
3452
|
}
|
|
3453
|
+
if (options.lockRoot) {
|
|
3454
|
+
await options.lockRoot.remove(relativeSidecarLockPath(options.lockRoot, lockPath));
|
|
3455
|
+
}
|
|
3456
|
+
else {
|
|
3457
|
+
await promises_namespaceObject.rm(lockPath, { force: true });
|
|
3458
|
+
}
|
|
3459
|
+
return true;
|
|
3536
3460
|
}
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
|
|
3461
|
+
async function sidecarLockSnapshotStillPresent(lockPath, observed, options = {}) {
|
|
3462
|
+
const current = await readSidecarLockSnapshot(lockPath, {
|
|
3463
|
+
...options,
|
|
3464
|
+
allowDescriptorIdentityDrift: observed?.ownershipToken !== undefined,
|
|
3465
|
+
});
|
|
3466
|
+
return !!current && !!observed && sidecarLockSnapshotMatches(current, observed);
|
|
3542
3467
|
}
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
throw new errors_FsSafeError("symlink", "symlink path could not be resolved", {
|
|
3554
|
-
cause: error instanceof Error ? error : undefined,
|
|
3555
|
-
});
|
|
3468
|
+
async function sidecarReclaimGuardExists(pathname) {
|
|
3469
|
+
try {
|
|
3470
|
+
await promises_namespaceObject.lstat(pathname);
|
|
3471
|
+
return true;
|
|
3472
|
+
}
|
|
3473
|
+
catch (err) {
|
|
3474
|
+
if (err.code === "ENOENT") {
|
|
3475
|
+
return false;
|
|
3476
|
+
}
|
|
3477
|
+
throw err;
|
|
3556
3478
|
}
|
|
3557
|
-
if (!path_isNotFoundPathError(error))
|
|
3558
|
-
throw error;
|
|
3559
3479
|
}
|
|
3560
|
-
async function
|
|
3480
|
+
async function tryAcquireSidecarReclaimGuard(reclaimGuards, reclaimGuardPath) {
|
|
3561
3481
|
try {
|
|
3562
|
-
|
|
3482
|
+
await promises_namespaceObject.mkdir(reclaimGuardPath);
|
|
3483
|
+
reclaimGuards.add(reclaimGuardPath);
|
|
3484
|
+
return true;
|
|
3563
3485
|
}
|
|
3564
|
-
catch (
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
|
|
3486
|
+
catch (err) {
|
|
3487
|
+
if (err.code === "EEXIST") {
|
|
3488
|
+
return false;
|
|
3489
|
+
}
|
|
3490
|
+
throw err;
|
|
3568
3491
|
}
|
|
3569
3492
|
}
|
|
3570
|
-
function
|
|
3493
|
+
async function releaseSidecarReclaimGuard(reclaimGuards, reclaimGuardPath) {
|
|
3494
|
+
await promises_namespaceObject.rmdir(reclaimGuardPath);
|
|
3495
|
+
reclaimGuards.delete(reclaimGuardPath);
|
|
3496
|
+
}
|
|
3497
|
+
async function removeStaleSidecarLockIfAllowed(params) {
|
|
3498
|
+
if (!params.shouldRemoveStaleLock || params.snapshot.raw === undefined) {
|
|
3499
|
+
return "not-approved";
|
|
3500
|
+
}
|
|
3501
|
+
const ioOptions = { lockRoot: params.lockRoot, parsePayload: params.parsePayload };
|
|
3502
|
+
if (!(await sidecarLockSnapshotStillPresent(params.lockPath, params.snapshot, ioOptions))) {
|
|
3503
|
+
return "changed";
|
|
3504
|
+
}
|
|
3505
|
+
if (!(await params.shouldRemoveStaleLock({
|
|
3506
|
+
lockPath: params.lockPath,
|
|
3507
|
+
normalizedTargetPath: params.normalizedTargetPath,
|
|
3508
|
+
raw: params.snapshot.raw,
|
|
3509
|
+
payload: params.snapshot.payload,
|
|
3510
|
+
}))) {
|
|
3511
|
+
return "not-approved";
|
|
3512
|
+
}
|
|
3513
|
+
if (!(await sidecarLockSnapshotStillPresent(params.lockPath, params.snapshot, ioOptions))) {
|
|
3514
|
+
return "changed";
|
|
3515
|
+
}
|
|
3571
3516
|
try {
|
|
3572
|
-
|
|
3517
|
+
if (params.lockRoot) {
|
|
3518
|
+
await params.lockRoot.remove(relativeSidecarLockPath(params.lockRoot, params.lockPath));
|
|
3519
|
+
}
|
|
3520
|
+
else {
|
|
3521
|
+
await promises_namespaceObject.rm(params.lockPath);
|
|
3522
|
+
}
|
|
3523
|
+
return "removed";
|
|
3573
3524
|
}
|
|
3574
|
-
catch (
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
3525
|
+
catch (err) {
|
|
3526
|
+
if (err.code === "ENOENT") {
|
|
3527
|
+
return "changed";
|
|
3528
|
+
}
|
|
3529
|
+
throw err;
|
|
3578
3530
|
}
|
|
3579
3531
|
}
|
|
3580
3532
|
|
|
3581
|
-
;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/
|
|
3533
|
+
;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/sidecar-lock-policy.js
|
|
3582
3534
|
|
|
3583
|
-
|
|
3584
|
-
|
|
3585
|
-
|
|
3586
|
-
// though path.win32.isAbsolute() reports false.
|
|
3587
|
-
const DRIVE_RELATIVE_PREFIX = /^[A-Za-z]:(?![\\/])/;
|
|
3588
|
-
const HYPHEN_CHAR_CODE = 0x2d;
|
|
3589
|
-
function safe_path_segment_isDriveRelativePath(value) {
|
|
3590
|
-
return DRIVE_RELATIVE_PREFIX.test(value);
|
|
3591
|
-
}
|
|
3592
|
-
function assertNoDriveRelativePathSegments(value, label) {
|
|
3593
|
-
if (value.split("/").some(safe_path_segment_isDriveRelativePath)) {
|
|
3594
|
-
throw new errors_FsSafeError("invalid-path", `${label} must not contain a drive letter`);
|
|
3535
|
+
function assertFiniteNonNegative(value, label) {
|
|
3536
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
3537
|
+
throw new RangeError(`${label} must be a finite non-negative number`);
|
|
3595
3538
|
}
|
|
3596
|
-
return value;
|
|
3597
3539
|
}
|
|
3598
|
-
function
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
while (start < end && value.charCodeAt(start) === HYPHEN_CHAR_CODE) {
|
|
3602
|
-
start += 1;
|
|
3540
|
+
function validateSidecarLockRetryOptions(retry) {
|
|
3541
|
+
if (retry.retries !== undefined && (!Number.isSafeInteger(retry.retries) || retry.retries < 0)) {
|
|
3542
|
+
throw new RangeError("lock retry.retries must be a non-negative safe integer");
|
|
3603
3543
|
}
|
|
3604
|
-
|
|
3605
|
-
|
|
3544
|
+
if (retry.factor !== undefined)
|
|
3545
|
+
assertFiniteNonNegative(retry.factor, "lock retry.factor");
|
|
3546
|
+
if (retry.minTimeout !== undefined) {
|
|
3547
|
+
assertFiniteNonNegative(retry.minTimeout, "lock retry.minTimeout");
|
|
3548
|
+
}
|
|
3549
|
+
if (retry.maxTimeout !== undefined) {
|
|
3550
|
+
assertFiniteNonNegative(retry.maxTimeout, "lock retry.maxTimeout");
|
|
3551
|
+
}
|
|
3552
|
+
if (retry.minTimeout !== undefined &&
|
|
3553
|
+
retry.maxTimeout !== undefined &&
|
|
3554
|
+
retry.minTimeout > retry.maxTimeout) {
|
|
3555
|
+
throw new RangeError("lock retry.minTimeout must not exceed retry.maxTimeout");
|
|
3606
3556
|
}
|
|
3607
|
-
return start === 0 && end === value.length ? value : value.slice(start, end);
|
|
3608
3557
|
}
|
|
3609
|
-
function
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
!segment.includes("/") &&
|
|
3614
|
-
!segment.includes("\\") &&
|
|
3615
|
-
!segment.includes("\0") &&
|
|
3616
|
-
(options.allowDotPrefix === true || !segment.startsWith(".")) &&
|
|
3617
|
-
(options.allowDotPrefix === true
|
|
3618
|
-
? SAFE_DOT_PREFIX_PATH_SEGMENT_PATTERN.test(segment)
|
|
3619
|
-
: SAFE_PATH_SEGMENT_PATTERN.test(segment)));
|
|
3558
|
+
function validateSidecarLockTimeoutMs(timeoutMs) {
|
|
3559
|
+
if (timeoutMs === undefined || timeoutMs === Number.POSITIVE_INFINITY)
|
|
3560
|
+
return;
|
|
3561
|
+
assertFiniteNonNegative(timeoutMs, "lock timeoutMs");
|
|
3620
3562
|
}
|
|
3621
|
-
function
|
|
3622
|
-
|
|
3623
|
-
|
|
3624
|
-
|
|
3625
|
-
|
|
3626
|
-
|
|
3627
|
-
|
|
3563
|
+
function computeSidecarLockDelayMs(retry, attempt) {
|
|
3564
|
+
validateSidecarLockRetryOptions(retry);
|
|
3565
|
+
const minTimeout = retry.minTimeout ?? 50;
|
|
3566
|
+
const maxTimeout = retry.maxTimeout ?? 1000;
|
|
3567
|
+
const factor = retry.factor ?? 1;
|
|
3568
|
+
const base = Math.min(maxTimeout, Math.max(minTimeout, minTimeout * factor ** attempt));
|
|
3569
|
+
const jitter = retry.randomize ? 1 + Math.random() : 1;
|
|
3570
|
+
return Math.min(maxTimeout, Math.round(base * jitter));
|
|
3628
3571
|
}
|
|
3629
|
-
|
|
3630
|
-
|
|
3631
|
-
|
|
3632
|
-
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
return
|
|
3572
|
+
// Windows denies access to a lock file while a just-unlinked directory entry
|
|
3573
|
+
// is still being torn down, so a contended acquire sees EPERM on a name that is
|
|
3574
|
+
// already gone -- both when creating it exclusively and when reading the
|
|
3575
|
+
// holder's snapshot. The next attempt succeeds, so this is contention rather
|
|
3576
|
+
// than a permission failure. The error must name the lock file itself: the
|
|
3577
|
+
// exclusive-create helper opens the parent directory first, and a denial from
|
|
3578
|
+
// that setup step carries no teardown evidence and has to reach the caller.
|
|
3579
|
+
const maxTransientLockDenials = 8;
|
|
3580
|
+
function isTransientLockFileDenial(error, lockPath) {
|
|
3581
|
+
const denial = error;
|
|
3582
|
+
return process.platform === "win32" && denial?.code === "EPERM" && denial.path === lockPath;
|
|
3640
3583
|
}
|
|
3641
|
-
function
|
|
3642
|
-
|
|
3643
|
-
|
|
3644
|
-
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
|
|
3584
|
+
function sidecarLockPayloadIsStale(payload, staleMs, nowMs) {
|
|
3585
|
+
const createdAtMs = sidecarLockPayloadCreatedAtMs(payload);
|
|
3586
|
+
return createdAtMs !== null && nowMs - createdAtMs > staleMs;
|
|
3587
|
+
}
|
|
3588
|
+
function sidecarLockPayloadCreatedAtMs(payload) {
|
|
3589
|
+
const createdAt = payload &&
|
|
3590
|
+
typeof payload === "object" &&
|
|
3591
|
+
"createdAt" in payload &&
|
|
3592
|
+
typeof payload.createdAt === "string"
|
|
3593
|
+
? payload.createdAt
|
|
3594
|
+
: "";
|
|
3595
|
+
const createdAtMs = Date.parse(createdAt);
|
|
3596
|
+
return Number.isFinite(createdAtMs) ? createdAtMs : null;
|
|
3597
|
+
}
|
|
3598
|
+
async function defaultSidecarLockShouldReclaim(params) {
|
|
3599
|
+
const createdAtMs = sidecarLockPayloadCreatedAtMs(params.payload);
|
|
3600
|
+
if (createdAtMs !== null)
|
|
3601
|
+
return params.nowMs - createdAtMs > params.staleMs;
|
|
3602
|
+
try {
|
|
3603
|
+
return params.nowMs - (await promises_namespaceObject.stat(params.lockPath)).mtimeMs > params.staleMs;
|
|
3604
|
+
}
|
|
3605
|
+
catch {
|
|
3606
|
+
return true;
|
|
3650
3607
|
}
|
|
3651
|
-
return assertSafePathSegment(prefix.replace(/[^A-Za-z0-9._-]+/g, "-"), {
|
|
3652
|
-
allowDotPrefix: true,
|
|
3653
|
-
...options,
|
|
3654
|
-
label: options.label ?? "path prefix",
|
|
3655
|
-
});
|
|
3656
3608
|
}
|
|
3657
3609
|
|
|
3658
|
-
;// CONCATENATED MODULE:
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
const
|
|
3665
|
-
|
|
3666
|
-
|
|
3610
|
+
;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/suppressed-error.js
|
|
3611
|
+
function createSuppressedError(error, suppressed, message) {
|
|
3612
|
+
const constructor = globalThis.SuppressedError;
|
|
3613
|
+
if (constructor) {
|
|
3614
|
+
return new constructor(error, suppressed, message);
|
|
3615
|
+
}
|
|
3616
|
+
const combined = new Error(message, {
|
|
3617
|
+
cause: error instanceof Error ? error : undefined,
|
|
3618
|
+
});
|
|
3619
|
+
combined.name = "SuppressedError";
|
|
3620
|
+
Object.defineProperties(combined, {
|
|
3621
|
+
error: { configurable: true, value: error },
|
|
3622
|
+
suppressed: { configurable: true, value: suppressed },
|
|
3623
|
+
});
|
|
3624
|
+
return combined;
|
|
3667
3625
|
}
|
|
3668
3626
|
|
|
3669
|
-
;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/
|
|
3670
|
-
|
|
3671
|
-
|
|
3627
|
+
;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/sidecar-lock-acquire.js
|
|
3672
3628
|
|
|
3673
3629
|
|
|
3674
3630
|
|
|
@@ -3678,517 +3634,823 @@ function shortPath(value) {
|
|
|
3678
3634
|
|
|
3679
3635
|
|
|
3680
3636
|
|
|
3681
|
-
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
|
|
3685
|
-
}),
|
|
3686
|
-
unlinkTarget: Object.freeze({
|
|
3687
|
-
allowFinalSymlinkForUnlink: true,
|
|
3688
|
-
allowFinalHardlinkForUnlink: true,
|
|
3689
|
-
}),
|
|
3690
|
-
};
|
|
3691
|
-
async function resolveRootPath(params) {
|
|
3637
|
+
async function resolveNormalizedTargetPath(targetPath) {
|
|
3638
|
+
const resolved = external_node_path_namespaceObject.resolve(targetPath);
|
|
3639
|
+
const dir = external_node_path_namespaceObject.dirname(resolved);
|
|
3640
|
+
await promises_namespaceObject.mkdir(dir, { recursive: true });
|
|
3692
3641
|
try {
|
|
3693
|
-
return await
|
|
3642
|
+
return external_node_path_namespaceObject.join(await promises_namespaceObject.realpath(dir), external_node_path_namespaceObject.basename(resolved));
|
|
3694
3643
|
}
|
|
3695
|
-
catch
|
|
3696
|
-
|
|
3644
|
+
catch {
|
|
3645
|
+
return resolved;
|
|
3697
3646
|
}
|
|
3698
3647
|
}
|
|
3699
|
-
async function
|
|
3700
|
-
|
|
3701
|
-
|
|
3702
|
-
|
|
3703
|
-
|
|
3704
|
-
|
|
3705
|
-
|
|
3706
|
-
|
|
3707
|
-
|
|
3708
|
-
|
|
3709
|
-
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
|
|
3713
|
-
|
|
3714
|
-
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
|
|
3648
|
+
async function acquireSidecarLock(options, context) {
|
|
3649
|
+
const retry = options.retry ?? {};
|
|
3650
|
+
validateSidecarLockRetryOptions(retry);
|
|
3651
|
+
validateSidecarLockTimeoutMs(options.timeoutMs);
|
|
3652
|
+
context.ensureExitCleanupRegistered();
|
|
3653
|
+
const normalizedTargetPath = await resolveNormalizedTargetPath(options.targetPath);
|
|
3654
|
+
const lockPath = options.lockPath ?? `${normalizedTargetPath}.lock`;
|
|
3655
|
+
let held = context.held.get(normalizedTargetPath);
|
|
3656
|
+
if (held &&
|
|
3657
|
+
options.reentrantOwner !== undefined &&
|
|
3658
|
+
held.reentrantOwner !== undefined &&
|
|
3659
|
+
options.reentrantOwner === held.reentrantOwner) {
|
|
3660
|
+
// A final release may already have decremented the count to zero and be
|
|
3661
|
+
// removing the sidecar. Do not admit a new reentrant handle until that
|
|
3662
|
+
// cleanup settles: successful cleanup requires a fresh acquisition, while
|
|
3663
|
+
// failed cleanup leaves the existing sidecar held for a retry.
|
|
3664
|
+
if (held.releasePromise) {
|
|
3665
|
+
await held.releasePromise.catch(() => undefined);
|
|
3666
|
+
held = context.held.get(normalizedTargetPath);
|
|
3667
|
+
}
|
|
3668
|
+
if (held &&
|
|
3669
|
+
options.reentrantOwner !== undefined &&
|
|
3670
|
+
held.reentrantOwner !== undefined &&
|
|
3671
|
+
options.reentrantOwner === held.reentrantOwner) {
|
|
3672
|
+
held.refCount += 1;
|
|
3673
|
+
return context.handleForHeldLock(normalizedTargetPath, held);
|
|
3674
|
+
}
|
|
3722
3675
|
}
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
|
|
3726
|
-
|
|
3727
|
-
|
|
3728
|
-
|
|
3729
|
-
|
|
3730
|
-
|
|
3676
|
+
const startedAt = Date.now();
|
|
3677
|
+
const maxRetries = options.timeoutMs === Number.POSITIVE_INFINITY ? undefined : retry.retries;
|
|
3678
|
+
const reclaimGuardPath = `${lockPath}.reclaim`;
|
|
3679
|
+
let ownsReclaimGuard = false;
|
|
3680
|
+
let attempt = 0;
|
|
3681
|
+
// Bounded so a genuine denial still surfaces as EPERM, not a lock timeout.
|
|
3682
|
+
let transientDenials = 0;
|
|
3683
|
+
const withinDenialBudget = () => ++transientDenials <= (/* inlined export .maxTransientLockDenials */8);
|
|
3684
|
+
const waitForRetry = async () => {
|
|
3685
|
+
const elapsed = Date.now() - startedAt;
|
|
3686
|
+
if ((options.timeoutMs !== undefined &&
|
|
3687
|
+
options.timeoutMs !== Number.POSITIVE_INFINITY &&
|
|
3688
|
+
elapsed >= options.timeoutMs) ||
|
|
3689
|
+
(maxRetries !== undefined && attempt >= maxRetries)) {
|
|
3690
|
+
throw Object.assign(new Error(`file lock timeout for ${normalizedTargetPath}`), {
|
|
3691
|
+
code: "file_lock_timeout",
|
|
3692
|
+
lockPath,
|
|
3693
|
+
normalizedTargetPath,
|
|
3694
|
+
});
|
|
3695
|
+
}
|
|
3696
|
+
const remaining = options.timeoutMs === undefined || options.timeoutMs === Number.POSITIVE_INFINITY
|
|
3697
|
+
? Number.POSITIVE_INFINITY
|
|
3698
|
+
: Math.max(0, options.timeoutMs - elapsed);
|
|
3699
|
+
const delay = Math.min(computeSidecarLockDelayMs(retry, attempt), remaining);
|
|
3700
|
+
attempt += 1;
|
|
3701
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
3702
|
+
};
|
|
3703
|
+
// Waiting can fail on the caller's own retry or deadline limits. Classifying
|
|
3704
|
+
// a denial as contention must not cost them the original diagnosis, so hand
|
|
3705
|
+
// the denial back when no further attempt can be scheduled.
|
|
3706
|
+
const retryOrRethrowDenial = async (denial) => {
|
|
3707
|
+
try {
|
|
3708
|
+
await waitForRetry();
|
|
3709
|
+
}
|
|
3710
|
+
catch (waitError) {
|
|
3711
|
+
if (waitError.code === "file_lock_timeout")
|
|
3712
|
+
throw denial;
|
|
3713
|
+
throw waitError;
|
|
3714
|
+
}
|
|
3715
|
+
};
|
|
3731
3716
|
try {
|
|
3732
|
-
|
|
3733
|
-
|
|
3734
|
-
|
|
3735
|
-
|
|
3736
|
-
|
|
3737
|
-
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
|
|
3745
|
-
|
|
3746
|
-
|
|
3747
|
-
|
|
3748
|
-
|
|
3749
|
-
|
|
3750
|
-
|
|
3751
|
-
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
|
|
3760
|
-
|
|
3761
|
-
|
|
3762
|
-
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
|
|
3770
|
-
|
|
3771
|
-
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
|
|
3717
|
+
while (true) {
|
|
3718
|
+
if (!ownsReclaimGuard && (await sidecarReclaimGuardExists(reclaimGuardPath))) {
|
|
3719
|
+
await waitForRetry();
|
|
3720
|
+
continue;
|
|
3721
|
+
}
|
|
3722
|
+
let handle = null;
|
|
3723
|
+
let createdSnapshot = null;
|
|
3724
|
+
let lockFileCreateDenied = false;
|
|
3725
|
+
const payload = await options.payload();
|
|
3726
|
+
const { raw, ownershipToken } = serializeSidecarLockPayload(payload);
|
|
3727
|
+
try {
|
|
3728
|
+
if (options.lockRoot) {
|
|
3729
|
+
const lockRoot = options.lockRoot;
|
|
3730
|
+
const relativeLockPath = relativeSidecarLockPath(lockRoot, lockPath);
|
|
3731
|
+
try {
|
|
3732
|
+
await lockRoot.create(relativeLockPath, raw, { mkdir: true, mode: 0o600 });
|
|
3733
|
+
}
|
|
3734
|
+
catch (error) {
|
|
3735
|
+
if (error instanceof errors_FsSafeError && error.code === "already-exists") {
|
|
3736
|
+
throw Object.assign(new Error("sidecar lock exists"), { code: "EEXIST" });
|
|
3737
|
+
}
|
|
3738
|
+
throw error;
|
|
3739
|
+
}
|
|
3740
|
+
createdSnapshot = { raw, payload, ownershipToken };
|
|
3741
|
+
const opened = await openSidecarRoot(lockRoot, relativeLockPath, "unlinked");
|
|
3742
|
+
if (!opened) {
|
|
3743
|
+
await waitForRetry();
|
|
3744
|
+
continue;
|
|
3745
|
+
}
|
|
3746
|
+
try {
|
|
3747
|
+
// Root.open admits the current file, not necessarily the one we created.
|
|
3748
|
+
const currentRaw = await readFileHandleBounded(opened.handle, Buffer.byteLength(raw));
|
|
3749
|
+
if (!currentRaw.equals(Buffer.from(raw))) {
|
|
3750
|
+
throw new errors_FsSafeError("path-mismatch", "created sidecar lock changed before admission");
|
|
3751
|
+
}
|
|
3752
|
+
}
|
|
3753
|
+
catch (error) {
|
|
3754
|
+
await opened.handle.close().catch(() => undefined);
|
|
3755
|
+
throw error;
|
|
3756
|
+
}
|
|
3757
|
+
handle = opened.handle;
|
|
3758
|
+
}
|
|
3759
|
+
else {
|
|
3760
|
+
try {
|
|
3761
|
+
handle =
|
|
3762
|
+
(await createNativeExclusiveFile(lockPath, 0o600)) ??
|
|
3763
|
+
(await promises_namespaceObject.open(lockPath, "wx"));
|
|
3764
|
+
}
|
|
3765
|
+
catch (createError) {
|
|
3766
|
+
lockFileCreateDenied = isTransientLockFileDenial(createError, lockPath);
|
|
3767
|
+
throw createError;
|
|
3768
|
+
}
|
|
3769
|
+
await handle.writeFile(raw, "utf8");
|
|
3770
|
+
}
|
|
3771
|
+
const snapshot = { raw, payload, stat: await handle.stat(), ownershipToken };
|
|
3772
|
+
if (snapshot.stat.nlink === 0) {
|
|
3773
|
+
await handle.close();
|
|
3774
|
+
handle = null;
|
|
3775
|
+
await waitForRetry();
|
|
3776
|
+
continue;
|
|
3777
|
+
}
|
|
3778
|
+
const createdHeld = {
|
|
3779
|
+
refCount: 1,
|
|
3780
|
+
reentrantOwner: options.reentrantOwner,
|
|
3781
|
+
handle,
|
|
3782
|
+
lockPath,
|
|
3783
|
+
snapshot,
|
|
3784
|
+
acquiredAt: Date.now(),
|
|
3785
|
+
metadata: options.metadata ?? {},
|
|
3786
|
+
lockRoot: options.lockRoot,
|
|
3787
|
+
parsePayload: options.parsePayload,
|
|
3788
|
+
};
|
|
3789
|
+
context.held.set(normalizedTargetPath, createdHeld);
|
|
3790
|
+
if (ownsReclaimGuard) {
|
|
3791
|
+
try {
|
|
3792
|
+
await releaseSidecarReclaimGuard(context.reclaimGuards, reclaimGuardPath);
|
|
3793
|
+
ownsReclaimGuard = false;
|
|
3794
|
+
}
|
|
3795
|
+
catch (err) {
|
|
3796
|
+
await context.releaseHeldLock(normalizedTargetPath, createdHeld, { force: true });
|
|
3797
|
+
throw err;
|
|
3798
|
+
}
|
|
3799
|
+
}
|
|
3800
|
+
const returnedHandle = context.handleForHeldLock(normalizedTargetPath, createdHeld);
|
|
3801
|
+
const interval = options.compromiseCheckIntervalMs;
|
|
3802
|
+
if (options.onCompromised && interval !== undefined && interval > 0) {
|
|
3803
|
+
createdHeld.compromiseTimer = setInterval(() => {
|
|
3804
|
+
void returnedHandle
|
|
3805
|
+
.verifyStillHeld()
|
|
3806
|
+
.catch(() => false)
|
|
3807
|
+
.then((stillHeld) => {
|
|
3808
|
+
if (!stillHeld && createdHeld.compromiseTimer) {
|
|
3809
|
+
clearInterval(createdHeld.compromiseTimer);
|
|
3810
|
+
createdHeld.compromiseTimer = undefined;
|
|
3811
|
+
options.onCompromised?.({ lockPath, normalizedTargetPath });
|
|
3812
|
+
}
|
|
3813
|
+
});
|
|
3814
|
+
}, interval);
|
|
3815
|
+
createdHeld.compromiseTimer.unref();
|
|
3816
|
+
}
|
|
3817
|
+
return returnedHandle;
|
|
3818
|
+
}
|
|
3819
|
+
catch (err) {
|
|
3820
|
+
try {
|
|
3821
|
+
if (handle) {
|
|
3822
|
+
const failedSnapshot = createdSnapshot ?? { payload: null };
|
|
3823
|
+
try {
|
|
3824
|
+
failedSnapshot.stat = await handle.stat();
|
|
3825
|
+
}
|
|
3826
|
+
catch {
|
|
3827
|
+
// Best-effort cleanup of a failed exclusive create.
|
|
3828
|
+
}
|
|
3829
|
+
const current = context.held.get(normalizedTargetPath);
|
|
3830
|
+
if (current?.handle === handle) {
|
|
3831
|
+
context.held.delete(normalizedTargetPath);
|
|
3832
|
+
}
|
|
3833
|
+
await handle.close().catch(() => undefined);
|
|
3834
|
+
// Root-created records retain the creator's byte/token receipt;
|
|
3835
|
+
// partial direct writes use the exclusive descriptor's identity.
|
|
3836
|
+
await removeSidecarLockIfUnchanged(lockPath, failedSnapshot, {
|
|
3837
|
+
lockRoot: options.lockRoot,
|
|
3838
|
+
parsePayload: options.parsePayload,
|
|
3839
|
+
});
|
|
3840
|
+
}
|
|
3841
|
+
else if (createdSnapshot) {
|
|
3842
|
+
await removeSidecarLockIfUnchanged(lockPath, createdSnapshot, {
|
|
3843
|
+
lockRoot: options.lockRoot,
|
|
3844
|
+
parsePayload: options.parsePayload,
|
|
3845
|
+
});
|
|
3846
|
+
}
|
|
3847
|
+
}
|
|
3848
|
+
catch (cleanupError) {
|
|
3849
|
+
throw createSuppressedError(cleanupError, err, "file lock acquisition and cleanup both failed");
|
|
3850
|
+
}
|
|
3851
|
+
if (lockFileCreateDenied && withinDenialBudget()) {
|
|
3852
|
+
await retryOrRethrowDenial(err);
|
|
3853
|
+
continue;
|
|
3854
|
+
}
|
|
3855
|
+
if (err.code !== "EEXIST") {
|
|
3856
|
+
throw err;
|
|
3857
|
+
}
|
|
3858
|
+
if (ownsReclaimGuard) {
|
|
3859
|
+
await releaseSidecarReclaimGuard(context.reclaimGuards, reclaimGuardPath);
|
|
3860
|
+
ownsReclaimGuard = false;
|
|
3861
|
+
await waitForRetry();
|
|
3862
|
+
continue;
|
|
3863
|
+
}
|
|
3864
|
+
const nowMs = Date.now();
|
|
3865
|
+
let rawSnapshot;
|
|
3866
|
+
let lockFileOpenDenied = false;
|
|
3867
|
+
try {
|
|
3868
|
+
rawSnapshot = await readSidecarLockRawSnapshot(lockPath, {
|
|
3869
|
+
lockRoot: options.lockRoot,
|
|
3870
|
+
rejectNonFile: true,
|
|
3871
|
+
discardObservation: "changed",
|
|
3872
|
+
onOpenFailure: (error) => { lockFileOpenDenied = isTransientLockFileDenial(error, lockPath); },
|
|
3873
|
+
});
|
|
3874
|
+
}
|
|
3875
|
+
catch (readErr) {
|
|
3876
|
+
if (!lockFileOpenDenied || !withinDenialBudget())
|
|
3877
|
+
throw readErr;
|
|
3878
|
+
await retryOrRethrowDenial(readErr);
|
|
3879
|
+
continue;
|
|
3880
|
+
}
|
|
3881
|
+
const snapshot = parseSidecarLockSnapshot(rawSnapshot, options.parsePayload);
|
|
3882
|
+
if (!snapshot) {
|
|
3883
|
+
await waitForRetry();
|
|
3884
|
+
continue;
|
|
3885
|
+
}
|
|
3886
|
+
if (context.held.has(normalizedTargetPath)) {
|
|
3887
|
+
await waitForRetry();
|
|
3888
|
+
continue;
|
|
3889
|
+
}
|
|
3890
|
+
const shouldReclaim = options.shouldReclaim ?? defaultSidecarLockShouldReclaim;
|
|
3891
|
+
if (await shouldReclaim({
|
|
3892
|
+
lockPath,
|
|
3893
|
+
normalizedTargetPath,
|
|
3894
|
+
payload: snapshot?.payload ?? null,
|
|
3895
|
+
staleMs: options.staleMs,
|
|
3896
|
+
nowMs,
|
|
3897
|
+
heldByThisProcess: context.held.has(normalizedTargetPath),
|
|
3898
|
+
})) {
|
|
3899
|
+
if (!(await sidecarLockSnapshotStillPresent(lockPath, snapshot, {
|
|
3900
|
+
lockRoot: options.lockRoot,
|
|
3901
|
+
parsePayload: options.parsePayload,
|
|
3902
|
+
}))) {
|
|
3903
|
+
await waitForRetry();
|
|
3904
|
+
continue;
|
|
3905
|
+
}
|
|
3906
|
+
const staleRecovery = options.staleRecovery ?? "fail-closed";
|
|
3907
|
+
if (staleRecovery === "remove-if-unchanged") {
|
|
3908
|
+
if (!(await tryAcquireSidecarReclaimGuard(context.reclaimGuards, reclaimGuardPath))) {
|
|
3909
|
+
await waitForRetry();
|
|
3910
|
+
continue;
|
|
3911
|
+
}
|
|
3912
|
+
ownsReclaimGuard = true;
|
|
3913
|
+
const removal = await removeStaleSidecarLockIfAllowed({
|
|
3914
|
+
lockPath,
|
|
3915
|
+
normalizedTargetPath,
|
|
3916
|
+
snapshot,
|
|
3917
|
+
shouldRemoveStaleLock: options.shouldRemoveStaleLock,
|
|
3918
|
+
lockRoot: options.lockRoot,
|
|
3919
|
+
parsePayload: options.parsePayload,
|
|
3920
|
+
});
|
|
3921
|
+
if (removal === "removed" || removal === "changed") {
|
|
3922
|
+
if (removal === "changed")
|
|
3923
|
+
await waitForRetry();
|
|
3924
|
+
continue;
|
|
3925
|
+
}
|
|
3926
|
+
await releaseSidecarReclaimGuard(context.reclaimGuards, reclaimGuardPath);
|
|
3927
|
+
ownsReclaimGuard = false;
|
|
3928
|
+
}
|
|
3929
|
+
throw Object.assign(new Error(`file lock stale for ${normalizedTargetPath}`), {
|
|
3930
|
+
code: "file_lock_stale",
|
|
3931
|
+
lockPath,
|
|
3932
|
+
normalizedTargetPath,
|
|
3933
|
+
});
|
|
3934
|
+
}
|
|
3935
|
+
await waitForRetry();
|
|
3936
|
+
}
|
|
3937
|
+
}
|
|
3783
3938
|
}
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
3939
|
+
finally {
|
|
3940
|
+
if (ownsReclaimGuard) {
|
|
3941
|
+
await releaseSidecarReclaimGuard(context.reclaimGuards, reclaimGuardPath).catch(() => undefined);
|
|
3942
|
+
}
|
|
3788
3943
|
}
|
|
3789
|
-
const root = external_node_path_namespaceObject.parse(filePath).root;
|
|
3790
|
-
assertNoDriveRelativePathSegments(filePath.slice(root.length).replaceAll("\\", "/"), label);
|
|
3791
|
-
}
|
|
3792
|
-
function isPromiseLike(value) {
|
|
3793
|
-
return Boolean(value &&
|
|
3794
|
-
(typeof value === "object" || typeof value === "function") &&
|
|
3795
|
-
"then" in value &&
|
|
3796
|
-
typeof value.then === "function");
|
|
3797
3944
|
}
|
|
3798
|
-
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
|
|
3807
|
-
|
|
3945
|
+
|
|
3946
|
+
;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/sidecar-lock-handle.js
|
|
3947
|
+
|
|
3948
|
+
function createSidecarLockHandle(params) {
|
|
3949
|
+
let released = false;
|
|
3950
|
+
let releasePromise;
|
|
3951
|
+
let releaseStarted = false;
|
|
3952
|
+
const release = async () => {
|
|
3953
|
+
if (released)
|
|
3954
|
+
return;
|
|
3955
|
+
if (!releasePromise) {
|
|
3956
|
+
const retry = releaseStarted;
|
|
3957
|
+
releaseStarted = true;
|
|
3958
|
+
releasePromise = (async () => {
|
|
3959
|
+
await params.release({ retry });
|
|
3960
|
+
released = true;
|
|
3961
|
+
})();
|
|
3962
|
+
}
|
|
3963
|
+
try {
|
|
3964
|
+
await releasePromise;
|
|
3965
|
+
}
|
|
3966
|
+
finally {
|
|
3967
|
+
releasePromise = undefined;
|
|
3968
|
+
}
|
|
3808
3969
|
};
|
|
3809
|
-
}
|
|
3810
|
-
function createLexicalTraversalContext(params) {
|
|
3811
3970
|
return {
|
|
3812
|
-
|
|
3813
|
-
|
|
3814
|
-
|
|
3815
|
-
|
|
3816
|
-
|
|
3971
|
+
lockPath: params.lockPath,
|
|
3972
|
+
normalizedTargetPath: params.normalizedTargetPath,
|
|
3973
|
+
verifyStillHeld: params.verifyStillHeld,
|
|
3974
|
+
release,
|
|
3975
|
+
[Symbol.asyncDispose]: release,
|
|
3817
3976
|
};
|
|
3818
3977
|
}
|
|
3819
|
-
function
|
|
3820
|
-
return
|
|
3821
|
-
.
|
|
3822
|
-
|
|
3823
|
-
|
|
3824
|
-
|
|
3825
|
-
|
|
3826
|
-
|
|
3827
|
-
|
|
3828
|
-
const root = external_node_path_namespaceObject.resolve(rootPath);
|
|
3829
|
-
const candidate = process.platform === "win32"
|
|
3830
|
-
? candidatePath.replaceAll("/", external_node_path_namespaceObject.sep)
|
|
3831
|
-
: candidatePath;
|
|
3832
|
-
if (candidate === root) {
|
|
3833
|
-
return "";
|
|
3834
|
-
}
|
|
3835
|
-
const rootWithSep = root.endsWith(external_node_path_namespaceObject.sep) ? root : `${root}${external_node_path_namespaceObject.sep}`;
|
|
3836
|
-
const candidatePrefix = candidate.slice(0, rootWithSep.length);
|
|
3837
|
-
const prefixMatches = process.platform === "win32"
|
|
3838
|
-
? candidatePrefix.toLowerCase() === rootWithSep.toLowerCase()
|
|
3839
|
-
: candidatePrefix === rootWithSep;
|
|
3840
|
-
return prefixMatches ? candidate.slice(rootWithSep.length) : undefined;
|
|
3841
|
-
}
|
|
3842
|
-
function assertLexicalCursorInsideBoundary(context, candidatePath) {
|
|
3843
|
-
assertInsideBoundary({
|
|
3844
|
-
boundaryLabel: context.resolveParams.boundaryLabel,
|
|
3845
|
-
rootCanonicalPath: context.rootCanonicalPath,
|
|
3846
|
-
candidatePath,
|
|
3847
|
-
absolutePath: context.absolutePath,
|
|
3848
|
-
});
|
|
3849
|
-
}
|
|
3850
|
-
function applyMissingSuffixToCanonicalCursor(context, missingFromIndex) {
|
|
3851
|
-
const missingSuffix = context.state.segments.slice(missingFromIndex);
|
|
3852
|
-
for (const segment of missingSuffix) {
|
|
3853
|
-
advanceCanonicalCursorForSegment(context, segment);
|
|
3854
|
-
}
|
|
3855
|
-
}
|
|
3856
|
-
function advanceCanonicalCursorForSegment(context, segment) {
|
|
3857
|
-
context.state.canonicalCursor = external_node_path_namespaceObject.resolve(context.state.canonicalCursor, segment);
|
|
3858
|
-
assertLexicalCursorInsideBoundary(context, context.state.canonicalCursor);
|
|
3859
|
-
}
|
|
3860
|
-
function finalizeLexicalResolution(context, kind) {
|
|
3861
|
-
assertLexicalCursorInsideBoundary(context, context.state.canonicalCursor);
|
|
3862
|
-
return buildResolvedRootPath({
|
|
3863
|
-
absolutePath: context.absolutePath,
|
|
3864
|
-
canonicalPath: context.state.canonicalCursor,
|
|
3865
|
-
rootPath: context.rootPath,
|
|
3866
|
-
rootCanonicalPath: context.rootCanonicalPath,
|
|
3867
|
-
kind,
|
|
3978
|
+
function createHeldSidecarLockHandle(params) {
|
|
3979
|
+
return createSidecarLockHandle({
|
|
3980
|
+
lockPath: params.held.lockPath,
|
|
3981
|
+
normalizedTargetPath: params.normalizedTargetPath,
|
|
3982
|
+
verifyStillHeld: async () => await sidecarLockSnapshotStillPresent(params.held.lockPath, params.held.snapshot, {
|
|
3983
|
+
lockRoot: params.held.lockRoot,
|
|
3984
|
+
parsePayload: params.held.parsePayload,
|
|
3985
|
+
}),
|
|
3986
|
+
release: (options) => params.release(options),
|
|
3868
3987
|
});
|
|
3869
3988
|
}
|
|
3870
|
-
|
|
3871
|
-
|
|
3872
|
-
|
|
3873
|
-
|
|
3874
|
-
|
|
3875
|
-
|
|
3876
|
-
|
|
3877
|
-
|
|
3878
|
-
|
|
3879
|
-
|
|
3880
|
-
|
|
3881
|
-
|
|
3882
|
-
|
|
3883
|
-
|
|
3884
|
-
|
|
3885
|
-
advanceCanonicalCursorForSegment(context, params.segment);
|
|
3886
|
-
return "continue";
|
|
3887
|
-
}
|
|
3888
|
-
if (context.resolveParams.rejectSymlinks === true && params.isLast) {
|
|
3889
|
-
throw new errors_FsSafeError("symlink", "symlink path component not allowed");
|
|
3890
|
-
}
|
|
3891
|
-
if (context.state.allowFinalSymlink && params.isLast) {
|
|
3892
|
-
context.state.preserveFinalSymlink = true;
|
|
3893
|
-
advanceCanonicalCursorForSegment(context, params.segment);
|
|
3894
|
-
return "break";
|
|
3989
|
+
|
|
3990
|
+
;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/sidecar-lock.js
|
|
3991
|
+
|
|
3992
|
+
|
|
3993
|
+
|
|
3994
|
+
|
|
3995
|
+
|
|
3996
|
+
|
|
3997
|
+
const GLOBAL_STATE_KEY = Symbol.for("fsSafe.sidecarLockManagers");
|
|
3998
|
+
const GLOBAL_CLEANUP_KEY = Symbol.for("fsSafe.sidecarLockCleanupRegistered");
|
|
3999
|
+
const GLOBAL_CLEANUP_HANDLER_KEY = Symbol.for("fsSafe.sidecarLockCleanupHandler");
|
|
4000
|
+
function getGlobalManagers() {
|
|
4001
|
+
const globalWithState = globalThis;
|
|
4002
|
+
if (!globalWithState[GLOBAL_STATE_KEY]) {
|
|
4003
|
+
globalWithState[GLOBAL_STATE_KEY] = new Map();
|
|
3895
4004
|
}
|
|
3896
|
-
return
|
|
4005
|
+
return globalWithState[GLOBAL_STATE_KEY];
|
|
3897
4006
|
}
|
|
3898
|
-
function
|
|
3899
|
-
|
|
3900
|
-
|
|
3901
|
-
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
|
|
4007
|
+
function resolveManagerState(key) {
|
|
4008
|
+
const managers = getGlobalManagers();
|
|
4009
|
+
let state = managers.get(key);
|
|
4010
|
+
if (!state) {
|
|
4011
|
+
state = {
|
|
4012
|
+
cleanupRegistered: false,
|
|
4013
|
+
held: new Map(),
|
|
4014
|
+
reclaimCleanupRegistered: false,
|
|
4015
|
+
reclaimGuards: new Set(),
|
|
4016
|
+
};
|
|
4017
|
+
managers.set(key, state);
|
|
3905
4018
|
}
|
|
3906
|
-
|
|
3907
|
-
|
|
4019
|
+
else {
|
|
4020
|
+
// The global manager symbol is shared across package copies and hot reloads.
|
|
4021
|
+
// Backfill state created by fs-safe versions that predate reclaim guards.
|
|
4022
|
+
state.reclaimCleanupRegistered ??= false;
|
|
4023
|
+
state.reclaimGuards ??= new Set();
|
|
4024
|
+
for (const held of state.held.values()) {
|
|
4025
|
+
held.refCount ??= 1;
|
|
4026
|
+
}
|
|
4027
|
+
}
|
|
4028
|
+
return state;
|
|
3908
4029
|
}
|
|
3909
|
-
function
|
|
4030
|
+
function snapshotMatchesSync(lockPath, observed) {
|
|
4031
|
+
let fd;
|
|
3910
4032
|
try {
|
|
3911
|
-
const
|
|
3912
|
-
if (
|
|
3913
|
-
return
|
|
4033
|
+
const beforeStat = external_node_fs_namespaceObject.lstatSync(lockPath);
|
|
4034
|
+
if (!beforeStat.isFile()) {
|
|
4035
|
+
return false;
|
|
3914
4036
|
}
|
|
3915
|
-
|
|
3916
|
-
|
|
3917
|
-
|
|
3918
|
-
|
|
4037
|
+
const openFlags = external_node_fs_namespaceObject.constants.O_RDONLY |
|
|
4038
|
+
(process.platform !== "win32" && typeof external_node_fs_namespaceObject.constants.O_NOFOLLOW === "number"
|
|
4039
|
+
? external_node_fs_namespaceObject.constants.O_NOFOLLOW
|
|
4040
|
+
: 0) |
|
|
4041
|
+
(typeof external_node_fs_namespaceObject.constants.O_NONBLOCK === "number" ? external_node_fs_namespaceObject.constants.O_NONBLOCK : 0);
|
|
4042
|
+
fd = external_node_fs_namespaceObject.openSync(lockPath, openFlags);
|
|
4043
|
+
const openedStat = external_node_fs_namespaceObject.fstatSync(fd);
|
|
4044
|
+
if (!openedStat.isFile()) {
|
|
4045
|
+
return false;
|
|
4046
|
+
}
|
|
4047
|
+
if (observed.raw !== undefined && openedStat.size !== Buffer.byteLength(observed.raw)) {
|
|
4048
|
+
return false;
|
|
4049
|
+
}
|
|
4050
|
+
const raw = external_node_fs_namespaceObject.readFileSync(fd, "utf8");
|
|
4051
|
+
const afterStat = external_node_fs_namespaceObject.lstatSync(lockPath);
|
|
4052
|
+
if (!afterStat.isFile() || !file_identity_sameFileIdentity(beforeStat, afterStat)) {
|
|
4053
|
+
return false;
|
|
4054
|
+
}
|
|
4055
|
+
return sidecarLockSnapshotMatches({ raw, payload: null, stat: afterStat }, observed);
|
|
3919
4056
|
}
|
|
3920
|
-
|
|
3921
|
-
|
|
3922
|
-
const linkCanonical = params.resolveLinkCanonical(context.state.lexicalCursor);
|
|
3923
|
-
if (isPromiseLike(linkCanonical)) {
|
|
3924
|
-
return Promise.resolve(linkCanonical).then((value) => {
|
|
3925
|
-
applyResolvedSymlinkHop(context, value);
|
|
3926
|
-
});
|
|
4057
|
+
catch {
|
|
4058
|
+
return false;
|
|
3927
4059
|
}
|
|
3928
|
-
|
|
3929
|
-
|
|
3930
|
-
|
|
3931
|
-
|
|
3932
|
-
|
|
3933
|
-
|
|
3934
|
-
|
|
3935
|
-
|
|
3936
|
-
|
|
3937
|
-
const isLast = idx === state.segments.length - 1;
|
|
3938
|
-
yield { idx, segment, isLast };
|
|
4060
|
+
finally {
|
|
4061
|
+
if (fd !== undefined) {
|
|
4062
|
+
try {
|
|
4063
|
+
external_node_fs_namespaceObject.closeSync(fd);
|
|
4064
|
+
}
|
|
4065
|
+
catch {
|
|
4066
|
+
// Best-effort process-exit cleanup.
|
|
4067
|
+
}
|
|
4068
|
+
}
|
|
3939
4069
|
}
|
|
3940
4070
|
}
|
|
3941
|
-
|
|
3942
|
-
const
|
|
3943
|
-
|
|
3944
|
-
|
|
3945
|
-
|
|
3946
|
-
applyParentTraversalStep(context);
|
|
3947
|
-
continue;
|
|
3948
|
-
}
|
|
3949
|
-
state.lexicalCursor = external_node_path_namespaceObject.join(state.lexicalCursor, segment);
|
|
3950
|
-
const stat = await readLexicalStat(context, {
|
|
3951
|
-
missingFromIndex: idx,
|
|
3952
|
-
read: (cursor) => promises_namespaceObject.lstat(cursor),
|
|
3953
|
-
});
|
|
3954
|
-
if (!stat) {
|
|
3955
|
-
break;
|
|
4071
|
+
function releaseAllReclaimGuardsSync(state) {
|
|
4072
|
+
for (const reclaimGuardPath of state.reclaimGuards) {
|
|
4073
|
+
try {
|
|
4074
|
+
external_node_fs_namespaceObject.rmdirSync(reclaimGuardPath);
|
|
4075
|
+
state.reclaimGuards.delete(reclaimGuardPath);
|
|
3956
4076
|
}
|
|
3957
|
-
|
|
3958
|
-
|
|
3959
|
-
segment,
|
|
3960
|
-
isLast,
|
|
3961
|
-
});
|
|
3962
|
-
if (disposition === "continue") {
|
|
3963
|
-
continue;
|
|
4077
|
+
catch {
|
|
4078
|
+
// Best-effort process-exit cleanup. A surviving guard fails closed.
|
|
3964
4079
|
}
|
|
3965
|
-
|
|
3966
|
-
|
|
4080
|
+
}
|
|
4081
|
+
}
|
|
4082
|
+
function releaseAllLocksSync(state) {
|
|
4083
|
+
for (const [normalizedTargetPath, held] of state.held) {
|
|
4084
|
+
void held.handle.close().catch(() => undefined);
|
|
4085
|
+
try {
|
|
4086
|
+
if (!held.lockRoot && snapshotMatchesSync(held.lockPath, held.snapshot)) {
|
|
4087
|
+
external_node_fs_namespaceObject.rmSync(held.lockPath, { force: true });
|
|
4088
|
+
}
|
|
3967
4089
|
}
|
|
3968
|
-
|
|
3969
|
-
|
|
3970
|
-
});
|
|
3971
|
-
if (context.resolveParams.rejectSymlinks === true) {
|
|
3972
|
-
throw new errors_FsSafeError("symlink", "symlink path component not allowed");
|
|
4090
|
+
catch {
|
|
4091
|
+
// Best-effort process-exit cleanup.
|
|
3973
4092
|
}
|
|
4093
|
+
state.held.delete(normalizedTargetPath);
|
|
3974
4094
|
}
|
|
3975
|
-
|
|
3976
|
-
return finalizeLexicalResolution(context, kind);
|
|
4095
|
+
releaseAllReclaimGuardsSync(state);
|
|
3977
4096
|
}
|
|
3978
|
-
function
|
|
3979
|
-
const
|
|
3980
|
-
|
|
3981
|
-
|
|
3982
|
-
|
|
3983
|
-
|
|
3984
|
-
|
|
3985
|
-
|
|
3986
|
-
continue;
|
|
4097
|
+
function ensureGlobalExitCleanupRegistered() {
|
|
4098
|
+
const globalWithCleanup = globalThis;
|
|
4099
|
+
if (globalWithCleanup[GLOBAL_CLEANUP_KEY])
|
|
4100
|
+
return;
|
|
4101
|
+
globalWithCleanup[GLOBAL_CLEANUP_KEY] = true;
|
|
4102
|
+
const cleanup = () => {
|
|
4103
|
+
for (const state of getGlobalManagers().values()) {
|
|
4104
|
+
releaseAllLocksSync(state);
|
|
3987
4105
|
}
|
|
3988
|
-
|
|
3989
|
-
|
|
3990
|
-
|
|
3991
|
-
|
|
4106
|
+
};
|
|
4107
|
+
globalWithCleanup[GLOBAL_CLEANUP_HANDLER_KEY] = cleanup;
|
|
4108
|
+
process.on("exit", cleanup);
|
|
4109
|
+
}
|
|
4110
|
+
async function releaseHeldLock(state, normalizedTargetPath, held, options = {}) {
|
|
4111
|
+
const current = state.held.get(normalizedTargetPath);
|
|
4112
|
+
if (current !== held) {
|
|
4113
|
+
return false;
|
|
4114
|
+
}
|
|
4115
|
+
if (held.releasePromise) {
|
|
4116
|
+
await held.releasePromise;
|
|
4117
|
+
return true;
|
|
4118
|
+
}
|
|
4119
|
+
if (options.force) {
|
|
4120
|
+
held.refCount = 0;
|
|
4121
|
+
}
|
|
4122
|
+
else if (!options.retry && held.refCount > 0) {
|
|
4123
|
+
held.refCount -= 1;
|
|
4124
|
+
}
|
|
4125
|
+
if (held.refCount > 0) {
|
|
4126
|
+
return false;
|
|
4127
|
+
}
|
|
4128
|
+
held.releasePromise = (async () => {
|
|
4129
|
+
await held.handle.close().catch(() => undefined);
|
|
4130
|
+
await removeSidecarLockIfUnchanged(held.lockPath, held.snapshot, {
|
|
4131
|
+
lockRoot: held.lockRoot,
|
|
4132
|
+
parsePayload: held.parsePayload,
|
|
3992
4133
|
});
|
|
3993
|
-
if (
|
|
3994
|
-
|
|
4134
|
+
if (state.held.get(normalizedTargetPath) === held) {
|
|
4135
|
+
state.held.delete(normalizedTargetPath);
|
|
3995
4136
|
}
|
|
3996
|
-
|
|
3997
|
-
|
|
3998
|
-
|
|
4137
|
+
if (held.compromiseTimer) {
|
|
4138
|
+
clearInterval(held.compromiseTimer);
|
|
4139
|
+
held.compromiseTimer = undefined;
|
|
3999
4140
|
}
|
|
4000
|
-
|
|
4001
|
-
|
|
4002
|
-
|
|
4003
|
-
|
|
4141
|
+
})();
|
|
4142
|
+
try {
|
|
4143
|
+
await held.releasePromise;
|
|
4144
|
+
return true;
|
|
4145
|
+
}
|
|
4146
|
+
finally {
|
|
4147
|
+
held.releasePromise = undefined;
|
|
4148
|
+
}
|
|
4149
|
+
}
|
|
4150
|
+
function handleForHeldLock(state, normalizedTargetPath, held) {
|
|
4151
|
+
return createHeldSidecarLockHandle({
|
|
4152
|
+
normalizedTargetPath,
|
|
4153
|
+
held,
|
|
4154
|
+
release: async (options) => await releaseHeldLock(state, normalizedTargetPath, held, { retry: options?.retry }),
|
|
4155
|
+
});
|
|
4156
|
+
}
|
|
4157
|
+
function createSidecarLockManager(key) {
|
|
4158
|
+
const state = resolveManagerState(key);
|
|
4159
|
+
function ensureExitCleanupRegistered() {
|
|
4160
|
+
state.cleanupRegistered = true;
|
|
4161
|
+
state.reclaimCleanupRegistered = true;
|
|
4162
|
+
ensureGlobalExitCleanupRegistered();
|
|
4163
|
+
}
|
|
4164
|
+
async function acquire(options) {
|
|
4165
|
+
return await acquireSidecarLock(options, {
|
|
4166
|
+
held: state.held,
|
|
4167
|
+
reclaimGuards: state.reclaimGuards,
|
|
4168
|
+
ensureExitCleanupRegistered,
|
|
4169
|
+
handleForHeldLock: (normalizedTargetPath, held) => handleForHeldLock(state, normalizedTargetPath, held),
|
|
4170
|
+
releaseHeldLock: async (normalizedTargetPath, held, releaseOptions) => await releaseHeldLock(state, normalizedTargetPath, held, releaseOptions),
|
|
4004
4171
|
});
|
|
4005
|
-
|
|
4006
|
-
|
|
4007
|
-
|
|
4008
|
-
|
|
4009
|
-
|
|
4172
|
+
}
|
|
4173
|
+
async function withLock(options, fn) {
|
|
4174
|
+
const lock = await acquire(options);
|
|
4175
|
+
let result;
|
|
4176
|
+
try {
|
|
4177
|
+
result = await fn();
|
|
4010
4178
|
}
|
|
4011
|
-
|
|
4012
|
-
|
|
4013
|
-
|
|
4014
|
-
|
|
4015
|
-
|
|
4179
|
+
catch (bodyError) {
|
|
4180
|
+
try {
|
|
4181
|
+
await lock.release();
|
|
4182
|
+
}
|
|
4183
|
+
catch (releaseError) {
|
|
4184
|
+
throw createSuppressedError(releaseError, bodyError, "file lock callback and release both failed");
|
|
4185
|
+
}
|
|
4186
|
+
throw bodyError;
|
|
4016
4187
|
}
|
|
4017
|
-
|
|
4018
|
-
|
|
4188
|
+
await lock.release();
|
|
4189
|
+
return result;
|
|
4190
|
+
}
|
|
4191
|
+
async function drain() {
|
|
4192
|
+
for (const [normalizedTargetPath, held] of Array.from(state.held.entries())) {
|
|
4193
|
+
await releaseHeldLock(state, normalizedTargetPath, held, { force: true }).catch(() => undefined);
|
|
4019
4194
|
}
|
|
4020
4195
|
}
|
|
4021
|
-
|
|
4022
|
-
|
|
4196
|
+
function reset() {
|
|
4197
|
+
releaseAllLocksSync(state);
|
|
4198
|
+
}
|
|
4199
|
+
function heldEntries() {
|
|
4200
|
+
return Array.from(state.held.entries()).map(([normalizedTargetPath, held]) => ({
|
|
4201
|
+
normalizedTargetPath,
|
|
4202
|
+
lockPath: held.lockPath,
|
|
4203
|
+
acquiredAt: held.acquiredAt,
|
|
4204
|
+
metadata: held.metadata,
|
|
4205
|
+
forceRelease: () => releaseHeldLock(state, normalizedTargetPath, held, { force: true }),
|
|
4206
|
+
}));
|
|
4207
|
+
}
|
|
4208
|
+
return { acquire, withLock, drain, reset, heldEntries };
|
|
4023
4209
|
}
|
|
4024
|
-
function
|
|
4025
|
-
|
|
4210
|
+
async function withSidecarLock(targetPath, options, fn) {
|
|
4211
|
+
const manager = createSidecarLockManager(options.managerKey ?? `fs-safe.sidecar-lock:${targetPath}`);
|
|
4212
|
+
const { managerKey: _managerKey, ...acquireOptions } = options;
|
|
4213
|
+
return await manager.withLock({ ...acquireOptions, targetPath }, fn);
|
|
4026
4214
|
}
|
|
4027
|
-
|
|
4028
|
-
|
|
4029
|
-
|
|
4030
|
-
|
|
4031
|
-
|
|
4032
|
-
|
|
4033
|
-
|
|
4034
|
-
|
|
4035
|
-
|
|
4036
|
-
|
|
4037
|
-
|
|
4038
|
-
|
|
4039
|
-
|
|
4040
|
-
|
|
4041
|
-
|
|
4042
|
-
|
|
4043
|
-
|
|
4044
|
-
|
|
4045
|
-
|
|
4046
|
-
|
|
4047
|
-
|
|
4048
|
-
|
|
4215
|
+
|
|
4216
|
+
;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/pinned-write.js
|
|
4217
|
+
|
|
4218
|
+
|
|
4219
|
+
|
|
4220
|
+
|
|
4221
|
+
|
|
4222
|
+
|
|
4223
|
+
|
|
4224
|
+
|
|
4225
|
+
|
|
4226
|
+
|
|
4227
|
+
|
|
4228
|
+
|
|
4229
|
+
|
|
4230
|
+
|
|
4231
|
+
|
|
4232
|
+
|
|
4233
|
+
|
|
4234
|
+
function byteLength(input, encoding) {
|
|
4235
|
+
return typeof input === "string"
|
|
4236
|
+
? Buffer.byteLength(input, encoding ?? "utf8")
|
|
4237
|
+
: input.byteLength;
|
|
4049
4238
|
}
|
|
4050
|
-
|
|
4051
|
-
if (
|
|
4052
|
-
|
|
4239
|
+
function assertSafeBasename(basename) {
|
|
4240
|
+
if (!basename ||
|
|
4241
|
+
basename === "." ||
|
|
4242
|
+
basename === ".." ||
|
|
4243
|
+
basename.includes("/") ||
|
|
4244
|
+
basename.includes("\0")) {
|
|
4245
|
+
throw new errors_FsSafeError("invalid-path", "invalid target path");
|
|
4053
4246
|
}
|
|
4054
|
-
const kind = await getPathKind(params.context.absolutePath, false);
|
|
4055
|
-
return buildOutsideRootPathFromContext({
|
|
4056
|
-
boundaryLabel: params.boundaryLabel,
|
|
4057
|
-
context: params.context,
|
|
4058
|
-
kind,
|
|
4059
|
-
});
|
|
4060
4247
|
}
|
|
4061
|
-
function
|
|
4062
|
-
if (
|
|
4063
|
-
|
|
4248
|
+
function assertWithinMaxBytes(bytes, maxBytes) {
|
|
4249
|
+
if (maxBytes !== undefined && bytes > maxBytes) {
|
|
4250
|
+
throw new errors_FsSafeError("too-large", `file exceeds limit of ${maxBytes} bytes (got at least ${bytes})`);
|
|
4064
4251
|
}
|
|
4065
|
-
const kind = getPathKindSync(params.context.absolutePath, false);
|
|
4066
|
-
return buildOutsideRootPathFromContext({
|
|
4067
|
-
boundaryLabel: params.boundaryLabel,
|
|
4068
|
-
context: params.context,
|
|
4069
|
-
kind,
|
|
4070
|
-
});
|
|
4071
|
-
}
|
|
4072
|
-
function buildOutsideRootPathFromContext(params) {
|
|
4073
|
-
return buildOutsideLexicalRootPath({
|
|
4074
|
-
boundaryLabel: params.boundaryLabel,
|
|
4075
|
-
rootCanonicalPath: params.context.rootCanonicalPath,
|
|
4076
|
-
absolutePath: params.context.absolutePath,
|
|
4077
|
-
canonicalOutsideLexicalPath: params.context.canonicalOutsideLexicalPath,
|
|
4078
|
-
rootPath: params.context.rootPath,
|
|
4079
|
-
kind: params.kind,
|
|
4080
|
-
});
|
|
4081
4252
|
}
|
|
4082
|
-
async function
|
|
4083
|
-
|
|
4084
|
-
|
|
4253
|
+
async function syncFileBestEffort(handle) {
|
|
4254
|
+
try {
|
|
4255
|
+
await handle.sync();
|
|
4256
|
+
}
|
|
4257
|
+
catch (error) {
|
|
4258
|
+
if (error?.code !== "EPERM") {
|
|
4259
|
+
throw error;
|
|
4260
|
+
}
|
|
4085
4261
|
}
|
|
4086
|
-
return await resolvePathViaExistingAncestor(params.absolutePath);
|
|
4087
4262
|
}
|
|
4088
|
-
function
|
|
4089
|
-
|
|
4090
|
-
|
|
4263
|
+
async function writeStreamToHandle(stream, handle, maxBytes) {
|
|
4264
|
+
let bytes = 0;
|
|
4265
|
+
for await (const chunk of stream) {
|
|
4266
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
4267
|
+
bytes += buffer.byteLength;
|
|
4268
|
+
assertWithinMaxBytes(bytes, maxBytes);
|
|
4269
|
+
let offset = 0;
|
|
4270
|
+
while (offset < buffer.byteLength) {
|
|
4271
|
+
const { bytesWritten } = await handle.write(buffer, offset, buffer.byteLength - offset);
|
|
4272
|
+
if (bytesWritten <= 0) {
|
|
4273
|
+
throw new errors_FsSafeError("helper-failed", "fallback stream write made no progress");
|
|
4274
|
+
}
|
|
4275
|
+
offset += bytesWritten;
|
|
4276
|
+
}
|
|
4091
4277
|
}
|
|
4092
|
-
return resolvePathViaExistingAncestorSync(params.absolutePath);
|
|
4093
4278
|
}
|
|
4094
|
-
function
|
|
4095
|
-
|
|
4096
|
-
|
|
4097
|
-
|
|
4098
|
-
|
|
4099
|
-
absolutePath: params.absolutePath,
|
|
4100
|
-
});
|
|
4101
|
-
return buildResolvedRootPath({
|
|
4102
|
-
absolutePath: params.absolutePath,
|
|
4103
|
-
canonicalPath: params.canonicalOutsideLexicalPath,
|
|
4104
|
-
rootPath: params.rootPath,
|
|
4105
|
-
rootCanonicalPath: params.rootCanonicalPath,
|
|
4106
|
-
kind: params.kind,
|
|
4279
|
+
async function runPinnedWriteHelper(params) {
|
|
4280
|
+
const normalizedParams = { ...params, maxBytes: byte_budget_normalizeMaxBytes(params.maxBytes) };
|
|
4281
|
+
assertSafeBasename(params.basename);
|
|
4282
|
+
validatePinnedOperationPayload({
|
|
4283
|
+
relativeParentPath: params.relativeParentPath,
|
|
4107
4284
|
});
|
|
4108
|
-
|
|
4109
|
-
|
|
4110
|
-
if (
|
|
4111
|
-
return;
|
|
4285
|
+
// The explicit compatibility policy uses the guarded Node fallback, where
|
|
4286
|
+
// content verification can replace the strict post-rename inode check.
|
|
4287
|
+
if (normalizedParams.onRenameIdentityMismatch === "verify-content") {
|
|
4288
|
+
return await runPinnedWriteFallback(normalizedParams);
|
|
4112
4289
|
}
|
|
4113
|
-
|
|
4114
|
-
|
|
4290
|
+
const native = getNativeBinding();
|
|
4291
|
+
if (native) {
|
|
4292
|
+
return await runPinnedWriteNative(native, normalizedParams);
|
|
4115
4293
|
}
|
|
4116
|
-
|
|
4117
|
-
boundaryLabel: params.boundaryLabel,
|
|
4118
|
-
rootPath: params.rootPath,
|
|
4119
|
-
absolutePath: params.absolutePath,
|
|
4120
|
-
});
|
|
4294
|
+
return await runPinnedWriteFallback(normalizedParams);
|
|
4121
4295
|
}
|
|
4122
|
-
function
|
|
4123
|
-
|
|
4124
|
-
|
|
4125
|
-
|
|
4126
|
-
|
|
4127
|
-
|
|
4128
|
-
|
|
4129
|
-
|
|
4130
|
-
|
|
4131
|
-
|
|
4296
|
+
async function runPinnedWriteWithRenamePolicy(params) {
|
|
4297
|
+
const { targetPath, renameIdentity, ...writeParams } = params;
|
|
4298
|
+
if (renameIdentity !== "verify-content-with-lock") {
|
|
4299
|
+
return await runPinnedWriteHelper(writeParams);
|
|
4300
|
+
}
|
|
4301
|
+
const relativeTargetPath = writeParams.relativeParentPath
|
|
4302
|
+
? `${writeParams.relativeParentPath}/${writeParams.basename}`
|
|
4303
|
+
: writeParams.basename;
|
|
4304
|
+
const lockPath = external_node_path_namespaceObject.join(writeParams.rootPath, `.fs-safe-write-${sha256Hex(relativeTargetPath)}.lock`);
|
|
4305
|
+
return await withSidecarLock(writeParams.rootPath, {
|
|
4306
|
+
managerKey: `fs-safe.write:${targetPath}`,
|
|
4307
|
+
lockPath,
|
|
4308
|
+
staleMs: 30_000,
|
|
4309
|
+
timeoutMs: 5_000,
|
|
4310
|
+
payload: () => ({ pid: process.pid, createdAt: new Date().toISOString() }),
|
|
4311
|
+
retry: { retries: 5, minTimeout: 100, maxTimeout: 2_000, factor: 2 },
|
|
4312
|
+
}, async () => await runPinnedWriteHelper({
|
|
4313
|
+
...writeParams,
|
|
4314
|
+
onRenameIdentityMismatch: "verify-content",
|
|
4315
|
+
}));
|
|
4132
4316
|
}
|
|
4133
|
-
async function
|
|
4134
|
-
|
|
4135
|
-
|
|
4136
|
-
|
|
4137
|
-
|
|
4138
|
-
|
|
4317
|
+
async function runPinnedWriteFallback(params) {
|
|
4318
|
+
let parentPath = params.relativeParentPath
|
|
4319
|
+
? external_node_path_namespaceObject.join(params.rootPath, ...params.relativeParentPath.split("/"))
|
|
4320
|
+
: params.rootPath;
|
|
4321
|
+
if (params.mkdir) {
|
|
4322
|
+
// mkdirPathComponentsWithGuards may resolve the final component through
|
|
4323
|
+
// an in-root symlink (e.g. a skill-bank layout). Use its returned real
|
|
4324
|
+
// path for the subsequent guard and target path so we don't re-check the
|
|
4325
|
+
// original, possibly-symlinked, lexical path and reject it outright.
|
|
4326
|
+
parentPath = await mkdirPathComponentsWithGuards({
|
|
4327
|
+
rootReal: params.rootPath,
|
|
4328
|
+
targetPath: parentPath,
|
|
4329
|
+
beforeComponent: async (componentPath) => await getFsSafeTestHooks()?.beforeRootFallbackMutation?.("mkdir", componentPath),
|
|
4330
|
+
});
|
|
4139
4331
|
}
|
|
4140
|
-
|
|
4141
|
-
|
|
4142
|
-
|
|
4332
|
+
const parentGuard = params.mkdir
|
|
4333
|
+
? await directory_guard_createAsyncDirectoryGuard(parentPath)
|
|
4334
|
+
: await directory_guard_createNearestExistingDirectoryGuard(params.rootPath, parentPath);
|
|
4335
|
+
const targetPath = external_node_path_namespaceObject.join(parentPath, params.basename);
|
|
4336
|
+
if (params.overwrite === false) {
|
|
4337
|
+
const handle = await withAsyncDirectoryGuards([parentGuard], async () => await promises_namespaceObject.open(targetPath, external_node_fs_namespaceObject.constants.O_WRONLY | external_node_fs_namespaceObject.constants.O_CREAT | external_node_fs_namespaceObject.constants.O_EXCL, params.mode), {
|
|
4338
|
+
onPostGuardFailure: async (openedHandle) => {
|
|
4339
|
+
// The parent failed verification, so targetPath may now resolve
|
|
4340
|
+
// somewhere else. Close the fd, but do not clean up by path.
|
|
4341
|
+
await openedHandle.close().catch(() => undefined);
|
|
4342
|
+
},
|
|
4343
|
+
});
|
|
4344
|
+
let created = true;
|
|
4345
|
+
try {
|
|
4346
|
+
const verificationIdentity = await handle.stat({ bigint: true });
|
|
4347
|
+
await handle.chmod(params.mode);
|
|
4348
|
+
if (params.input.kind === "buffer") {
|
|
4349
|
+
assertWithinMaxBytes(byteLength(params.input.data, params.input.encoding), params.maxBytes);
|
|
4350
|
+
if (typeof params.input.data === "string") {
|
|
4351
|
+
await handle.writeFile(params.input.data, params.input.encoding ?? "utf8");
|
|
4352
|
+
}
|
|
4353
|
+
else {
|
|
4354
|
+
await handle.writeFile(params.input.data);
|
|
4355
|
+
}
|
|
4356
|
+
}
|
|
4357
|
+
else {
|
|
4358
|
+
await writeStreamToHandle(params.input.stream, handle, params.maxBytes);
|
|
4359
|
+
}
|
|
4360
|
+
await syncFileBestEffort(handle);
|
|
4361
|
+
const stat = await handle.stat();
|
|
4362
|
+
await syncDirectoryBestEffort(parentPath);
|
|
4363
|
+
// Publication is complete. A failed outer check must not remove its target.
|
|
4364
|
+
created = false;
|
|
4365
|
+
await params.verifyPublished?.(handle.fd, verificationIdentity, parentGuard);
|
|
4366
|
+
return { dev: stat.dev, ino: stat.ino };
|
|
4367
|
+
}
|
|
4368
|
+
finally {
|
|
4369
|
+
await handle.close().catch(() => undefined);
|
|
4370
|
+
if (created) {
|
|
4371
|
+
await promises_namespaceObject.rm(targetPath, { force: true }).catch(() => undefined);
|
|
4372
|
+
}
|
|
4143
4373
|
}
|
|
4144
|
-
throw error;
|
|
4145
4374
|
}
|
|
4146
|
-
|
|
4147
|
-
|
|
4375
|
+
// Private staging must not consume the destination basename's filename budget.
|
|
4376
|
+
const tempPath = external_node_path_namespaceObject.join(parentPath, `.fs-safe-${(0,external_node_crypto_namespaceObject.randomUUID)()}.tmp`);
|
|
4377
|
+
const tempFlags = external_node_fs_namespaceObject.constants.O_WRONLY |
|
|
4378
|
+
external_node_fs_namespaceObject.constants.O_CREAT |
|
|
4379
|
+
external_node_fs_namespaceObject.constants.O_EXCL |
|
|
4380
|
+
(process.platform !== "win32" && "O_NOFOLLOW" in external_node_fs_namespaceObject.constants
|
|
4381
|
+
? external_node_fs_namespaceObject.constants.O_NOFOLLOW
|
|
4382
|
+
: 0);
|
|
4383
|
+
let handle;
|
|
4384
|
+
let tempStat;
|
|
4385
|
+
let readHandle;
|
|
4386
|
+
let renamed = false;
|
|
4148
4387
|
try {
|
|
4149
|
-
|
|
4150
|
-
|
|
4151
|
-
|
|
4152
|
-
|
|
4153
|
-
|
|
4154
|
-
|
|
4388
|
+
handle = await promises_namespaceObject.open(tempPath, tempFlags, params.mode);
|
|
4389
|
+
let verificationIdentity = await handle.stat({ bigint: true });
|
|
4390
|
+
await handle.chmod(params.mode);
|
|
4391
|
+
if (params.input.kind === "buffer") {
|
|
4392
|
+
assertWithinMaxBytes(byteLength(params.input.data, params.input.encoding), params.maxBytes);
|
|
4393
|
+
if (typeof params.input.data === "string") {
|
|
4394
|
+
await handle.writeFile(params.input.data, params.input.encoding ?? "utf8");
|
|
4395
|
+
}
|
|
4396
|
+
else {
|
|
4397
|
+
await handle.writeFile(params.input.data);
|
|
4398
|
+
}
|
|
4155
4399
|
}
|
|
4156
|
-
|
|
4157
|
-
|
|
4158
|
-
}
|
|
4159
|
-
|
|
4160
|
-
|
|
4161
|
-
|
|
4162
|
-
|
|
4163
|
-
|
|
4164
|
-
|
|
4165
|
-
|
|
4166
|
-
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
|
|
4172
|
-
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
|
|
4400
|
+
else {
|
|
4401
|
+
await writeStreamToHandle(params.input.stream, handle, params.maxBytes);
|
|
4402
|
+
}
|
|
4403
|
+
tempStat = await handle.stat();
|
|
4404
|
+
const tempPathStat = await promises_namespaceObject.lstat(tempPath);
|
|
4405
|
+
if (tempPathStat.isSymbolicLink() || !file_identity_sameFileIdentity(tempPathStat, tempStat)) {
|
|
4406
|
+
throw new errors_FsSafeError("path-mismatch", "fallback temp path changed during write");
|
|
4407
|
+
}
|
|
4408
|
+
const expectedTempStat = tempStat;
|
|
4409
|
+
await syncFileBestEffort(handle);
|
|
4410
|
+
let verifiedIdentity = expectedTempStat;
|
|
4411
|
+
await withAsyncDirectoryGuards([parentGuard], async () => {
|
|
4412
|
+
await promises_namespaceObject.rename(tempPath, targetPath);
|
|
4413
|
+
renamed = true;
|
|
4414
|
+
await getFsSafeTestHooks()?.afterPinnedWriteFallbackRename?.(targetPath);
|
|
4415
|
+
await syncDirectoryBestEffort(parentPath);
|
|
4416
|
+
const targetStat = await promises_namespaceObject.lstat(targetPath);
|
|
4417
|
+
if (targetStat.isSymbolicLink()) {
|
|
4418
|
+
throw new errors_FsSafeError("path-mismatch", "fallback target changed during write");
|
|
4419
|
+
}
|
|
4420
|
+
if (!file_identity_sameFileIdentity(targetStat, expectedTempStat)) {
|
|
4421
|
+
// On filesystems like rclone FUSE, rename(2) can give the destination a
|
|
4422
|
+
// different inode from the source temp fd even with zero concurrency. The
|
|
4423
|
+
// caller must ensure mutual exclusion before passing "verify-content";
|
|
4424
|
+
// fall back to a content hash for this rename-boundary check only.
|
|
4425
|
+
if (params.onRenameIdentityMismatch !== "verify-content") {
|
|
4426
|
+
throw new errors_FsSafeError("path-mismatch", "fallback target changed during write");
|
|
4427
|
+
}
|
|
4428
|
+
if (params.input.kind !== "buffer") {
|
|
4429
|
+
throw new errors_FsSafeError("path-mismatch", "fallback target changed during write");
|
|
4430
|
+
}
|
|
4431
|
+
const expectedHash = sha256Hex(params.input.data, params.input.encoding);
|
|
4432
|
+
readHandle = await promises_namespaceObject.open(targetPath, read_open_flags_resolveReadOpenFlags());
|
|
4433
|
+
const readHandleStat = await readHandle.stat({ bigint: true });
|
|
4434
|
+
const actualHash = sha256Hex(await readHandle.readFile());
|
|
4435
|
+
if (actualHash !== expectedHash) {
|
|
4436
|
+
throw new errors_FsSafeError("path-mismatch", "fallback target changed during write");
|
|
4437
|
+
}
|
|
4438
|
+
// The content-verified destination, not the old temp inode, is now pinned.
|
|
4439
|
+
verificationIdentity = readHandleStat;
|
|
4440
|
+
// Preserve the helper's legacy numeric return facts, not the private proof.
|
|
4441
|
+
verifiedIdentity = { dev: Number(readHandleStat.dev), ino: Number(readHandleStat.ino) };
|
|
4442
|
+
}
|
|
4443
|
+
});
|
|
4444
|
+
await params.verifyPublished?.((readHandle ?? handle).fd, verificationIdentity, parentGuard);
|
|
4445
|
+
return { dev: verifiedIdentity.dev, ino: verifiedIdentity.ino };
|
|
4178
4446
|
}
|
|
4179
|
-
|
|
4180
|
-
|
|
4181
|
-
|
|
4182
|
-
|
|
4183
|
-
|
|
4447
|
+
finally {
|
|
4448
|
+
await readHandle?.close().catch(() => undefined);
|
|
4449
|
+
await handle?.close().catch(() => undefined);
|
|
4450
|
+
if (!renamed) {
|
|
4451
|
+
await promises_namespaceObject.rm(tempPath, { force: true }).catch(() => undefined);
|
|
4452
|
+
}
|
|
4184
4453
|
}
|
|
4185
|
-
throw new Error(`Path resolves outside ${params.boundaryLabel} (${shortPath(params.rootCanonicalPath)}): ${shortPath(params.absolutePath)}`);
|
|
4186
|
-
}
|
|
4187
|
-
function pathEscapeError(params) {
|
|
4188
|
-
return new Error(`Path escapes ${params.boundaryLabel} (${shortPath(params.rootPath)}): ${shortPath(params.absolutePath)}`);
|
|
4189
|
-
}
|
|
4190
|
-
function symlinkEscapeError(params) {
|
|
4191
|
-
return new Error(`Symlink escapes ${params.boundaryLabel} (${shortPath(params.rootCanonicalPath)}): ${shortPath(params.symlinkPath)}`);
|
|
4192
4454
|
}
|
|
4193
4455
|
|
|
4194
4456
|
;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/path-policy.js
|
|
@@ -5137,56 +5399,6 @@ async function verifyAtomicWriteResult(params) {
|
|
|
5137
5399
|
}
|
|
5138
5400
|
}
|
|
5139
5401
|
|
|
5140
|
-
;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/strict-file-identity.js
|
|
5141
|
-
|
|
5142
|
-
function identityMismatch() {
|
|
5143
|
-
return new errors_FsSafeError("path-mismatch", "file identity changed or could not be verified");
|
|
5144
|
-
}
|
|
5145
|
-
function identityCheck(expected, platform) {
|
|
5146
|
-
const known = {};
|
|
5147
|
-
const check = (stat) => {
|
|
5148
|
-
let complete = true;
|
|
5149
|
-
for (const field of ["dev", "ino"]) {
|
|
5150
|
-
const value = stat[field];
|
|
5151
|
-
// Numeric receipts cannot recover identity bits already lost to rounding.
|
|
5152
|
-
if (typeof value !== "bigint")
|
|
5153
|
-
throw identityMismatch();
|
|
5154
|
-
if (platform === "win32" && value === 0n) {
|
|
5155
|
-
complete = false;
|
|
5156
|
-
}
|
|
5157
|
-
else {
|
|
5158
|
-
if (known[field] !== undefined && known[field] !== value)
|
|
5159
|
-
throw identityMismatch();
|
|
5160
|
-
known[field] = value;
|
|
5161
|
-
}
|
|
5162
|
-
}
|
|
5163
|
-
return complete;
|
|
5164
|
-
};
|
|
5165
|
-
if (expected && !check(expected))
|
|
5166
|
-
throw identityMismatch();
|
|
5167
|
-
return check;
|
|
5168
|
-
}
|
|
5169
|
-
// Retry only unknown Windows identities, retaining every known component so a
|
|
5170
|
-
// later observation cannot erase a definite mismatch. Never reopen the file.
|
|
5171
|
-
async function inspectFileIdentity(inspect, expected, platform = process.platform) {
|
|
5172
|
-
const check = identityCheck(expected, platform);
|
|
5173
|
-
for (let attempt = 0; attempt < 2; attempt++) {
|
|
5174
|
-
const stat = await inspect();
|
|
5175
|
-
if (check(stat))
|
|
5176
|
-
return stat;
|
|
5177
|
-
}
|
|
5178
|
-
throw identityMismatch();
|
|
5179
|
-
}
|
|
5180
|
-
function inspectFileIdentitySync(inspect, expected, platform = process.platform) {
|
|
5181
|
-
const check = identityCheck(expected, platform);
|
|
5182
|
-
for (let attempt = 0; attempt < 2; attempt++) {
|
|
5183
|
-
const stat = inspect();
|
|
5184
|
-
if (check(stat))
|
|
5185
|
-
return stat;
|
|
5186
|
-
}
|
|
5187
|
-
throw identityMismatch();
|
|
5188
|
-
}
|
|
5189
|
-
|
|
5190
5402
|
;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/root-impl.js
|
|
5191
5403
|
|
|
5192
5404
|
|
|
@@ -5217,6 +5429,7 @@ function inspectFileIdentitySync(inspect, expected, platform = process.platform)
|
|
|
5217
5429
|
|
|
5218
5430
|
|
|
5219
5431
|
|
|
5432
|
+
|
|
5220
5433
|
|
|
5221
5434
|
|
|
5222
5435
|
function logWarn(message) {
|
|
@@ -5292,25 +5505,15 @@ async function openVerifiedLocalFile(filePath, options) {
|
|
|
5292
5505
|
if (preOpenStat) {
|
|
5293
5506
|
await fsSafeTestHooks?.afterPreOpenLstat?.(filePath);
|
|
5294
5507
|
}
|
|
5508
|
+
const openFlags = options?.symlinks === "follow-within-root"
|
|
5509
|
+
? OPEN_READ_FOLLOW_FLAGS
|
|
5510
|
+
: OPEN_READ_FLAGS;
|
|
5511
|
+
await fsSafeTestHooks?.beforeOpen?.(filePath, openFlags);
|
|
5295
5512
|
let handle;
|
|
5296
5513
|
try {
|
|
5297
|
-
|
|
5298
|
-
? OPEN_READ_FOLLOW_FLAGS
|
|
5299
|
-
: OPEN_READ_FLAGS;
|
|
5300
|
-
await fsSafeTestHooks?.beforeOpen?.(filePath, openFlags);
|
|
5301
|
-
handle = await promises_namespaceObject.open(filePath, openFlags);
|
|
5302
|
-
try {
|
|
5303
|
-
await fsSafeTestHooks?.afterOpen?.(filePath, handle);
|
|
5304
|
-
}
|
|
5305
|
-
catch (err) {
|
|
5306
|
-
await handle.close().catch(() => { });
|
|
5307
|
-
throw err;
|
|
5308
|
-
}
|
|
5514
|
+
handle = await promises_namespaceObject.open(filePath, openFlags).catch((error) => recordFileOpenFailure(path_isNotFoundPathError(error) ? fileNotFoundError() : error, filePath));
|
|
5309
5515
|
}
|
|
5310
5516
|
catch (err) {
|
|
5311
|
-
if (path_isNotFoundPathError(err)) {
|
|
5312
|
-
throw fileNotFoundError();
|
|
5313
|
-
}
|
|
5314
5517
|
if (isSymlinkOpenError(err)) {
|
|
5315
5518
|
throw new errors_FsSafeError("symlink", "symlink open blocked", { cause: err });
|
|
5316
5519
|
}
|
|
@@ -5321,16 +5524,35 @@ async function openVerifiedLocalFile(filePath, options) {
|
|
|
5321
5524
|
throw err;
|
|
5322
5525
|
}
|
|
5323
5526
|
try {
|
|
5527
|
+
await fsSafeTestHooks?.afterOpen?.(filePath, handle);
|
|
5324
5528
|
const stat = await handle.stat();
|
|
5325
5529
|
if (!stat.isFile()) {
|
|
5326
5530
|
throw new errors_FsSafeError("not-file", "not a file");
|
|
5327
5531
|
}
|
|
5328
5532
|
// Keep numeric Stats for the public receipt, never for identity verification.
|
|
5329
|
-
|
|
5533
|
+
let openedIdentity;
|
|
5534
|
+
const identity = await inspectFileIdentity(async () => (openedIdentity = await handle.stat({ bigint: true })), preOpenStat && !preOpenStat.isSymbolicLink() ? preOpenStat : undefined).catch(async (error) => {
|
|
5535
|
+
if ([0, 1].includes(stat.nlink)) {
|
|
5536
|
+
await recordPreOpenFileChange(error, handle, filePath, preOpenStat, openedIdentity);
|
|
5537
|
+
}
|
|
5538
|
+
throw error;
|
|
5539
|
+
});
|
|
5330
5540
|
if (options?.hardlinks === "reject" && stat.nlink > 1) {
|
|
5331
5541
|
throw hardlinkedPathNotAllowedError();
|
|
5332
5542
|
}
|
|
5333
|
-
|
|
5543
|
+
const inspectPathIdentity = async (inspect) => {
|
|
5544
|
+
try {
|
|
5545
|
+
return await inspectFileIdentity(inspect, identity);
|
|
5546
|
+
}
|
|
5547
|
+
catch (error) {
|
|
5548
|
+
const failure = path_isNotFoundPathError(error) ? openedPathResolutionError(fileNotFoundError()) : error;
|
|
5549
|
+
if (stat.nlink <= 1 && (!preOpenStat || preOpenStat.nlink <= 1n)) {
|
|
5550
|
+
await recordOpenedFileFailure(failure, handle, filePath, identity);
|
|
5551
|
+
}
|
|
5552
|
+
throw failure;
|
|
5553
|
+
}
|
|
5554
|
+
};
|
|
5555
|
+
await inspectPathIdentity(async () => {
|
|
5334
5556
|
const pathStat = options?.symlinks === "follow-within-root"
|
|
5335
5557
|
? await promises_namespaceObject.stat(filePath, { bigint: true })
|
|
5336
5558
|
: await promises_namespaceObject.lstat(filePath, { bigint: true });
|
|
@@ -5338,26 +5560,26 @@ async function openVerifiedLocalFile(filePath, options) {
|
|
|
5338
5560
|
throw new errors_FsSafeError("symlink", "symlink not allowed");
|
|
5339
5561
|
}
|
|
5340
5562
|
return pathStat;
|
|
5341
|
-
}
|
|
5563
|
+
});
|
|
5342
5564
|
await fsSafeTestHooks?.afterOpenedPathIdentityCheck?.(filePath, handle);
|
|
5343
|
-
const realPath = await resolveOpenedFileRealPathForFd(handle.fd, identity, filePath)
|
|
5344
|
-
|
|
5565
|
+
const realPath = await resolveOpenedFileRealPathForFd(handle.fd, identity, filePath)
|
|
5566
|
+
.catch(async (error) => {
|
|
5567
|
+
if (stat.nlink <= 1 && (!preOpenStat || preOpenStat.nlink <= 1n)) {
|
|
5568
|
+
await recordOpenedFileFailure(error, handle, filePath, identity);
|
|
5569
|
+
}
|
|
5570
|
+
throw error;
|
|
5571
|
+
});
|
|
5572
|
+
await inspectPathIdentity(async () => {
|
|
5345
5573
|
const realStat = await promises_namespaceObject.stat(realPath, { bigint: true });
|
|
5346
5574
|
if (options?.hardlinks === "reject" && realStat.nlink > 1n) {
|
|
5347
5575
|
throw hardlinkedPathNotAllowedError();
|
|
5348
5576
|
}
|
|
5349
5577
|
return realStat;
|
|
5350
|
-
}
|
|
5578
|
+
});
|
|
5351
5579
|
return { opened: openResult({ handle, realPath, stat }), identity };
|
|
5352
5580
|
}
|
|
5353
5581
|
catch (err) {
|
|
5354
5582
|
await handle.close().catch(() => { });
|
|
5355
|
-
if (err instanceof errors_FsSafeError) {
|
|
5356
|
-
throw err;
|
|
5357
|
-
}
|
|
5358
|
-
if (path_isNotFoundPathError(err)) {
|
|
5359
|
-
throw fileNotFoundError();
|
|
5360
|
-
}
|
|
5361
5583
|
throw err;
|
|
5362
5584
|
}
|
|
5363
5585
|
}
|
|
@@ -5586,19 +5808,10 @@ async function openFileInRoot(root, params) {
|
|
|
5586
5808
|
rejectUnsafeDeviceReads: true,
|
|
5587
5809
|
rejectSymlinks: params.symlinks !== "follow-within-root",
|
|
5588
5810
|
});
|
|
5589
|
-
|
|
5590
|
-
|
|
5591
|
-
|
|
5592
|
-
|
|
5593
|
-
symlinks: params.symlinks,
|
|
5594
|
-
}));
|
|
5595
|
-
}
|
|
5596
|
-
catch (err) {
|
|
5597
|
-
if (err instanceof errors_FsSafeError) {
|
|
5598
|
-
throw err;
|
|
5599
|
-
}
|
|
5600
|
-
throw err;
|
|
5601
|
-
}
|
|
5811
|
+
const { opened } = await openVerifiedLocalFile(resolved, {
|
|
5812
|
+
nonBlockingRead: params.nonBlockingRead,
|
|
5813
|
+
symlinks: params.symlinks,
|
|
5814
|
+
});
|
|
5602
5815
|
if (params.hardlinks !== "allow" && opened.stat.nlink > 1) {
|
|
5603
5816
|
await opened.handle.close().catch(() => { });
|
|
5604
5817
|
throw hardlinkedPathNotAllowedError();
|
|
@@ -5650,9 +5863,7 @@ function emitWriteBoundaryWarning(reason) {
|
|
|
5650
5863
|
logWarn(`security: fs-safe write boundary warning (${reason})`);
|
|
5651
5864
|
}
|
|
5652
5865
|
function buildAtomicWriteTempPath(targetPath) {
|
|
5653
|
-
|
|
5654
|
-
const base = external_node_path_namespaceObject.basename(targetPath);
|
|
5655
|
-
return external_node_path_namespaceObject.join(dir, `.${base}.${process.pid}.${(0,external_node_crypto_namespaceObject.randomUUID)()}.tmp`);
|
|
5866
|
+
return external_node_path_namespaceObject.join(external_node_path_namespaceObject.dirname(targetPath), `.fs-safe-${(0,external_node_crypto_namespaceObject.randomUUID)()}.tmp`);
|
|
5656
5867
|
}
|
|
5657
5868
|
function rootWriteQueueKey(root, relativePath) {
|
|
5658
5869
|
return `${root.rootReal}\0${relativePath}`;
|
|
@@ -5891,7 +6102,7 @@ async function writeFileInRoot(root, params) {
|
|
|
5891
6102
|
await writeFileFallback(root, params);
|
|
5892
6103
|
return;
|
|
5893
6104
|
}
|
|
5894
|
-
const pinned = await resolvePinnedWriteTargetInRoot(root, params.relativePath, params.mode, params.denyMutations);
|
|
6105
|
+
const pinned = await resolvePinnedWriteTargetInRoot(root, params.relativePath, params.mode, params.denyMutations, params.overwrite);
|
|
5895
6106
|
await serializePathWrite(pinned.targetPath, async () => {
|
|
5896
6107
|
await commitPinnedWriteInRoot(root, pinned, params);
|
|
5897
6108
|
});
|
|
@@ -6013,7 +6224,7 @@ async function removePathIfIdentityUnchanged(targetPath, identity) {
|
|
|
6013
6224
|
await promises_namespaceObject.rm(targetPath);
|
|
6014
6225
|
});
|
|
6015
6226
|
}
|
|
6016
|
-
async function resolvePinnedWriteTargetInRoot(root, relativePath, requestedMode, denyMutations) {
|
|
6227
|
+
async function resolvePinnedWriteTargetInRoot(root, relativePath, requestedMode, denyMutations, overwrite = true) {
|
|
6017
6228
|
const { rootReal, rootWithSep, resolved } = await resolveGuardedWritePathInRoot(root, {
|
|
6018
6229
|
relativePath,
|
|
6019
6230
|
denyMutations,
|
|
@@ -6031,22 +6242,40 @@ async function resolvePinnedWriteTargetInRoot(root, relativePath, requestedMode,
|
|
|
6031
6242
|
if (!basename || basename === "." || basename === "/") {
|
|
6032
6243
|
throw new errors_FsSafeError("invalid-path", "invalid target path");
|
|
6033
6244
|
}
|
|
6245
|
+
// Exclusive publication cannot inherit an existing file's mode. Keep type
|
|
6246
|
+
// and alias checks, but never open a competing owner's record just to reject it.
|
|
6247
|
+
if (!overwrite) {
|
|
6248
|
+
try {
|
|
6249
|
+
const existing = await promises_namespaceObject.stat(resolved);
|
|
6250
|
+
if (!existing.isFile())
|
|
6251
|
+
throw new errors_FsSafeError("not-file", "not a file");
|
|
6252
|
+
if (existing.nlink > 1)
|
|
6253
|
+
throw hardlinkedPathNotAllowedError();
|
|
6254
|
+
throw new errors_FsSafeError("already-exists", "file already exists");
|
|
6255
|
+
}
|
|
6256
|
+
catch (error) {
|
|
6257
|
+
if (!path_isNotFoundPathError(error))
|
|
6258
|
+
throw error;
|
|
6259
|
+
}
|
|
6260
|
+
}
|
|
6034
6261
|
let mode = requestedMode ?? 0o600;
|
|
6035
6262
|
try {
|
|
6036
|
-
|
|
6037
|
-
|
|
6038
|
-
|
|
6039
|
-
|
|
6040
|
-
|
|
6041
|
-
|
|
6042
|
-
|
|
6043
|
-
|
|
6044
|
-
|
|
6045
|
-
|
|
6263
|
+
if (overwrite) {
|
|
6264
|
+
const opened = await openFileInRoot(root, {
|
|
6265
|
+
relativePath,
|
|
6266
|
+
hardlinks: "reject",
|
|
6267
|
+
nonBlockingRead: true,
|
|
6268
|
+
symlinks: "follow-within-root",
|
|
6269
|
+
});
|
|
6270
|
+
try {
|
|
6271
|
+
mode = requestedMode ?? (opened.stat.mode & 0o777);
|
|
6272
|
+
if (!path_isPathInside(rootWithSep, opened.realPath)) {
|
|
6273
|
+
throw outsideWorkspaceError();
|
|
6274
|
+
}
|
|
6275
|
+
}
|
|
6276
|
+
finally {
|
|
6277
|
+
await opened.handle.close().catch(() => { });
|
|
6046
6278
|
}
|
|
6047
|
-
}
|
|
6048
|
-
finally {
|
|
6049
|
-
await opened.handle.close().catch(() => { });
|
|
6050
6279
|
}
|
|
6051
6280
|
}
|
|
6052
6281
|
catch (err) {
|
|
@@ -18836,4 +19065,4 @@ if (installedChunkData !== 0) { // 0 means "already installed".'
|
|
|
18836
19065
|
// module factories are used so entry inlining is disabled
|
|
18837
19066
|
// startup
|
|
18838
19067
|
// Load entry module and return exports
|
|
18839
|
-
var __webpack_exports__ = __webpack_require__(
|
|
19068
|
+
var __webpack_exports__ = __webpack_require__(461);
|