mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 08:30:42 +00:00
fix: stage mirrored logger runtime deps
This commit is contained in:
@@ -18,6 +18,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Agents/bootstrap: dedupe hook-injected bootstrap context files by workspace-relative path and store normalized resolved paths so duplicate relative and absolute hook paths no longer depend on the process cwd. (#59344; fixes #59319; related #56721, #56725, and #57587) Thanks @koen666.
|
||||
- Agents/bootstrap: refresh cached workspace bootstrap snapshots on long-lived main-session turns when `AGENTS.md`, `SOUL.md`, `MEMORY.md`, or `TOOLS.md` change on disk, while preserving unchanged snapshot identity through the workspace file cache. (#64871; related #43901, #26497, #28594, #30896) Thanks @aimqwest and @mikejuyoon.
|
||||
- macOS Gateway: detect installed-but-unloaded LaunchAgent split-brain states during status, doctor, and restart, and re-bootstrap launchd supervision before falling back to unmanaged listener restarts. Fixes #67335, #53475, and #71060; refs #58890, #60885, and #70801. Thanks @ze1tgeist88, @dafacto, and @vishutdhar.
|
||||
- Plugins/install: treat mirrored core logger dependencies as staged bundled runtime deps so packaged Gateway starts do not crash when the external plugin-runtime-deps root is missing `tslog`. Fixes #72228; supersedes #72493. Thanks @deepujain.
|
||||
- Plugins/install: stage bundled plugin runtime dependencies before Gateway startup, drain update restarts, and materialize plugin-owned root chunks in external mirrors so staged deps resolve under native ESM. Fixes #72058; supersedes #72084. Thanks @amnesia106 and @drvoss.
|
||||
- TTS/SecretRef: resolve `messages.tts.providers.*.apiKey` from the active runtime snapshot so SecretRef-backed MiniMax and other TTS provider keys work in runtime reply/audio paths. Fixes #68690. Thanks @joshavant.
|
||||
- Gateway/install: surface systemd user-bus recovery hints during Linux service activation and retry via the machine user scope when `systemctl --user` reports no-medium bus failures. Fixes #39673; refs #44417 and #63561. Thanks @Arbor4, @myrsu, and @mssteuer.
|
||||
|
||||
@@ -859,6 +859,101 @@ describe("scanBundledPluginRuntimeDeps config policy", () => {
|
||||
readFileSyncSpy.mock.calls.filter((call) => path.resolve(String(call[0])) === manifestPath),
|
||||
).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("reports missing mirrored core runtime deps for doctor repair", () => {
|
||||
const packageRoot = makeTempDir();
|
||||
const stageDir = makeTempDir();
|
||||
fs.writeFileSync(
|
||||
path.join(packageRoot, "package.json"),
|
||||
JSON.stringify({
|
||||
name: "openclaw",
|
||||
version: "2026.4.25",
|
||||
dependencies: { tslog: "^4.10.2" },
|
||||
}),
|
||||
);
|
||||
writeBundledPluginPackage({
|
||||
packageRoot,
|
||||
pluginId: "discord",
|
||||
deps: { "discord-runtime": "1.0.0" },
|
||||
enabledByDefault: true,
|
||||
});
|
||||
|
||||
const result = scanBundledPluginRuntimeDeps({
|
||||
packageRoot,
|
||||
config: {},
|
||||
env: { OPENCLAW_PLUGIN_STAGE_DIR: stageDir },
|
||||
});
|
||||
|
||||
expect(result.deps.map((dep) => `${dep.name}@${dep.version}`)).toEqual([
|
||||
"discord-runtime@1.0.0",
|
||||
"tslog@^4.10.2",
|
||||
]);
|
||||
expect(result.missing.map((dep) => `${dep.name}@${dep.version}`)).toEqual([
|
||||
"discord-runtime@1.0.0",
|
||||
"tslog@^4.10.2",
|
||||
]);
|
||||
});
|
||||
|
||||
it("reports missing mirrored core runtime deps for startup plugins without own deps", () => {
|
||||
const packageRoot = makeTempDir();
|
||||
const stageDir = makeTempDir();
|
||||
fs.writeFileSync(
|
||||
path.join(packageRoot, "package.json"),
|
||||
JSON.stringify({
|
||||
name: "openclaw",
|
||||
version: "2026.4.25",
|
||||
dependencies: { tslog: "^4.10.2" },
|
||||
}),
|
||||
);
|
||||
writeBundledPluginPackage({
|
||||
packageRoot,
|
||||
pluginId: "slack",
|
||||
deps: {},
|
||||
channels: ["slack"],
|
||||
});
|
||||
|
||||
const result = scanBundledPluginRuntimeDeps({
|
||||
packageRoot,
|
||||
selectedPluginIds: ["slack"],
|
||||
config: {
|
||||
channels: { slack: { botToken: "xoxb-token" } },
|
||||
},
|
||||
env: { OPENCLAW_PLUGIN_STAGE_DIR: stageDir },
|
||||
});
|
||||
|
||||
expect(result.deps.map((dep) => `${dep.name}@${dep.version}`)).toEqual(["tslog@^4.10.2"]);
|
||||
expect(result.deps[0]?.pluginIds).toEqual(["openclaw-core"]);
|
||||
expect(result.missing.map((dep) => `${dep.name}@${dep.version}`)).toEqual(["tslog@^4.10.2"]);
|
||||
});
|
||||
|
||||
it("deduplicates mirrored core runtime deps already declared by a plugin", () => {
|
||||
const packageRoot = makeTempDir();
|
||||
const stageDir = makeTempDir();
|
||||
fs.writeFileSync(
|
||||
path.join(packageRoot, "package.json"),
|
||||
JSON.stringify({
|
||||
name: "openclaw",
|
||||
version: "2026.4.25",
|
||||
dependencies: { tslog: "^4.10.2" },
|
||||
}),
|
||||
);
|
||||
writeBundledPluginPackage({
|
||||
packageRoot,
|
||||
pluginId: "logger-plugin",
|
||||
deps: { tslog: "^4.10.2" },
|
||||
enabledByDefault: true,
|
||||
});
|
||||
|
||||
const result = scanBundledPluginRuntimeDeps({
|
||||
packageRoot,
|
||||
config: {},
|
||||
env: { OPENCLAW_PLUGIN_STAGE_DIR: stageDir },
|
||||
});
|
||||
|
||||
expect(result.deps.map((dep) => `${dep.name}@${dep.version}`)).toEqual(["tslog@^4.10.2"]);
|
||||
expect(result.deps[0]?.pluginIds).toEqual(["logger-plugin", "openclaw-core"]);
|
||||
expect(result.missing.map((dep) => `${dep.name}@${dep.version}`)).toEqual(["tslog@^4.10.2"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("ensureBundledPluginRuntimeDeps", () => {
|
||||
@@ -957,6 +1052,47 @@ describe("ensureBundledPluginRuntimeDeps", () => {
|
||||
expect(installRoot).not.toBe(pluginRoot);
|
||||
});
|
||||
|
||||
it("installs mirrored core logger deps even when the plugin has no external deps", () => {
|
||||
const packageRoot = makeTempDir();
|
||||
const stageDir = makeTempDir();
|
||||
fs.writeFileSync(
|
||||
path.join(packageRoot, "package.json"),
|
||||
JSON.stringify({
|
||||
name: "openclaw",
|
||||
version: "2026.4.25",
|
||||
dependencies: { tslog: "^4.10.2" },
|
||||
}),
|
||||
);
|
||||
const pluginRoot = path.join(packageRoot, "dist", "extensions", "slack");
|
||||
fs.mkdirSync(pluginRoot, { recursive: true });
|
||||
fs.writeFileSync(path.join(pluginRoot, "package.json"), JSON.stringify({ dependencies: {} }));
|
||||
|
||||
const calls: BundledRuntimeDepsInstallParams[] = [];
|
||||
const result = ensureBundledPluginRuntimeDeps({
|
||||
env: { OPENCLAW_PLUGIN_STAGE_DIR: stageDir },
|
||||
installDeps: (params) => {
|
||||
calls.push(params);
|
||||
},
|
||||
pluginId: "slack",
|
||||
pluginRoot,
|
||||
});
|
||||
|
||||
const installRoot = resolveBundledRuntimeDependencyInstallRoot(pluginRoot, {
|
||||
env: { OPENCLAW_PLUGIN_STAGE_DIR: stageDir },
|
||||
});
|
||||
expect(result).toEqual({
|
||||
installedSpecs: ["tslog@^4.10.2"],
|
||||
retainSpecs: ["tslog@^4.10.2"],
|
||||
});
|
||||
expect(calls).toEqual([
|
||||
{
|
||||
installRoot,
|
||||
missingSpecs: ["tslog@^4.10.2"],
|
||||
installSpecs: ["tslog@^4.10.2"],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("uses external staging when a packaged plugin declares workspace:* deps", () => {
|
||||
// Regression guard for packaged/Docker bundled plugins whose `package.json`
|
||||
// still lists `"@openclaw/plugin-sdk": "workspace:*"` (and similar) alongside
|
||||
|
||||
@@ -63,6 +63,8 @@ const BUNDLED_RUNTIME_DEPS_LOCK_STALE_MS = 10 * 60_000;
|
||||
const BUNDLED_RUNTIME_DEPS_OWNERLESS_LOCK_STALE_MS = 30_000;
|
||||
const BUNDLED_RUNTIME_MIRROR_MATERIALIZED_EXTENSIONS = new Set([".cjs", ".js", ".mjs"]);
|
||||
const BUNDLED_RUNTIME_MIRROR_PLUGIN_REGION_RE = /(?:^|\n)\/\/#region extensions\/[^/\s]+(?:\/|$)/u;
|
||||
const MIRRORED_PACKAGE_RUNTIME_DEP_NAMES = ["tslog"] as const;
|
||||
const MIRRORED_PACKAGE_RUNTIME_DEP_PLUGIN_ID = "openclaw-core";
|
||||
|
||||
const registeredBundledRuntimeDepNodePaths = new Set<string>();
|
||||
|
||||
@@ -456,6 +458,56 @@ function collectRuntimeDeps(packageJson: JsonObject): Record<string, unknown> {
|
||||
};
|
||||
}
|
||||
|
||||
function collectMirroredPackageRuntimeDeps(packageRoot: string | null): {
|
||||
name: string;
|
||||
version: string;
|
||||
}[] {
|
||||
if (!packageRoot) {
|
||||
return [];
|
||||
}
|
||||
const packageJson = readJsonObject(path.join(packageRoot, "package.json"));
|
||||
if (!packageJson) {
|
||||
return [];
|
||||
}
|
||||
const runtimeDeps = collectRuntimeDeps(packageJson);
|
||||
return MIRRORED_PACKAGE_RUNTIME_DEP_NAMES.flatMap((name) => {
|
||||
const dep = parseInstallableRuntimeDep(name, runtimeDeps[name]);
|
||||
return dep ? [dep] : [];
|
||||
});
|
||||
}
|
||||
|
||||
function mergeInstallableRuntimeDeps(
|
||||
deps: readonly { name: string; version: string }[],
|
||||
): { name: string; version: string }[] {
|
||||
const bySpec = new Map<string, { name: string; version: string }>();
|
||||
for (const dep of deps) {
|
||||
bySpec.set(`${dep.name}@${dep.version}`, dep);
|
||||
}
|
||||
return [...bySpec.values()].toSorted((left, right) => {
|
||||
const nameOrder = left.name.localeCompare(right.name);
|
||||
return nameOrder === 0 ? left.version.localeCompare(right.version) : nameOrder;
|
||||
});
|
||||
}
|
||||
|
||||
function mergeRuntimeDepEntries(deps: readonly RuntimeDepEntry[]): RuntimeDepEntry[] {
|
||||
const bySpec = new Map<string, RuntimeDepEntry>();
|
||||
for (const dep of deps) {
|
||||
const spec = `${dep.name}@${dep.version}`;
|
||||
const existing = bySpec.get(spec);
|
||||
if (!existing) {
|
||||
bySpec.set(spec, { ...dep, pluginIds: [...dep.pluginIds] });
|
||||
continue;
|
||||
}
|
||||
existing.pluginIds = [...new Set([...existing.pluginIds, ...dep.pluginIds])].toSorted(
|
||||
(left, right) => left.localeCompare(right),
|
||||
);
|
||||
}
|
||||
return [...bySpec.values()].toSorted((left, right) => {
|
||||
const nameOrder = left.name.localeCompare(right.name);
|
||||
return nameOrder === 0 ? left.version.localeCompare(right.version) : nameOrder;
|
||||
});
|
||||
}
|
||||
|
||||
function isSourceCheckoutRoot(packageRoot: string): boolean {
|
||||
return (
|
||||
(fs.existsSync(path.join(packageRoot, ".git")) ||
|
||||
@@ -1083,9 +1135,11 @@ function collectBundledPluginRuntimeDeps(params: {
|
||||
}): {
|
||||
deps: RuntimeDepEntry[];
|
||||
conflicts: RuntimeDepConflict[];
|
||||
pluginIds: string[];
|
||||
} {
|
||||
const versionMap = new Map<string, Map<string, Set<string>>>();
|
||||
const manifestCache: BundledPluginRuntimeDepsManifestCache = new Map();
|
||||
const includedPluginIds = new Set<string>();
|
||||
|
||||
for (const entry of fs.readdirSync(params.extensionsDir, { withFileTypes: true })) {
|
||||
if (!entry.isDirectory()) {
|
||||
@@ -1106,6 +1160,7 @@ function collectBundledPluginRuntimeDeps(params: {
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
includedPluginIds.add(pluginId);
|
||||
const packageJson = readJsonObject(path.join(pluginDir, "package.json"));
|
||||
if (!packageJson) {
|
||||
continue;
|
||||
@@ -1155,6 +1210,7 @@ function collectBundledPluginRuntimeDeps(params: {
|
||||
return {
|
||||
deps: deps.toSorted((a, b) => a.name.localeCompare(b.name)),
|
||||
conflicts: conflicts.toSorted((a, b) => a.name.localeCompare(b.name)),
|
||||
pluginIds: [...includedPluginIds].toSorted((a, b) => a.localeCompare(b)),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1189,29 +1245,42 @@ export function scanBundledPluginRuntimeDeps(params: {
|
||||
if (!fs.existsSync(extensionsDir)) {
|
||||
return { deps: [], missing: [], conflicts: [] };
|
||||
}
|
||||
const { deps, conflicts } = collectBundledPluginRuntimeDeps({
|
||||
const { deps, conflicts, pluginIds } = collectBundledPluginRuntimeDeps({
|
||||
extensionsDir,
|
||||
config: params.config,
|
||||
pluginIds: normalizePluginIdSet(params.pluginIds),
|
||||
selectedPluginIds: normalizePluginIdSet(params.selectedPluginIds),
|
||||
includeConfiguredChannels: params.includeConfiguredChannels,
|
||||
});
|
||||
const packageRuntimeDeps =
|
||||
pluginIds.length > 0
|
||||
? collectMirroredPackageRuntimeDeps(params.packageRoot).map((dep) => ({
|
||||
name: dep.name,
|
||||
version: dep.version,
|
||||
pluginIds: [MIRRORED_PACKAGE_RUNTIME_DEP_PLUGIN_ID],
|
||||
}))
|
||||
: [];
|
||||
const allDeps = mergeRuntimeDepEntries([...deps, ...packageRuntimeDeps]);
|
||||
const packageInstallRoot = resolveBundledRuntimeDependencyPackageInstallRoot(params.packageRoot, {
|
||||
env: params.env,
|
||||
});
|
||||
const packageSearchRoots = [packageInstallRoot];
|
||||
const missing = deps.filter(
|
||||
(dep) =>
|
||||
!hasDependencySentinel(packageSearchRoots, dep) &&
|
||||
dep.pluginIds.every((pluginId) => {
|
||||
const pluginRoot = path.join(extensionsDir, pluginId);
|
||||
const installRoot = resolveBundledRuntimeDependencyInstallRoot(pluginRoot, {
|
||||
env: params.env,
|
||||
});
|
||||
return !hasDependencySentinel([installRoot], dep);
|
||||
}),
|
||||
);
|
||||
return { deps, missing, conflicts };
|
||||
const missing = allDeps.filter((dep) => {
|
||||
if (hasDependencySentinel(packageSearchRoots, dep)) {
|
||||
return false;
|
||||
}
|
||||
if (dep.pluginIds.includes(MIRRORED_PACKAGE_RUNTIME_DEP_PLUGIN_ID)) {
|
||||
return true;
|
||||
}
|
||||
return dep.pluginIds.every((pluginId) => {
|
||||
const pluginRoot = path.join(extensionsDir, pluginId);
|
||||
const installRoot = resolveBundledRuntimeDependencyInstallRoot(pluginRoot, {
|
||||
env: params.env,
|
||||
});
|
||||
return !hasDependencySentinel([installRoot], dep);
|
||||
});
|
||||
});
|
||||
return { deps: allDeps, missing, conflicts };
|
||||
}
|
||||
|
||||
export function resolveBundledRuntimeDependencyPackageInstallRoot(
|
||||
@@ -1654,16 +1723,22 @@ export function ensureBundledPluginRuntimeDeps(params: {
|
||||
if (!packageJson) {
|
||||
return { installedSpecs: [], retainSpecs: [] };
|
||||
}
|
||||
const deps = Object.entries(collectRuntimeDeps(packageJson))
|
||||
const pluginDeps = Object.entries(collectRuntimeDeps(packageJson))
|
||||
.map(([name, rawVersion]) => parseInstallableRuntimeDep(name, rawVersion))
|
||||
.filter((entry): entry is { name: string; version: string } => Boolean(entry));
|
||||
if (deps.length === 0) {
|
||||
return { installedSpecs: [], retainSpecs: [] };
|
||||
}
|
||||
|
||||
const installRoot = resolveBundledRuntimeDependencyInstallRoot(params.pluginRoot, {
|
||||
env: params.env,
|
||||
});
|
||||
const packageRoot = resolveBundledRuntimeDependencyPackageRoot(params.pluginRoot);
|
||||
const packageRuntimeDeps =
|
||||
packageRoot && path.resolve(installRoot) !== path.resolve(params.pluginRoot)
|
||||
? collectMirroredPackageRuntimeDeps(packageRoot)
|
||||
: [];
|
||||
const deps = mergeInstallableRuntimeDeps([...pluginDeps, ...packageRuntimeDeps]);
|
||||
if (deps.length === 0) {
|
||||
return { installedSpecs: [], retainSpecs: [] };
|
||||
}
|
||||
return withBundledRuntimeDepsInstallRootLock(installRoot, () => {
|
||||
const persistRetainedManifest = shouldPersistRetainedRuntimeDepsManifest({
|
||||
pluginRoot: params.pluginRoot,
|
||||
|
||||
Reference in New Issue
Block a user