mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:40:44 +00:00
fix(plugins): harden runtime mirrors
This commit is contained in:
@@ -6,6 +6,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Fixes
|
||||
|
||||
- Plugins/runtime-deps: replace stale symlinked mirror target roots before writing runtime-mirror temp files and skip rewriting already materialized hardlinks, so cross-version container upgrades no longer crash-loop on read-only image-layer paths while warm mirrors do less churn. Fixes #75108; refs #75069. Thanks @coletebou and @xiaohuaxi.
|
||||
- Auto-reply/group chats: fall back to automatic source delivery when a channel precomputes message-tool-only replies but the `message` tool is unavailable, so Discord/Slack-style group turns do not silently complete without a visible reply. Fixes #74868. Thanks @kagura-agent.
|
||||
- Browser/gateway: share one browser control runtime across the HTTP control server and `browser.request`, and refresh browser profile config from the source snapshot, so CLI status/start honors configured `browser.executablePath`, `headless`, and `noSandbox` instead of falling back to stale auto-detection. Fixes #75087; repairs #73617. Thanks @civiltox and @martingarramon.
|
||||
- Agents/subagents: bound automatic orphan recovery with persisted recovery attempts and a wedged-session tombstone, and teach task maintenance/doctor to reconcile those sessions so restart loops no longer require manual `sessions.json` surgery. Fixes #74864. Thanks @solosage1.
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { refreshBundledPluginRuntimeMirrorRoot } from "./bundled-runtime-mirror.js";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
materializeBundledRuntimeMirrorFile,
|
||||
refreshBundledPluginRuntimeMirrorRoot,
|
||||
} from "./bundled-runtime-mirror.js";
|
||||
|
||||
const tempRoots: string[] = [];
|
||||
|
||||
@@ -13,6 +16,7 @@ function makeTempRoot(): string {
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
for (const root of tempRoots.splice(0)) {
|
||||
fs.rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
@@ -89,4 +93,50 @@ describe("refreshBundledPluginRuntimeMirrorRoot", () => {
|
||||
expect(fs.lstatSync(path.join(targetRoot, "entry")).isFile()).toBe(true);
|
||||
expect(fs.readFileSync(path.join(targetRoot, "entry"), "utf8")).toContain("2");
|
||||
});
|
||||
|
||||
it("replaces stale symlinked mirror roots before creating temp files", () => {
|
||||
const root = makeTempRoot();
|
||||
const sourceRoot = path.join(root, "source");
|
||||
const targetRoot = path.join(root, "target");
|
||||
const staleRoot = path.join(root, "stale-image-layer");
|
||||
fs.mkdirSync(sourceRoot, { recursive: true });
|
||||
fs.mkdirSync(staleRoot, { recursive: true });
|
||||
fs.writeFileSync(path.join(sourceRoot, "fresh.js"), "export const value = 'fresh';\n", "utf8");
|
||||
fs.symlinkSync(staleRoot, targetRoot, "dir");
|
||||
|
||||
expect(
|
||||
refreshBundledPluginRuntimeMirrorRoot({
|
||||
pluginId: "demo",
|
||||
sourceRoot,
|
||||
targetRoot,
|
||||
}),
|
||||
).toBe(true);
|
||||
|
||||
expect(fs.lstatSync(targetRoot).isSymbolicLink()).toBe(false);
|
||||
expect(fs.readFileSync(path.join(targetRoot, "fresh.js"), "utf8")).toContain("fresh");
|
||||
expect(fs.existsSync(path.join(staleRoot, "fresh.js"))).toBe(false);
|
||||
});
|
||||
|
||||
it("does not rewrite already materialized hardlinks", () => {
|
||||
const root = makeTempRoot();
|
||||
const sourcePath = path.join(root, "source.js");
|
||||
const targetPath = path.join(root, "target.js");
|
||||
fs.writeFileSync(sourcePath, "export const value = 1;\n", "utf8");
|
||||
fs.linkSync(sourcePath, targetPath);
|
||||
const linkSpy = vi.spyOn(fs, "linkSync");
|
||||
const copySpy = vi.spyOn(fs, "copyFileSync");
|
||||
const renameSpy = vi.spyOn(fs, "renameSync");
|
||||
|
||||
materializeBundledRuntimeMirrorFile(sourcePath, targetPath);
|
||||
|
||||
expect(linkSpy).not.toHaveBeenCalled();
|
||||
expect(copySpy).not.toHaveBeenCalled();
|
||||
expect(renameSpy).not.toHaveBeenCalled();
|
||||
const sourceStat = fs.lstatSync(sourcePath);
|
||||
const targetStat = fs.lstatSync(targetPath);
|
||||
expect({ dev: targetStat.dev, ino: targetStat.ino }).toEqual({
|
||||
dev: sourceStat.dev,
|
||||
ino: sourceStat.ino,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -47,7 +47,7 @@ export function copyBundledPluginRuntimeRoot(sourceRoot: string, targetRoot: str
|
||||
if (path.resolve(sourceRoot) === path.resolve(targetRoot)) {
|
||||
return;
|
||||
}
|
||||
fs.mkdirSync(targetRoot, { recursive: true, mode: 0o755 });
|
||||
ensureBundledRuntimeMirrorDirectory(targetRoot);
|
||||
const mirroredNames = new Set<string>();
|
||||
for (const entry of fs.readdirSync(sourceRoot, { withFileTypes: true })) {
|
||||
if (shouldIgnoreBundledRuntimeMirrorEntry(entry.name)) {
|
||||
@@ -81,16 +81,13 @@ export function materializeBundledRuntimeMirrorFile(sourcePath: string, targetPa
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (
|
||||
fs.realpathSync(sourcePath) === fs.realpathSync(targetPath) &&
|
||||
!fs.lstatSync(targetPath).isSymbolicLink()
|
||||
) {
|
||||
if (isBundledRuntimeMirrorFileAlreadyMaterialized(sourcePath, targetPath)) {
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
// Missing targets are expected before the mirror file is materialized.
|
||||
}
|
||||
fs.mkdirSync(path.dirname(targetPath), { recursive: true, mode: 0o755 });
|
||||
ensureBundledRuntimeMirrorDirectory(path.dirname(targetPath));
|
||||
removeBundledRuntimeMirrorPathIfTypeChanged(targetPath, "file");
|
||||
const tempPath = createBundledRuntimeMirrorTempPath(targetPath);
|
||||
try {
|
||||
@@ -107,6 +104,20 @@ export function materializeBundledRuntimeMirrorFile(sourcePath: string, targetPa
|
||||
}
|
||||
}
|
||||
|
||||
function isBundledRuntimeMirrorFileAlreadyMaterialized(
|
||||
sourcePath: string,
|
||||
targetPath: string,
|
||||
): boolean {
|
||||
const sourceStat = fs.lstatSync(sourcePath);
|
||||
const targetStat = fs.lstatSync(targetPath);
|
||||
return (
|
||||
sourceStat.isFile() &&
|
||||
targetStat.isFile() &&
|
||||
sourceStat.dev === targetStat.dev &&
|
||||
sourceStat.ino === targetStat.ino
|
||||
);
|
||||
}
|
||||
|
||||
function chmodBundledRuntimeMirrorFileReadable(sourcePath: string, targetPath: string): void {
|
||||
try {
|
||||
const sourceMode = fs.statSync(sourcePath).mode;
|
||||
@@ -131,6 +142,21 @@ function pruneStaleBundledRuntimeMirrorEntries(
|
||||
}
|
||||
}
|
||||
|
||||
function ensureBundledRuntimeMirrorDirectory(targetRoot: string): void {
|
||||
try {
|
||||
const stat = fs.lstatSync(targetRoot);
|
||||
if (stat.isDirectory() && !stat.isSymbolicLink()) {
|
||||
return;
|
||||
}
|
||||
fs.rmSync(targetRoot, { recursive: true, force: true });
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
fs.mkdirSync(targetRoot, { recursive: true, mode: 0o755 });
|
||||
}
|
||||
|
||||
function removeBundledRuntimeMirrorPathIfTypeChanged(
|
||||
targetPath: string,
|
||||
expectedType: "directory" | "file" | "symlink",
|
||||
@@ -153,7 +179,7 @@ function removeBundledRuntimeMirrorPathIfTypeChanged(
|
||||
}
|
||||
|
||||
function replaceBundledRuntimeMirrorSymlinkAtomic(linkTarget: string, targetPath: string): void {
|
||||
fs.mkdirSync(path.dirname(targetPath), { recursive: true, mode: 0o755 });
|
||||
ensureBundledRuntimeMirrorDirectory(path.dirname(targetPath));
|
||||
const tempPath = createBundledRuntimeMirrorTempPath(targetPath);
|
||||
try {
|
||||
fs.symlinkSync(linkTarget, tempPath);
|
||||
@@ -164,7 +190,7 @@ function replaceBundledRuntimeMirrorSymlinkAtomic(linkTarget: string, targetPath
|
||||
}
|
||||
|
||||
function copyBundledRuntimeMirrorFileAtomic(sourcePath: string, targetPath: string): void {
|
||||
fs.mkdirSync(path.dirname(targetPath), { recursive: true, mode: 0o755 });
|
||||
ensureBundledRuntimeMirrorDirectory(path.dirname(targetPath));
|
||||
const tempPath = createBundledRuntimeMirrorTempPath(targetPath);
|
||||
try {
|
||||
fs.copyFileSync(sourcePath, tempPath);
|
||||
|
||||
Reference in New Issue
Block a user