mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 19:20:43 +00:00
fix(plugins): stabilize bundled setup runtimes (#67200)
Merged via squash.
Prepared head SHA: e8d6738fd0
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
This commit is contained in:
committed by
GitHub
parent
ee6b7daca3
commit
78ac118427
@@ -638,15 +638,20 @@ function resolvePluginModuleExport(moduleExport: unknown): {
|
||||
return {};
|
||||
}
|
||||
|
||||
function mergeSetupPluginSection<T>(
|
||||
function mergeChannelPluginSection<T>(
|
||||
baseValue: T | undefined,
|
||||
setupValue: T | undefined,
|
||||
overrideValue: T | undefined,
|
||||
): T | undefined {
|
||||
if (baseValue && setupValue && typeof baseValue === "object" && typeof setupValue === "object") {
|
||||
if (
|
||||
baseValue &&
|
||||
overrideValue &&
|
||||
typeof baseValue === "object" &&
|
||||
typeof overrideValue === "object"
|
||||
) {
|
||||
const merged = {
|
||||
...(baseValue as Record<string, unknown>),
|
||||
};
|
||||
for (const [key, value] of Object.entries(setupValue as Record<string, unknown>)) {
|
||||
for (const [key, value] of Object.entries(overrideValue as Record<string, unknown>)) {
|
||||
if (value !== undefined) {
|
||||
merged[key] = value;
|
||||
}
|
||||
@@ -655,11 +660,102 @@ function mergeSetupPluginSection<T>(
|
||||
...merged,
|
||||
} as T;
|
||||
}
|
||||
return setupValue ?? baseValue;
|
||||
return overrideValue ?? baseValue;
|
||||
}
|
||||
|
||||
function mergeSetupRuntimeChannelPlugin(
|
||||
runtimePlugin: ChannelPlugin,
|
||||
setupPlugin: ChannelPlugin,
|
||||
): ChannelPlugin {
|
||||
return {
|
||||
...runtimePlugin,
|
||||
...setupPlugin,
|
||||
meta: mergeChannelPluginSection(runtimePlugin.meta, setupPlugin.meta),
|
||||
capabilities: mergeChannelPluginSection(runtimePlugin.capabilities, setupPlugin.capabilities),
|
||||
commands: mergeChannelPluginSection(runtimePlugin.commands, setupPlugin.commands),
|
||||
doctor: mergeChannelPluginSection(runtimePlugin.doctor, setupPlugin.doctor),
|
||||
reload: mergeChannelPluginSection(runtimePlugin.reload, setupPlugin.reload),
|
||||
config: mergeChannelPluginSection(runtimePlugin.config, setupPlugin.config),
|
||||
setup: mergeChannelPluginSection(runtimePlugin.setup, setupPlugin.setup),
|
||||
messaging: mergeChannelPluginSection(runtimePlugin.messaging, setupPlugin.messaging),
|
||||
actions: mergeChannelPluginSection(runtimePlugin.actions, setupPlugin.actions),
|
||||
secrets: mergeChannelPluginSection(runtimePlugin.secrets, setupPlugin.secrets),
|
||||
} as ChannelPlugin;
|
||||
}
|
||||
|
||||
function resolveBundledRuntimeChannelRegistration(moduleExport: unknown): {
|
||||
id?: string;
|
||||
loadChannelPlugin?: () => ChannelPlugin;
|
||||
loadChannelSecrets?: () => ChannelPlugin["secrets"] | undefined;
|
||||
setChannelRuntime?: (runtime: PluginRuntime) => void;
|
||||
} {
|
||||
const resolved = unwrapDefaultModuleExport(moduleExport);
|
||||
if (!resolved || typeof resolved !== "object") {
|
||||
return {};
|
||||
}
|
||||
const entryRecord = resolved as {
|
||||
kind?: unknown;
|
||||
id?: unknown;
|
||||
loadChannelPlugin?: unknown;
|
||||
loadChannelSecrets?: unknown;
|
||||
setChannelRuntime?: unknown;
|
||||
};
|
||||
if (
|
||||
entryRecord.kind !== "bundled-channel-entry" ||
|
||||
typeof entryRecord.id !== "string" ||
|
||||
typeof entryRecord.loadChannelPlugin !== "function"
|
||||
) {
|
||||
return {};
|
||||
}
|
||||
return {
|
||||
id: entryRecord.id,
|
||||
loadChannelPlugin: entryRecord.loadChannelPlugin as () => ChannelPlugin,
|
||||
...(typeof entryRecord.loadChannelSecrets === "function"
|
||||
? {
|
||||
loadChannelSecrets: entryRecord.loadChannelSecrets as () =>
|
||||
| ChannelPlugin["secrets"]
|
||||
| undefined,
|
||||
}
|
||||
: {}),
|
||||
...(typeof entryRecord.setChannelRuntime === "function"
|
||||
? {
|
||||
setChannelRuntime: entryRecord.setChannelRuntime as (runtime: PluginRuntime) => void,
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
|
||||
function loadBundledRuntimeChannelPlugin(params: {
|
||||
registration: ReturnType<typeof resolveBundledRuntimeChannelRegistration>;
|
||||
}): {
|
||||
plugin?: ChannelPlugin;
|
||||
loadError?: unknown;
|
||||
} {
|
||||
if (typeof params.registration.loadChannelPlugin !== "function") {
|
||||
return {};
|
||||
}
|
||||
try {
|
||||
const loadedPlugin = params.registration.loadChannelPlugin();
|
||||
const loadedSecrets = params.registration.loadChannelSecrets?.();
|
||||
if (!loadedPlugin || typeof loadedPlugin !== "object") {
|
||||
return {};
|
||||
}
|
||||
const mergedSecrets = mergeChannelPluginSection(loadedPlugin.secrets, loadedSecrets);
|
||||
return {
|
||||
plugin: {
|
||||
...loadedPlugin,
|
||||
...(mergedSecrets !== undefined ? { secrets: mergedSecrets } : {}),
|
||||
},
|
||||
};
|
||||
} catch (err) {
|
||||
return { loadError: err };
|
||||
}
|
||||
}
|
||||
|
||||
function resolveSetupChannelRegistration(moduleExport: unknown): {
|
||||
plugin?: ChannelPlugin;
|
||||
setChannelRuntime?: (runtime: PluginRuntime) => void;
|
||||
usesBundledSetupContract?: boolean;
|
||||
loadError?: unknown;
|
||||
} {
|
||||
const resolved = unwrapDefaultModuleExport(moduleExport);
|
||||
@@ -670,6 +766,7 @@ function resolveSetupChannelRegistration(moduleExport: unknown): {
|
||||
kind?: unknown;
|
||||
loadSetupPlugin?: unknown;
|
||||
loadSetupSecrets?: unknown;
|
||||
setChannelRuntime?: unknown;
|
||||
};
|
||||
if (
|
||||
setupEntryRecord.kind === "bundled-channel-setup-entry" &&
|
||||
@@ -682,7 +779,7 @@ function resolveSetupChannelRegistration(moduleExport: unknown): {
|
||||
? (setupEntryRecord.loadSetupSecrets() as ChannelPlugin["secrets"] | undefined)
|
||||
: undefined;
|
||||
if (loadedPlugin && typeof loadedPlugin === "object") {
|
||||
const mergedSecrets = mergeSetupPluginSection(
|
||||
const mergedSecrets = mergeChannelPluginSection(
|
||||
(loadedPlugin as ChannelPlugin).secrets,
|
||||
loadedSecrets,
|
||||
);
|
||||
@@ -691,6 +788,14 @@ function resolveSetupChannelRegistration(moduleExport: unknown): {
|
||||
...(loadedPlugin as ChannelPlugin),
|
||||
...(mergedSecrets !== undefined ? { secrets: mergedSecrets } : {}),
|
||||
},
|
||||
usesBundledSetupContract: true,
|
||||
...(typeof setupEntryRecord.setChannelRuntime === "function"
|
||||
? {
|
||||
setChannelRuntime: setupEntryRecord.setChannelRuntime as (
|
||||
runtime: PluginRuntime,
|
||||
) => void,
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -1709,7 +1814,147 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi
|
||||
hookPolicy: entry?.hooks,
|
||||
registrationMode,
|
||||
});
|
||||
api.registerChannel(setupRegistration.plugin);
|
||||
let mergedSetupRegistration = setupRegistration;
|
||||
let runtimeSetterApplied = false;
|
||||
if (
|
||||
registrationMode === "setup-runtime" &&
|
||||
setupRegistration.usesBundledSetupContract &&
|
||||
candidate.source !== safeSource
|
||||
) {
|
||||
const runtimeOpened = openBoundaryFileSync({
|
||||
absolutePath: candidate.source,
|
||||
rootPath: pluginRoot,
|
||||
boundaryLabel: "plugin root",
|
||||
rejectHardlinks: candidate.origin !== "bundled",
|
||||
skipLexicalRootCheck: true,
|
||||
});
|
||||
if (!runtimeOpened.ok) {
|
||||
pushPluginLoadError("plugin entry path escapes plugin root or fails alias checks");
|
||||
continue;
|
||||
}
|
||||
const safeRuntimeSource = runtimeOpened.path;
|
||||
fs.closeSync(runtimeOpened.fd);
|
||||
const safeRuntimeImportSource = toSafeImportPath(safeRuntimeSource);
|
||||
let runtimeMod: OpenClawPluginModule | null = null;
|
||||
try {
|
||||
runtimeMod = profilePluginLoaderSync({
|
||||
phase: "load-setup-runtime-entry",
|
||||
pluginId: record.id,
|
||||
source: safeRuntimeSource,
|
||||
run: () =>
|
||||
getJiti(safeRuntimeSource)(safeRuntimeImportSource) as OpenClawPluginModule,
|
||||
});
|
||||
} catch (err) {
|
||||
recordPluginError({
|
||||
logger,
|
||||
registry,
|
||||
record,
|
||||
seenIds,
|
||||
pluginId,
|
||||
origin: candidate.origin,
|
||||
phase: "load",
|
||||
error: err,
|
||||
logPrefix: `[plugins] ${record.id} failed to load setup-runtime entry from ${record.source}: `,
|
||||
diagnosticMessagePrefix: "failed to load setup-runtime entry: ",
|
||||
});
|
||||
continue;
|
||||
}
|
||||
const runtimeRegistration = resolveBundledRuntimeChannelRegistration(runtimeMod);
|
||||
if (runtimeRegistration.id && runtimeRegistration.id !== record.id) {
|
||||
pushPluginLoadError(
|
||||
`plugin id mismatch (config uses "${record.id}", runtime entry uses "${runtimeRegistration.id}")`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if (runtimeRegistration.setChannelRuntime) {
|
||||
try {
|
||||
runtimeRegistration.setChannelRuntime(api.runtime);
|
||||
runtimeSetterApplied = true;
|
||||
} catch (err) {
|
||||
recordPluginError({
|
||||
logger,
|
||||
registry,
|
||||
record,
|
||||
seenIds,
|
||||
pluginId,
|
||||
origin: candidate.origin,
|
||||
phase: "load",
|
||||
error: err,
|
||||
logPrefix: `[plugins] ${record.id} failed to apply setup-runtime channel runtime from ${record.source}: `,
|
||||
diagnosticMessagePrefix: "failed to apply setup-runtime channel runtime: ",
|
||||
});
|
||||
continue;
|
||||
}
|
||||
}
|
||||
const runtimePluginRegistration = loadBundledRuntimeChannelPlugin({
|
||||
registration: runtimeRegistration,
|
||||
});
|
||||
if (runtimePluginRegistration.loadError) {
|
||||
recordPluginError({
|
||||
logger,
|
||||
registry,
|
||||
record,
|
||||
seenIds,
|
||||
pluginId,
|
||||
origin: candidate.origin,
|
||||
phase: "load",
|
||||
error: runtimePluginRegistration.loadError,
|
||||
logPrefix: `[plugins] ${record.id} failed to load setup-runtime channel entry from ${record.source}: `,
|
||||
diagnosticMessagePrefix: "failed to load setup-runtime channel entry: ",
|
||||
});
|
||||
continue;
|
||||
}
|
||||
if (runtimePluginRegistration.plugin) {
|
||||
if (
|
||||
runtimePluginRegistration.plugin.id &&
|
||||
runtimePluginRegistration.plugin.id !== record.id
|
||||
) {
|
||||
pushPluginLoadError(
|
||||
`plugin id mismatch (config uses "${record.id}", runtime export uses "${runtimePluginRegistration.plugin.id}")`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
mergedSetupRegistration = {
|
||||
...setupRegistration,
|
||||
plugin: mergeSetupRuntimeChannelPlugin(
|
||||
runtimePluginRegistration.plugin,
|
||||
setupRegistration.plugin,
|
||||
),
|
||||
setChannelRuntime:
|
||||
runtimeRegistration.setChannelRuntime ?? setupRegistration.setChannelRuntime,
|
||||
};
|
||||
}
|
||||
}
|
||||
const mergedSetupPlugin = mergedSetupRegistration.plugin;
|
||||
if (!mergedSetupPlugin) {
|
||||
continue;
|
||||
}
|
||||
if (mergedSetupPlugin.id && mergedSetupPlugin.id !== record.id) {
|
||||
pushPluginLoadError(
|
||||
`plugin id mismatch (config uses "${record.id}", setup export uses "${mergedSetupPlugin.id}")`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if (!runtimeSetterApplied) {
|
||||
try {
|
||||
mergedSetupRegistration.setChannelRuntime?.(api.runtime);
|
||||
} catch (err) {
|
||||
recordPluginError({
|
||||
logger,
|
||||
registry,
|
||||
record,
|
||||
seenIds,
|
||||
pluginId,
|
||||
origin: candidate.origin,
|
||||
phase: "load",
|
||||
error: err,
|
||||
logPrefix: `[plugins] ${record.id} failed to apply setup channel runtime from ${record.source}: `,
|
||||
diagnosticMessagePrefix: "failed to apply setup channel runtime: ",
|
||||
});
|
||||
continue;
|
||||
}
|
||||
}
|
||||
api.registerChannel(mergedSetupPlugin);
|
||||
registry.plugins.push(record);
|
||||
seenIds.set(pluginId, candidate.origin);
|
||||
continue;
|
||||
|
||||
Reference in New Issue
Block a user