@lousy-agents/lint 5.15.7 → 5.16.0
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 +468 -28
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -566,7 +566,7 @@ module.exports = webpackEmptyAsyncContext;
|
|
|
566
566
|
|
|
567
567
|
|
|
568
568
|
},
|
|
569
|
-
|
|
569
|
+
6179(__unused_rspack_module, __webpack_exports__, __webpack_require__) {
|
|
570
570
|
|
|
571
571
|
// EXPORTS
|
|
572
572
|
__webpack_require__.d(__webpack_exports__, {
|
|
@@ -884,16 +884,24 @@ function createBoundedReadStream(opened, maxBytes) {
|
|
|
884
884
|
}
|
|
885
885
|
|
|
886
886
|
;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/file-identity.js
|
|
887
|
+
|
|
887
888
|
function isZero(value) {
|
|
888
889
|
return value === 0 || value === 0n;
|
|
889
890
|
}
|
|
891
|
+
function sameStatValue(left, right) {
|
|
892
|
+
return typeof left === typeof right ? left === right : BigInt(left) === BigInt(right);
|
|
893
|
+
}
|
|
894
|
+
function sha256Hex(data, encoding) {
|
|
895
|
+
const buffer = typeof data === "string" ? Buffer.from(data, encoding ?? "utf8") : data;
|
|
896
|
+
return (0,external_node_crypto_.createHash)("sha256").update(buffer).digest("hex");
|
|
897
|
+
}
|
|
890
898
|
function file_identity_sameFileIdentity(left, right, platform = process.platform) {
|
|
891
|
-
if (left.ino
|
|
899
|
+
if (!sameStatValue(left.ino, right.ino)) {
|
|
892
900
|
return false;
|
|
893
901
|
}
|
|
894
902
|
// On Windows, path-based stat calls can report dev=0 while fd-based stat
|
|
895
903
|
// reports a real volume serial; treat either-side dev=0 as "unknown device".
|
|
896
|
-
if (left.dev
|
|
904
|
+
if (sameStatValue(left.dev, right.dev)) {
|
|
897
905
|
return true;
|
|
898
906
|
}
|
|
899
907
|
return platform === "win32" && (isZero(left.dev) || isZero(right.dev));
|
|
@@ -2287,6 +2295,377 @@ async function runPinnedPathHelper(params) {
|
|
|
2287
2295
|
}
|
|
2288
2296
|
}
|
|
2289
2297
|
|
|
2298
|
+
;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/sidecar-lock.js
|
|
2299
|
+
|
|
2300
|
+
|
|
2301
|
+
|
|
2302
|
+
|
|
2303
|
+
const GLOBAL_STATE_KEY = Symbol.for("fsSafe.sidecarLockManagers");
|
|
2304
|
+
function getGlobalManagers() {
|
|
2305
|
+
const globalWithState = globalThis;
|
|
2306
|
+
if (!globalWithState[GLOBAL_STATE_KEY]) {
|
|
2307
|
+
globalWithState[GLOBAL_STATE_KEY] = new Map();
|
|
2308
|
+
}
|
|
2309
|
+
return globalWithState[GLOBAL_STATE_KEY];
|
|
2310
|
+
}
|
|
2311
|
+
function resolveManagerState(key) {
|
|
2312
|
+
const managers = getGlobalManagers();
|
|
2313
|
+
let state = managers.get(key);
|
|
2314
|
+
if (!state) {
|
|
2315
|
+
state = { cleanupRegistered: false, held: new Map() };
|
|
2316
|
+
managers.set(key, state);
|
|
2317
|
+
}
|
|
2318
|
+
return state;
|
|
2319
|
+
}
|
|
2320
|
+
async function readLockSnapshot(lockPath) {
|
|
2321
|
+
try {
|
|
2322
|
+
const stat = await promises_.lstat(lockPath);
|
|
2323
|
+
const raw = await promises_.readFile(lockPath, "utf8");
|
|
2324
|
+
try {
|
|
2325
|
+
const parsed = JSON.parse(raw);
|
|
2326
|
+
const payload = parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
|
2327
|
+
? parsed
|
|
2328
|
+
: null;
|
|
2329
|
+
return { raw, payload, stat };
|
|
2330
|
+
}
|
|
2331
|
+
catch {
|
|
2332
|
+
return { raw, payload: null, stat };
|
|
2333
|
+
}
|
|
2334
|
+
}
|
|
2335
|
+
catch (err) {
|
|
2336
|
+
if (err.code === "ENOENT") {
|
|
2337
|
+
return null;
|
|
2338
|
+
}
|
|
2339
|
+
throw err;
|
|
2340
|
+
}
|
|
2341
|
+
}
|
|
2342
|
+
function snapshotMatches(current, observed) {
|
|
2343
|
+
if (observed.stat && current.stat && !file_identity_sameFileIdentity(observed.stat, current.stat)) {
|
|
2344
|
+
return false;
|
|
2345
|
+
}
|
|
2346
|
+
if (observed.raw !== undefined) {
|
|
2347
|
+
return current.raw === observed.raw;
|
|
2348
|
+
}
|
|
2349
|
+
return observed.stat !== undefined && current.stat !== undefined;
|
|
2350
|
+
}
|
|
2351
|
+
async function removeLockIfUnchanged(lockPath, observed) {
|
|
2352
|
+
const current = await readLockSnapshot(lockPath);
|
|
2353
|
+
if (!current || !observed) {
|
|
2354
|
+
return false;
|
|
2355
|
+
}
|
|
2356
|
+
if (!snapshotMatches(current, observed)) {
|
|
2357
|
+
// The lock changed after we decided it was stale. Leave the fresh holder's
|
|
2358
|
+
// file alone; deleting by path here would break mutual exclusion.
|
|
2359
|
+
return false;
|
|
2360
|
+
}
|
|
2361
|
+
await promises_.rm(lockPath, { force: true }).catch(() => undefined);
|
|
2362
|
+
return true;
|
|
2363
|
+
}
|
|
2364
|
+
async function lockSnapshotStillPresent(lockPath, observed) {
|
|
2365
|
+
const current = await readLockSnapshot(lockPath);
|
|
2366
|
+
return !!current && !!observed && snapshotMatches(current, observed);
|
|
2367
|
+
}
|
|
2368
|
+
async function removeStaleLockIfAllowed(params) {
|
|
2369
|
+
if (!params.shouldRemoveStaleLock) {
|
|
2370
|
+
return "not-approved";
|
|
2371
|
+
}
|
|
2372
|
+
if (params.snapshot.raw === undefined) {
|
|
2373
|
+
return "not-approved";
|
|
2374
|
+
}
|
|
2375
|
+
if (!(await params.shouldRemoveStaleLock({
|
|
2376
|
+
lockPath: params.lockPath,
|
|
2377
|
+
normalizedTargetPath: params.normalizedTargetPath,
|
|
2378
|
+
raw: params.snapshot.raw,
|
|
2379
|
+
payload: params.snapshot.payload,
|
|
2380
|
+
}))) {
|
|
2381
|
+
return "not-approved";
|
|
2382
|
+
}
|
|
2383
|
+
const current = await readLockSnapshot(params.lockPath);
|
|
2384
|
+
if (!current || !snapshotMatches(current, params.snapshot)) {
|
|
2385
|
+
return "changed";
|
|
2386
|
+
}
|
|
2387
|
+
try {
|
|
2388
|
+
await promises_.rm(params.lockPath, { force: true });
|
|
2389
|
+
}
|
|
2390
|
+
catch (err) {
|
|
2391
|
+
if (err.code === "ENOENT") {
|
|
2392
|
+
return "changed";
|
|
2393
|
+
}
|
|
2394
|
+
return "not-approved";
|
|
2395
|
+
}
|
|
2396
|
+
return "removed";
|
|
2397
|
+
}
|
|
2398
|
+
function snapshotMatchesSync(lockPath, observed) {
|
|
2399
|
+
try {
|
|
2400
|
+
const stat = external_node_fs_.lstatSync(lockPath);
|
|
2401
|
+
if (observed.stat && !file_identity_sameFileIdentity(observed.stat, stat)) {
|
|
2402
|
+
return false;
|
|
2403
|
+
}
|
|
2404
|
+
return observed.raw === undefined || external_node_fs_.readFileSync(lockPath, "utf8") === observed.raw;
|
|
2405
|
+
}
|
|
2406
|
+
catch {
|
|
2407
|
+
return false;
|
|
2408
|
+
}
|
|
2409
|
+
}
|
|
2410
|
+
async function resolveNormalizedTargetPath(targetPath) {
|
|
2411
|
+
const resolved = external_node_path_.resolve(targetPath);
|
|
2412
|
+
const dir = external_node_path_.dirname(resolved);
|
|
2413
|
+
await promises_.mkdir(dir, { recursive: true });
|
|
2414
|
+
try {
|
|
2415
|
+
return external_node_path_.join(await promises_.realpath(dir), external_node_path_.basename(resolved));
|
|
2416
|
+
}
|
|
2417
|
+
catch {
|
|
2418
|
+
return resolved;
|
|
2419
|
+
}
|
|
2420
|
+
}
|
|
2421
|
+
function computeDelayMs(retry, attempt) {
|
|
2422
|
+
const minTimeout = retry.minTimeout ?? 50;
|
|
2423
|
+
const maxTimeout = retry.maxTimeout ?? 1000;
|
|
2424
|
+
const factor = retry.factor ?? 1;
|
|
2425
|
+
const base = Math.min(maxTimeout, Math.max(minTimeout, minTimeout * factor ** attempt));
|
|
2426
|
+
const jitter = retry.randomize ? 1 + Math.random() : 1;
|
|
2427
|
+
return Math.min(maxTimeout, Math.round(base * jitter));
|
|
2428
|
+
}
|
|
2429
|
+
async function defaultShouldReclaim(params) {
|
|
2430
|
+
const createdAt = typeof params.payload?.createdAt === "string" ? params.payload.createdAt : "";
|
|
2431
|
+
const createdAtMs = Date.parse(createdAt);
|
|
2432
|
+
if (Number.isFinite(createdAtMs) && params.nowMs - createdAtMs > params.staleMs) {
|
|
2433
|
+
return true;
|
|
2434
|
+
}
|
|
2435
|
+
try {
|
|
2436
|
+
const stat = await promises_.stat(params.lockPath);
|
|
2437
|
+
return params.nowMs - stat.mtimeMs > params.staleMs;
|
|
2438
|
+
}
|
|
2439
|
+
catch {
|
|
2440
|
+
return true;
|
|
2441
|
+
}
|
|
2442
|
+
}
|
|
2443
|
+
function releaseAllLocksSync(state) {
|
|
2444
|
+
for (const [normalizedTargetPath, held] of state.held) {
|
|
2445
|
+
void held.handle.close().catch(() => undefined);
|
|
2446
|
+
try {
|
|
2447
|
+
if (snapshotMatchesSync(held.lockPath, held.snapshot)) {
|
|
2448
|
+
external_node_fs_.rmSync(held.lockPath, { force: true });
|
|
2449
|
+
}
|
|
2450
|
+
}
|
|
2451
|
+
catch {
|
|
2452
|
+
// Best-effort process-exit cleanup.
|
|
2453
|
+
}
|
|
2454
|
+
state.held.delete(normalizedTargetPath);
|
|
2455
|
+
}
|
|
2456
|
+
}
|
|
2457
|
+
async function releaseHeldLock(state, normalizedTargetPath, held, opts = {}) {
|
|
2458
|
+
const current = state.held.get(normalizedTargetPath);
|
|
2459
|
+
if (current !== held) {
|
|
2460
|
+
return false;
|
|
2461
|
+
}
|
|
2462
|
+
if (opts.force) {
|
|
2463
|
+
held.count = 0;
|
|
2464
|
+
}
|
|
2465
|
+
else {
|
|
2466
|
+
held.count -= 1;
|
|
2467
|
+
if (held.count > 0) {
|
|
2468
|
+
return false;
|
|
2469
|
+
}
|
|
2470
|
+
}
|
|
2471
|
+
if (held.releasePromise) {
|
|
2472
|
+
await held.releasePromise.catch(() => undefined);
|
|
2473
|
+
return true;
|
|
2474
|
+
}
|
|
2475
|
+
state.held.delete(normalizedTargetPath);
|
|
2476
|
+
held.releasePromise = (async () => {
|
|
2477
|
+
await held.handle.close().catch(() => undefined);
|
|
2478
|
+
await removeLockIfUnchanged(held.lockPath, held.snapshot);
|
|
2479
|
+
})();
|
|
2480
|
+
try {
|
|
2481
|
+
await held.releasePromise;
|
|
2482
|
+
return true;
|
|
2483
|
+
}
|
|
2484
|
+
finally {
|
|
2485
|
+
held.releasePromise = undefined;
|
|
2486
|
+
}
|
|
2487
|
+
}
|
|
2488
|
+
function createSidecarLockManager(key) {
|
|
2489
|
+
const state = resolveManagerState(key);
|
|
2490
|
+
function ensureExitCleanupRegistered() {
|
|
2491
|
+
if (state.cleanupRegistered) {
|
|
2492
|
+
return;
|
|
2493
|
+
}
|
|
2494
|
+
state.cleanupRegistered = true;
|
|
2495
|
+
process.on("exit", () => releaseAllLocksSync(state));
|
|
2496
|
+
}
|
|
2497
|
+
async function acquire(options) {
|
|
2498
|
+
ensureExitCleanupRegistered();
|
|
2499
|
+
const normalizedTargetPath = await resolveNormalizedTargetPath(options.targetPath);
|
|
2500
|
+
const lockPath = options.lockPath ?? `${normalizedTargetPath}.lock`;
|
|
2501
|
+
const held = state.held.get(normalizedTargetPath);
|
|
2502
|
+
if (held && options.allowReentrant) {
|
|
2503
|
+
held.count += 1;
|
|
2504
|
+
const release = () => releaseHeldLock(state, normalizedTargetPath, held).then(() => undefined);
|
|
2505
|
+
return {
|
|
2506
|
+
lockPath,
|
|
2507
|
+
normalizedTargetPath,
|
|
2508
|
+
release,
|
|
2509
|
+
[Symbol.asyncDispose]: release,
|
|
2510
|
+
};
|
|
2511
|
+
}
|
|
2512
|
+
const startedAt = Date.now();
|
|
2513
|
+
const retry = options.retry ?? {};
|
|
2514
|
+
const maxRetries = options.timeoutMs === Number.POSITIVE_INFINITY ? undefined : retry.retries;
|
|
2515
|
+
let attempt = 0;
|
|
2516
|
+
while (true) {
|
|
2517
|
+
let handle = null;
|
|
2518
|
+
try {
|
|
2519
|
+
handle = await promises_.open(lockPath, "wx");
|
|
2520
|
+
const payload = await options.payload();
|
|
2521
|
+
const raw = `${JSON.stringify(payload, null, 2)}\n`;
|
|
2522
|
+
await handle.writeFile(raw, "utf8");
|
|
2523
|
+
const snapshot = { raw, payload, stat: await handle.stat() };
|
|
2524
|
+
const createdHeld = {
|
|
2525
|
+
count: 1,
|
|
2526
|
+
handle,
|
|
2527
|
+
lockPath,
|
|
2528
|
+
snapshot,
|
|
2529
|
+
acquiredAt: Date.now(),
|
|
2530
|
+
metadata: options.metadata ?? {},
|
|
2531
|
+
};
|
|
2532
|
+
state.held.set(normalizedTargetPath, createdHeld);
|
|
2533
|
+
const release = () => releaseHeldLock(state, normalizedTargetPath, createdHeld).then(() => undefined);
|
|
2534
|
+
return {
|
|
2535
|
+
lockPath,
|
|
2536
|
+
normalizedTargetPath,
|
|
2537
|
+
release,
|
|
2538
|
+
[Symbol.asyncDispose]: release,
|
|
2539
|
+
};
|
|
2540
|
+
}
|
|
2541
|
+
catch (err) {
|
|
2542
|
+
if (handle) {
|
|
2543
|
+
const failedSnapshot = { payload: null };
|
|
2544
|
+
try {
|
|
2545
|
+
failedSnapshot.stat = await handle.stat();
|
|
2546
|
+
}
|
|
2547
|
+
catch {
|
|
2548
|
+
// Best-effort cleanup of a failed exclusive create.
|
|
2549
|
+
}
|
|
2550
|
+
const current = state.held.get(normalizedTargetPath);
|
|
2551
|
+
if (current?.handle === handle) {
|
|
2552
|
+
state.held.delete(normalizedTargetPath);
|
|
2553
|
+
}
|
|
2554
|
+
// If payload serialization/write fails, the file may be empty or
|
|
2555
|
+
// partial JSON, so remove while our exclusive handle is still open.
|
|
2556
|
+
await promises_.rm(lockPath, { force: true }).catch(() => undefined);
|
|
2557
|
+
await handle.close().catch(() => undefined);
|
|
2558
|
+
// Windows can refuse removing an open file; retry after close but
|
|
2559
|
+
// only if the path still points at the file identity we created.
|
|
2560
|
+
await removeLockIfUnchanged(lockPath, failedSnapshot);
|
|
2561
|
+
}
|
|
2562
|
+
if (err.code !== "EEXIST") {
|
|
2563
|
+
throw err;
|
|
2564
|
+
}
|
|
2565
|
+
const nowMs = Date.now();
|
|
2566
|
+
const snapshot = await readLockSnapshot(lockPath);
|
|
2567
|
+
if (!snapshot) {
|
|
2568
|
+
continue;
|
|
2569
|
+
}
|
|
2570
|
+
const shouldReclaim = options.shouldReclaim ?? defaultShouldReclaim;
|
|
2571
|
+
if (await shouldReclaim({
|
|
2572
|
+
lockPath,
|
|
2573
|
+
normalizedTargetPath,
|
|
2574
|
+
payload: snapshot?.payload ?? null,
|
|
2575
|
+
staleMs: options.staleMs,
|
|
2576
|
+
nowMs,
|
|
2577
|
+
heldByThisProcess: state.held.has(normalizedTargetPath),
|
|
2578
|
+
})) {
|
|
2579
|
+
if (!(await lockSnapshotStillPresent(lockPath, snapshot))) {
|
|
2580
|
+
continue;
|
|
2581
|
+
}
|
|
2582
|
+
const staleRecovery = options.staleRecovery ?? "fail-closed";
|
|
2583
|
+
if (staleRecovery === "remove-if-unchanged") {
|
|
2584
|
+
const removal = await removeStaleLockIfAllowed({
|
|
2585
|
+
lockPath,
|
|
2586
|
+
normalizedTargetPath,
|
|
2587
|
+
snapshot,
|
|
2588
|
+
shouldRemoveStaleLock: options.shouldRemoveStaleLock,
|
|
2589
|
+
});
|
|
2590
|
+
if (removal === "removed" || removal === "changed") {
|
|
2591
|
+
continue;
|
|
2592
|
+
}
|
|
2593
|
+
}
|
|
2594
|
+
throw Object.assign(new Error(`file lock stale for ${normalizedTargetPath}`), {
|
|
2595
|
+
code: "file_lock_stale",
|
|
2596
|
+
lockPath,
|
|
2597
|
+
normalizedTargetPath,
|
|
2598
|
+
});
|
|
2599
|
+
}
|
|
2600
|
+
const elapsed = Date.now() - startedAt;
|
|
2601
|
+
if ((options.timeoutMs !== undefined &&
|
|
2602
|
+
options.timeoutMs !== Number.POSITIVE_INFINITY &&
|
|
2603
|
+
elapsed >= options.timeoutMs) ||
|
|
2604
|
+
(maxRetries !== undefined && attempt >= maxRetries)) {
|
|
2605
|
+
throw Object.assign(new Error(`file lock timeout for ${normalizedTargetPath}`), {
|
|
2606
|
+
code: "file_lock_timeout",
|
|
2607
|
+
lockPath,
|
|
2608
|
+
normalizedTargetPath,
|
|
2609
|
+
});
|
|
2610
|
+
}
|
|
2611
|
+
const remaining = options.timeoutMs === undefined || options.timeoutMs === Number.POSITIVE_INFINITY
|
|
2612
|
+
? Number.POSITIVE_INFINITY
|
|
2613
|
+
: Math.max(0, options.timeoutMs - elapsed);
|
|
2614
|
+
const delay = Math.min(computeDelayMs(retry, attempt), remaining);
|
|
2615
|
+
attempt += 1;
|
|
2616
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
2617
|
+
}
|
|
2618
|
+
}
|
|
2619
|
+
}
|
|
2620
|
+
async function withLock(options, fn) {
|
|
2621
|
+
const lock = await acquire(options);
|
|
2622
|
+
try {
|
|
2623
|
+
return await fn();
|
|
2624
|
+
}
|
|
2625
|
+
finally {
|
|
2626
|
+
await lock.release();
|
|
2627
|
+
}
|
|
2628
|
+
}
|
|
2629
|
+
async function drain() {
|
|
2630
|
+
for (const [normalizedTargetPath, held] of Array.from(state.held.entries())) {
|
|
2631
|
+
await releaseHeldLock(state, normalizedTargetPath, held, { force: true }).catch(() => undefined);
|
|
2632
|
+
}
|
|
2633
|
+
}
|
|
2634
|
+
function reset() {
|
|
2635
|
+
releaseAllLocksSync(state);
|
|
2636
|
+
}
|
|
2637
|
+
function heldEntries() {
|
|
2638
|
+
return Array.from(state.held.entries()).map(([normalizedTargetPath, held]) => ({
|
|
2639
|
+
normalizedTargetPath,
|
|
2640
|
+
lockPath: held.lockPath,
|
|
2641
|
+
acquiredAt: held.acquiredAt,
|
|
2642
|
+
metadata: held.metadata,
|
|
2643
|
+
forceRelease: () => releaseHeldLock(state, normalizedTargetPath, held, { force: true }),
|
|
2644
|
+
}));
|
|
2645
|
+
}
|
|
2646
|
+
return { acquire, withLock, drain, reset, heldEntries };
|
|
2647
|
+
}
|
|
2648
|
+
async function withSidecarLock(targetPath, options, fn) {
|
|
2649
|
+
const manager = createSidecarLockManager(options.managerKey ?? `fs-safe.sidecar-lock:${targetPath}`);
|
|
2650
|
+
const { managerKey: _managerKey, ...acquireOptions } = options;
|
|
2651
|
+
return await manager.withLock({ ...acquireOptions, targetPath }, fn);
|
|
2652
|
+
}
|
|
2653
|
+
|
|
2654
|
+
;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/test-hooks.js
|
|
2655
|
+
let test_hooks_fsSafeTestHooks;
|
|
2656
|
+
function allowFsSafeTestHooks() {
|
|
2657
|
+
return false || process.env.VITEST === "true";
|
|
2658
|
+
}
|
|
2659
|
+
function getFsSafeTestHooks() {
|
|
2660
|
+
return test_hooks_fsSafeTestHooks;
|
|
2661
|
+
}
|
|
2662
|
+
function __setFsSafeTestHooksForTest(hooks) {
|
|
2663
|
+
if (hooks && !allowFsSafeTestHooks()) {
|
|
2664
|
+
throw new Error("__setFsSafeTestHooksForTest is only available in tests");
|
|
2665
|
+
}
|
|
2666
|
+
test_hooks_fsSafeTestHooks = hooks;
|
|
2667
|
+
}
|
|
2668
|
+
|
|
2290
2669
|
;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/pinned-write.js
|
|
2291
2670
|
|
|
2292
2671
|
|
|
@@ -2300,6 +2679,8 @@ async function runPinnedPathHelper(params) {
|
|
|
2300
2679
|
|
|
2301
2680
|
|
|
2302
2681
|
|
|
2682
|
+
|
|
2683
|
+
|
|
2303
2684
|
function byteLength(input, encoding) {
|
|
2304
2685
|
return typeof input === "string"
|
|
2305
2686
|
? Buffer.byteLength(input, encoding ?? "utf8")
|
|
@@ -2357,6 +2738,12 @@ async function runPinnedWriteHelper(params) {
|
|
|
2357
2738
|
validatePinnedOperationPayload({
|
|
2358
2739
|
relativeParentPath: params.relativeParentPath,
|
|
2359
2740
|
});
|
|
2741
|
+
// The Python helper deliberately enforces the strict post-rename inode
|
|
2742
|
+
// contract. The explicit compatibility policy therefore uses the guarded
|
|
2743
|
+
// Node fallback, where content verification can replace that one check.
|
|
2744
|
+
if (params.onRenameIdentityMismatch === "verify-content") {
|
|
2745
|
+
return await runPinnedWriteFallback(params);
|
|
2746
|
+
}
|
|
2360
2747
|
if (getFsSafePythonConfig().mode === "off") {
|
|
2361
2748
|
return await runPinnedWriteFallback(params);
|
|
2362
2749
|
}
|
|
@@ -2371,8 +2758,11 @@ async function runPinnedWriteHelper(params) {
|
|
|
2371
2758
|
throw error;
|
|
2372
2759
|
}
|
|
2373
2760
|
}
|
|
2761
|
+
const input = params.input.kind === "stream"
|
|
2762
|
+
? { kind: "buffer", data: Buffer.from(await inputToBase64(params.input, params.maxBytes), "base64") }
|
|
2763
|
+
: params.input;
|
|
2374
2764
|
const payload = {
|
|
2375
|
-
base64: await inputToBase64(
|
|
2765
|
+
base64: await inputToBase64(input, params.maxBytes),
|
|
2376
2766
|
basename: params.basename,
|
|
2377
2767
|
maxBytes: params.maxBytes ?? -1,
|
|
2378
2768
|
mkdir: params.mkdir,
|
|
@@ -2390,11 +2780,32 @@ async function runPinnedWriteHelper(params) {
|
|
|
2390
2780
|
}
|
|
2391
2781
|
catch (error) {
|
|
2392
2782
|
if (canFallbackFromPythonError(error)) {
|
|
2393
|
-
return await runPinnedWriteFallback(params);
|
|
2783
|
+
return await runPinnedWriteFallback({ ...params, input });
|
|
2394
2784
|
}
|
|
2395
2785
|
throw error;
|
|
2396
2786
|
}
|
|
2397
2787
|
}
|
|
2788
|
+
async function runPinnedWriteWithRenamePolicy(params) {
|
|
2789
|
+
const { targetPath, renameIdentity, ...writeParams } = params;
|
|
2790
|
+
if (renameIdentity !== "verify-content-with-lock") {
|
|
2791
|
+
return await runPinnedWriteHelper(writeParams);
|
|
2792
|
+
}
|
|
2793
|
+
const relativeTargetPath = writeParams.relativeParentPath
|
|
2794
|
+
? `${writeParams.relativeParentPath}/${writeParams.basename}`
|
|
2795
|
+
: writeParams.basename;
|
|
2796
|
+
const lockPath = external_node_path_.join(writeParams.rootPath, `.fs-safe-write-${sha256Hex(relativeTargetPath)}.lock`);
|
|
2797
|
+
return await withSidecarLock(writeParams.rootPath, {
|
|
2798
|
+
managerKey: `fs-safe.write:${targetPath}`,
|
|
2799
|
+
lockPath,
|
|
2800
|
+
staleMs: 30_000,
|
|
2801
|
+
timeoutMs: 5_000,
|
|
2802
|
+
payload: () => ({ pid: process.pid, createdAt: new Date().toISOString() }),
|
|
2803
|
+
retry: { retries: 5, minTimeout: 100, maxTimeout: 2_000, factor: 2 },
|
|
2804
|
+
}, async () => await runPinnedWriteHelper({
|
|
2805
|
+
...writeParams,
|
|
2806
|
+
onRenameIdentityMismatch: "verify-content",
|
|
2807
|
+
}));
|
|
2808
|
+
}
|
|
2398
2809
|
async function runPinnedCopyHelper(params) {
|
|
2399
2810
|
assertSafeBasename(params.basename);
|
|
2400
2811
|
validatePinnedOperationPayload({
|
|
@@ -2450,7 +2861,10 @@ async function runPinnedWriteFallback(params) {
|
|
|
2450
2861
|
else {
|
|
2451
2862
|
await writeStreamToHandle(params.input.stream, handle, params.maxBytes);
|
|
2452
2863
|
}
|
|
2864
|
+
await handle.sync();
|
|
2453
2865
|
const stat = await handle.stat();
|
|
2866
|
+
await handle.close().catch(() => undefined);
|
|
2867
|
+
await syncDirectoryBestEffort(parentPath);
|
|
2454
2868
|
created = false;
|
|
2455
2869
|
return { dev: stat.dev, ino: stat.ino };
|
|
2456
2870
|
}
|
|
@@ -2498,11 +2912,49 @@ async function runPinnedWriteFallback(params) {
|
|
|
2498
2912
|
await withAsyncDirectoryGuards([parentGuard], async () => {
|
|
2499
2913
|
await promises_.rename(tempPath, targetPath);
|
|
2500
2914
|
renamed = true;
|
|
2915
|
+
await getFsSafeTestHooks()?.afterPinnedWriteFallbackRename?.(targetPath);
|
|
2501
2916
|
await syncDirectoryBestEffort(parentPath);
|
|
2502
2917
|
targetStat = await promises_.lstat(targetPath);
|
|
2503
|
-
if (targetStat.isSymbolicLink()
|
|
2918
|
+
if (targetStat.isSymbolicLink()) {
|
|
2504
2919
|
throw new errors_FsSafeError("path-mismatch", "fallback target changed during write");
|
|
2505
2920
|
}
|
|
2921
|
+
if (!file_identity_sameFileIdentity(targetStat, expectedTempStat)) {
|
|
2922
|
+
// On filesystems like rclone FUSE, rename(2) can give the destination a
|
|
2923
|
+
// different inode from the source temp fd even with zero concurrency. The
|
|
2924
|
+
// caller must ensure mutual exclusion before passing "verify-content";
|
|
2925
|
+
// fall back to a content hash for this rename-boundary check only.
|
|
2926
|
+
if (params.onRenameIdentityMismatch !== "verify-content") {
|
|
2927
|
+
throw new errors_FsSafeError("path-mismatch", "fallback target changed during write");
|
|
2928
|
+
}
|
|
2929
|
+
if (params.input.kind !== "buffer") {
|
|
2930
|
+
throw new errors_FsSafeError("path-mismatch", "fallback target changed during write");
|
|
2931
|
+
}
|
|
2932
|
+
const expectedHash = sha256Hex(params.input.data, params.input.encoding);
|
|
2933
|
+
const readFlags = external_node_fs_.constants.O_RDONLY |
|
|
2934
|
+
(process.platform !== "win32" && "O_NOFOLLOW" in external_node_fs_.constants
|
|
2935
|
+
? external_node_fs_.constants.O_NOFOLLOW
|
|
2936
|
+
: 0);
|
|
2937
|
+
const readHandle = await promises_.open(targetPath, readFlags);
|
|
2938
|
+
let actualHash;
|
|
2939
|
+
let readHandleStat;
|
|
2940
|
+
try {
|
|
2941
|
+
// Capture fd-based identity before reading — this is stable across all
|
|
2942
|
+
// subsequent lookups (on FUSE and locally), unlike the lstat-based
|
|
2943
|
+
// targetStat that triggered this fallback.
|
|
2944
|
+
readHandleStat = await readHandle.stat();
|
|
2945
|
+
actualHash = sha256Hex(await readHandle.readFile());
|
|
2946
|
+
}
|
|
2947
|
+
finally {
|
|
2948
|
+
await readHandle.close().catch(() => undefined);
|
|
2949
|
+
}
|
|
2950
|
+
if (actualHash !== expectedHash) {
|
|
2951
|
+
throw new errors_FsSafeError("path-mismatch", "fallback target changed during write");
|
|
2952
|
+
}
|
|
2953
|
+
// Replace the unreliable lstat-based targetStat with the fd-based stat so
|
|
2954
|
+
// the returned identity is consistent with what subsequent verifications
|
|
2955
|
+
// (e.g. verifyAtomicWriteResult) will obtain by opening the same file.
|
|
2956
|
+
targetStat = readHandleStat;
|
|
2957
|
+
}
|
|
2506
2958
|
});
|
|
2507
2959
|
}
|
|
2508
2960
|
catch (error) {
|
|
@@ -3598,21 +4050,6 @@ function normalizePinnedPathError(error) {
|
|
|
3598
4050
|
});
|
|
3599
4051
|
}
|
|
3600
4052
|
|
|
3601
|
-
;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/test-hooks.js
|
|
3602
|
-
let test_hooks_fsSafeTestHooks;
|
|
3603
|
-
function allowFsSafeTestHooks() {
|
|
3604
|
-
return false || process.env.VITEST === "true";
|
|
3605
|
-
}
|
|
3606
|
-
function getFsSafeTestHooks() {
|
|
3607
|
-
return test_hooks_fsSafeTestHooks;
|
|
3608
|
-
}
|
|
3609
|
-
function __setFsSafeTestHooksForTest(hooks) {
|
|
3610
|
-
if (hooks && !allowFsSafeTestHooks()) {
|
|
3611
|
-
throw new Error("__setFsSafeTestHooksForTest is only available in tests");
|
|
3612
|
-
}
|
|
3613
|
-
test_hooks_fsSafeTestHooks = hooks;
|
|
3614
|
-
}
|
|
3615
|
-
|
|
3616
4053
|
;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/json-stringify.js
|
|
3617
4054
|
function stringifyJsonDocument(value, replacer, space) {
|
|
3618
4055
|
const text = JSON.stringify(value, replacer, space);
|
|
@@ -3948,6 +4385,7 @@ class RootHandle {
|
|
|
3948
4385
|
data,
|
|
3949
4386
|
mkdir: this.defaults.mkdir,
|
|
3950
4387
|
mode: this.defaults.mode,
|
|
4388
|
+
renameIdentity: this.defaults.renameIdentity,
|
|
3951
4389
|
...options,
|
|
3952
4390
|
denyMutations: mergeDenyMutationPolicies(this.defaults.denyMutations, options.denyMutations),
|
|
3953
4391
|
});
|
|
@@ -4382,21 +4820,23 @@ async function writeFileInRoot(root, params) {
|
|
|
4382
4820
|
async function commitPinnedWriteInRoot(root, pinned, params) {
|
|
4383
4821
|
let identity;
|
|
4384
4822
|
try {
|
|
4385
|
-
identity = await
|
|
4823
|
+
identity = await runPinnedWriteWithRenamePolicy({
|
|
4386
4824
|
rootPath: pinned.rootReal,
|
|
4387
4825
|
relativeParentPath: pinned.relativeParentPath,
|
|
4388
4826
|
basename: pinned.basename,
|
|
4827
|
+
targetPath: pinned.targetPath,
|
|
4828
|
+
renameIdentity: params.renameIdentity,
|
|
4389
4829
|
mkdir: params.mkdir !== false,
|
|
4390
4830
|
mode: params.mode ?? pinned.mode,
|
|
4391
4831
|
overwrite: params.overwrite,
|
|
4392
|
-
input: {
|
|
4393
|
-
kind: "buffer",
|
|
4394
|
-
data: params.data,
|
|
4395
|
-
encoding: params.encoding,
|
|
4396
|
-
},
|
|
4832
|
+
input: { kind: "buffer", data: params.data, encoding: params.encoding },
|
|
4397
4833
|
});
|
|
4398
4834
|
}
|
|
4399
4835
|
catch (error) {
|
|
4836
|
+
const errorCode = error?.code;
|
|
4837
|
+
if (errorCode === "file_lock_stale" || errorCode === "file_lock_timeout") {
|
|
4838
|
+
throw error;
|
|
4839
|
+
}
|
|
4400
4840
|
if (params.overwrite === false && isAlreadyExistsError(error)) {
|
|
4401
4841
|
throw new errors_FsSafeError("already-exists", "file already exists", {
|
|
4402
4842
|
cause: error instanceof Error ? error : undefined,
|
|
@@ -46550,7 +46990,7 @@ if (installedChunkData !== 0) { // 0 means "already installed".'
|
|
|
46550
46990
|
// module factories are used so entry inlining is disabled
|
|
46551
46991
|
// startup
|
|
46552
46992
|
// Load entry module and return exports
|
|
46553
|
-
var __webpack_exports__ = __webpack_require__(
|
|
46993
|
+
var __webpack_exports__ = __webpack_require__(6179);
|
|
46554
46994
|
var __webpack_exports__DEFAULT_LINT_RULES = __webpack_exports__.Kd;
|
|
46555
46995
|
var __webpack_exports__LintValidationError = __webpack_exports__.F5;
|
|
46556
46996
|
var __webpack_exports__createFormatter = __webpack_exports__.xi;
|
package/package.json
CHANGED